329 lines
8.5 KiB
Vue
329 lines
8.5 KiB
Vue
<template>
|
||
<n-collapse-item name="drilling" title="钻进工程活动">
|
||
<n-form label-placement="left" label-width="140" size="small">
|
||
<n-form-item label="钻进深度">
|
||
<n-input-number
|
||
v-model:value="drillingParams.depth"
|
||
:max="5"
|
||
:min="0.1"
|
||
:step="0.1"
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
|
||
<n-form-item label="动画时长(s)">
|
||
<n-input-number
|
||
v-model:value="drillingParams.drillingDuration"
|
||
:max="30"
|
||
:min="0.5"
|
||
:step="0.5"
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
|
||
<n-form-item label="管道半径(m)">
|
||
<n-input-number
|
||
v-model:value="drillingParams.pipeRadius"
|
||
:max="0.5"
|
||
:min="0.05"
|
||
:step="0.01"
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
|
||
<n-space style="width: 100%" vertical>
|
||
<n-button
|
||
:disabled="isSelectingDrillingPoint"
|
||
block
|
||
type="primary"
|
||
@click="startSelectingDrillingPoint">
|
||
{{ isSelectingDrillingPoint ? '请点击场景选择钻进点...' : '选择钻进点' }}
|
||
</n-button>
|
||
<n-button
|
||
:disabled="pipes.length === 0"
|
||
block
|
||
type="error"
|
||
@click="clearAllPipes">
|
||
清除所有管道 ({{ pipes.length }})
|
||
</n-button>
|
||
</n-space>
|
||
</n-collapse-item>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import {onMounted, ref} from 'vue';
|
||
import {NButton, NCollapseItem, NForm, NFormItem, NInputNumber, NSpace} from 'naive-ui';
|
||
import {useBus} from '@/hooks';
|
||
import {BusEvents} from '@/hooks/Bus.ts';
|
||
import {CSGOperationType, CssType, EventManagerEvents, ParametricPipe, Viewer} from '@deep/engine';
|
||
import {DrillingHtmlPanel} from '@/htmlPanel';
|
||
import * as THREE from 'three/webgpu';
|
||
|
||
const bus = useBus();
|
||
let viewer: Viewer | null = null;
|
||
let pipes: ParametricPipe[] = [];
|
||
let pipeCounter = 0;
|
||
const isSelectingDrillingPoint = ref(false);
|
||
const pipeModals = new Map<ParametricPipe, DrillingHtmlPanel>();
|
||
let selectedPipe: ParametricPipe | null = null;
|
||
|
||
// 钻进参数
|
||
const drillingParams = ref({
|
||
depth: 2.5,
|
||
drillingDuration: 0.1,
|
||
pipeRadius: 0.1,
|
||
});
|
||
|
||
onMounted(() => {
|
||
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||
viewer = bus.getViewer();
|
||
setupClickListener();
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 设置点击事件监听
|
||
*/
|
||
const setupClickListener = () => {
|
||
if (!viewer) return;
|
||
|
||
viewer.events.on(EventManagerEvents.RAYCAST_PICK, ({data}) => {
|
||
if (isSelectingDrillingPoint.value || !viewer) return;
|
||
|
||
const {intersects} = data;
|
||
if (!intersects || intersects.length === 0) return;
|
||
|
||
for (const intersection of intersects) {
|
||
const clickedObject = intersection.object;
|
||
const pipe = pipes.find(p => p === clickedObject);
|
||
|
||
if (pipe) {
|
||
// 如果钻进已完成,不显示弹窗
|
||
if (pipe.options.progress >= 1) {
|
||
break;
|
||
}
|
||
|
||
selectedPipe = pipe;
|
||
let modal = pipeModals.get(pipe);
|
||
if (!modal) {
|
||
modal = createDrillingModalForPipe(pipe);
|
||
}
|
||
updateDrillingModalInfo(pipe, modal);
|
||
if (modal && intersection.point) {
|
||
const modalObject = modal.getCssObject();
|
||
const clickPoint = intersection.point;
|
||
modalObject.position.copy(clickPoint);
|
||
modalObject.position.y += 0.5;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 为管道创建钻进信息弹窗
|
||
*/
|
||
const createDrillingModalForPipe = (pipe: ParametricPipe): DrillingHtmlPanel => {
|
||
if (!viewer) throw new Error('Viewer not initialized');
|
||
|
||
const modal = new DrillingHtmlPanel(CssType.CSS2D);
|
||
|
||
modal.onDelete(() => {
|
||
modal.hide();
|
||
const modalObject = modal.getCssObject();
|
||
if (modalObject.parent) {
|
||
modalObject.parent.remove(modalObject);
|
||
}
|
||
pipeModals.delete(pipe);
|
||
if (selectedPipe === pipe) {
|
||
selectedPipe = null;
|
||
}
|
||
});
|
||
|
||
const pipePosition = pipe.position;
|
||
const modalObject = modal.getCssObject();
|
||
modalObject.position.set(pipePosition.x, pipePosition.y + 1, pipePosition.z);
|
||
viewer.scene.add(modalObject);
|
||
modal.show();
|
||
pipeModals.set(pipe, modal);
|
||
|
||
return modal;
|
||
};
|
||
|
||
/**
|
||
* 更新钻进信息弹窗内容
|
||
*/
|
||
const updateDrillingModalInfo = (pipe: ParametricPipe, modal: DrillingHtmlPanel) => {
|
||
const progress = pipe.options.progress * 100;
|
||
const currentDepth = pipe.options.progress * drillingParams.value.depth;
|
||
const status = progress >= 100 ? 'completed' : progress > 0 ? 'drilling' : 'stopped';
|
||
|
||
modal.updateData({
|
||
pipeName: pipe.name,
|
||
drillingSpeed: drillingParams.value.drillingDuration,
|
||
currentDepth: currentDepth,
|
||
targetDepth: drillingParams.value.depth,
|
||
progress: progress,
|
||
pipeRadius: pipe.options.radius,
|
||
status: status
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 启动钻进信息实时更新
|
||
*/
|
||
const startDrillingInfoUpdate = (pipe: ParametricPipe) => {
|
||
const updateInterval = setInterval(() => {
|
||
if (!pipes.includes(pipe)) {
|
||
clearInterval(updateInterval);
|
||
return;
|
||
}
|
||
|
||
const modal = pipeModals.get(pipe);
|
||
if (modal) {
|
||
updateDrillingModalInfo(pipe, modal);
|
||
}
|
||
|
||
if (pipe.options.progress >= 1) {
|
||
clearInterval(updateInterval);
|
||
if (modal) {
|
||
updateDrillingModalInfo(pipe, modal);
|
||
}
|
||
}
|
||
}, 100);
|
||
};
|
||
|
||
/**
|
||
* 开始选择钻进点
|
||
*/
|
||
const startSelectingDrillingPoint = () => {
|
||
if (!viewer) return;
|
||
|
||
isSelectingDrillingPoint.value = true;
|
||
const canvas = viewer.renderer!.domElement;
|
||
canvas.addEventListener('click', onCanvasClick, {once: true});
|
||
canvas.style.cursor = 'crosshair';
|
||
};
|
||
|
||
/**
|
||
* 处理画布点击事件
|
||
*/
|
||
const onCanvasClick = (event: MouseEvent) => {
|
||
if (!viewer || !isSelectingDrillingPoint.value) return;
|
||
|
||
const canvas = viewer.renderer!.domElement;
|
||
const rect = canvas.getBoundingClientRect();
|
||
|
||
const mouse = new THREE.Vector2();
|
||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
|
||
const raycaster = new THREE.Raycaster();
|
||
raycaster.setFromCamera(mouse, viewer.camera);
|
||
|
||
const intersects = raycaster.intersectObjects(viewer.scene.children, true);
|
||
|
||
if (intersects.length > 0) {
|
||
const intersection = intersects[0];
|
||
if (intersection) {
|
||
createDrillingPipe(intersection.point);
|
||
}
|
||
}
|
||
|
||
canvas.style.cursor = 'default';
|
||
isSelectingDrillingPoint.value = false;
|
||
};
|
||
|
||
/**
|
||
* 创建钻进管道
|
||
*/
|
||
const createDrillingPipe = (startPoint: THREE.Vector3) => {
|
||
if (!viewer) return;
|
||
|
||
const points: THREE.Vector3[] = [];
|
||
const topPoint = new THREE.Vector3(startPoint.x, startPoint.y, startPoint.z);
|
||
const bottomPoint = new THREE.Vector3(
|
||
startPoint.x,
|
||
startPoint.y - drillingParams.value.depth,
|
||
startPoint.z
|
||
);
|
||
|
||
const segments = 10;
|
||
for (let i = 0; i <= segments; i++) {
|
||
const t = i / segments;
|
||
const y = topPoint.y + (bottomPoint.y - topPoint.y) * t;
|
||
points.push(new THREE.Vector3(startPoint.x, y, startPoint.z));
|
||
}
|
||
|
||
const pipeMaterial = new THREE.MeshBasicNodeMaterial({
|
||
color: 0x1b1b1b,
|
||
side: THREE.DoubleSide,
|
||
});
|
||
|
||
const newPipe = new ParametricPipe(viewer, {
|
||
points: points,
|
||
radius: drillingParams.value.pipeRadius,
|
||
radialSegments: 16,
|
||
cornerRadius: 0.1,
|
||
cornerSplit: 5,
|
||
progress: 0,
|
||
material: pipeMaterial,
|
||
capStart: false,
|
||
capEnd: false,
|
||
enableDrillingRobot: true,
|
||
robotColor: 0xff0000,
|
||
});
|
||
viewer.scene.add(newPipe);
|
||
pipeCounter++;
|
||
newPipe.name = `钻进管道_${pipeCounter}`;
|
||
|
||
newPipe.setCollisionProxyType(true); // true = 静态
|
||
newPipe.addCollisionTarget(bus.getRockSample(), CSGOperationType.HOLLOW_SUBTRACTION);
|
||
|
||
// 存储 ParametricPipe 实例到 mesh.userData,方便其他组件访问
|
||
newPipe.userData.pipeInstance = newPipe;
|
||
|
||
pipes.push(newPipe);
|
||
|
||
const duration = drillingParams.value.drillingDuration;
|
||
newPipe.setDuration(duration);
|
||
newPipe.startAnimation();
|
||
|
||
startDrillingInfoUpdate(newPipe);
|
||
bus.triggerSceneTreeUpdate()
|
||
console.log(newPipe)
|
||
};
|
||
|
||
/**
|
||
* 清除所有管道
|
||
*/
|
||
const clearAllPipes = () => {
|
||
if (pipes.length > 0) {
|
||
pipes.forEach(pipe => {
|
||
const modal = pipeModals.get(pipe);
|
||
if (modal) {
|
||
modal.hide();
|
||
const modalObject = modal.getCssObject();
|
||
if (modalObject.parent) {
|
||
modalObject.parent.remove(modalObject);
|
||
}
|
||
}
|
||
pipe.dispose();
|
||
});
|
||
|
||
pipes = [];
|
||
pipeModals.clear();
|
||
pipeCounter = 0;
|
||
selectedPipe = null;
|
||
|
||
bus.triggerSceneTreeUpdate()
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
|
||
</style>
|