feat(all): init commit
This commit is contained in:
commit
5d7f479765
17
.claude/settings.local.json
Normal file
17
.claude/settings.local.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm run dev)",
|
||||||
|
"Bash(npx tsc:*)",
|
||||||
|
"Bash(npm run build)",
|
||||||
|
"Bash(pnpm add:*)",
|
||||||
|
"Bash(pnpm exec tsc:*)",
|
||||||
|
"Bash(npx vue-tsc:*)",
|
||||||
|
"Bash(pnpm:*)",
|
||||||
|
"Bash(git mv:*)",
|
||||||
|
"Bash(find e:/Astral/deep-engine -type f -name *iewer*.ts)",
|
||||||
|
"Bash(find e:Astraldeep-engine -name camera-controls* -o -name *CameraControls*)",
|
||||||
|
"Bash(find e:Astraldeep-enginenode_modules -path *camera-controls* -name *.d.ts)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
temp/*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
/nul
|
||||||
|
AGENTS.md
|
||||||
|
/other
|
||||||
|
/CLAUDE.md
|
||||||
|
/packages/docs/.vitepress/
|
||||||
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
网站: jieshi.astraltwin.cn
|
||||||
|
用户名: jieshi
|
||||||
|
密码: jieshi123456
|
||||||
|
|
||||||
|
|
||||||
|
https://www.kdocs.cn/l/cdLe8o8CJ0Ht
|
||||||
|
https://www.kdocs.cn/l/cowqe8VdXrwh
|
||||||
|
|
||||||
|
?ddtk=js
|
||||||
|
js
|
||||||
|
|
||||||
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "deep-engine",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"docs:dev": "pnpm run -C packages/docs docs:dev",
|
||||||
|
"docs:build": "pnpm run -C packages/docs docs:build",
|
||||||
|
"sdk:dev": "pnpm run -C packages/sdk dev",
|
||||||
|
"sdk:build": "pnpm run -C packages/sdk build",
|
||||||
|
"demo:dev": "pnpm run -C packages/demo dev",
|
||||||
|
"demo:build": "pnpm run sdk:build && pnpm run -C packages/demo build"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=23.0.0"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"packageManager": "pnpm@10.29.3"
|
||||||
|
}
|
||||||
25
packages/demo/.gitignore
vendored
Normal file
25
packages/demo/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
public
|
||||||
0
packages/demo/README.md
Normal file
0
packages/demo/README.md
Normal file
13
packages/demo/index.html
Normal file
13
packages/demo/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
packages/demo/package.json
Normal file
33
packages/demo/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@deep/demo",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@deep/engine": "workspace:*",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||||
|
"disable-devtool": "0.3.9",
|
||||||
|
"emittery": "^2.0.0",
|
||||||
|
"naive-ui": "^2.44.1",
|
||||||
|
"simplex-noise": "^4.0.3",
|
||||||
|
"three": "catalog:",
|
||||||
|
"vue": "catalog:",
|
||||||
|
"vue-router": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"@types/three": "catalog:",
|
||||||
|
"@vitejs/plugin-vue": "catalog:",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"unplugin-turbo-console": "^2.3.0",
|
||||||
|
"vite": "catalog:",
|
||||||
|
"vite-plugin-static-copy": "^4.0.1",
|
||||||
|
"vue-tsc": "^3.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/demo/src/App.vue
Normal file
13
packages/demo/src/App.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {NMessageProvider} from 'naive-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-message-provider>
|
||||||
|
<router-view/>
|
||||||
|
</n-message-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
6
packages/demo/src/assets/vue.svg
Normal file
6
packages/demo/src/assets/vue.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img"
|
||||||
|
class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198">
|
||||||
|
<path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path>
|
||||||
|
<path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path>
|
||||||
|
<path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 474 B |
211
packages/demo/src/components/SceneTree.vue
Normal file
211
packages/demo/src/components/SceneTree.vue
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scene-tree">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchKeyword"
|
||||||
|
clearable
|
||||||
|
placeholder="输入模型名称搜索"
|
||||||
|
style="margin-bottom: 10px;"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
<n-tree
|
||||||
|
ref="treeInstRef"
|
||||||
|
:data="treeData"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:node-props="nodeProps"
|
||||||
|
:render-label="renderLabel"
|
||||||
|
:selected-keys="selectedKeys"
|
||||||
|
key-field="uuid"
|
||||||
|
style="height: 320px"
|
||||||
|
virtual-scroll
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="tsx" setup>
|
||||||
|
import {onMounted, ref} from 'vue';
|
||||||
|
import {NCheckbox, NInput, NSlider, NSpace, NTree, type TreeOption} from 'naive-ui';
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {type SceneNode, SelectionManagerEvents, Tool} from '@deep/engine';
|
||||||
|
import {BusEvents} from '../hooks/Bus';
|
||||||
|
import type {TreeRenderProps} from "naive-ui/es/tree/src/interface";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const treeData = ref<any[]>([]);
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const selectedKeys = ref<string[]>([]);
|
||||||
|
const treeInstRef = ref(null);
|
||||||
|
|
||||||
|
// 渲染节点标签
|
||||||
|
const renderLabel = (node: TreeRenderProps) => {
|
||||||
|
const option = node.option as unknown as SceneNode
|
||||||
|
return (
|
||||||
|
<NSpace>
|
||||||
|
|
||||||
|
{/* 显示/隐藏控制 */}
|
||||||
|
<NCheckbox
|
||||||
|
v-model:checked={option.visible}
|
||||||
|
size="small"
|
||||||
|
onUpdateChecked={(value) => {
|
||||||
|
handleVisibilityChange(option.uuid, value);
|
||||||
|
}}
|
||||||
|
style={{marginRight: '8px'}}
|
||||||
|
/>
|
||||||
|
{/* 节点名称 */}
|
||||||
|
<span class="node-name" style={{flex: 1}}>{option.displayName}</span>
|
||||||
|
|
||||||
|
{/* 可选择控制 */}
|
||||||
|
{option.type !== "Group" && (
|
||||||
|
<NCheckbox
|
||||||
|
v-model:checked={option.selectable}
|
||||||
|
size="small"
|
||||||
|
onUpdateChecked={(value) => {
|
||||||
|
handleSelectableChange(option.uuid, value);
|
||||||
|
}}
|
||||||
|
style={{margin: '0 8px'}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.opacity !== -1 && (
|
||||||
|
<NSlider
|
||||||
|
v-model:value={option.opacity}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
onUpdateValue={(value) => {
|
||||||
|
handleOpacityChange(option.uuid, value);
|
||||||
|
}}
|
||||||
|
style={{width: '80px'}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</NSpace>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateTreeData();
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
// 监听选择管理器的选择事件
|
||||||
|
if (bus.viewer && bus.viewer.selection) {
|
||||||
|
bus.viewer.selection.on(SelectionManagerEvents.OBJECT_SELECTED, ({data}) => {
|
||||||
|
// 当选择管理器选择了一个物体时,在 tree 中也选择对应的物体
|
||||||
|
if (data) {
|
||||||
|
selectedKeys.value = [data.uuid];
|
||||||
|
console.log('Object selected in selection manager:', data.name);
|
||||||
|
// 滚动到选中的节点
|
||||||
|
if (treeInstRef.value) {
|
||||||
|
treeInstRef.value.scrollTo({key: data.uuid});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听选择管理器的取消选择事件
|
||||||
|
bus.viewer.selection.on(SelectionManagerEvents.OBJECT_UNSELECTED, () => {
|
||||||
|
// 当选择管理器取消选择时,也取消 tree 中的选择
|
||||||
|
selectedKeys.value = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听场景树更新事件
|
||||||
|
bus.on(BusEvents.SCENE_TREE_UPDATED, () => {
|
||||||
|
updateTreeData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTreeData = () => {
|
||||||
|
if (bus.viewer) {
|
||||||
|
treeData.value = bus.viewer.serializeSceneFiltered();
|
||||||
|
console.log(treeData.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (bus.viewer) {
|
||||||
|
const sceneData = bus.viewer.serializeSceneFiltered(searchKeyword.value);
|
||||||
|
treeData.value = sceneData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理显示/隐藏控制变化
|
||||||
|
const handleVisibilityChange = (uuid: string, visible: boolean) => {
|
||||||
|
if (bus.viewer) {
|
||||||
|
const object = bus.viewer.scene.getObjectByProperty('uuid', uuid);
|
||||||
|
if (object) {
|
||||||
|
object.visible = visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理可选择控制变化
|
||||||
|
const handleSelectableChange = (uuid: string, selectable: boolean) => {
|
||||||
|
if (bus.viewer) {
|
||||||
|
const object = bus.viewer.scene.getObjectByProperty('uuid', uuid);
|
||||||
|
if (object) {
|
||||||
|
Tool.setSelectionExclude(object, selectable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理透明度控制变化
|
||||||
|
const handleOpacityChange = (uuid: string, opacity: number) => {
|
||||||
|
if (bus.viewer) {
|
||||||
|
const object = bus.viewer.scene.getObjectByProperty('uuid', uuid);
|
||||||
|
if (object) {
|
||||||
|
Tool.setOpacity(object, opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function nodeProps({option}: { option: TreeOption }) {
|
||||||
|
return {
|
||||||
|
onClick() {
|
||||||
|
// 更新 tree 中的选中节点
|
||||||
|
selectedKeys.value = [option.uuid];
|
||||||
|
|
||||||
|
// 滚动到选中的节点
|
||||||
|
if (treeInstRef.value) {
|
||||||
|
treeInstRef.value.scrollTo({key: option.uuid});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过 UUID 选择对象
|
||||||
|
if (bus.viewer && bus.viewer.selection) {
|
||||||
|
bus.viewer.selection.selectObjectByUuid(option.uuid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
updateTreeData
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scene-tree {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background-color: #aeb0b2;
|
||||||
|
border-right: 1px solid #eaeaea;
|
||||||
|
border-bottom: 1px solid #eaeaea;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整滑块和复选框的大小 */
|
||||||
|
:deep(.n-checkbox) {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-slider) {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
packages/demo/src/components/Toolbar.vue
Normal file
142
packages/demo/src/components/Toolbar.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<ClippingPanel v-if="isClippingPanelVisible"/>
|
||||||
|
<n-space align="center" class="toolbar" horizontal>
|
||||||
|
<!-- <n-checkbox v-model:checked="isPerspectiveActive" :label-style="{ color: 'black' }" label="透视选择"/> -->
|
||||||
|
<!-- <n-button text-color="#000" type="primary" @click="handleBoxSelection">-->
|
||||||
|
<!-- 框选-->
|
||||||
|
<!-- </n-button>-->
|
||||||
|
<!-- <n-select-->
|
||||||
|
<!-- v-model:value="cameraType"-->
|
||||||
|
<!-- :options="cameraOptions"-->
|
||||||
|
<!-- style="width: 120px"-->
|
||||||
|
<!-- @update:value="handleCameraTypeChange"-->
|
||||||
|
<!-- />-->
|
||||||
|
<n-button text-color="#000" type="primary" @click="handleResetCamera">
|
||||||
|
回到原点
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref, watch} from 'vue'
|
||||||
|
import {NButton, NSpace} from 'naive-ui'
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {EventManagerEvents} from '@deep/engine'
|
||||||
|
import ClippingPanel from "../panels/base/ClippingPanel.vue";
|
||||||
|
|
||||||
|
const isPerspectiveActive = ref(false)
|
||||||
|
const isClippingPanelVisible = ref(false)
|
||||||
|
const cameraType = ref<'perspective' | 'orthographic'>('perspective')
|
||||||
|
const cameraOptions = [
|
||||||
|
{label: '透视相机', value: 'perspective'},
|
||||||
|
{label: '正交相机', value: 'orthographic'}
|
||||||
|
]
|
||||||
|
const bus = useBus()
|
||||||
|
let sampleObject: any = null
|
||||||
|
|
||||||
|
const handleResetCamera = () => {
|
||||||
|
const viewer = bus.getViewer()
|
||||||
|
if (viewer && viewer.resetCamera) {
|
||||||
|
viewer.resetCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCameraTypeChange = (value: 'perspective' | 'orthographic') => {
|
||||||
|
const viewer = bus.getViewer()
|
||||||
|
if (viewer && viewer.switchCameraType) {
|
||||||
|
viewer.switchCameraType(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理框选功能切换
|
||||||
|
const handleBoxSelection = () => {
|
||||||
|
const viewer = bus.getViewer()
|
||||||
|
if (viewer && viewer.events) {
|
||||||
|
const eventManager = viewer.events
|
||||||
|
if (eventManager.enableBoxSelection && eventManager.disableBoxSelection) {
|
||||||
|
// 启用框选
|
||||||
|
eventManager.enableBoxSelection()
|
||||||
|
// 添加事件监听器
|
||||||
|
eventManager.on(EventManagerEvents.BOX_SELECTION_MOVE, handleBoxSelectionMove)
|
||||||
|
eventManager.on(EventManagerEvents.BOX_SELECTION_COMPLETE, handleBoxSelectionComplete)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理框选移动事件
|
||||||
|
const handleBoxSelectionMove = (event: any) => {
|
||||||
|
const {objects, collection} = event.data
|
||||||
|
console.log('Box selection moving:', objects.length, 'objects selected')
|
||||||
|
console.log('Full collection:', collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理框选完成事件
|
||||||
|
const handleBoxSelectionComplete = (event: any) => {
|
||||||
|
const {objects} = event.data
|
||||||
|
console.log('Box selection complete:', objects.length, 'objects selected')
|
||||||
|
objects.forEach((obj: any) => {
|
||||||
|
console.log('- Selected:', obj.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 框选结束后自动取消框选
|
||||||
|
const viewer = bus.getViewer()
|
||||||
|
if (viewer && viewer.events) {
|
||||||
|
const eventManager = viewer.events
|
||||||
|
eventManager.disableBoxSelection()
|
||||||
|
// 移除事件监听器
|
||||||
|
eventManager.off(EventManagerEvents.BOX_SELECTION_MOVE, handleBoxSelectionMove)
|
||||||
|
eventManager.off(EventManagerEvents.BOX_SELECTION_COMPLETE, handleBoxSelectionComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时查找试样对象并缓存
|
||||||
|
const initSampleObject = () => {
|
||||||
|
const viewer = bus.getViewer()
|
||||||
|
if (viewer && viewer.scene && !sampleObject) {
|
||||||
|
viewer.scene.traverse((obj) => {
|
||||||
|
if (obj.name === '岩样') {
|
||||||
|
sampleObject = obj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听透视选择复选框的变化
|
||||||
|
watch(isPerspectiveActive, (newValue) => {
|
||||||
|
// 确保试样对象已缓存
|
||||||
|
initSampleObject()
|
||||||
|
const viewer = bus.getViewer()
|
||||||
|
|
||||||
|
// 根据复选框状态添加或移除过滤列表
|
||||||
|
if (sampleObject && viewer && viewer.events) {
|
||||||
|
if (newValue) {
|
||||||
|
// 透视选择激活时,将对象添加到过滤列表(不可被选中)
|
||||||
|
viewer.events.addToFilterList(sampleObject)
|
||||||
|
} else {
|
||||||
|
// 透视选择关闭时,从过滤列表中移除(可被选中)
|
||||||
|
viewer.events.removeFromFilterList(sampleObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {immediate: true})
|
||||||
|
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化试样对象
|
||||||
|
initSampleObject()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
background-color: #aeb0b2;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
333
packages/demo/src/components/htmlPanel/DrillingModal.ts
Normal file
333
packages/demo/src/components/htmlPanel/DrillingModal.ts
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钻进信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IDrillingData {
|
||||||
|
/** 管道名称 */
|
||||||
|
pipeName: string;
|
||||||
|
/** 当前钻进速率 */
|
||||||
|
drillingSpeed: number;
|
||||||
|
/** 当前深度 (m) */
|
||||||
|
currentDepth: number;
|
||||||
|
/** 目标深度 (m) */
|
||||||
|
targetDepth: number;
|
||||||
|
/** 进度百分比 (0-100) */
|
||||||
|
progress: number;
|
||||||
|
/** 管道半径 (m) */
|
||||||
|
pipeRadius: number;
|
||||||
|
/** 钻进状态 */
|
||||||
|
status: 'drilling' | 'completed' | 'stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钻进信息面板类
|
||||||
|
*/
|
||||||
|
export class DrillingModal extends HtmlPanel {
|
||||||
|
private drillingData: IDrillingData = {
|
||||||
|
pipeName: '',
|
||||||
|
drillingSpeed: 0,
|
||||||
|
currentDepth: 0,
|
||||||
|
targetDepth: 0,
|
||||||
|
progress: 0,
|
||||||
|
pipeRadius: 0,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private isHtmlInitialized = false;
|
||||||
|
private readonly elementIds = {
|
||||||
|
deleteButton: '',
|
||||||
|
pipeName: '',
|
||||||
|
statusText: '',
|
||||||
|
statusBadge: '',
|
||||||
|
drillingSpeed: '',
|
||||||
|
currentDepth: '',
|
||||||
|
targetDepth: '',
|
||||||
|
progressText: '',
|
||||||
|
progressBar: '',
|
||||||
|
pipeRadius: '',
|
||||||
|
contentContainer: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.initializeElementIds();
|
||||||
|
this.createHtmlStructure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新钻进数据
|
||||||
|
* @param data 新的钻进数据
|
||||||
|
*/
|
||||||
|
updateData(data: Partial<IDrillingData>): void {
|
||||||
|
this.drillingData = {
|
||||||
|
...this.drillingData,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
this.updateDomElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化元素 ID
|
||||||
|
*/
|
||||||
|
private initializeElementIds(): void {
|
||||||
|
const cssObject = this.getCssObject();
|
||||||
|
const baseId = cssObject.uuid;
|
||||||
|
|
||||||
|
this.elementIds.deleteButton = `${baseId}-delete`;
|
||||||
|
this.elementIds.pipeName = `${baseId}-pipeName`;
|
||||||
|
this.elementIds.statusText = `${baseId}-statusText`;
|
||||||
|
this.elementIds.statusBadge = `${baseId}-statusBadge`;
|
||||||
|
this.elementIds.drillingSpeed = `${baseId}-drillingSpeed`;
|
||||||
|
this.elementIds.currentDepth = `${baseId}-currentDepth`;
|
||||||
|
this.elementIds.targetDepth = `${baseId}-targetDepth`;
|
||||||
|
this.elementIds.progressText = `${baseId}-progressText`;
|
||||||
|
this.elementIds.progressBar = `${baseId}-progressBar`;
|
||||||
|
this.elementIds.pipeRadius = `${baseId}-pipeRadius`;
|
||||||
|
this.elementIds.contentContainer = `${baseId}-contentContainer`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 HTML 结构(只执行一次)
|
||||||
|
*/
|
||||||
|
private createHtmlStructure(): void {
|
||||||
|
if (this.isHtmlInitialized) return;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 280px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="${this.elementIds.deleteButton}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 id="${this.elementIds.pipeName}" style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
🔧 钻进信息
|
||||||
|
</h3>
|
||||||
|
<span id="${this.elementIds.statusBadge}" style="
|
||||||
|
background: #FF9800;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
<span id="${this.elementIds.statusText}">已停止</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="${this.elementIds.contentContainer}" style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 钻进速率 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⚡ 钻进速率</span>
|
||||||
|
</div>
|
||||||
|
<div id="${this.elementIds.drillingSpeed}" style="font-size: 24px; font-weight: 700; color: #FFD700;">
|
||||||
|
0.000
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 深度信息 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📏 当前深度</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 20px; font-weight: 600;">
|
||||||
|
<span id="${this.elementIds.currentDepth}">0.00</span> / <span id="${this.elementIds.targetDepth}">0.00</span> <span style="font-size: 14px; opacity: 0.8;">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 完成进度</span>
|
||||||
|
<span id="${this.elementIds.progressText}" style="font-size: 14px; font-weight: 600;">0.0%</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
">
|
||||||
|
<div id="${this.elementIds.progressBar}" style="
|
||||||
|
background: #FF9800;
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 管道半径 -->
|
||||||
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
|
||||||
|
<div style="display: flex; justify-content: space-between; font-size: 13px;">
|
||||||
|
<span style="opacity: 0.9;">🔵 管道半径</span>
|
||||||
|
<span id="${this.elementIds.pipeRadius}" style="font-weight: 600;">0.00 m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.updateContent(content);
|
||||||
|
this.attachDeleteButtonListener();
|
||||||
|
this.isHtmlInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
// 隐藏弹窗
|
||||||
|
this.hide();
|
||||||
|
|
||||||
|
// 调用删除回调
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.drillingData.status) {
|
||||||
|
case 'drilling':
|
||||||
|
return {text: '钻进中', color: '#4CAF50'};
|
||||||
|
case 'completed':
|
||||||
|
return {text: '已完成', color: '#2196F3'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 DOM 操作更新元素值
|
||||||
|
*/
|
||||||
|
private updateDomElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.drillingData.status === 'drilling' ? '#4CAF50' :
|
||||||
|
this.drillingData.status === 'completed' ? '#2196F3' : '#FF9800';
|
||||||
|
|
||||||
|
// 更新管道名称
|
||||||
|
const pipeNameEl = document.getElementById(this.elementIds.pipeName);
|
||||||
|
if (pipeNameEl) {
|
||||||
|
pipeNameEl.textContent = `🔧 ${this.drillingData.pipeName || '钻进信息'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态文本
|
||||||
|
const statusTextEl = document.getElementById(this.elementIds.statusText);
|
||||||
|
if (statusTextEl) {
|
||||||
|
statusTextEl.textContent = statusInfo.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态徽章颜色
|
||||||
|
const statusBadgeEl = document.getElementById(this.elementIds.statusBadge);
|
||||||
|
if (statusBadgeEl) {
|
||||||
|
statusBadgeEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新钻进速率
|
||||||
|
const drillingSpeedEl = document.getElementById(this.elementIds.drillingSpeed);
|
||||||
|
if (drillingSpeedEl) {
|
||||||
|
drillingSpeedEl.textContent = this.drillingData.drillingSpeed.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前深度
|
||||||
|
const currentDepthEl = document.getElementById(this.elementIds.currentDepth);
|
||||||
|
if (currentDepthEl) {
|
||||||
|
currentDepthEl.textContent = this.drillingData.currentDepth.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新目标深度
|
||||||
|
const targetDepthEl = document.getElementById(this.elementIds.targetDepth);
|
||||||
|
if (targetDepthEl) {
|
||||||
|
targetDepthEl.textContent = this.drillingData.targetDepth.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度文本
|
||||||
|
const progressTextEl = document.getElementById(this.elementIds.progressText);
|
||||||
|
if (progressTextEl) {
|
||||||
|
progressTextEl.textContent = `${this.drillingData.progress.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const progressBarEl = document.getElementById(this.elementIds.progressBar);
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.style.width = `${this.drillingData.progress}%`;
|
||||||
|
progressBarEl.style.background = progressBarColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新管道半径
|
||||||
|
const pipeRadiusEl = document.getElementById(this.elementIds.pipeRadius);
|
||||||
|
if (pipeRadiusEl) {
|
||||||
|
pipeRadiusEl.textContent = `${this.drillingData.pipeRadius.toFixed(2)} m`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件监听器
|
||||||
|
*/
|
||||||
|
private attachDeleteButtonListener(): void {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteButton = document.getElementById(this.elementIds.deleteButton);
|
||||||
|
if (deleteButton) {
|
||||||
|
deleteButton.onclick = () => {
|
||||||
|
this.handleDelete();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
packages/demo/src/components/htmlPanel/PrintModelModal.ts
Normal file
114
packages/demo/src/components/htmlPanel/PrintModelModal.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D打印模型面板类
|
||||||
|
*/
|
||||||
|
export class PrintModelModal extends HtmlPanel {
|
||||||
|
private modelData: {
|
||||||
|
components: Array<{
|
||||||
|
name: string;
|
||||||
|
meshSize: number;
|
||||||
|
ratio: number;
|
||||||
|
physicalProperties: {
|
||||||
|
uniaxialCompressiveStrength: number;
|
||||||
|
tensileStrength: number;
|
||||||
|
elasticModulus: number;
|
||||||
|
brittlenessIndex: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
} = {
|
||||||
|
components: []
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS3D) {
|
||||||
|
super(type);
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新3D打印模型数据
|
||||||
|
* @param data 新的模型数据
|
||||||
|
*/
|
||||||
|
updateData(data: {
|
||||||
|
components: Array<{
|
||||||
|
name: string;
|
||||||
|
meshSize: number;
|
||||||
|
ratio: number;
|
||||||
|
physicalProperties: {
|
||||||
|
uniaxialCompressiveStrength: number;
|
||||||
|
tensileStrength: number;
|
||||||
|
elasticModulus: number;
|
||||||
|
brittlenessIndex: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}): void {
|
||||||
|
this.modelData = data;
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据更新弹窗内容
|
||||||
|
*/
|
||||||
|
private updateContentFromData(): void {
|
||||||
|
// 使用 cssObject 的 uuid 作为唯一标识 作为 关闭按键的id
|
||||||
|
const cssObject = this.getCssObject();
|
||||||
|
const closeButtonId = cssObject.uuid;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="font-family: Arial, sans-serif; position: relative;">
|
||||||
|
<button id="${closeButtonId}" style="position: absolute; top: 10px; right: 10px; background: #ff4444; color: white; border: none; border-radius: 4px; width: 30px; height: 30px; cursor: pointer; font-size: 18px; line-height: 1; pointer-events: auto;">×</button>
|
||||||
|
<h2 style="margin-top: 0; color: #333;">3D打印模型数据</h2>
|
||||||
|
${this.modelData.components.length > 0 ?
|
||||||
|
this.modelData.components.map((component, index) => `
|
||||||
|
<div style="margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 4px;">
|
||||||
|
<h3 style="margin-top: 0; color: #555;">组分 ${index + 1}: ${component.name}</h3>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>目数:</strong> ${component.meshSize}
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>占比:</strong> ${component.ratio}%
|
||||||
|
</div>
|
||||||
|
<h4 style="margin-top: 10px; margin-bottom: 10px; color: #666;">物理属性</h4>
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<strong>单轴抗压强度:</strong> ${component.physicalProperties.uniaxialCompressiveStrength} MPa
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<strong>抗拉强度:</strong> ${component.physicalProperties.tensileStrength} MPa
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<strong>弹性模量:</strong> ${component.physicalProperties.elasticModulus} GPa
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<strong>脆性指数:</strong> ${component.physicalProperties.brittlenessIndex}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
: '<p style="color: #999;">暂无模型数据</p>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
this.attachCloseButtonListener(closeButtonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加关闭按钮事件监听器
|
||||||
|
* @param buttonId 按钮ID
|
||||||
|
*/
|
||||||
|
private attachCloseButtonListener(buttonId: string): void {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const closeButton = document.getElementById(buttonId);
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
this.hide();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
219
packages/demo/src/components/htmlPanel/RuptureEventModal.ts
Normal file
219
packages/demo/src/components/htmlPanel/RuptureEventModal.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 破裂事件弹窗类
|
||||||
|
*/
|
||||||
|
export class RuptureEventModal extends HtmlPanel {
|
||||||
|
private eventData: {
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
energy: number;
|
||||||
|
time: number;
|
||||||
|
} = {
|
||||||
|
position: {x: 0, y: 0, z: 0},
|
||||||
|
energy: 0,
|
||||||
|
time: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新破裂事件数据
|
||||||
|
* @param data 新的破裂事件数据
|
||||||
|
*/
|
||||||
|
updateData(data: {
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
energy: number;
|
||||||
|
time: number;
|
||||||
|
}): void {
|
||||||
|
this.eventData = data;
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调
|
||||||
|
*/
|
||||||
|
onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取根面板元素的 id,用于外部动态修改 transform
|
||||||
|
*/
|
||||||
|
getPanelElementId(): string {
|
||||||
|
return `${this.getCssObject().uuid}-panel`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据能量值获取背景渐变色
|
||||||
|
*/
|
||||||
|
private getBackgroundGradient(): string {
|
||||||
|
if (this.eventData.energy > 50) {
|
||||||
|
// 高能量:深红色渐变
|
||||||
|
return 'linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%)';
|
||||||
|
} else if (this.eventData.energy > 20) {
|
||||||
|
// 中等能量:橙色渐变
|
||||||
|
return 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)';
|
||||||
|
} else {
|
||||||
|
// 低能量:黄色渐变
|
||||||
|
return 'linear-gradient(135deg, #FFC107 0%, #FFA000 100%)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据更新弹窗内容
|
||||||
|
*/
|
||||||
|
private updateContentFromData(): void {
|
||||||
|
const timeStr = new Date(this.eventData.time).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 cssObject 的 uuid 作为唯一标识
|
||||||
|
const cssObject = this.getCssObject();
|
||||||
|
const closeButtonId = cssObject.uuid;
|
||||||
|
|
||||||
|
// 根据能量大小确定颜色
|
||||||
|
const energyColor = this.eventData.energy > 50 ? '#FF5722' :
|
||||||
|
this.eventData.energy > 20 ? '#FF9800' : '#FFC107';
|
||||||
|
|
||||||
|
// 根据能量值获取背景渐变
|
||||||
|
const backgroundGradient = this.getBackgroundGradient();
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div id="${closeButtonId}-panel" style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: ${backgroundGradient};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
transform: translate(-50%, -80%);
|
||||||
|
">
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<button id="${closeButtonId}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
⚠️ 破裂事件
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 能量信息 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⚡ 能量</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 24px; font-weight: 700; color: ${energyColor};">
|
||||||
|
${this.eventData.energy.toFixed(2)} <span style="font-size: 16px; opacity: 0.9;">kJ</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">🕐 发生时间</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 14px; font-weight: 500; opacity: 0.95;">
|
||||||
|
${timeStr}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 位置信息 -->
|
||||||
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📍 位置坐标</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.6;">
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span style="opacity: 0.9;">X:</span>
|
||||||
|
<span style="font-weight: 600;">${this.eventData.position.x.toFixed(3)} m</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span style="opacity: 0.9;">Y:</span>
|
||||||
|
<span style="font-weight: 600;">${this.eventData.position.y.toFixed(3)} m</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span style="opacity: 0.9;">Z:</span>
|
||||||
|
<span style="font-weight: 600;">${this.eventData.position.z.toFixed(3)} m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.updateContent(content);
|
||||||
|
this.attachCloseButtonListener(closeButtonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加关闭按钮事件监听器
|
||||||
|
* @param buttonId 按钮ID
|
||||||
|
*/
|
||||||
|
private attachCloseButtonListener(buttonId: string): void {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const closeButton = document.getElementById(buttonId);
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
packages/demo/src/components/htmlPanel/SensorModal.ts
Normal file
100
packages/demo/src/components/htmlPanel/SensorModal.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传感器面板类
|
||||||
|
*/
|
||||||
|
export class SensorModal extends HtmlPanel {
|
||||||
|
private sensorData: {
|
||||||
|
sensors: Array<{
|
||||||
|
type: string;
|
||||||
|
channel: number;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
} = {
|
||||||
|
sensors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS3D) {
|
||||||
|
super(type);
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新传感器数据
|
||||||
|
* @param data 新的传感器数据
|
||||||
|
*/
|
||||||
|
updateData(data: {
|
||||||
|
sensors: Array<{
|
||||||
|
type: string;
|
||||||
|
channel: number;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}): void {
|
||||||
|
this.sensorData = data;
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据更新弹窗内容
|
||||||
|
*/
|
||||||
|
private updateContentFromData(): void {
|
||||||
|
// 使用 cssObject 的 uuid 作为唯一标识 作为 关闭按键的id
|
||||||
|
const cssObject = this.getCssObject();
|
||||||
|
const closeButtonId = cssObject.uuid;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="font-family: Arial, sans-serif; position: relative;">
|
||||||
|
<button id="${closeButtonId}" style="position: absolute; top: 40px; right: 40px; background: #ff4444; color: white; border: none; border-radius: 20px; width: 120px; height: 120px; cursor: pointer; font-size: 80px; line-height: 1; pointer-events: auto;">×</button>
|
||||||
|
<h2 style="margin-top: 0; color: #333; font-size: 120px;">传感器数据</h2>
|
||||||
|
${this.sensorData.sensors.length > 0 ?
|
||||||
|
this.sensorData.sensors.map((sensor, index) => `
|
||||||
|
<div style="margin-bottom: 75px; padding: 60px; border: 5px solid #e0e0e0; border-radius: 20px;">
|
||||||
|
<h3 style="margin-top: 0; color: #555; font-size: 100px;">传感器 ${index + 1}</h3>
|
||||||
|
<div style="margin-bottom: 40px; font-size: 80px;">
|
||||||
|
<strong>类型:</strong> ${sensor.type}
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 40px; font-size: 80px;">
|
||||||
|
<strong>通道:</strong> ${sensor.channel}
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 40px; font-size: 80px;">
|
||||||
|
<strong>位置:</strong> (${sensor.position.x}, ${sensor.position.y}, ${sensor.position.z})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
: '<p style="color: #999; font-size: 80px;">暂无传感器数据</p>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
this.attachCloseButtonListener(closeButtonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加关闭按钮事件监听器
|
||||||
|
* @param buttonId 按钮ID
|
||||||
|
*/
|
||||||
|
private attachCloseButtonListener(buttonId: string): void {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const closeButton = document.getElementById(buttonId);
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
this.hide();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
packages/demo/src/components/htmlPanel/SingleSensorModal.ts
Normal file
123
packages/demo/src/components/htmlPanel/SingleSensorModal.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个传感器弹窗类
|
||||||
|
*/
|
||||||
|
export class SingleSensorModal extends HtmlPanel {
|
||||||
|
private sensorData: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
channel: number;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
channel: 0,
|
||||||
|
position: {x: 0, y: 0, z: 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新传感器数据
|
||||||
|
* @param data 新的传感器数据
|
||||||
|
*/
|
||||||
|
updateData(data: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
channel: number;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}): void {
|
||||||
|
this.sensorData = data;
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调
|
||||||
|
*/
|
||||||
|
onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据更新弹窗内容
|
||||||
|
*/
|
||||||
|
private updateContentFromData(): void {
|
||||||
|
// 使用 cssObject 的 uuid 作为唯一标识 作为 关闭按键的id
|
||||||
|
const cssObject = this.getCssObject();
|
||||||
|
const closeButtonId = cssObject.uuid;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="font-family: Arial, sans-serif; min-width: 280px; background: rgba(255, 255, 255, 0.95); padding: 16px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); position: relative;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||||
|
<h3 style="margin: 0; color: #333; font-size: 16px;">${this.sensorData.name || '传感器信息'}</h3>
|
||||||
|
<button
|
||||||
|
id="${closeButtonId}"
|
||||||
|
style="background: none; border: none; color: #999; font-size: 20px; cursor: pointer; padding: 0; width: 24px; height: 24px; line-height: 20px; pointer-events: auto;"
|
||||||
|
title="关闭"
|
||||||
|
>×</button>
|
||||||
|
</div>
|
||||||
|
${this.sensorData.type ?
|
||||||
|
`
|
||||||
|
<div style="border-top: 1px solid #e0e0e0; padding-top: 12px;">
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<div style="color: #666; font-size: 12px; margin-bottom: 4px;">传感器类型</div>
|
||||||
|
<div style="color: #333; font-size: 14px; font-weight: 500;">${this.sensorData.type}</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<div style="color: #666; font-size: 12px; margin-bottom: 4px;">传感器通道</div>
|
||||||
|
<div style="color: #333; font-size: 14px; font-weight: 500;">通道 ${this.sensorData.channel}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="color: #666; font-size: 12px; margin-bottom: 4px;">传感器位置</div>
|
||||||
|
<div style="color: #333; font-size: 13px; font-family: monospace;">
|
||||||
|
X: ${this.sensorData.position.x.toFixed(3)}<br/>
|
||||||
|
Y: ${this.sensorData.position.y.toFixed(3)}<br/>
|
||||||
|
Z: ${this.sensorData.position.z.toFixed(3)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: '<p style="color: #999; margin: 0;">暂无传感器数据</p>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
this.attachCloseButtonListener(closeButtonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加关闭按钮事件监听器
|
||||||
|
* @param buttonId 按钮ID
|
||||||
|
*/
|
||||||
|
private attachCloseButtonListener(buttonId: string): void {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const closeButton = document.getElementById(buttonId);
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
packages/demo/src/components/htmlPanel/TunnelModal.ts
Normal file
103
packages/demo/src/components/htmlPanel/TunnelModal.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隧道面板类
|
||||||
|
*/
|
||||||
|
export class TunnelModal extends HtmlPanel {
|
||||||
|
private tunnelData: {
|
||||||
|
type: string;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
depth: number;
|
||||||
|
radius: number;
|
||||||
|
} = {
|
||||||
|
type: '',
|
||||||
|
position: {x: 0, y: 0, z: 0},
|
||||||
|
depth: 0,
|
||||||
|
radius: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS3D) {
|
||||||
|
super(type);
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新隧道数据
|
||||||
|
* @param data 新的隧道数据
|
||||||
|
*/
|
||||||
|
updateData(data: {
|
||||||
|
type: string;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
depth: number;
|
||||||
|
radius: number;
|
||||||
|
}): void {
|
||||||
|
this.tunnelData = data;
|
||||||
|
this.updateContentFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据更新弹窗内容
|
||||||
|
*/
|
||||||
|
private updateContentFromData(): void {
|
||||||
|
// 使用 cssObject 的 uuid 作为唯一标识 作为 关闭按键的id
|
||||||
|
const cssObject = this.getCssObject();
|
||||||
|
const closeButtonId = cssObject.uuid;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="font-family: Arial, sans-serif; position: relative;">
|
||||||
|
<button id="${closeButtonId}" style="position: absolute; top: 10px; right: 10px; background: #ff4444; color: white; border: none; border-radius: 4px; width: 30px; height: 30px; cursor: pointer; font-size: 18px; line-height: 1; pointer-events: auto;">×</button>
|
||||||
|
<h2 style="margin-top: 0; color: #333;">隧道数据</h2>
|
||||||
|
${this.tunnelData.type ?
|
||||||
|
`
|
||||||
|
<div style="margin-bottom: 15px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 4px;">
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>隧道类型:</strong> ${this.tunnelData.type}
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>位置:</strong> (${this.tunnelData.position.x}, ${this.tunnelData.position.y}, ${this.tunnelData.position.z})
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>深度:</strong> ${this.tunnelData.depth} m
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>半径:</strong> ${this.tunnelData.radius} m
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: '<p style="color: #999;">暂无隧道数据</p>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
this.attachCloseButtonListener(closeButtonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加关闭按钮事件监听器
|
||||||
|
* @param buttonId 按钮ID
|
||||||
|
*/
|
||||||
|
private attachCloseButtonListener(buttonId: string): void {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const closeButton = document.getElementById(buttonId);
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
this.hide();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/demo/src/components/htmlPanel/index.ts
Normal file
6
packages/demo/src/components/htmlPanel/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './PrintModelModal';
|
||||||
|
export * from './SensorModal';
|
||||||
|
export * from './TunnelModal';
|
||||||
|
export * from './DrillingModal';
|
||||||
|
export * from './SingleSensorModal';
|
||||||
|
export * from './RuptureEventModal';
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="disasterFormation" title="灾害孕育">
|
||||||
|
<n-collapse>
|
||||||
|
<GeothermalCasingDamageDisasterPanel/>
|
||||||
|
<GeothermalWellboreInstabilityDisasterPanel/>
|
||||||
|
<ThermalBreakthroughDisasterPanel/>
|
||||||
|
</n-collapse>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NCollapse, NCollapseItem} from 'naive-ui';
|
||||||
|
import GeothermalCasingDamageDisasterPanel
|
||||||
|
from "@/disasterFormationPanel/GeothermalScene/GeothermalCasingDamageDisasterPanel.vue";
|
||||||
|
import GeothermalWellboreInstabilityDisasterPanel
|
||||||
|
from "@/disasterFormationPanel/GeothermalScene/GeothermalWellboreInstabilityDisasterPanel.vue";
|
||||||
|
import ThermalBreakthroughDisasterPanel
|
||||||
|
from "@/disasterFormationPanel/GeothermalScene/ThermalBreakthroughDisasterPanel.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="casingDamageDisaster" title="套管损坏灾害">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-space>
|
||||||
|
<n-button :disabled="hasDamage" type="primary" @click="applyDamage">
|
||||||
|
生成套管损害模型
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasDamage" type="error" @click="clearDamage">
|
||||||
|
移除套管损害模型
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {ParametricCylinder} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasDamage = ref(false);
|
||||||
|
|
||||||
|
let damageCylinder: ParametricCylinder | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用损坏效果 - 生成套管损害圆柱体
|
||||||
|
*/
|
||||||
|
const applyDamage = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const position = {x: 0, y: 0.5, z: 0};
|
||||||
|
|
||||||
|
const cylinder = new ParametricCylinder({
|
||||||
|
radiusTop: 0.21,
|
||||||
|
radiusBottom: 0.21,
|
||||||
|
height: 0.2,
|
||||||
|
//------ 圆柱上下不闭合 开始------
|
||||||
|
openEnded: true,
|
||||||
|
//------ 圆柱上下不闭合 结束------
|
||||||
|
//------ 圆柱细分优化 开始------
|
||||||
|
radialSegments: 96,
|
||||||
|
//------ 圆柱细分优化 结束------
|
||||||
|
material: new THREE.MeshBasicMaterial({
|
||||||
|
color: '#ff0000',
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
cylinder.renderOrder = 1;
|
||||||
|
cylinder.position.set(position.x, position.y, position.z);
|
||||||
|
viewer!.scene.add(cylinder);
|
||||||
|
|
||||||
|
damageCylinder = cylinder;
|
||||||
|
hasDamage.value = true;
|
||||||
|
|
||||||
|
console.log('已生成套管损害圆柱体模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除损坏
|
||||||
|
*/
|
||||||
|
const clearDamage = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
if (damageCylinder) {
|
||||||
|
viewer!.scene.remove(damageCylinder);
|
||||||
|
damageCylinder.dispose();
|
||||||
|
damageCylinder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDamage.value = false;
|
||||||
|
console.log('已清除套管损害模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearDamage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="wellboreInstabilityDisaster" title="井壁失稳灾害">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="hasInstability"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="generateInstability">
|
||||||
|
生成井壁失稳灾害模型
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!hasInstability"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="removeInstability">
|
||||||
|
移除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {ParametricCylinder} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasInstability = ref(false);
|
||||||
|
|
||||||
|
let instabilityCylinder: ParametricCylinder | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成井壁失稳灾害模型
|
||||||
|
*/
|
||||||
|
const generateInstability = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const position = {x: 1.5019181086420077, y: 1.1203964954097632, z: 1.5};
|
||||||
|
|
||||||
|
const cylinder = new ParametricCylinder({
|
||||||
|
radiusTop: 0.21,
|
||||||
|
radiusBottom: 0.21,
|
||||||
|
height: 0.2,
|
||||||
|
//------ 圆柱上下不闭合 开始------
|
||||||
|
openEnded: true,
|
||||||
|
//------ 圆柱上下不闭合 结束------
|
||||||
|
//------ 圆柱细分优化 开始------
|
||||||
|
radialSegments: 96,
|
||||||
|
//------ 圆柱细分优化 结束------
|
||||||
|
material: new THREE.MeshBasicMaterial({
|
||||||
|
color: '#ffff00',
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
cylinder.renderOrder = 1;
|
||||||
|
cylinder.name = 'WellboreInstability_1';
|
||||||
|
cylinder.position.set(position.x, position.y, position.z);
|
||||||
|
viewer!.scene.add(cylinder);
|
||||||
|
|
||||||
|
instabilityCylinder = cylinder;
|
||||||
|
hasInstability.value = true;
|
||||||
|
console.log('生成井壁失稳灾害模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除井壁失稳灾害模型
|
||||||
|
*/
|
||||||
|
const removeInstability = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
if (instabilityCylinder) {
|
||||||
|
viewer!.scene.remove(instabilityCylinder);
|
||||||
|
instabilityCylinder.dispose();
|
||||||
|
instabilityCylinder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasInstability.value = false;
|
||||||
|
console.log('移除井壁失稳灾害模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeInstability();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="thermalBreakthroughDisaster" title="热突破灾害">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="warning"
|
||||||
|
@click="triggerThermalBreakthrough">
|
||||||
|
触发热突破
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!isActive"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="stopThermalBreakthrough">
|
||||||
|
停止热突破
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onBeforeUnmount, onMounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const isActive = ref(false);
|
||||||
|
|
||||||
|
const triggerThermalBreakthrough = () => {
|
||||||
|
const flowParticles = bus.getFlowParticles();
|
||||||
|
if (flowParticles) {
|
||||||
|
flowParticles.setColorStops([
|
||||||
|
{color: '#00FF00', step: 0.0},
|
||||||
|
{color: '#FFFF00', step: 0.5},
|
||||||
|
{color: '#FF0000', step: 0.7},
|
||||||
|
]);
|
||||||
|
// 约 30% 的粒子保持初始颜色,模拟热突破中部分流体未被加热
|
||||||
|
flowParticles.setFrozenProbability(1);
|
||||||
|
}
|
||||||
|
isActive.value = !!flowParticles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopThermalBreakthrough = () => {
|
||||||
|
const flowParticles = bus.getFlowParticles();
|
||||||
|
if (!flowParticles) return;
|
||||||
|
flowParticles.setColorStops([
|
||||||
|
{color: '#00FF00', step: 0},
|
||||||
|
{color: '#FFFF00', step: 0.4},
|
||||||
|
{color: '#FF0000', step: 0.6},
|
||||||
|
]);
|
||||||
|
flowParticles.setFrozenProbability(0);
|
||||||
|
isActive.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
isActive.value = !!bus.getFlowParticles();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
isActive.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="disasterFormation" title="灾害孕育">
|
||||||
|
<n-collapse>
|
||||||
|
<MineSeismicPanel/>
|
||||||
|
<TemperaturePanel/>
|
||||||
|
<DustDisasterLabelPanel/>
|
||||||
|
</n-collapse>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NCollapse, NCollapseItem} from 'naive-ui';
|
||||||
|
import MineSeismicPanel from "@/disasterFormationPanel/GoldMineScene/MineSeismicPanel.vue";
|
||||||
|
import TemperaturePanel from "@/disasterFormationPanel/GoldMineScene/TemperaturePanel.vue";
|
||||||
|
import DustDisasterLabelPanel from "@/disasterFormationPanel/GoldMineScene/DustDisasterLabelPanel.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="dustConcentration" title="尘害">
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasVolume" size="small" type="primary" @click="create">创建</n-button>
|
||||||
|
<n-button :disabled="!hasVolume" size="small" type="error" @click="remove">删除</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="hasVolume">
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">粒子大小</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="pointSize" :max="0.02" :min="0.001" :step="0.001" @update:value="onPointSizeChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">强度过滤</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="intensityRange" :max="1" :min="0" :step="0.01" range @update:value="onFilterChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSlider, useMessage} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {PointCloud, PointCloudTool, VolumeMesh} from '@deep/engine';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
const message = useMessage();
|
||||||
|
const bus = useBus();
|
||||||
|
const hasVolume = ref(false);
|
||||||
|
const intensityRange = ref<[number, number]>([0, 1]);
|
||||||
|
const pointSize = ref<number>(0.01);
|
||||||
|
let volume: PointCloud | null = null;
|
||||||
|
|
||||||
|
const create = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const mesh = viewer.scene.getObjectByName("温度") as VolumeMesh;
|
||||||
|
if (!mesh) {
|
||||||
|
message.warning('请先创建热害!')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = new THREE.Box3().setFromObject(mesh);
|
||||||
|
const pointCloudData = PointCloudTool.convertVolumeToPointCloudData({
|
||||||
|
data: mesh.data,
|
||||||
|
x: mesh.sizeX,
|
||||||
|
y: mesh.sizeY,
|
||||||
|
z: mesh.sizeZ,
|
||||||
|
min: [box.min.x, box.min.y, box.min.z],
|
||||||
|
max: [box.max.x, box.max.y, box.max.z],
|
||||||
|
threshold: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
volume = new PointCloud(viewer, {
|
||||||
|
name: '粉尘浓度',
|
||||||
|
pointCloudData,
|
||||||
|
pointSize: pointSize.value,
|
||||||
|
colorStops: [
|
||||||
|
{color: '#d18f0a', step: 0.0},
|
||||||
|
{color: '#f23100', step: 0.05},
|
||||||
|
{color: '#6609ff', step: 0.1},
|
||||||
|
{color: '#06ffaa', step: 0.15},
|
||||||
|
{color: '#ce0e59', step: 0.2},
|
||||||
|
{color: '#00f60b', step: 0.3},
|
||||||
|
{color: '#ffef00', step: 0.5},
|
||||||
|
{color: '#fd0000', step: 1.0}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
volume.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
volume.setPointSize(pointSize.value);
|
||||||
|
hasVolume.value = true;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
if (volume) {
|
||||||
|
volume.dispose();
|
||||||
|
volume = null;
|
||||||
|
}
|
||||||
|
hasVolume.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilterChange = (): void => {
|
||||||
|
volume?.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointSizeChange = (): void => {
|
||||||
|
volume?.setPointSize(pointSize.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="heatDisasterLabel" title="热害">
|
||||||
|
<n-space>
|
||||||
|
<n-button :disabled="hasLabel" type="primary" @click="create">
|
||||||
|
创建
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasLabel" type="error" @click="remove">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {SpritePanel} from '@deep/engine';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasLabel = ref(false);
|
||||||
|
let label: SpritePanel | null = null;
|
||||||
|
|
||||||
|
const create = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
label = new SpritePanel({
|
||||||
|
text: '热害',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
backgroundColor: '#ff4444',
|
||||||
|
borderColor: '#cc0000',
|
||||||
|
borderWidth: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
label.position.set(0, 2, 0);
|
||||||
|
viewer.scene.add(label);
|
||||||
|
hasLabel.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
if (label) {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
viewer.scene.remove(label);
|
||||||
|
label.dispose();
|
||||||
|
label = null;
|
||||||
|
}
|
||||||
|
hasLabel.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="mineSeismic" title="矿震信息">
|
||||||
|
<n-space>
|
||||||
|
<n-button :disabled="hasFaultSlip" type="primary" @click="createFaultSlip">
|
||||||
|
断层滑移矿震
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasFaultSlip" type="error" @click="removeFaultSlip">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<n-space style="margin-top: 8px;">
|
||||||
|
<n-button :disabled="hasRockBurst" type="primary" @click="createRockBurst">
|
||||||
|
岩爆矿震
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasRockBurst" type="error" @click="removeRockBurst">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {FaultSlip, SeismicWave} from '@deep/engine';
|
||||||
|
import * as THREE from "three/webgpu";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasFaultSlip = ref(false);
|
||||||
|
const hasRockBurst = ref(false);
|
||||||
|
let faultSlip: FaultSlip | null = null;
|
||||||
|
let faultSlipSeismicWave: SeismicWave | null = null;
|
||||||
|
let rockBurstSeismicWave: SeismicWave | null = null;
|
||||||
|
|
||||||
|
const createFaultSlip = () => {
|
||||||
|
const section = bus.getSection();
|
||||||
|
faultSlip = new FaultSlip(section, {autoStart: true});
|
||||||
|
faultSlipSeismicWave = new SeismicWave({
|
||||||
|
emitterPositions: [new THREE.Vector3(0, 0, 0)],
|
||||||
|
shape: 'sphere',
|
||||||
|
autoStart: true
|
||||||
|
});
|
||||||
|
hasFaultSlip.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFaultSlip = () => {
|
||||||
|
if (faultSlip) {
|
||||||
|
faultSlip.dispose();
|
||||||
|
faultSlip = null;
|
||||||
|
}
|
||||||
|
if (faultSlipSeismicWave) {
|
||||||
|
faultSlipSeismicWave.dispose();
|
||||||
|
faultSlipSeismicWave = null;
|
||||||
|
}
|
||||||
|
hasFaultSlip.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRockBurst = () => {
|
||||||
|
rockBurstSeismicWave = new SeismicWave({
|
||||||
|
emitterPositions: [new THREE.Vector3(1.5, 1.5, 1.5)],
|
||||||
|
shape: 'sphere',
|
||||||
|
autoStart: true
|
||||||
|
});
|
||||||
|
hasRockBurst.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRockBurst = () => {
|
||||||
|
if (rockBurstSeismicWave) {
|
||||||
|
rockBurstSeismicWave.dispose();
|
||||||
|
rockBurstSeismicWave = null;
|
||||||
|
}
|
||||||
|
hasRockBurst.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="temperature" title="热害">
|
||||||
|
<n-space>
|
||||||
|
<n-button :disabled="hasVolume" type="primary" @click="create">
|
||||||
|
创建
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasVolume" type="error" @click="remove">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { NButton, NCollapseItem, NSpace } from "naive-ui";
|
||||||
|
import { useBus } from "@/hooks";
|
||||||
|
import {VolumeMesh, VolumeRenderMode, VolumeTool} from "@deep/engine";
|
||||||
|
import { createNoise3D } from "simplex-noise";
|
||||||
|
import * as THREE from "three/webgpu";
|
||||||
|
const bus = useBus();
|
||||||
|
const hasVolume = ref(false);
|
||||||
|
let volume: VolumeMesh | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 温度体固定分辨率。
|
||||||
|
*/
|
||||||
|
const TEMPERATURE_VOLUME_SIZE = 256;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 温度场 RAW 文件路径。
|
||||||
|
*/
|
||||||
|
const TEMPERATURE_RAW_DATA_PATH = "/model/raw/temperature-volume.raw";
|
||||||
|
/**
|
||||||
|
* 温度噪声最小值。
|
||||||
|
*/
|
||||||
|
const TEMPERATURE_NOISE_MIN_VALUE = 0;
|
||||||
|
/**
|
||||||
|
* 温度噪声最大值。
|
||||||
|
*/
|
||||||
|
const TEMPERATURE_NOISE_MAX_VALUE = 255;
|
||||||
|
/**
|
||||||
|
* 温度噪声采样缩放系数。
|
||||||
|
*/
|
||||||
|
const TEMPERATURE_NOISE_SCALE = 10;
|
||||||
|
|
||||||
|
//------ 温度体 RAW 加载 开始------
|
||||||
|
/**
|
||||||
|
* 获取温度体数据(通过 RAW 文件加载)。
|
||||||
|
* @returns 温度体体素数据。
|
||||||
|
*/
|
||||||
|
const getTemperatureVolumeData = async (): Promise<Uint8Array> => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const rawData = await viewer.resources.loadRawData(TEMPERATURE_RAW_DATA_PATH);
|
||||||
|
return new Uint8Array(rawData);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算体素线性索引。
|
||||||
|
* @param x - X 轴索引。
|
||||||
|
* @param y - Y 轴索引。
|
||||||
|
* @param z - Z 轴索引。
|
||||||
|
* @param size - 体素边长。
|
||||||
|
* @returns 线性索引。
|
||||||
|
*/
|
||||||
|
const getVoxelLinearIndex = (x: number, y: number, z: number, size: number): number => {
|
||||||
|
return x + y * size + z * size * size;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新遍历体数据并使用 noise3d 重生成值。
|
||||||
|
* @param rawData - 原始体数据。
|
||||||
|
* @param size - 体素边长。
|
||||||
|
* @returns 重生成后的体数据。
|
||||||
|
*/
|
||||||
|
const regenerateVolumeDataWithNoise3D = (rawData: Uint8Array, size: number): Uint8Array => {
|
||||||
|
const expectedLength = size * size * size;
|
||||||
|
// 统一输出固定长度数组,避免 RAW 长度异常导致写入越界。
|
||||||
|
const regeneratedData = rawData.length === expectedLength ? new Uint8Array(rawData) : new Uint8Array(expectedLength);
|
||||||
|
const noise3D = createNoise3D(Math.random);
|
||||||
|
|
||||||
|
for (let x = 0; x < size; x++) {
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
for (let z = 0; z < size; z++) {
|
||||||
|
const linearIndex = getVoxelLinearIndex(x, y, z, size);
|
||||||
|
// 仅对原始值不为 0 的体素执行噪声重生成。
|
||||||
|
if (regeneratedData[linearIndex] === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 使用归一化坐标采样噪声,并将 [-1,1] 映射到 [0,255]。
|
||||||
|
const noiseValue = noise3D(
|
||||||
|
(x / size) * TEMPERATURE_NOISE_SCALE,
|
||||||
|
(y / size) * TEMPERATURE_NOISE_SCALE,
|
||||||
|
(z / size) * TEMPERATURE_NOISE_SCALE
|
||||||
|
);
|
||||||
|
const mappedNoiseValue = Math.round(
|
||||||
|
((noiseValue + 1) / 2) * (TEMPERATURE_NOISE_MAX_VALUE - TEMPERATURE_NOISE_MIN_VALUE) + TEMPERATURE_NOISE_MIN_VALUE
|
||||||
|
);
|
||||||
|
regeneratedData[linearIndex] = mappedNoiseValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return regeneratedData;
|
||||||
|
};
|
||||||
|
//------ 温度体 RAW 加载 结束------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建温度体(create11:使用 temperatureLine 进行 SDF 采样)。
|
||||||
|
*/
|
||||||
|
const create11 = async (): Promise<void> => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const size = TEMPERATURE_VOLUME_SIZE;
|
||||||
|
const box = bus.getRockSample();
|
||||||
|
const outerBox = new THREE.Box3().setFromObject(box);
|
||||||
|
let data: Uint8Array;
|
||||||
|
try {
|
||||||
|
// 与 StressDisplayPanel 一致:通过 resources.loadRawData 加载 RAW。
|
||||||
|
data = await getTemperatureVolumeData();
|
||||||
|
data = VolumeTool.generateRandomVolumeAroundPipeWithBaseData({
|
||||||
|
outerBox: outerBox,
|
||||||
|
pipeStart: new THREE.Vector3(-0.11, 0, -2.505),
|
||||||
|
pipeEnd: new THREE.Vector3(-0.11, 0, -1.4),
|
||||||
|
pipeRadius: 0.25,
|
||||||
|
size: 256,
|
||||||
|
rangeMin: 6,
|
||||||
|
rangeMax: 53,
|
||||||
|
baseData: data
|
||||||
|
});
|
||||||
|
// 获取 RAW 后重新遍历体数据,使用 noise3d 重新生成体素值。
|
||||||
|
data = regenerateVolumeDataWithNoise3D(data, size);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// RAW 加载失败时回退为全 0 数据,保证面板不崩溃。
|
||||||
|
console.error("温度体 RAW 加载失败,已回退为默认空数据:", error);
|
||||||
|
data = new Uint8Array(size * size * size);
|
||||||
|
}
|
||||||
|
|
||||||
|
volume = new VolumeMesh(viewer, {
|
||||||
|
name: "温度",
|
||||||
|
size,
|
||||||
|
scale: 4.99,
|
||||||
|
data: data,
|
||||||
|
mode: VolumeRenderMode.MaximumIntensityProjection,
|
||||||
|
colorStops: [
|
||||||
|
{ color: "#ae1be9", step: 0.0 },
|
||||||
|
{ color: "#0bdb04", step: 0.05 },
|
||||||
|
{ color: "#10e52b", step: 0.1 },
|
||||||
|
{ color: "#1232db", step: 0.15 },
|
||||||
|
{ color: "#0000FF", step: 0.2 },
|
||||||
|
{ color: "#00FFFF", step: 0.33 },
|
||||||
|
{ color: "#FFFF00", step: 0.66 },
|
||||||
|
{ color: "#FF0000", step: 1.0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
volume.renderOrder = 3;
|
||||||
|
// volume.material.depthTest = true
|
||||||
|
hasVolume.value = true;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建温度体。
|
||||||
|
*/
|
||||||
|
const create = (): void => {
|
||||||
|
void create11();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除温度体。
|
||||||
|
*/
|
||||||
|
const remove = (): void => {
|
||||||
|
if (volume) {
|
||||||
|
volume.dispose();
|
||||||
|
volume = null;
|
||||||
|
}
|
||||||
|
hasVolume.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
//------ 温度线 SDF 体数据源 结束------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="disasterFormation" title="灾害孕育">
|
||||||
|
<n-collapse>
|
||||||
|
<OilGasMineSeismicPanel/>
|
||||||
|
<CasingDamageDisasterPanel/>
|
||||||
|
<WellboreInstabilityDisasterPanel/>
|
||||||
|
</n-collapse>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NCollapse, NCollapseItem} from 'naive-ui';
|
||||||
|
import OilGasMineSeismicPanel from "@/disasterFormationPanel/OilGasScene/OilGasMineSeismicPanel.vue";
|
||||||
|
import {CasingDamageDisasterPanel, WellboreInstabilityDisasterPanel} from "@/disasterFormationPanel/OilGasScene";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="casingDamageDisaster" title="套管损坏灾害">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-space>
|
||||||
|
<n-button :disabled="hasDamage" type="primary" @click="applyDamage">
|
||||||
|
生成套管损害模型
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasDamage" type="error" @click="clearDamage">
|
||||||
|
移除套管损害模型
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {ParametricCylinder} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasDamage = ref(false);
|
||||||
|
|
||||||
|
let damageCylinder: ParametricCylinder | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用损坏效果 - 生成套管损害圆柱体
|
||||||
|
*/
|
||||||
|
const applyDamage = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
const position = {x: 1.3, y: 1.1, z: 1.5};
|
||||||
|
|
||||||
|
const cylinder = new ParametricCylinder({
|
||||||
|
radiusTop: 0.155,
|
||||||
|
radiusBottom: 0.155,
|
||||||
|
height: 0.2,
|
||||||
|
//------ 圆柱上下不闭合 开始------
|
||||||
|
openEnded: true,
|
||||||
|
//------ 圆柱上下不闭合 结束------
|
||||||
|
//------ 圆柱细分优化 开始------
|
||||||
|
radialSegments: 96,
|
||||||
|
//------ 圆柱细分优化 结束------
|
||||||
|
material: new THREE.MeshBasicMaterial({
|
||||||
|
color: '#ff0000',
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
cylinder.renderOrder = 1;
|
||||||
|
cylinder.position.set(position.x, position.y, position.z);
|
||||||
|
viewer!.scene.add(cylinder);
|
||||||
|
|
||||||
|
damageCylinder = cylinder;
|
||||||
|
hasDamage.value = true;
|
||||||
|
|
||||||
|
console.log('已生成套管损害圆柱体模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除损坏
|
||||||
|
*/
|
||||||
|
const clearDamage = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
if (damageCylinder) {
|
||||||
|
viewer!.scene.remove(damageCylinder);
|
||||||
|
damageCylinder.dispose();
|
||||||
|
damageCylinder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDamage.value = false;
|
||||||
|
console.log('已清除套管损害模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearDamage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="mineSeismic" title="地震信息">
|
||||||
|
<n-space style="margin-top: 8px;">
|
||||||
|
<n-button :disabled="hasRockBurst" type="primary" @click="createRockBurst">
|
||||||
|
开始地震
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasRockBurst" type="error" @click="removeRockBurst">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {FaultSlip, SeismicWave} from '@deep/engine';
|
||||||
|
import * as THREE from "three/webgpu";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasFaultSlip = ref(false);
|
||||||
|
const hasRockBurst = ref(false);
|
||||||
|
let faultSlip: FaultSlip | null = null;
|
||||||
|
let faultSlipSeismicWave: SeismicWave | null = null;
|
||||||
|
let rockBurstSeismicWave: SeismicWave | null = null;
|
||||||
|
|
||||||
|
const createFaultSlip = () => {
|
||||||
|
const section = bus.getSection();
|
||||||
|
faultSlip = new FaultSlip(section, {autoStart: true});
|
||||||
|
faultSlipSeismicWave = new SeismicWave({
|
||||||
|
emitterPositions: [new THREE.Vector3(0, 0, 0)],
|
||||||
|
shape: 'sphere',
|
||||||
|
autoStart: true
|
||||||
|
});
|
||||||
|
hasFaultSlip.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFaultSlip = () => {
|
||||||
|
if (faultSlip) {
|
||||||
|
faultSlip.dispose();
|
||||||
|
faultSlip = null;
|
||||||
|
}
|
||||||
|
if (faultSlipSeismicWave) {
|
||||||
|
faultSlipSeismicWave.dispose();
|
||||||
|
faultSlipSeismicWave = null;
|
||||||
|
}
|
||||||
|
hasFaultSlip.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRockBurst = () => {
|
||||||
|
rockBurstSeismicWave = new SeismicWave({
|
||||||
|
emitterPositions: [new THREE.Vector3(1.5, 1.5, 1.5)],
|
||||||
|
shape: 'sphere',
|
||||||
|
autoStart: true
|
||||||
|
});
|
||||||
|
hasRockBurst.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRockBurst = () => {
|
||||||
|
if (rockBurstSeismicWave) {
|
||||||
|
rockBurstSeismicWave.dispose();
|
||||||
|
rockBurstSeismicWave = null;
|
||||||
|
}
|
||||||
|
hasRockBurst.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="wellboreInstabilityDisaster" title="井壁失稳灾害">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="hasInstability"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="generateInstability">
|
||||||
|
生成井壁失稳灾害模型
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!hasInstability"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="removeInstability">
|
||||||
|
移除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {ParametricCylinder} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasInstability = ref(false);
|
||||||
|
|
||||||
|
let instabilityCylinder: ParametricCylinder | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成井壁失稳灾害模型
|
||||||
|
*/
|
||||||
|
const generateInstability = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
const cylinder = new ParametricCylinder({
|
||||||
|
radiusTop: 0.153,
|
||||||
|
radiusBottom: 0.153,
|
||||||
|
height: 0.2,
|
||||||
|
//------ 圆柱上下不闭合 开始------
|
||||||
|
openEnded: true,
|
||||||
|
//------ 圆柱上下不闭合 结束------
|
||||||
|
//------ 圆柱细分优化 开始------
|
||||||
|
radialSegments: 96,
|
||||||
|
//------ 圆柱细分优化 结束------
|
||||||
|
material: new THREE.MeshBasicMaterial({
|
||||||
|
color: '#ffff00',
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
cylinder.renderOrder = 1;
|
||||||
|
cylinder.name = 'WellboreInstability_1';
|
||||||
|
cylinder.position.set( 0.7894510233342803,
|
||||||
|
0.2,
|
||||||
|
0.94);
|
||||||
|
cylinder.rotation.set( 3.059352285009745,
|
||||||
|
0.8295716061828269,
|
||||||
|
-1.531659379307599);
|
||||||
|
viewer!.scene.add(cylinder);
|
||||||
|
|
||||||
|
instabilityCylinder = cylinder;
|
||||||
|
hasInstability.value = true;
|
||||||
|
console.log('生成井壁失稳灾害模型');
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除井壁失稳灾害模型
|
||||||
|
*/
|
||||||
|
const removeInstability = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
if (instabilityCylinder) {
|
||||||
|
viewer!.scene.remove(instabilityCylinder);
|
||||||
|
instabilityCylinder.dispose();
|
||||||
|
instabilityCylinder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasInstability.value = false;
|
||||||
|
console.log('移除井壁失稳灾害模型');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeInstability();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export {default as CasingDamageDisasterPanel} from "./CasingDamageDisasterPanel.vue"
|
||||||
|
export {default as WellboreInstabilityDisasterPanel} from "./WellboreInstabilityDisasterPanel.vue"
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="disasterFormation" title="灾害孕育">
|
||||||
|
<n-collapse>
|
||||||
|
|
||||||
|
<RuptureEventPanel/>
|
||||||
|
<RockBurstPanel/>
|
||||||
|
</n-collapse>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NCollapse, NCollapseItem} from 'naive-ui';
|
||||||
|
import {RockBurstPanel} from './TunnelScene/';
|
||||||
|
import RuptureEventPanel from "@/disasterFormationPanel/TunnelScene/RuptureEventPanel.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="rockBurst" title="岩爆">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-space>
|
||||||
|
<n-button type="primary" @click="spawn">开始岩爆</n-button>
|
||||||
|
<n-button :disabled="!debrisReady" type="error" @click="destroy">销毁</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {CssType, RockDebris, RockDebrisModelSourceType} from '@deep/engine';
|
||||||
|
import {RockBurstLabelHtmlPanel} from '@/htmlPanel';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let debris: RockDebris | null = null;
|
||||||
|
let rockBurstLabelPanel: RockBurstLabelHtmlPanel | null = null;
|
||||||
|
const debrisReady = ref(false);
|
||||||
|
|
||||||
|
const ROCK_BURST_START_POSITION = new THREE.Vector3(
|
||||||
|
0,
|
||||||
|
0.2746796297125781,
|
||||||
|
1.826940338498639
|
||||||
|
);
|
||||||
|
|
||||||
|
// 懒初始化:首次触发时创建平面和 RockDebris
|
||||||
|
function ensureDebris() {
|
||||||
|
if (debris) return;
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
const archMesh = viewer.scene.getObjectByName("机器人开挖隧道碰撞体") as THREE.Mesh;
|
||||||
|
if (!archMesh){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debris = new RockDebris({
|
||||||
|
count: 10,
|
||||||
|
size: [0.05, 0.1],
|
||||||
|
office: [-0.3, 0.3],
|
||||||
|
speed: 3,
|
||||||
|
startPosition: ROCK_BURST_START_POSITION.clone(),
|
||||||
|
mesh: archMesh,
|
||||||
|
// modelSourceType: RockDebrisModelSourceType.URL,
|
||||||
|
modelSourceType: RockDebrisModelSourceType.GENERATED
|
||||||
|
});
|
||||||
|
debrisReady.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRockBurstLabel() {
|
||||||
|
if (rockBurstLabelPanel) return;
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
rockBurstLabelPanel = new RockBurstLabelHtmlPanel(CssType.CSS2D);
|
||||||
|
const panelObject = rockBurstLabelPanel.getCssObject();
|
||||||
|
let point = ROCK_BURST_START_POSITION.clone();
|
||||||
|
point.y += 0.4
|
||||||
|
panelObject.position.copy(point)
|
||||||
|
viewer.scene.add(panelObject);
|
||||||
|
rockBurstLabelPanel.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeRockBurstLabel() {
|
||||||
|
if (!rockBurstLabelPanel) return;
|
||||||
|
rockBurstLabelPanel.hide();
|
||||||
|
const panelObject = rockBurstLabelPanel.getCssObject();
|
||||||
|
panelObject.parent?.remove(panelObject);
|
||||||
|
rockBurstLabelPanel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawn = () => {
|
||||||
|
ensureDebris();
|
||||||
|
ensureRockBurstLabel();
|
||||||
|
debris?.spawn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
debris?.dispose();
|
||||||
|
debris = null;
|
||||||
|
disposeRockBurstLabel();
|
||||||
|
debrisReady.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,643 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="rockBurst" title="破裂事件">
|
||||||
|
<div class="panel-section">
|
||||||
|
<!-- 显示控制 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">显示控制</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<n-checkbox v-model:checked="showCrackModels">
|
||||||
|
裂缝模型
|
||||||
|
</n-checkbox>
|
||||||
|
<n-checkbox v-model:checked="showRuptureSpheres">
|
||||||
|
破裂球
|
||||||
|
</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间窗口滑块 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">时间范围</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider
|
||||||
|
v-model:value="timeRange"
|
||||||
|
:marks="timeMarks"
|
||||||
|
:max="24"
|
||||||
|
:min="0"
|
||||||
|
:step="0.5"
|
||||||
|
range
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 能量阈值 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">能量阈值 (kJ)</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="energyThreshold"
|
||||||
|
:min="0"
|
||||||
|
:show-button="false"
|
||||||
|
:step="0.1"
|
||||||
|
class="full-input"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 创建破裂事件按钮 -->
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
@click="handleCreateEvents"
|
||||||
|
>
|
||||||
|
创建破裂事件
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
@click="handleClearEvents"
|
||||||
|
>
|
||||||
|
清除事件
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 过滤按钮 -->
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="handleApplyFilter"
|
||||||
|
>
|
||||||
|
过滤
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleCancelFilter"
|
||||||
|
>
|
||||||
|
取消过滤
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref, watch} from 'vue';
|
||||||
|
import {NButton, NCheckbox, NCollapseItem, NInputNumber, NSlider} from 'naive-ui';
|
||||||
|
import {CssType, EventManagerEvents, ViewerEvents, type RuptureEvent} from '@deep/engine';
|
||||||
|
import {RuptureEventModal} from "@/components/htmlPanel";
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
// 显示控制
|
||||||
|
const showCrackModels = ref(true);
|
||||||
|
const showRuptureSpheres = ref(true);
|
||||||
|
|
||||||
|
// 时间范围滑块 (0-24小时)
|
||||||
|
const timeRange = ref<[number, number]>([0, 24]);
|
||||||
|
|
||||||
|
// 时间刻度标记
|
||||||
|
const timeMarks = {
|
||||||
|
0: '0:00',
|
||||||
|
6: '6:00',
|
||||||
|
12: '12:00',
|
||||||
|
18: '18:00',
|
||||||
|
24: '24:00'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 能量阈值
|
||||||
|
const energyThreshold = ref(0);
|
||||||
|
|
||||||
|
// 存储所有生成的测试数据
|
||||||
|
const allTestEvents = ref<RuptureEvent[]>([]);
|
||||||
|
|
||||||
|
// 破裂事件弹窗管理 - 每个事件对应一个弹窗
|
||||||
|
const eventModals = new Map<number, RuptureEventModal>();
|
||||||
|
|
||||||
|
// 存储事件处理函数引用,用于移除监听器
|
||||||
|
let raycastPickHandler: ((event: any) => void) | null = null;
|
||||||
|
let afterRenderHandler: (() => void) | null = null;
|
||||||
|
let lastCameraDistance: number | null = null;
|
||||||
|
|
||||||
|
// 每个弹窗当前的 Y 偏移百分比(初始 -100%)
|
||||||
|
const modalOffsetMap = new Map<number, number>();
|
||||||
|
|
||||||
|
// 获取今天的开始时间戳(0点)
|
||||||
|
const getTodayStartTime = () => {
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
return now.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成今天24小时内的随机测试数据
|
||||||
|
const generateTestData = () => {
|
||||||
|
const events: RuptureEvent[] = [];
|
||||||
|
const todayStart = getTodayStartTime();
|
||||||
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
// 时间范围:今天0-24小时内随机
|
||||||
|
const time = todayStart + Math.random() * oneDayMs;
|
||||||
|
|
||||||
|
const event: RuptureEvent = {
|
||||||
|
position: {
|
||||||
|
x: (Math.random() - 0.5) * 4,
|
||||||
|
y: (Math.random() - 0.5) * 4,
|
||||||
|
z: (Math.random() - 0.5) * 4
|
||||||
|
},
|
||||||
|
time: time,
|
||||||
|
energy: Math.random() * 100 // 0-100 kJ
|
||||||
|
};
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
console.log(events)
|
||||||
|
return events;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用过滤条件(只隐藏/显示,不删除重建)
|
||||||
|
const applyFilterVisibility = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer || allTestEvents.value.length === 0) return 0;
|
||||||
|
|
||||||
|
const [startHour, endHour] = timeRange.value;
|
||||||
|
const todayStart = getTodayStartTime();
|
||||||
|
const startTime = todayStart + startHour * 60 * 60 * 1000;
|
||||||
|
const endTime = todayStart + endHour * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
|
// 遍历所有事件,根据过滤条件设置可见性
|
||||||
|
const allEvents = viewer.ruptureEvents.getAllEvents();
|
||||||
|
allEvents.forEach((item) => {
|
||||||
|
const event = item.event;
|
||||||
|
const passTimeFilter = event.time >= startTime && event.time <= endTime;
|
||||||
|
const passEnergyFilter = event.energy >= energyThreshold.value;
|
||||||
|
const shouldBeVisible = passTimeFilter && passEnergyFilter;
|
||||||
|
|
||||||
|
// 设置破裂球可见性
|
||||||
|
item.mesh.visible = shouldBeVisible && showRuptureSpheres.value;
|
||||||
|
|
||||||
|
// 设置裂缝模型可见性(只有当破裂球应该可见且裂缝模型开关打开时才显示)
|
||||||
|
if (item.crackModel) {
|
||||||
|
item.crackModel.visible = shouldBeVisible && showCrackModels.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldBeVisible) visibleCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return visibleCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听时间范围变化,动态过滤数据
|
||||||
|
watch(timeRange, ([startHour, endHour]) => {
|
||||||
|
const visibleCount = applyFilterVisibility();
|
||||||
|
console.log(`时间范围更新: ${startHour}:00 - ${endHour}:00, 显示事件数: ${visibleCount}`);
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
|
// 监听能量阈值变化
|
||||||
|
watch(energyThreshold, () => {
|
||||||
|
const visibleCount = applyFilterVisibility();
|
||||||
|
console.log(`能量阈值更新: ${energyThreshold.value} kJ, 显示事件数: ${visibleCount}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听裂缝模型显示开关
|
||||||
|
watch(showCrackModels, () => {
|
||||||
|
// 重新应用完整的过滤逻辑,确保裂缝模型根据当前状态正确显示
|
||||||
|
applyFilterVisibility();
|
||||||
|
console.log(`裂缝模型显示: ${showCrackModels.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听破裂球显示开关
|
||||||
|
watch(showRuptureSpheres, () => {
|
||||||
|
// 重新应用完整的过滤逻辑,确保破裂球根据当前状态正确显示
|
||||||
|
applyFilterVisibility();
|
||||||
|
console.log(`破裂球显示: ${showRuptureSpheres.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件挂载时只设置监听器,不自动创建事件
|
||||||
|
onMounted(() => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) {
|
||||||
|
console.warn('Viewer not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置破裂事件点击监听
|
||||||
|
setupEventClickListener();
|
||||||
|
setupAfterRenderListener();
|
||||||
|
|
||||||
|
console.log('破裂事件面板已初始化,等待用户创建事件');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听渲染事件,动态更新弹窗位置偏移,使其始终悬浮在球体上方
|
||||||
|
*/
|
||||||
|
const setupAfterRenderListener = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
if (afterRenderHandler) {
|
||||||
|
viewer.emitter.off(ViewerEvents.AFTER_RENDER, afterRenderHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterRenderHandler = () => {
|
||||||
|
const dist = viewer.cameraControls.distance;
|
||||||
|
|
||||||
|
// 首次记录基准距离
|
||||||
|
if (lastCameraDistance === null) {
|
||||||
|
lastCameraDistance = dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = dist - lastCameraDistance;
|
||||||
|
lastCameraDistance = dist;
|
||||||
|
|
||||||
|
eventModals.forEach((modal, time) => {
|
||||||
|
const eventItem = viewer.ruptureEvents.getEvent(time);
|
||||||
|
if (!eventItem || !eventItem.mesh.visible) return;
|
||||||
|
|
||||||
|
const panelEl = document.getElementById(modal.getPanelElementId());
|
||||||
|
if (!panelEl) return;
|
||||||
|
|
||||||
|
// 拉远(dist增大) → 偏移绝对值减小(如-100→-80),拉近(dist减小) → 偏移绝对值增大(如-100→-120)
|
||||||
|
const current = modalOffsetMap.get(time) ?? -80;
|
||||||
|
const next = current + delta * 1.2;
|
||||||
|
modalOffsetMap.set(time, next);
|
||||||
|
panelEl.style.transform = `translate(-50%, ${next}%)`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
viewer.emitter.on(ViewerEvents.AFTER_RENDER, afterRenderHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置破裂事件点击监听
|
||||||
|
*/
|
||||||
|
const setupEventClickListener = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 如果已经有监听器,先移除
|
||||||
|
if (raycastPickHandler) {
|
||||||
|
viewer.events.off(EventManagerEvents.RAYCAST_PICK, raycastPickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建事件处理函数
|
||||||
|
raycastPickHandler = (event: any) => {
|
||||||
|
const {intersects} = event.data;
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
// 检测是否点击了破裂事件球,只取第一个射线对象
|
||||||
|
const intersection = intersects[0];
|
||||||
|
const clickedObject = intersection.object;
|
||||||
|
|
||||||
|
// 检查对象名称是否以 "RuptureEvent_" 开头
|
||||||
|
if (clickedObject.name && clickedObject.name.startsWith('RuptureEvent_')) {
|
||||||
|
console.log('点击了破裂事件:', clickedObject.name);
|
||||||
|
|
||||||
|
// 从 userData 中读取事件数据
|
||||||
|
const eventData = clickedObject.userData?.ruptureEvent;
|
||||||
|
if (!eventData) {
|
||||||
|
console.warn('未找到事件数据 userData');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = eventData.time;
|
||||||
|
|
||||||
|
// 获取或创建该事件的弹窗
|
||||||
|
let modal = eventModals.get(time);
|
||||||
|
if (!modal) {
|
||||||
|
modal = createEventModal(eventData);
|
||||||
|
eventModals.set(time, modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听射线拾取事件
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, raycastPickHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为破裂事件创建弹窗
|
||||||
|
*/
|
||||||
|
const createEventModal = (event: RuptureEvent): RuptureEventModal => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) throw new Error('Viewer not initialized');
|
||||||
|
|
||||||
|
// 创建破裂事件弹窗
|
||||||
|
const modal = new RuptureEventModal(CssType.CSS2D);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
modal.onDelete(() => {
|
||||||
|
modal.hide();
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
if (modalObject.parent) {
|
||||||
|
modalObject.parent.remove(modalObject);
|
||||||
|
}
|
||||||
|
eventModals.delete(event.time);
|
||||||
|
|
||||||
|
// 从球体的 userData 中移除 modal 引用
|
||||||
|
const eventItem = viewer.ruptureEvents.getEvent(event.time);
|
||||||
|
if (eventItem) {
|
||||||
|
delete eventItem.mesh.userData.modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`破裂事件弹窗已关闭: ${event.time}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新事件数据
|
||||||
|
modal.updateData({
|
||||||
|
position: event.position,
|
||||||
|
energy: event.energy,
|
||||||
|
time: event.time
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取球体mesh并计算包围盒
|
||||||
|
const eventItem = viewer.ruptureEvents.getEvent(event.time);
|
||||||
|
|
||||||
|
if (eventItem) {
|
||||||
|
const mesh = eventItem.mesh;
|
||||||
|
|
||||||
|
// 计算球体包围盒
|
||||||
|
const boundingBox = new THREE.Box3().setFromObject(mesh);
|
||||||
|
const boxSize = new THREE.Vector3();
|
||||||
|
boundingBox.getSize(boxSize);
|
||||||
|
|
||||||
|
// 获取包围盒中心
|
||||||
|
const center = new THREE.Vector3();
|
||||||
|
boundingBox.getCenter(center);
|
||||||
|
|
||||||
|
// 设置弹窗位置在球体顶部
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.copy(center);
|
||||||
|
modalObject.position.y += 0; // 在球体顶部显示
|
||||||
|
|
||||||
|
// 将 modal 引用存储到球体的 userData 中
|
||||||
|
mesh.userData.modal = modal;
|
||||||
|
} else {
|
||||||
|
// 如果无法获取球体,使用事件位置作为备选
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.set(
|
||||||
|
event.position.x,
|
||||||
|
event.position.y + 0,
|
||||||
|
event.position.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到场景
|
||||||
|
viewer.scene.add(modal.getCssObject());
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建破裂事件
|
||||||
|
*/
|
||||||
|
const handleCreateEvents = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) {
|
||||||
|
console.warn('Viewer not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已有事件,先清除
|
||||||
|
if (allTestEvents.value.length > 0) {
|
||||||
|
console.log('已存在事件,先清除旧事件');
|
||||||
|
cleanup();
|
||||||
|
allTestEvents.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成测试数据
|
||||||
|
allTestEvents.value = generateTestData();
|
||||||
|
|
||||||
|
// 一次性创建所有事件(包括破裂球和裂缝模型)
|
||||||
|
viewer.ruptureEvents.addEvents(allTestEvents.value);
|
||||||
|
|
||||||
|
// 应用初始过滤和显示设置
|
||||||
|
applyFilterVisibility();
|
||||||
|
|
||||||
|
console.log('创建破裂事件数据:', {
|
||||||
|
eventCount: allTestEvents.value.length,
|
||||||
|
totalCount: viewer.ruptureEvents.getTotalEventCount()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有破裂事件
|
||||||
|
*/
|
||||||
|
const handleClearEvents = () => {
|
||||||
|
cleanup();
|
||||||
|
allTestEvents.value = [];
|
||||||
|
console.log('已清除所有破裂事件');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用过滤(手动触发)
|
||||||
|
const handleApplyFilter = () => {
|
||||||
|
const visibleCount = applyFilterVisibility();
|
||||||
|
const [startHour, endHour] = timeRange.value;
|
||||||
|
console.log(`手动过滤: ${startHour}:00 - ${endHour}:00, 能量阈值: ${energyThreshold.value} kJ, 显示事件数: ${visibleCount}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消过滤(重置为显示所有数据)
|
||||||
|
const handleCancelFilter = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) {
|
||||||
|
console.warn('Viewer not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置时间范围为全天
|
||||||
|
timeRange.value = [0, 24];
|
||||||
|
// 重置能量阈值
|
||||||
|
energyThreshold.value = 0;
|
||||||
|
|
||||||
|
// 显示所有数据
|
||||||
|
applyFilterVisibility();
|
||||||
|
|
||||||
|
console.log('取消过滤,显示所有数据');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理所有破裂事件和弹窗
|
||||||
|
*/
|
||||||
|
const cleanup = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 移除事件监听器
|
||||||
|
if (raycastPickHandler) {
|
||||||
|
viewer.events.off(EventManagerEvents.RAYCAST_PICK, raycastPickHandler);
|
||||||
|
raycastPickHandler = null;
|
||||||
|
}
|
||||||
|
if (afterRenderHandler) {
|
||||||
|
viewer.emitter.off(ViewerEvents.AFTER_RENDER, afterRenderHandler);
|
||||||
|
afterRenderHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先清理所有球体上关联的弹窗(包括那些可能不在 eventModals 中的)
|
||||||
|
const allEvents = viewer.ruptureEvents.getAllEvents();
|
||||||
|
for (const item of allEvents.values()) {
|
||||||
|
const modal = item.mesh.userData.modal as RuptureEventModal | undefined;
|
||||||
|
if (modal) {
|
||||||
|
modal.dispose();
|
||||||
|
delete item.mesh.userData.modal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再清理 eventModals Map 中的弹窗(以防有遗漏)
|
||||||
|
eventModals.forEach((modal) => {
|
||||||
|
modal.dispose();
|
||||||
|
});
|
||||||
|
eventModals.clear();
|
||||||
|
modalOffsetMap.clear();
|
||||||
|
lastCameraDistance = null;
|
||||||
|
|
||||||
|
// 最后清理所有破裂事件球体
|
||||||
|
viewer.ruptureEvents.clearEvents();
|
||||||
|
|
||||||
|
console.log('已清理所有破裂事件和弹窗');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露配置和方法供外部使用
|
||||||
|
defineExpose({
|
||||||
|
timeRange,
|
||||||
|
energyThreshold,
|
||||||
|
cleanup
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rupture-event-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 400px;
|
||||||
|
width: 340px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: calc(100vh - 40px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rupture-event-panel h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rupture-event-panel h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 16px 0 24px 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-input {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
.rupture-event-panel::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rupture-event-panel::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rupture-event-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rupture-event-panel::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义 naive-ui 组件样式 */
|
||||||
|
:deep(.n-input-number) {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input-number:hover) {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input-number:focus) {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 169, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--primary-type) {
|
||||||
|
background-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button--primary-type:hover) {
|
||||||
|
background-color: #40a9ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export {default as StressDisplayPanel} from "../../panels/TunnelScene/FieldData/StressDisplayPanel.vue"
|
||||||
|
export {default as StrainDisplayPanel} from "../../panels/TunnelScene/FieldData/StrainDisplayPanel.vue"
|
||||||
|
export {default as TemperatureDisplayPanel} from "../../panels/TunnelScene/FieldData/TemperatureDisplayPanel.vue"
|
||||||
|
export {default as RockBurstPanel} from "./RockBurstPanel.vue"
|
||||||
4
packages/demo/src/disasterFormationPanel/index.ts
Normal file
4
packages/demo/src/disasterFormationPanel/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export {default as GeothermalDisasterFormationPanel} from "./GeothermalDisasterFormationPanel.vue"
|
||||||
|
export {default as GoldMineDisasterFormationPanel} from "./GoldMineDisasterFormationPanel.vue"
|
||||||
|
export {default as OilGasDisasterFormationPanel} from "./OilGasDisasterFormationPanel.vue"
|
||||||
|
export {default as TunnelDisasterFormationPanel} from "./TunnelDisasterFormationPanel.vue"
|
||||||
136
packages/demo/src/hooks/Bus.ts
Normal file
136
packages/demo/src/hooks/Bus.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import {FlowParticles, ParametricBox, ParametricPipe, Tool, Viewer} from "@deep/engine";
|
||||||
|
import Emittery from 'emittery';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
export enum BusEvents {
|
||||||
|
VIEWER_INITIALIZED,
|
||||||
|
SCENE_TREE_UPDATED,
|
||||||
|
CLIPPING_MODE_CHANGED,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClippingMode = 'three' | 'single' | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bus 事件映射类型
|
||||||
|
*/
|
||||||
|
export type BusEventMap = {
|
||||||
|
[BusEvents.VIEWER_INITIALIZED]: Viewer;
|
||||||
|
[BusEvents.SCENE_TREE_UPDATED]: null;
|
||||||
|
[BusEvents.CLIPPING_MODE_CHANGED]: ClippingMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Bus extends Emittery<BusEventMap> {
|
||||||
|
|
||||||
|
// 视图
|
||||||
|
viewer: Viewer | null = null;
|
||||||
|
|
||||||
|
// 岩样
|
||||||
|
rockSample: THREE.Mesh | null = null;
|
||||||
|
section: THREE.Mesh | null = null;
|
||||||
|
|
||||||
|
// 金属场景管道
|
||||||
|
pipe: ParametricPipe | null = null;
|
||||||
|
// 地热热突破流体粒子
|
||||||
|
flowParticles: FlowParticles | null = null;
|
||||||
|
// 加载面存在状态
|
||||||
|
loadSurfaceExists: boolean = false;
|
||||||
|
temperatureLine: ParametricPipe | null = null;
|
||||||
|
// 剖切状态
|
||||||
|
clippingMode: ClippingMode = null;
|
||||||
|
isClipping: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置视图
|
||||||
|
setViewer(value: Viewer) {
|
||||||
|
this.viewer = value;
|
||||||
|
window.viewer = this.viewer
|
||||||
|
// 派发视图设置事件
|
||||||
|
this.emit(BusEvents.VIEWER_INITIALIZED, value).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取视图
|
||||||
|
getViewer() {
|
||||||
|
return this.viewer!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置岩样
|
||||||
|
setRockSample(value: ParametricBox | THREE.Mesh) {
|
||||||
|
this.rockSample = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取岩样
|
||||||
|
getRockSample() {
|
||||||
|
return this.rockSample!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发场景树更新事件
|
||||||
|
triggerSceneTreeUpdate() {
|
||||||
|
this.emit(BusEvents.SCENE_TREE_UPDATED, null).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setSection(section: ParametricBox | THREE.Mesh) {
|
||||||
|
Tool.defineMaterialOpacityMapping(section);
|
||||||
|
this.section = section;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSection() {
|
||||||
|
return this.section!
|
||||||
|
}
|
||||||
|
|
||||||
|
setGoldMineScenePip(pipe) {
|
||||||
|
this.pipe = pipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGoldMineScenePip() {
|
||||||
|
return this.pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置地热热突破流体粒子
|
||||||
|
setFlowParticles(flowParticles: FlowParticles | null) {
|
||||||
|
this.flowParticles = flowParticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取地热热突破流体粒子
|
||||||
|
getFlowParticles() {
|
||||||
|
return this.flowParticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置加载面存在状态
|
||||||
|
setLoadSurfaceExists(value: boolean) {
|
||||||
|
this.loadSurfaceExists = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取加载面存在状态
|
||||||
|
getLoadSurfaceExists() {
|
||||||
|
return this.loadSurfaceExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置剖切模式
|
||||||
|
setClippingMode(mode: ClippingMode) {
|
||||||
|
this.clippingMode = mode;
|
||||||
|
this.isClipping = mode !== null;
|
||||||
|
this.emit(BusEvents.CLIPPING_MODE_CHANGED, mode).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取剖切模式
|
||||||
|
getClippingMode() {
|
||||||
|
return this.clippingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setTemperatureLine(value:ParametricPipe){
|
||||||
|
this.temperatureLine = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTemperatureLine() {
|
||||||
|
return this.temperatureLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
5
packages/demo/src/hooks/index.ts
Normal file
5
packages/demo/src/hooks/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './Bus';
|
||||||
|
export * from './useBus';
|
||||||
|
export * from './useDebug';
|
||||||
|
export * from './useRockSample';
|
||||||
|
export * from './useSection';
|
||||||
8
packages/demo/src/hooks/useBus.ts
Normal file
8
packages/demo/src/hooks/useBus.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {getCurrentInstance} from "vue";
|
||||||
|
import type {Bus} from "./Bus.ts";
|
||||||
|
|
||||||
|
export function useBus(): Bus {
|
||||||
|
const {proxy} = getCurrentInstance()!
|
||||||
|
return proxy!.bus;
|
||||||
|
}
|
||||||
|
|
||||||
383
packages/demo/src/hooks/useDebug.ts
Normal file
383
packages/demo/src/hooks/useDebug.ts
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
import {SelectionManagerEvents, Tool} from "@deep/engine";
|
||||||
|
import type {Inspector} from "three/examples/jsm/inspector/Inspector";
|
||||||
|
import type {Value} from "three/examples/jsm/inspector/ui/Values";
|
||||||
|
import {Color} from "three/webgpu";
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {type Bus} from './Bus';
|
||||||
|
|
||||||
|
export function useDebug(bus: Bus) {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
let selectedObject: THREE.Object3D | null = null;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
positionX: 0,
|
||||||
|
positionY: 0,
|
||||||
|
positionZ: 0,
|
||||||
|
rotationX: 0,
|
||||||
|
rotationY: 0,
|
||||||
|
rotationZ: 0,
|
||||||
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
|
scaleZ: 1,
|
||||||
|
opacity: 1,
|
||||||
|
visible: true,
|
||||||
|
outline: false,
|
||||||
|
bloom: false,
|
||||||
|
bloomColor: new Color(),
|
||||||
|
renderOrder: 0,
|
||||||
|
color: new Color(0xffffff),
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 1,
|
||||||
|
depthTest: true,
|
||||||
|
depthWrite: true,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
};
|
||||||
|
|
||||||
|
const paramsValue: {
|
||||||
|
positionX: Value;
|
||||||
|
positionY: Value;
|
||||||
|
positionZ: Value;
|
||||||
|
rotationX: Value;
|
||||||
|
rotationY: Value;
|
||||||
|
rotationZ: Value;
|
||||||
|
scaleX: Value;
|
||||||
|
scaleY: Value;
|
||||||
|
scaleZ: Value;
|
||||||
|
opacity: Value;
|
||||||
|
visible: Value;
|
||||||
|
outline: Value;
|
||||||
|
bloom: Value;
|
||||||
|
bloomColor: Value;
|
||||||
|
renderOrder: Value;
|
||||||
|
color: Value;
|
||||||
|
metalness: Value;
|
||||||
|
roughness: Value;
|
||||||
|
depthTest: Value;
|
||||||
|
depthWrite: Value;
|
||||||
|
side: Value;
|
||||||
|
} = {} as any;
|
||||||
|
|
||||||
|
// 打开调试
|
||||||
|
viewer.openInspector();
|
||||||
|
|
||||||
|
// 添加 GUI 控件
|
||||||
|
const inspector = viewer.renderer!.inspector as Inspector;
|
||||||
|
|
||||||
|
// 创建对象控制面板
|
||||||
|
const gui = inspector.createParameters('对象控制');
|
||||||
|
gui.close();
|
||||||
|
|
||||||
|
// 位置控制文件夹
|
||||||
|
const positionFolder = gui.addFolder('位置');
|
||||||
|
|
||||||
|
paramsValue.positionX = positionFolder.add(params, 'positionX').name('位置x')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.position.x = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paramsValue.positionY = positionFolder.add(params, 'positionY').name('位置y')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.position.y = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paramsValue.positionZ = positionFolder.add(params, 'positionZ').name('位置z')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.position.z = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 旋转控制文件夹
|
||||||
|
const rotationFolder = gui.addFolder('旋转');
|
||||||
|
|
||||||
|
paramsValue.rotationX = rotationFolder.add(params, 'rotationX', -360, 360).name('旋转x')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.rotation.x = value * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paramsValue.rotationY = rotationFolder.add(params, 'rotationY', -360, 360).name('旋转y')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.rotation.y = value * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paramsValue.rotationZ = rotationFolder.add(params, 'rotationZ', -360, 360).name('旋转z')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.rotation.z = value * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 缩放控制文件夹
|
||||||
|
const scaleFolder = gui.addFolder('缩放');
|
||||||
|
|
||||||
|
paramsValue.scaleX = scaleFolder.add(params, 'scaleX', 0.1, 200).name('缩放x')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.scale.x = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paramsValue.scaleY = scaleFolder.add(params, 'scaleY', 0.1, 200).name('缩放y')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.scale.y = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paramsValue.scaleZ = scaleFolder.add(params, 'scaleZ', 0.1, 200).name('缩放z')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.scale.z = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 外观控制文件夹
|
||||||
|
const appearanceFolder = gui.addFolder('材质');
|
||||||
|
|
||||||
|
// 透明度控制
|
||||||
|
paramsValue.opacity = appearanceFolder.add(params, 'opacity', 0, 1).name('透明度')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
Tool.setOpacity(selectedObject, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示隐藏控制
|
||||||
|
paramsValue.visible = appearanceFolder.add(params, 'visible').name('显示')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.visible = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染次序控制
|
||||||
|
paramsValue.renderOrder = appearanceFolder.add(params, 'renderOrder', -5, 5, 1).name('渲染次序')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
selectedObject.renderOrder = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 材质颜色控制
|
||||||
|
paramsValue.color = appearanceFolder.addColor(params, 'color').name('颜色')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject && 'material' in selectedObject) {
|
||||||
|
const material = (selectedObject as any).material;
|
||||||
|
if (material && 'color' in material) {
|
||||||
|
material.color.copy(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 深度测试控制
|
||||||
|
paramsValue.depthTest = appearanceFolder.add(params, 'depthTest').name('深度测试')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject && 'material' in selectedObject) {
|
||||||
|
const material = (selectedObject as any).material;
|
||||||
|
if (material) {
|
||||||
|
material.depthTest = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 深度写入控制
|
||||||
|
paramsValue.depthWrite = appearanceFolder.add(params, 'depthWrite').name('深度写入')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject && 'material' in selectedObject) {
|
||||||
|
const material = (selectedObject as any).material;
|
||||||
|
if (material) {
|
||||||
|
material.depthWrite = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 效果控制文件夹
|
||||||
|
const effectsFolder = gui.addFolder('效果');
|
||||||
|
|
||||||
|
// 描边控制
|
||||||
|
paramsValue.outline = effectsFolder.add(params, 'outline').name('描边')
|
||||||
|
.onChange((value) => {
|
||||||
|
if (selectedObject && viewer.pipelineManager) {
|
||||||
|
if (value) {
|
||||||
|
viewer.pipelineManager.addSelectedObject(selectedObject);
|
||||||
|
} else {
|
||||||
|
viewer.pipelineManager.removeSelectedObject(selectedObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 高亮控制
|
||||||
|
paramsValue.bloom = effectsFolder.add(params, 'bloom').name('高亮')
|
||||||
|
.onChange((value) => {
|
||||||
|
console.log("设置");
|
||||||
|
if (selectedObject && viewer.pipelineManager) {
|
||||||
|
if (value) {
|
||||||
|
viewer.pipelineManager.addHighlightedObject(selectedObject);
|
||||||
|
} else {
|
||||||
|
viewer.pipelineManager.removeHighlightedObject(selectedObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 高亮颜色控制
|
||||||
|
paramsValue.bloomColor = effectsFolder.addColor(params, 'bloomColor').name('高亮颜色')
|
||||||
|
.onChange((value) => {
|
||||||
|
updateHighlightColor();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 控制设置文件夹
|
||||||
|
const controlFolder = gui.addFolder('控制');
|
||||||
|
|
||||||
|
// 控制状态
|
||||||
|
const controlParams = {
|
||||||
|
enableControl: false,
|
||||||
|
controlMode: 'translate',
|
||||||
|
printTransform: () => {
|
||||||
|
if (selectedObject) {
|
||||||
|
const transformInfo = {
|
||||||
|
position: [selectedObject.position.x, selectedObject.position.y, selectedObject.position.z],
|
||||||
|
rotation: [selectedObject.rotation.x, selectedObject.rotation.y, selectedObject.rotation.z],
|
||||||
|
scale: [selectedObject.scale.x, selectedObject.scale.y, selectedObject.scale.z]
|
||||||
|
};
|
||||||
|
console.log('模型变换信息:', transformInfo);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
printCameraState: () => {
|
||||||
|
const cameraState = viewer.getCameraState();
|
||||||
|
console.log('相机状态:', cameraState);
|
||||||
|
},
|
||||||
|
addTestBox: () => {
|
||||||
|
// 创建测试box
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||||||
|
const material = new THREE.MeshPhysicalMaterial({color: 0x00ff00});
|
||||||
|
const box = new THREE.Mesh(geometry, material);
|
||||||
|
box.position.set(0, 0, 0);
|
||||||
|
box.name = "Test Box";
|
||||||
|
viewer.scene.add(box);
|
||||||
|
|
||||||
|
// 派发树更新
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
|
||||||
|
// 选择对象
|
||||||
|
viewer!.selection!.setSelectedObject(box);
|
||||||
|
},
|
||||||
|
exportSceneJSON: () => {
|
||||||
|
viewer.downloadSceneJSON();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开启/关闭控制
|
||||||
|
controlFolder.add(controlParams, 'enableControl').name('开启控制')
|
||||||
|
.onChange((value) => {
|
||||||
|
viewer.selection.isControl = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 控制模式
|
||||||
|
controlFolder.add(controlParams, 'controlMode', ['translate', 'rotate', 'scale']).name('控制模式')
|
||||||
|
.onChange((value) => {
|
||||||
|
viewer.selection.setTransformMode(value as 'translate' | 'rotate' | 'scale');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加测试Box
|
||||||
|
controlFolder.add(controlParams, 'addTestBox').name('添加测试Box');
|
||||||
|
|
||||||
|
// 打印变换信息
|
||||||
|
controlFolder.add(controlParams, 'printTransform').name('打印变换信息');
|
||||||
|
|
||||||
|
// 打印相机状态
|
||||||
|
controlFolder.add(controlParams, 'printCameraState').name('打印相机状态');
|
||||||
|
|
||||||
|
// 导出场景JSON
|
||||||
|
controlFolder.add(controlParams, 'exportSceneJSON').name('导出场景JSON');
|
||||||
|
|
||||||
|
// 更新高亮颜色
|
||||||
|
function updateHighlightColor() {
|
||||||
|
if (selectedObject && viewer.pipelineManager && params.bloom) {
|
||||||
|
const color = new THREE.Color(params.bloomColor.r, params.bloomColor.g, params.bloomColor.b);
|
||||||
|
viewer.pipelineManager.removeHighlightedObject(selectedObject);
|
||||||
|
viewer.pipelineManager.addHighlightedObject(selectedObject, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听选择事件
|
||||||
|
viewer.selection.on(SelectionManagerEvents.OBJECT_SELECTED, ({data}) => {
|
||||||
|
selectedObject = data || null;
|
||||||
|
if (selectedObject) {
|
||||||
|
|
||||||
|
// 更新位置
|
||||||
|
paramsValue.positionX.setValue(selectedObject.position.x);
|
||||||
|
paramsValue.positionY.setValue(selectedObject.position.y);
|
||||||
|
paramsValue.positionZ.setValue(selectedObject.position.z);
|
||||||
|
|
||||||
|
// 更新旋转
|
||||||
|
paramsValue.rotationX.setValue(selectedObject.rotation.x * (180 / Math.PI));
|
||||||
|
paramsValue.rotationY.setValue(selectedObject.rotation.y * (180 / Math.PI));
|
||||||
|
paramsValue.rotationZ.setValue(selectedObject.rotation.z * (180 / Math.PI));
|
||||||
|
|
||||||
|
// 更新缩放
|
||||||
|
paramsValue.scaleX.setValue(selectedObject.scale.x);
|
||||||
|
paramsValue.scaleY.setValue(selectedObject.scale.y);
|
||||||
|
paramsValue.scaleZ.setValue(selectedObject.scale.z);
|
||||||
|
|
||||||
|
// 更新透明度
|
||||||
|
if ('material' in selectedObject) {
|
||||||
|
const material = (selectedObject as any).material;
|
||||||
|
if (material && typeof material.opacity === 'number') {
|
||||||
|
paramsValue.opacity.setValue(material.opacity);
|
||||||
|
}
|
||||||
|
// 更新深度测试
|
||||||
|
if (material && typeof material.depthTest === 'boolean') {
|
||||||
|
paramsValue.depthTest.checkbox.checked = material.depthTest;
|
||||||
|
}
|
||||||
|
// 更新深度写入
|
||||||
|
if (material && typeof material.depthWrite === 'boolean') {
|
||||||
|
paramsValue.depthWrite.checkbox.checked = material.depthWrite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新渲染次序
|
||||||
|
paramsValue.renderOrder.setValue(selectedObject.renderOrder);
|
||||||
|
|
||||||
|
// 更新显示状态
|
||||||
|
paramsValue.visible.checkbox.checked = selectedObject.visible;
|
||||||
|
console.log(selectedObject);
|
||||||
|
// 更新描边和高亮状态
|
||||||
|
if (viewer.pipelineManager) {
|
||||||
|
paramsValue.outline.checkbox.checked = viewer.pipelineManager.isObjectInOutlineList(selectedObject);
|
||||||
|
paramsValue.bloom.checkbox.checked = viewer.pipelineManager.isObjectInBloomList(selectedObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewer.selection.on(SelectionManagerEvents.OBJECT_UNSELECTED, () => {
|
||||||
|
selectedObject = null;
|
||||||
|
// 重置控件状态
|
||||||
|
paramsValue.positionX.setValue(0);
|
||||||
|
paramsValue.positionY.setValue(0);
|
||||||
|
paramsValue.positionZ.setValue(0);
|
||||||
|
paramsValue.rotationX.setValue(0);
|
||||||
|
paramsValue.rotationY.setValue(0);
|
||||||
|
paramsValue.rotationZ.setValue(0);
|
||||||
|
paramsValue.scaleX.setValue(1);
|
||||||
|
paramsValue.scaleY.setValue(1);
|
||||||
|
paramsValue.scaleZ.setValue(1);
|
||||||
|
paramsValue.opacity.setValue(1);
|
||||||
|
paramsValue.visible.setValue(true);
|
||||||
|
paramsValue.outline.setValue(false);
|
||||||
|
paramsValue.bloom.setValue(false);
|
||||||
|
paramsValue.renderOrder.setValue(0);
|
||||||
|
paramsValue.depthTest.setValue(true);
|
||||||
|
paramsValue.depthWrite.setValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
25
packages/demo/src/hooks/useRockSample.ts
Normal file
25
packages/demo/src/hooks/useRockSample.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {createRock6Material, ParametricBox} from "@deep/engine";
|
||||||
|
import type {Bus} from "./Bus";
|
||||||
|
|
||||||
|
export const useRockSample = (bus: Bus): ParametricBox => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
const boxMaterial = createRock6Material({
|
||||||
|
opacity: 0.6,
|
||||||
|
// depthWrite:false
|
||||||
|
});
|
||||||
|
|
||||||
|
const box = new ParametricBox({
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
depth: 5,
|
||||||
|
material: boxMaterial
|
||||||
|
});
|
||||||
|
|
||||||
|
box.name = "岩样";
|
||||||
|
viewer.scene.add(box);
|
||||||
|
|
||||||
|
bus.setRockSample(box);
|
||||||
|
|
||||||
|
return box;
|
||||||
|
};
|
||||||
33
packages/demo/src/hooks/useSection.ts
Normal file
33
packages/demo/src/hooks/useSection.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {createRock2Material, ParametricBox} from "@deep/engine";
|
||||||
|
import type {Bus} from "./Bus";
|
||||||
|
|
||||||
|
interface SectionOptions {
|
||||||
|
position: { x: number; y: number; z: number };
|
||||||
|
rotation: { x: number; y: number; z: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useSection = (bus: Bus, options: SectionOptions): ParametricBox => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
const section = new ParametricBox({
|
||||||
|
width: 0.1,
|
||||||
|
height: 4.8,
|
||||||
|
depth: 4.7,
|
||||||
|
material: createRock2Material({}, false)
|
||||||
|
});
|
||||||
|
section.name = "断面";
|
||||||
|
|
||||||
|
// Set position
|
||||||
|
section.position.set(options.position.x, options.position.y, options.position.z);
|
||||||
|
|
||||||
|
// Set rotation
|
||||||
|
section.rotation.set(options.rotation.x, options.rotation.y, options.rotation.z);
|
||||||
|
|
||||||
|
// Set scale
|
||||||
|
section.scale.set(1, 1, 1);
|
||||||
|
|
||||||
|
viewer.scene.add(section);
|
||||||
|
bus.setSection(section);
|
||||||
|
return section;
|
||||||
|
};
|
||||||
298
packages/demo/src/htmlPanel/GoldMineScene/AirSupplyHtmlPanel.ts
Normal file
298
packages/demo/src/htmlPanel/GoldMineScene/AirSupplyHtmlPanel.ts
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送风信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IAirSupplyData {
|
||||||
|
/** 进度百分比 (0-100) */
|
||||||
|
progress: number;
|
||||||
|
/** 当前时间 (秒) */
|
||||||
|
currentTime: number;
|
||||||
|
/** 结束时间 (秒) */
|
||||||
|
endTime: number;
|
||||||
|
/** 压力 (MPa) */
|
||||||
|
pressure: number;
|
||||||
|
/** 流量 (m³/min) */
|
||||||
|
flowRate: number;
|
||||||
|
/** 温度 (°C) */
|
||||||
|
temperature: number;
|
||||||
|
/** 湿度 (%) */
|
||||||
|
humidity: number;
|
||||||
|
/** 状态 */
|
||||||
|
status: 'running' | 'completed' | 'stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送风信息面板类
|
||||||
|
*/
|
||||||
|
export class AirSupplyHtmlPanel extends HtmlPanel {
|
||||||
|
private airSupplyData: IAirSupplyData = {
|
||||||
|
progress: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
pressure: 1.2,
|
||||||
|
flowRate: 150,
|
||||||
|
temperature: 25,
|
||||||
|
humidity: 60,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新送风数据
|
||||||
|
*/
|
||||||
|
public updateData(data: Partial<IAirSupplyData>): void {
|
||||||
|
this.airSupplyData = {...this.airSupplyData, ...data};
|
||||||
|
this.updateElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.airSupplyData.status === 'running' ? '#00ff00' :
|
||||||
|
this.airSupplyData.status === 'completed' ? '#4CAF50' : '#FF9800';
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #00ff00 0%, #00cc00 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 320px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
💨 进风井
|
||||||
|
</h3>
|
||||||
|
<span id="status-${this.uuid}" style="
|
||||||
|
background: ${statusInfo.color};
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
${statusInfo.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 参数网格 -->
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
">
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">压力</div>
|
||||||
|
<div id="pressure-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.airSupplyData.pressure.toFixed(1)} <span style="font-size: 12px; opacity: 0.8;">MPa</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">流量</div>
|
||||||
|
<div id="flow-rate-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.airSupplyData.flowRate} <span style="font-size: 12px; opacity: 0.8;">m³/min</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">温度</div>
|
||||||
|
<div id="temperature-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.airSupplyData.temperature} <span style="font-size: 12px; opacity: 0.8;">°C</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">湿度</div>
|
||||||
|
<div id="humidity-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.airSupplyData.humidity} <span style="font-size: 12px; opacity: 0.8;">%</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 运行时间 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⏱️ 运行时间</span>
|
||||||
|
</div>
|
||||||
|
<div id="time-${this.uuid}" style="font-size: 20px; font-weight: 700; color: #FFD700;">
|
||||||
|
${this.airSupplyData.currentTime.toFixed(1)} / ${this.airSupplyData.endTime.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div style="margin-bottom: 0;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 完成进度</span>
|
||||||
|
<span id="progress-text-${this.uuid}" style="font-size: 14px; font-weight: 600;">${this.airSupplyData.progress.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
">
|
||||||
|
<div id="progress-bar-${this.uuid}" style="
|
||||||
|
background: ${progressBarColor};
|
||||||
|
height: 100%;
|
||||||
|
width: ${this.airSupplyData.progress}%;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新DOM元素
|
||||||
|
*/
|
||||||
|
private updateElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.airSupplyData.status === 'running' ? '#00ff00' :
|
||||||
|
this.airSupplyData.status === 'completed' ? '#4CAF50' : '#FF9800';
|
||||||
|
|
||||||
|
// 重新附加删除按钮事件(防止内容更新后事件丢失)
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = document.getElementById(`status-${this.uuid}`) as HTMLElement;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = statusInfo.text;
|
||||||
|
statusEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新压力
|
||||||
|
const pressureEl = document.getElementById(`pressure-${this.uuid}`);
|
||||||
|
if (pressureEl) {
|
||||||
|
pressureEl.innerHTML = `${this.airSupplyData.pressure.toFixed(1)} <span style="font-size: 12px; opacity: 0.8;">MPa</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新流量
|
||||||
|
const flowRateEl = document.getElementById(`flow-rate-${this.uuid}`);
|
||||||
|
if (flowRateEl) {
|
||||||
|
flowRateEl.innerHTML = `${this.airSupplyData.flowRate} <span style="font-size: 12px; opacity: 0.8;">m³/min</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新温度
|
||||||
|
const temperatureEl = document.getElementById(`temperature-${this.uuid}`);
|
||||||
|
if (temperatureEl) {
|
||||||
|
temperatureEl.innerHTML = `${this.airSupplyData.temperature} <span style="font-size: 12px; opacity: 0.8;">°C</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新湿度
|
||||||
|
const humidityEl = document.getElementById(`humidity-${this.uuid}`);
|
||||||
|
if (humidityEl) {
|
||||||
|
humidityEl.innerHTML = `${this.airSupplyData.humidity} <span style="font-size: 12px; opacity: 0.8;">%</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新运行时间
|
||||||
|
const timeEl = document.getElementById(`time-${this.uuid}`);
|
||||||
|
if (timeEl) {
|
||||||
|
timeEl.innerHTML = `${this.airSupplyData.currentTime.toFixed(1)} / ${this.airSupplyData.endTime.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">秒</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度文本
|
||||||
|
const progressTextEl = document.getElementById(`progress-text-${this.uuid}`);
|
||||||
|
if (progressTextEl) {
|
||||||
|
progressTextEl.textContent = `${this.airSupplyData.progress.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const progressBarEl = document.getElementById(`progress-bar-${this.uuid}`) as HTMLElement;
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.style.width = `${this.airSupplyData.progress}%`;
|
||||||
|
progressBarEl.style.background = progressBarColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.airSupplyData.status) {
|
||||||
|
case 'running':
|
||||||
|
return {text: '送风中', color: '#00ff00'};
|
||||||
|
case 'completed':
|
||||||
|
return {text: '已完成', color: '#4CAF50'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
283
packages/demo/src/htmlPanel/GoldMineScene/DrillingHtmlPanel.ts
Normal file
283
packages/demo/src/htmlPanel/GoldMineScene/DrillingHtmlPanel.ts
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钻进信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IDrillingData {
|
||||||
|
/** 管道名称 */
|
||||||
|
pipeName: string;
|
||||||
|
/** 当前钻进速率 */
|
||||||
|
drillingSpeed: number;
|
||||||
|
/** 当前深度 (m) */
|
||||||
|
currentDepth: number;
|
||||||
|
/** 目标深度 (m) */
|
||||||
|
targetDepth: number;
|
||||||
|
/** 进度百分比 (0-100) */
|
||||||
|
progress: number;
|
||||||
|
/** 管道半径 (m) */
|
||||||
|
pipeRadius: number;
|
||||||
|
/** 钻进状态 */
|
||||||
|
status: 'drilling' | 'completed' | 'stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钻进信息面板类
|
||||||
|
*/
|
||||||
|
export class DrillingHtmlPanel extends HtmlPanel {
|
||||||
|
private drillingData: IDrillingData = {
|
||||||
|
pipeName: '',
|
||||||
|
drillingSpeed: 0,
|
||||||
|
currentDepth: 0,
|
||||||
|
targetDepth: 0,
|
||||||
|
progress: 0,
|
||||||
|
pipeRadius: 0,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新钻进数据
|
||||||
|
* @param data 新的钻进数据
|
||||||
|
*/
|
||||||
|
updateData(data: Partial<IDrillingData>): void {
|
||||||
|
this.drillingData = {
|
||||||
|
...this.drillingData,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
this.updateElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.drillingData.status === 'drilling' ? '#4CAF50' :
|
||||||
|
this.drillingData.status === 'completed' ? '#2196F3' : '#FF9800';
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 280px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 id="title-${this.uuid}" style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
🔧 ${this.drillingData.pipeName || '钻进信息'}
|
||||||
|
</h3>
|
||||||
|
<span id="status-${this.uuid}" style="
|
||||||
|
background: ${statusInfo.color};
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
${statusInfo.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 钻进速率 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⚡ 钻进速率</span>
|
||||||
|
</div>
|
||||||
|
<div id="drilling-speed-${this.uuid}" style="font-size: 24px; font-weight: 700; color: #FFD700;">
|
||||||
|
${this.drillingData.drillingSpeed.toFixed(3)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 深度信息 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📏 当前深度</span>
|
||||||
|
</div>
|
||||||
|
<div id="depth-${this.uuid}" style="font-size: 20px; font-weight: 600;">
|
||||||
|
${this.drillingData.currentDepth.toFixed(2)} / ${this.drillingData.targetDepth.toFixed(2)} <span style="font-size: 14px; opacity: 0.8;">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 完成进度</span>
|
||||||
|
<span id="progress-text-${this.uuid}" style="font-size: 14px; font-weight: 600;">${this.drillingData.progress.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
">
|
||||||
|
<div id="progress-bar-${this.uuid}" style="
|
||||||
|
background: ${progressBarColor};
|
||||||
|
height: 100%;
|
||||||
|
width: ${this.drillingData.progress}%;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 管道半径 -->
|
||||||
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
|
||||||
|
<div style="display: flex; justify-content: space-between; font-size: 13px;">
|
||||||
|
<span style="opacity: 0.9;">🔵 管道半径</span>
|
||||||
|
<span id="pipe-radius-${this.uuid}" style="font-weight: 600;">${this.drillingData.pipeRadius.toFixed(2)} m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新DOM元素
|
||||||
|
*/
|
||||||
|
private updateElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.drillingData.status === 'drilling' ? '#4CAF50' :
|
||||||
|
this.drillingData.status === 'completed' ? '#2196F3' : '#FF9800';
|
||||||
|
|
||||||
|
// 更新标题
|
||||||
|
const titleEl = document.getElementById(`title-${this.uuid}`);
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = `🔧 ${this.drillingData.pipeName || '钻进信息'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = document.getElementById(`status-${this.uuid}`) as HTMLElement;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = statusInfo.text;
|
||||||
|
statusEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新钻进速率
|
||||||
|
const drillingSpeedEl = document.getElementById(`drilling-speed-${this.uuid}`);
|
||||||
|
if (drillingSpeedEl) {
|
||||||
|
drillingSpeedEl.textContent = this.drillingData.drillingSpeed.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新深度信息
|
||||||
|
const depthEl = document.getElementById(`depth-${this.uuid}`);
|
||||||
|
if (depthEl) {
|
||||||
|
depthEl.innerHTML = `${this.drillingData.currentDepth.toFixed(2)} / ${this.drillingData.targetDepth.toFixed(2)} <span style="font-size: 14px; opacity: 0.8;">m</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度文本
|
||||||
|
const progressTextEl = document.getElementById(`progress-text-${this.uuid}`);
|
||||||
|
if (progressTextEl) {
|
||||||
|
progressTextEl.textContent = `${this.drillingData.progress.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const progressBarEl = document.getElementById(`progress-bar-${this.uuid}`) as HTMLElement;
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.style.width = `${this.drillingData.progress}%`;
|
||||||
|
progressBarEl.style.background = progressBarColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新管道半径
|
||||||
|
const pipeRadiusEl = document.getElementById(`pipe-radius-${this.uuid}`);
|
||||||
|
if (pipeRadiusEl) {
|
||||||
|
pipeRadiusEl.textContent = `${this.drillingData.pipeRadius.toFixed(2)} m`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.drillingData.status) {
|
||||||
|
case 'drilling':
|
||||||
|
return {text: '钻进中', color: '#4CAF50'};
|
||||||
|
case 'completed':
|
||||||
|
return {text: '已完成', color: '#2196F3'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
285
packages/demo/src/htmlPanel/GoldMineScene/FillingHtmlPanel.ts
Normal file
285
packages/demo/src/htmlPanel/GoldMineScene/FillingHtmlPanel.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 充填信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IFillingData {
|
||||||
|
/** 进度百分比 (0-100) */
|
||||||
|
progress: number;
|
||||||
|
/** 当前时间 (秒) */
|
||||||
|
currentTime: number;
|
||||||
|
/** 结束时间 (秒) */
|
||||||
|
endTime: number;
|
||||||
|
/** 充填材料 */
|
||||||
|
material: string;
|
||||||
|
/** 充填速率 (m³/h) */
|
||||||
|
fillingRate: number;
|
||||||
|
/** 充填压力 (MPa) */
|
||||||
|
pressure: number;
|
||||||
|
/** 状态 */
|
||||||
|
status: 'running' | 'completed' | 'stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 充填信息面板类
|
||||||
|
*/
|
||||||
|
export class FillingHtmlPanel extends HtmlPanel {
|
||||||
|
private fillingData: IFillingData = {
|
||||||
|
progress: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
material: '尾砂胶结',
|
||||||
|
fillingRate: 80,
|
||||||
|
pressure: 1.5,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新充填数据
|
||||||
|
*/
|
||||||
|
public updateData(data: Partial<IFillingData>): void {
|
||||||
|
this.fillingData = {...this.fillingData, ...data};
|
||||||
|
this.updateElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.fillingData.status === 'running' ? '#00bfff' :
|
||||||
|
this.fillingData.status === 'completed' ? '#4CAF50' : '#FF9800';
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #00bfff 0%, #0099cc 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 280px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
🏗️ 充填井
|
||||||
|
</h3>
|
||||||
|
<span id="status-${this.uuid}" style="
|
||||||
|
background: ${statusInfo.color};
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
${statusInfo.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 参数网格 -->
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
">
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px; grid-column: 1 / -1;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">充填材料</div>
|
||||||
|
<div id="material-${this.uuid}" style="font-size: 16px; font-weight: 700;">${this.fillingData.material}</div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">充填速率</div>
|
||||||
|
<div id="filling-rate-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.fillingData.fillingRate} <span style="font-size: 12px; opacity: 0.8;">m³/h</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">充填压力</div>
|
||||||
|
<div id="pressure-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.fillingData.pressure.toFixed(1)} <span style="font-size: 12px; opacity: 0.8;">MPa</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 运行时间 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⏱️ 运行时间</span>
|
||||||
|
</div>
|
||||||
|
<div id="time-${this.uuid}" style="font-size: 24px; font-weight: 700; color: #FFD700;">
|
||||||
|
${this.fillingData.currentTime.toFixed(1)} / ${this.fillingData.endTime.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div style="margin-bottom: 0;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 完成进度</span>
|
||||||
|
<span id="progress-text-${this.uuid}" style="font-size: 14px; font-weight: 600;">${this.fillingData.progress.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
">
|
||||||
|
<div id="progress-bar-${this.uuid}" style="
|
||||||
|
background: ${progressBarColor};
|
||||||
|
height: 100%;
|
||||||
|
width: ${this.fillingData.progress}%;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新DOM元素
|
||||||
|
*/
|
||||||
|
private updateElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.fillingData.status === 'running' ? '#00bfff' :
|
||||||
|
this.fillingData.status === 'completed' ? '#4CAF50' : '#FF9800';
|
||||||
|
|
||||||
|
// 重新附加删除按钮事件(防止内容更新后事件丢失)
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = document.getElementById(`status-${this.uuid}`) as HTMLElement;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = statusInfo.text;
|
||||||
|
statusEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新充填材料
|
||||||
|
const materialEl = document.getElementById(`material-${this.uuid}`);
|
||||||
|
if (materialEl) {
|
||||||
|
materialEl.textContent = this.fillingData.material;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新充填速率
|
||||||
|
const fillingRateEl = document.getElementById(`filling-rate-${this.uuid}`);
|
||||||
|
if (fillingRateEl) {
|
||||||
|
fillingRateEl.innerHTML = `${this.fillingData.fillingRate} <span style="font-size: 12px; opacity: 0.8;">m³/h</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新充填压力
|
||||||
|
const pressureEl = document.getElementById(`pressure-${this.uuid}`);
|
||||||
|
if (pressureEl) {
|
||||||
|
pressureEl.innerHTML = `${this.fillingData.pressure.toFixed(1)} <span style="font-size: 12px; opacity: 0.8;">MPa</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新运行时间
|
||||||
|
const timeEl = document.getElementById(`time-${this.uuid}`);
|
||||||
|
if (timeEl) {
|
||||||
|
timeEl.innerHTML = `${this.fillingData.currentTime.toFixed(1)} / ${this.fillingData.endTime.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">秒</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度文本
|
||||||
|
const progressTextEl = document.getElementById(`progress-text-${this.uuid}`);
|
||||||
|
if (progressTextEl) {
|
||||||
|
progressTextEl.textContent = `${this.fillingData.progress.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const progressBarEl = document.getElementById(`progress-bar-${this.uuid}`) as HTMLElement;
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.style.width = `${this.fillingData.progress}%`;
|
||||||
|
progressBarEl.style.background = progressBarColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.fillingData.status) {
|
||||||
|
case 'running':
|
||||||
|
return {text: '充填中', color: '#00bfff'};
|
||||||
|
case 'completed':
|
||||||
|
return {text: '已完成', color: '#4CAF50'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
298
packages/demo/src/htmlPanel/GoldMineScene/ReturnAirHtmlPanel.ts
Normal file
298
packages/demo/src/htmlPanel/GoldMineScene/ReturnAirHtmlPanel.ts
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回风信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IReturnAirData {
|
||||||
|
/** 进度百分比 (0-100) */
|
||||||
|
progress: number;
|
||||||
|
/** 当前时间 (秒) */
|
||||||
|
currentTime: number;
|
||||||
|
/** 结束时间 (秒) */
|
||||||
|
endTime: number;
|
||||||
|
/** 压力 (MPa) */
|
||||||
|
pressure: number;
|
||||||
|
/** 流量 (m³/min) */
|
||||||
|
flowRate: number;
|
||||||
|
/** 温度 (°C) */
|
||||||
|
temperature: number;
|
||||||
|
/** 湿度 (%) */
|
||||||
|
humidity: number;
|
||||||
|
/** 状态 */
|
||||||
|
status: 'running' | 'completed' | 'stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回风信息面板类
|
||||||
|
*/
|
||||||
|
export class ReturnAirHtmlPanel extends HtmlPanel {
|
||||||
|
private returnAirData: IReturnAirData = {
|
||||||
|
progress: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
pressure: 0.8,
|
||||||
|
flowRate: 140,
|
||||||
|
temperature: 28,
|
||||||
|
humidity: 65,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新回风数据
|
||||||
|
*/
|
||||||
|
public updateData(data: Partial<IReturnAirData>): void {
|
||||||
|
this.returnAirData = {...this.returnAirData, ...data};
|
||||||
|
this.updateElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.returnAirData.status === 'running' ? '#ffa500' :
|
||||||
|
this.returnAirData.status === 'completed' ? '#4CAF50' : '#FF9800';
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #ffa500 0%, #ff8c00 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 320px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
🌪️ 回风井
|
||||||
|
</h3>
|
||||||
|
<span id="status-${this.uuid}" style="
|
||||||
|
background: ${statusInfo.color};
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
${statusInfo.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 参数网格 -->
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
">
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">压力</div>
|
||||||
|
<div id="pressure-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.returnAirData.pressure.toFixed(1)} <span style="font-size: 12px; opacity: 0.8;">MPa</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">流量</div>
|
||||||
|
<div id="flow-rate-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.returnAirData.flowRate} <span style="font-size: 12px; opacity: 0.8;">m³/min</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">温度</div>
|
||||||
|
<div id="temperature-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.returnAirData.temperature} <span style="font-size: 12px; opacity: 0.8;">°C</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 6px; padding: 10px;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 4px;">湿度</div>
|
||||||
|
<div id="humidity-${this.uuid}" style="font-size: 18px; font-weight: 700;">${this.returnAirData.humidity} <span style="font-size: 12px; opacity: 0.8;">%</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 运行时间 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⏱️ 运行时间</span>
|
||||||
|
</div>
|
||||||
|
<div id="time-${this.uuid}" style="font-size: 20px; font-weight: 700; color: #FFD700;">
|
||||||
|
${this.returnAirData.currentTime.toFixed(1)} / ${this.returnAirData.endTime.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div style="margin-bottom: 0;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 完成进度</span>
|
||||||
|
<span id="progress-text-${this.uuid}" style="font-size: 14px; font-weight: 600;">${this.returnAirData.progress.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
">
|
||||||
|
<div id="progress-bar-${this.uuid}" style="
|
||||||
|
background: ${progressBarColor};
|
||||||
|
height: 100%;
|
||||||
|
width: ${this.returnAirData.progress}%;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新DOM元素
|
||||||
|
*/
|
||||||
|
private updateElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
const progressBarColor = this.returnAirData.status === 'running' ? '#ffa500' :
|
||||||
|
this.returnAirData.status === 'completed' ? '#4CAF50' : '#FF9800';
|
||||||
|
|
||||||
|
// 重新附加删除按钮事件(防止内容更新后事件丢失)
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = document.getElementById(`status-${this.uuid}`) as HTMLElement;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = statusInfo.text;
|
||||||
|
statusEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新压力
|
||||||
|
const pressureEl = document.getElementById(`pressure-${this.uuid}`);
|
||||||
|
if (pressureEl) {
|
||||||
|
pressureEl.innerHTML = `${this.returnAirData.pressure.toFixed(1)} <span style="font-size: 12px; opacity: 0.8;">MPa</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新流量
|
||||||
|
const flowRateEl = document.getElementById(`flow-rate-${this.uuid}`);
|
||||||
|
if (flowRateEl) {
|
||||||
|
flowRateEl.innerHTML = `${this.returnAirData.flowRate} <span style="font-size: 12px; opacity: 0.8;">m³/min</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新温度
|
||||||
|
const temperatureEl = document.getElementById(`temperature-${this.uuid}`);
|
||||||
|
if (temperatureEl) {
|
||||||
|
temperatureEl.innerHTML = `${this.returnAirData.temperature} <span style="font-size: 12px; opacity: 0.8;">°C</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新湿度
|
||||||
|
const humidityEl = document.getElementById(`humidity-${this.uuid}`);
|
||||||
|
if (humidityEl) {
|
||||||
|
humidityEl.innerHTML = `${this.returnAirData.humidity} <span style="font-size: 12px; opacity: 0.8;">%</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新运行时间
|
||||||
|
const timeEl = document.getElementById(`time-${this.uuid}`);
|
||||||
|
if (timeEl) {
|
||||||
|
timeEl.innerHTML = `${this.returnAirData.currentTime.toFixed(1)} / ${this.returnAirData.endTime.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">秒</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度文本
|
||||||
|
const progressTextEl = document.getElementById(`progress-text-${this.uuid}`);
|
||||||
|
if (progressTextEl) {
|
||||||
|
progressTextEl.textContent = `${this.returnAirData.progress.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const progressBarEl = document.getElementById(`progress-bar-${this.uuid}`) as HTMLElement;
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.style.width = `${this.returnAirData.progress}%`;
|
||||||
|
progressBarEl.style.background = progressBarColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.returnAirData.status) {
|
||||||
|
case 'running':
|
||||||
|
return {text: '回风中', color: '#ffa500'};
|
||||||
|
case 'completed':
|
||||||
|
return {text: '已完成', color: '#4CAF50'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
packages/demo/src/htmlPanel/OilGasScene/InjectionHtmlPanel.ts
Normal file
276
packages/demo/src/htmlPanel/OilGasScene/InjectionHtmlPanel.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IInjectionData {
|
||||||
|
/** 井名称 */
|
||||||
|
wellName: string;
|
||||||
|
/** 注入流量 (m³/s) */
|
||||||
|
flowRate: number;
|
||||||
|
/** 注入速率 (m/s) */
|
||||||
|
injectionRate: number;
|
||||||
|
/** 注入压力 (MPa) */
|
||||||
|
pressure: number;
|
||||||
|
/** 注入体积 (m³) */
|
||||||
|
volume: number;
|
||||||
|
/** 流体温度 (°C) */
|
||||||
|
temperature: number;
|
||||||
|
/** 注入状态 */
|
||||||
|
status: 'injecting' | 'stopped' | 'maintenance';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入信息面板类
|
||||||
|
*/
|
||||||
|
export class InjectionHtmlPanel extends HtmlPanel {
|
||||||
|
private injectionData: IInjectionData = {
|
||||||
|
wellName: '',
|
||||||
|
flowRate: 0,
|
||||||
|
injectionRate: 0,
|
||||||
|
pressure: 0,
|
||||||
|
volume: 0,
|
||||||
|
temperature: 0,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新注入数据
|
||||||
|
* @param data 新的注入数据
|
||||||
|
*/
|
||||||
|
updateData(data: Partial<IInjectionData>): void {
|
||||||
|
this.injectionData = {
|
||||||
|
...this.injectionData,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
this.updateElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 id="title-${this.uuid}" style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
💉 注入信息
|
||||||
|
</h3>
|
||||||
|
<span id="status-${this.uuid}" style="
|
||||||
|
background: #FF9800;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
已停止
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content-${this.uuid}" style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 注入流量 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">💧 注入流量</span>
|
||||||
|
</div>
|
||||||
|
<div id="flow-rate-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #4FC3F7;">
|
||||||
|
0.00 <span style="font-size: 14px; opacity: 0.8;">m³/s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注入速率 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⚡ 注入速度</span>
|
||||||
|
</div>
|
||||||
|
<div id="injection-rate-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #81C784;">
|
||||||
|
0.0 <span style="font-size: 14px; opacity: 0.8;">m/s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注入压力 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 注入压力</span>
|
||||||
|
</div>
|
||||||
|
<div id="pressure-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #FFB74D;">
|
||||||
|
0.0 <span style="font-size: 14px; opacity: 0.8;">MPa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注入体积 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📦 注入体积</span>
|
||||||
|
</div>
|
||||||
|
<div id="volume-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #BA68C8;">
|
||||||
|
0.0 <span style="font-size: 14px; opacity: 0.8;">m³</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 流体温度 -->
|
||||||
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">🌡️ 流体温度</span>
|
||||||
|
<span id="temperature-${this.uuid}" style="font-size: 20px; font-weight: 700; color: #FF7043;">
|
||||||
|
0 <span style="font-size: 14px; opacity: 0.8;">°C</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新DOM元素
|
||||||
|
*/
|
||||||
|
private updateElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
|
||||||
|
// 更新标题
|
||||||
|
const titleEl = document.getElementById(`title-${this.uuid}`);
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = `💉 ${this.injectionData.wellName || '注入信息'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = document.getElementById(`status-${this.uuid}`) as HTMLElement;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = statusInfo.text;
|
||||||
|
statusEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新注入流量
|
||||||
|
const flowRateEl = document.getElementById(`flow-rate-${this.uuid}`);
|
||||||
|
if (flowRateEl) {
|
||||||
|
flowRateEl.innerHTML = `${this.injectionData.flowRate.toFixed(2)} <span style="font-size: 14px; opacity: 0.8;">m³/s</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新注入速率
|
||||||
|
const injectionRateEl = document.getElementById(`injection-rate-${this.uuid}`);
|
||||||
|
if (injectionRateEl) {
|
||||||
|
injectionRateEl.innerHTML = `${this.injectionData.injectionRate.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">m/s</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新注入压力
|
||||||
|
const pressureEl = document.getElementById(`pressure-${this.uuid}`);
|
||||||
|
if (pressureEl) {
|
||||||
|
pressureEl.innerHTML = `${this.injectionData.pressure.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">MPa</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新注入体积
|
||||||
|
const volumeEl = document.getElementById(`volume-${this.uuid}`);
|
||||||
|
if (volumeEl) {
|
||||||
|
volumeEl.innerHTML = `${this.injectionData.volume.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">m³</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新流体温度
|
||||||
|
const temperatureEl = document.getElementById(`temperature-${this.uuid}`);
|
||||||
|
if (temperatureEl) {
|
||||||
|
temperatureEl.innerHTML = `${this.injectionData.temperature.toFixed(0)} <span style="font-size: 14px; opacity: 0.8;">°C</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.injectionData.status) {
|
||||||
|
case 'injecting':
|
||||||
|
return {text: '注入中', color: '#4CAF50'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
case 'maintenance':
|
||||||
|
return {text: '维护中', color: '#F44336'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
packages/demo/src/htmlPanel/OilGasScene/ProductionHtmlPanel.ts
Normal file
276
packages/demo/src/htmlPanel/OilGasScene/ProductionHtmlPanel.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采出信息数据接口
|
||||||
|
*/
|
||||||
|
export interface IProductionData {
|
||||||
|
/** 井名称 */
|
||||||
|
wellName: string;
|
||||||
|
/** 采出流量 (m³/h) */
|
||||||
|
flowRate: number;
|
||||||
|
/** 采出速率 (m/s) */
|
||||||
|
productionRate: number;
|
||||||
|
/** 采出压力 (MPa) */
|
||||||
|
pressure: number;
|
||||||
|
/** 采出体积 (m³) */
|
||||||
|
volume: number;
|
||||||
|
/** 流体温度 (°C) */
|
||||||
|
temperature: number;
|
||||||
|
/** 生产状态 */
|
||||||
|
status: 'producing' | 'stopped' | 'maintenance';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采出信息面板类
|
||||||
|
*/
|
||||||
|
export class ProductionHtmlPanel extends HtmlPanel {
|
||||||
|
private productionData: IProductionData = {
|
||||||
|
wellName: '',
|
||||||
|
flowRate: 0,
|
||||||
|
productionRate: 0,
|
||||||
|
pressure: 0,
|
||||||
|
volume: 0,
|
||||||
|
temperature: 0,
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新采出数据
|
||||||
|
* @param data 新的采出数据
|
||||||
|
*/
|
||||||
|
updateData(data: Partial<IProductionData>): void {
|
||||||
|
this.productionData = {
|
||||||
|
...this.productionData,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
this.updateElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
">
|
||||||
|
<h3 id="title-${this.uuid}" style="margin: 0; font-size: 18px; font-weight: 600;">
|
||||||
|
🛢️ 采出信息
|
||||||
|
</h3>
|
||||||
|
<span id="status-${this.uuid}" style="
|
||||||
|
background: #FF9800;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
">
|
||||||
|
已停止
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content-${this.uuid}" style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<!-- 采出流量 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">💧 采出流量</span>
|
||||||
|
</div>
|
||||||
|
<div id="flow-rate-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #4FC3F7;">
|
||||||
|
0.00 <span style="font-size: 14px; opacity: 0.8;">m³/h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 采出速率 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">⚡ 采出速率</span>
|
||||||
|
</div>
|
||||||
|
<div id="production-rate-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #81C784;">
|
||||||
|
0.000 <span style="font-size: 14px; opacity: 0.8;">m/s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 采出压力 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📊 采出压力</span>
|
||||||
|
</div>
|
||||||
|
<div id="pressure-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #FFB74D;">
|
||||||
|
0.00 <span style="font-size: 14px; opacity: 0.8;">MPa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 采出体积 -->
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">📦 采出体积</span>
|
||||||
|
</div>
|
||||||
|
<div id="volume-${this.uuid}" style="font-size: 22px; font-weight: 700; color: #BA68C8;">
|
||||||
|
0.00 <span style="font-size: 14px; opacity: 0.8;">m³</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 流体温度 -->
|
||||||
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span style="font-size: 14px; opacity: 0.9;">🌡️ 流体温度</span>
|
||||||
|
<span id="temperature-${this.uuid}" style="font-size: 20px; font-weight: 700; color: #FF7043;">
|
||||||
|
0.0 <span style="font-size: 14px; opacity: 0.8;">°C</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新DOM元素
|
||||||
|
*/
|
||||||
|
private updateElements(): void {
|
||||||
|
const statusInfo = this.getStatusInfo();
|
||||||
|
|
||||||
|
// 更新标题
|
||||||
|
const titleEl = document.getElementById(`title-${this.uuid}`);
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = `🛢️ ${this.productionData.wellName || '采出信息'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = document.getElementById(`status-${this.uuid}`) as HTMLElement;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = statusInfo.text;
|
||||||
|
statusEl.style.background = statusInfo.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新采出流量
|
||||||
|
const flowRateEl = document.getElementById(`flow-rate-${this.uuid}`);
|
||||||
|
if (flowRateEl) {
|
||||||
|
flowRateEl.innerHTML = `${this.productionData.flowRate.toFixed(2)} <span style="font-size: 14px; opacity: 0.8;">m³/h</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新采出速率
|
||||||
|
const productionRateEl = document.getElementById(`production-rate-${this.uuid}`);
|
||||||
|
if (productionRateEl) {
|
||||||
|
productionRateEl.innerHTML = `${this.productionData.productionRate.toFixed(3)} <span style="font-size: 14px; opacity: 0.8;">m/s</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新采出压力
|
||||||
|
const pressureEl = document.getElementById(`pressure-${this.uuid}`);
|
||||||
|
if (pressureEl) {
|
||||||
|
pressureEl.innerHTML = `${this.productionData.pressure.toFixed(2)} <span style="font-size: 14px; opacity: 0.8;">MPa</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新采出体积
|
||||||
|
const volumeEl = document.getElementById(`volume-${this.uuid}`);
|
||||||
|
if (volumeEl) {
|
||||||
|
volumeEl.innerHTML = `${this.productionData.volume.toFixed(2)} <span style="font-size: 14px; opacity: 0.8;">m³</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新流体温度
|
||||||
|
const temperatureEl = document.getElementById(`temperature-${this.uuid}`);
|
||||||
|
if (temperatureEl) {
|
||||||
|
temperatureEl.innerHTML = `${this.productionData.temperature.toFixed(1)} <span style="font-size: 14px; opacity: 0.8;">°C</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusInfo(): { text: string; color: string } {
|
||||||
|
switch (this.productionData.status) {
|
||||||
|
case 'producing':
|
||||||
|
return {text: '生产中', color: '#4CAF50'};
|
||||||
|
case 'stopped':
|
||||||
|
return {text: '已停止', color: '#FF9800'};
|
||||||
|
case 'maintenance':
|
||||||
|
return {text: '维护中', color: '#F44336'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#999'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
export interface IDisturbanceLabelData {
|
||||||
|
waveFunction: string;
|
||||||
|
frequency: number;
|
||||||
|
amplitude: number;
|
||||||
|
count: number;
|
||||||
|
interval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DisturbanceLabelHtmlPanel extends HtmlPanel {
|
||||||
|
private data: IDisturbanceLabelData;
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
constructor(data: IDisturbanceLabelData, type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.data = data;
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
this.onDeleteCallback?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initContent(): void {
|
||||||
|
const {waveFunction, frequency, amplitude, count, interval} = this.data;
|
||||||
|
const rows = [
|
||||||
|
{label: '扰动波函数', value: waveFunction, unit: ''},
|
||||||
|
{label: '扰动频率', value: frequency.toFixed(1), unit: 'Hz'},
|
||||||
|
{label: '扰动振幅', value: amplitude.toFixed(2), unit: 'mm'},
|
||||||
|
{label: '扰动次数', value: String(count), unit: '次'},
|
||||||
|
{label: '扰动间隔', value: interval.toFixed(1), unit: 's'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const rowsHtml = rows.map(r => `
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center;
|
||||||
|
padding:8px 0; border-bottom:1px solid rgba(255,255,255,0.08);">
|
||||||
|
<span style="font-size:12px; opacity:0.7;">${r.label}</span>
|
||||||
|
<span style="font-size:13px; font-weight:600; color:#7ecfff; font-family:'Courier New',monospace;">
|
||||||
|
${r.value}<span style="font-size:10px; opacity:0.6; margin-left:3px;">${r.unit}</span>
|
||||||
|
</span>
|
||||||
|
</div>`).join('');
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;
|
||||||
|
background:linear-gradient(135deg,#0d1b2a 0%,#1b2838 100%);
|
||||||
|
border:1px solid rgba(126,207,255,0.35);
|
||||||
|
border-radius:12px;
|
||||||
|
padding:18px 20px;
|
||||||
|
color:white;
|
||||||
|
box-shadow:0 8px 32px rgba(0,0,0,0.5),0 0 16px rgba(126,207,255,0.08);
|
||||||
|
position:relative;
|
||||||
|
min-width:240px;
|
||||||
|
">
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position:absolute; top:10px; right:10px;
|
||||||
|
background:rgba(255,255,255,0.08); border:none; border-radius:50%;
|
||||||
|
width:26px; height:26px; cursor:pointer; color:white; font-size:13px;
|
||||||
|
pointer-events:auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255,255,255,0.2)'"
|
||||||
|
onmouseout="this.style.background='rgba(255,255,255,0.08)'">✕</button>
|
||||||
|
|
||||||
|
<div style="display:flex; align-items:center; margin-bottom:14px;
|
||||||
|
padding-bottom:12px; border-bottom:1px solid rgba(126,207,255,0.25);">
|
||||||
|
<div style="font-size:22px; margin-right:10px;">〜</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:14px; font-weight:700; color:#7ecfff;">扰动加载参数</div>
|
||||||
|
<div style="font-size:10px; opacity:0.55; margin-top:2px;">Disturbance Load Definition</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>${rowsHtml}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const btn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
btn?.addEventListener('click', () => this.handleDelete());
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岩爆标签面板
|
||||||
|
*/
|
||||||
|
export class RockBurstLabelHtmlPanel extends HtmlPanel {
|
||||||
|
constructor(type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.initContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initContent(): void {
|
||||||
|
const content = `
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:center; pointer-events:none;transform: translate(0%, -50%)">
|
||||||
|
<div style="
|
||||||
|
padding:6px 12px;
|
||||||
|
border-radius:8px;
|
||||||
|
border:1px solid rgba(255,255,255,0.35);
|
||||||
|
background:linear-gradient(135deg, rgba(111,0,0,0.9) 0%, rgba(170,0,0,0.9) 100%);
|
||||||
|
color:#ffffff;
|
||||||
|
font-size:13px;
|
||||||
|
font-weight:700;
|
||||||
|
letter-spacing:1px;
|
||||||
|
box-shadow:0 4px 14px rgba(0,0,0,0.35);
|
||||||
|
white-space:nowrap;
|
||||||
|
">岩爆</div>
|
||||||
|
<div style="
|
||||||
|
width:0;
|
||||||
|
height:0;
|
||||||
|
margin-top:4px;
|
||||||
|
border-left:8px solid transparent;
|
||||||
|
border-right:8px solid transparent;
|
||||||
|
border-top:14px solid rgba(170,0,0,0.95);
|
||||||
|
filter:drop-shadow(0 2px 3px rgba(0,0,0,0.35));
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
packages/demo/src/htmlPanel/TunnelScene/RockSampleHtmlPanel.ts
Normal file
155
packages/demo/src/htmlPanel/TunnelScene/RockSampleHtmlPanel.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岩样数据接口
|
||||||
|
*/
|
||||||
|
export interface IRockSampleData {
|
||||||
|
/** 材料名称 */
|
||||||
|
materialName: string;
|
||||||
|
/** 目数 */
|
||||||
|
meshSize: string;
|
||||||
|
/** 占比 */
|
||||||
|
ratio: string;
|
||||||
|
/** 单轴抗压强度 (MPa) */
|
||||||
|
uniaxialCompressiveStrength: number;
|
||||||
|
/** 抗拉强度 (MPa) */
|
||||||
|
tensileStrength: number;
|
||||||
|
/** 弹性模量 (GPa) */
|
||||||
|
elasticModulus: number;
|
||||||
|
/** 脆性指数 */
|
||||||
|
brittlenessIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岩样展示面板类
|
||||||
|
*/
|
||||||
|
export class RockSampleHtmlPanel extends HtmlPanel {
|
||||||
|
private sampleData: IRockSampleData;
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
* @param data 岩样数据
|
||||||
|
*/
|
||||||
|
constructor(type: CssType = CssType.CSS3D, data: IRockSampleData) {
|
||||||
|
super(type);
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.sampleData = data;
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const content = `
|
||||||
|
<div style="font-family: 'Microsoft YaHei', Arial, sans-serif; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; padding: 20px; min-width: 320px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); color: white; position: relative;">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 0; margin-bottom: 15px; padding-right: 30px; font-size: 18px; border-bottom: 2px solid rgba(255, 255, 255, 0.3); padding-bottom: 10px;">
|
||||||
|
🪨 ${this.sampleData.materialName}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- 组分配比 -->
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px); margin-bottom: 15px;">
|
||||||
|
<h3 style="margin: 0 0 12px 0; font-size: 15px; opacity: 0.95;">组分配比</h3>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<div style="font-size: 12px; opacity: 0.85; margin-bottom: 4px;">材料名称</div>
|
||||||
|
<div style="font-size: 15px; font-weight: 600;">${this.sampleData.materialName}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<div style="font-size: 12px; opacity: 0.85; margin-bottom: 4px;">目数</div>
|
||||||
|
<div style="font-size: 15px; font-weight: 600;">${this.sampleData.meshSize}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 12px; opacity: 0.85; margin-bottom: 4px;">占比</div>
|
||||||
|
<div style="font-size: 15px; font-weight: 600;">${this.sampleData.ratio}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物理属性 -->
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 15px; backdrop-filter: blur(10px);">
|
||||||
|
<h3 style="margin: 0 0 12px 0; font-size: 15px; opacity: 0.95;">物理属性</h3>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 8px; display: flex; justify-content: space-between;">
|
||||||
|
<span style="font-size: 12px; opacity: 0.85;">单轴抗压强度</span>
|
||||||
|
<span style="font-size: 13px; font-weight: 600;">${this.sampleData.uniaxialCompressiveStrength} MPa</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 8px; display: flex; justify-content: space-between;">
|
||||||
|
<span style="font-size: 12px; opacity: 0.85;">抗拉强度</span>
|
||||||
|
<span style="font-size: 13px; font-weight: 600;">${this.sampleData.tensileStrength} MPa</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 8px; display: flex; justify-content: space-between;">
|
||||||
|
<span style="font-size: 12px; opacity: 0.85;">弹性模量</span>
|
||||||
|
<span style="font-size: 13px; font-weight: 600;">${this.sampleData.elasticModulus} GPa</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span style="font-size: 12px; opacity: 0.85;">脆性指数</span>
|
||||||
|
<span style="font-size: 13px; font-weight: 600;">${this.sampleData.brittlenessIndex}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
packages/demo/src/htmlPanel/TunnelScene/SensorHtmlPanel.ts
Normal file
170
packages/demo/src/htmlPanel/TunnelScene/SensorHtmlPanel.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传感器数据接口
|
||||||
|
*/
|
||||||
|
export interface ISensorData {
|
||||||
|
type: string;
|
||||||
|
channel: number;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传感器面板类
|
||||||
|
*/
|
||||||
|
export class SensorHtmlPanel extends HtmlPanel {
|
||||||
|
private sensor: ISensorData;
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param sensor 传感器数据对象
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(sensor: ISensorData, type: CssType = CssType.CSS3D) {
|
||||||
|
super(type);
|
||||||
|
this.sensor = sensor;
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||||
|
position: relative;
|
||||||
|
min-width: 280px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
">
|
||||||
|
📡
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding-right: 30px;">
|
||||||
|
<div style="font-size: 18px; font-weight: 700; margin-bottom: 4px;">
|
||||||
|
${this.sensor.type}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 13px; opacity: 0.85;">
|
||||||
|
通道 ${this.sensor.channel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
坐标位置
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">X</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.sensor.position.x.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Y</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.sensor.position.y.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Z</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.sensor.position.z.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,232 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应力施加数据接口
|
||||||
|
*/
|
||||||
|
export interface IStressData {
|
||||||
|
name: string;
|
||||||
|
stressValue: number; // MPa
|
||||||
|
direction: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
activationTime: number; // 秒
|
||||||
|
status: 'inactive' | 'applying' | 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应力施加面板类
|
||||||
|
*/
|
||||||
|
export class StressApplicationHtmlPanel extends HtmlPanel {
|
||||||
|
private stressData: IStressData;
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param stressData 应力数据对象
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(stressData: IStressData, type: CssType = CssType.CSS3D) {
|
||||||
|
super(type);
|
||||||
|
this.stressData = stressData;
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新应力数据
|
||||||
|
* @param data 新的应力数据
|
||||||
|
*/
|
||||||
|
public updateData(data: Partial<IStressData>): void {
|
||||||
|
this.stressData = {...this.stressData, ...data};
|
||||||
|
this.initContent();
|
||||||
|
this.attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态显示文本和颜色
|
||||||
|
*/
|
||||||
|
private getStatusDisplay(): { text: string; color: string } {
|
||||||
|
switch (this.stressData.status) {
|
||||||
|
case 'inactive':
|
||||||
|
return {text: '未激活', color: '#95a5a6'};
|
||||||
|
case 'applying':
|
||||||
|
return {text: '施加中', color: '#f39c12'};
|
||||||
|
case 'completed':
|
||||||
|
return {text: '已完成', color: '#27ae60'};
|
||||||
|
default:
|
||||||
|
return {text: '未知', color: '#95a5a6'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const statusDisplay = this.getStatusDisplay();
|
||||||
|
const directionMagnitude = Math.sqrt(
|
||||||
|
this.stressData.direction.x ** 2 +
|
||||||
|
this.stressData.direction.y ** 2 +
|
||||||
|
this.stressData.direction.z ** 2
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||||
|
position: relative;
|
||||||
|
min-width: 320px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
">
|
||||||
|
⚡
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding-right: 30px;">
|
||||||
|
<div style="font-size: 18px; font-weight: 700; margin-bottom: 4px;">
|
||||||
|
${this.stressData.name}
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: ${statusDisplay.color};
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
">
|
||||||
|
${statusDisplay.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 应力值显示 -->
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
应力值
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 28px; font-weight: 700; font-family: 'Courier New', monospace;">
|
||||||
|
${this.stressData.stressValue.toFixed(2)} <span style="font-size: 16px; opacity: 0.8;">MPa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 应力方向 -->
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
应力方向
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">X</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.stressData.direction.x.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Y</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.stressData.direction.y.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Z</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.stressData.direction.z.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-top: 8px; text-align: center;">
|
||||||
|
模长: ${directionMagnitude.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加事件监听
|
||||||
|
*/
|
||||||
|
private attachEvents(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
export interface IStressWaveDeviceData {
|
||||||
|
position: { x: number; y: number; z: number };
|
||||||
|
excitationEnergy: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StressWaveDeviceHtmlPanel extends HtmlPanel {
|
||||||
|
private data: IStressWaveDeviceData;
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
constructor(data: IStressWaveDeviceData, type: CssType = CssType.CSS2D) {
|
||||||
|
super(type);
|
||||||
|
this.data = data;
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initContent(): void {
|
||||||
|
const {position, excitationEnergy} = this.data;
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
border: 1px solid rgba(255, 165, 0, 0.4);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 20px rgba(255,165,0,0.1);
|
||||||
|
position: relative;
|
||||||
|
min-width: 260px;
|
||||||
|
">
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">✕</button>
|
||||||
|
|
||||||
|
<div style="display:flex; align-items:center; margin-bottom:14px; padding-bottom:14px; border-bottom:1px solid rgba(255,165,0,0.3);">
|
||||||
|
<div style="font-size:28px; margin-right:12px;">⚡</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:15px; font-weight:700; color:#ffa500;">内部应力波激发装置</div>
|
||||||
|
<div style="font-size:11px; opacity:0.7; margin-top:2px;">Stress Wave Excitation Device</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<div style="font-size:10px; opacity:0.6; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">模型位置</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:8px; font-family:'Courier New',monospace;">
|
||||||
|
<div style="background:rgba(255,255,255,0.07); border-radius:6px; padding:8px; text-align:center;">
|
||||||
|
<div style="font-size:10px; opacity:0.6; margin-bottom:4px;">X</div>
|
||||||
|
<div style="font-size:14px; font-weight:700;">${position.x.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:rgba(255,255,255,0.07); border-radius:6px; padding:8px; text-align:center;">
|
||||||
|
<div style="font-size:10px; opacity:0.6; margin-bottom:4px;">Y</div>
|
||||||
|
<div style="font-size:14px; font-weight:700;">${position.y.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:rgba(255,255,255,0.07); border-radius:6px; padding:8px; text-align:center;">
|
||||||
|
<div style="font-size:10px; opacity:0.6; margin-bottom:4px;">Z</div>
|
||||||
|
<div style="font-size:14px; font-weight:700;">${position.z.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:rgba(255,165,0,0.1); border:1px solid rgba(255,165,0,0.3); border-radius:8px; padding:12px; display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<div style="font-size:11px; opacity:0.8;">激发能量</div>
|
||||||
|
<div style="font-size:18px; font-weight:700; color:#ffa500;">${excitationEnergy.toFixed(1)} <span style="font-size:11px; opacity:0.7;">kJ</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const btn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,245 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 温度施加数据接口
|
||||||
|
*/
|
||||||
|
export interface ITemperatureData {
|
||||||
|
name: string;
|
||||||
|
temperature: number; // °C
|
||||||
|
location: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 温度施加面板类
|
||||||
|
*/
|
||||||
|
export class TemperatureApplicationHtmlPanel extends HtmlPanel {
|
||||||
|
private temperatureData: ITemperatureData;
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
private isExpanded: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param temperatureData 温度数据对象
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(temperatureData: ITemperatureData, type: CssType = CssType.CSS3D) {
|
||||||
|
super(type);
|
||||||
|
this.temperatureData = temperatureData;
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换展开/收起状态
|
||||||
|
*/
|
||||||
|
private toggleExpand(): void {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
this.initContent();
|
||||||
|
this.attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取温度颜色
|
||||||
|
*/
|
||||||
|
private getTemperatureColor(): string {
|
||||||
|
const temp = this.temperatureData.temperature;
|
||||||
|
if (temp < 0) return '#3498db'; // 蓝色 - 冷
|
||||||
|
if (temp < 20) return '#1abc9c'; // 青色 - 凉
|
||||||
|
if (temp < 50) return '#f39c12'; // 橙色 - 温
|
||||||
|
if (temp < 100) return '#e74c3c'; // 红色 - 热
|
||||||
|
return '#c0392b'; // 深红色 - 极热
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const tempColor = this.getTemperatureColor();
|
||||||
|
const locationMagnitude = Math.sqrt(
|
||||||
|
this.temperatureData.location.x ** 2 +
|
||||||
|
this.temperatureData.location.y ** 2 +
|
||||||
|
this.temperatureData.location.z ** 2
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||||
|
position: relative;
|
||||||
|
min-width: 320px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
">
|
||||||
|
🌡️
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding-right: 30px;">
|
||||||
|
<div style="font-size: 18px; font-weight: 700;">
|
||||||
|
${this.temperatureData.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 展开/收起按钮 -->
|
||||||
|
<button id="toggle-btn-${this.uuid}" style="
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'">
|
||||||
|
<span>${this.isExpanded ? '▼' : '▶'}</span>
|
||||||
|
<span>${this.isExpanded ? '收起信息' : '展开信息'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
${this.isExpanded ? `
|
||||||
|
<!-- 温度值显示 -->
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
温度值
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 28px; font-weight: 700; font-family: 'Courier New', monospace; color: ${tempColor};">
|
||||||
|
${this.temperatureData.temperature.toFixed(1)} <span style="font-size: 16px; opacity: 0.8;">°C</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 施加位置 -->
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
施加位置
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">X</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.temperatureData.location.x.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Y</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.temperatureData.location.y.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Z</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.temperatureData.location.z.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-top: 8px; text-align: center;">
|
||||||
|
距离原点: ${locationMagnitude.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加事件监听
|
||||||
|
*/
|
||||||
|
private attachEvents(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBtn = document.getElementById(`toggle-btn-${this.uuid}`);
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', () => this.toggleExpand());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
packages/demo/src/htmlPanel/TunnelScene/TunnelHtmlPanel.ts
Normal file
191
packages/demo/src/htmlPanel/TunnelScene/TunnelHtmlPanel.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import {CssType, HtmlPanel} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隧道数据接口
|
||||||
|
*/
|
||||||
|
export interface ITunnelData {
|
||||||
|
type: string;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
depth: number;
|
||||||
|
radius: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隧道面板类
|
||||||
|
*/
|
||||||
|
export class TunnelHtmlPanel extends HtmlPanel {
|
||||||
|
private tunnelData: ITunnelData;
|
||||||
|
|
||||||
|
private onDeleteCallback: (() => void) | null = null;
|
||||||
|
private uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param data 隧道数据
|
||||||
|
* @param type CSS渲染类型
|
||||||
|
*/
|
||||||
|
constructor(data: ITunnelData, type: CssType = CssType.CSS3DSprite) {
|
||||||
|
super(type);
|
||||||
|
this.tunnelData = data;
|
||||||
|
this.uuid = this.getUniqueId();
|
||||||
|
this.initContent();
|
||||||
|
this.attachDeleteEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置删除回调函数
|
||||||
|
* @param callback 删除时的回调函数
|
||||||
|
*/
|
||||||
|
public onDelete(callback: () => void): void {
|
||||||
|
this.onDeleteCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除按钮点击
|
||||||
|
*/
|
||||||
|
private handleDelete(): void {
|
||||||
|
this.hide();
|
||||||
|
if (this.onDeleteCallback) {
|
||||||
|
this.onDeleteCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内容结构
|
||||||
|
*/
|
||||||
|
private initContent(): void {
|
||||||
|
const content = `
|
||||||
|
<div style="
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #2193b0 0%, #6dd5ed 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||||
|
position: relative;
|
||||||
|
min-width: 300px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button id="delete-btn-${this.uuid}" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
">
|
||||||
|
🚇
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding-right: 30px;">
|
||||||
|
<div style="font-size: 18px; font-weight: 700; margin-bottom: 4px;">
|
||||||
|
${this.tunnelData.type}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 13px; opacity: 0.85;">
|
||||||
|
隧道信息
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
坐标位置
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">X</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.tunnelData.position.x.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Y</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.tunnelData.position.y.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 11px; opacity: 0.75; margin-bottom: 4px;">Z</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700;">${this.tunnelData.position.z.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
">
|
||||||
|
<div style="font-size: 11px; opacity: 0.85; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">
|
||||||
|
隧道参数
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
||||||
|
<span style="font-size: 13px; opacity: 0.85;">深度</span>
|
||||||
|
<span style="font-size: 15px; font-weight: 700;">${this.tunnelData.depth} m</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span style="font-size: 13px; opacity: 0.85;">半径</span>
|
||||||
|
<span style="font-size: 15px; font-weight: 700;">${this.tunnelData.radius} m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.updateContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加删除按钮事件
|
||||||
|
*/
|
||||||
|
private attachDeleteEvent(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const deleteBtn = document.getElementById(`delete-btn-${this.uuid}`);
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDelete());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/demo/src/htmlPanel/index.ts
Normal file
15
packages/demo/src/htmlPanel/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export {HtmlPanel} from '@deep/engine';
|
||||||
|
export * from './TunnelScene/SensorHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/TunnelHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/RockSampleHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/StressApplicationHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/TemperatureApplicationHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/StressWaveDeviceHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/DisturbanceLabelHtmlPanel.ts';
|
||||||
|
export * from './TunnelScene/RockBurstLabelHtmlPanel.ts';
|
||||||
|
export * from './GoldMineScene/DrillingHtmlPanel.ts';
|
||||||
|
export * from './GoldMineScene/FillingHtmlPanel.ts';
|
||||||
|
export * from './GoldMineScene/AirSupplyHtmlPanel.ts';
|
||||||
|
export * from './GoldMineScene/ReturnAirHtmlPanel.ts';
|
||||||
|
export * from './OilGasScene/ProductionHtmlPanel.ts';
|
||||||
|
export * from './OilGasScene/InjectionHtmlPanel.ts';
|
||||||
19
packages/demo/src/main.ts
Normal file
19
packages/demo/src/main.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {createApp} from 'vue'
|
||||||
|
import './style/style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import {Bus} from "./hooks/Bus.ts";
|
||||||
|
import DisableDevtool from 'disable-devtool';
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.config.globalProperties.bus = new Bus();
|
||||||
|
window.bus = app.config.globalProperties.bus
|
||||||
|
app.use(router).mount('#app')
|
||||||
|
if (import.meta.env.PROD) {
|
||||||
|
DisableDevtool({
|
||||||
|
md5: "32981a13284db7a021131df49e6cd203",
|
||||||
|
clearLog: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(DisableDevtool.md5('js'))
|
||||||
574
packages/demo/src/panels/FracturingPanel.vue
Normal file
574
packages/demo/src/panels/FracturingPanel.vue
Normal file
@ -0,0 +1,574 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="fracturing" title="压裂注采">
|
||||||
|
<n-tabs animated type="segment">
|
||||||
|
<!-- 注入井 -->
|
||||||
|
<n-tab-pane name="injection" tab="注入井">
|
||||||
|
<n-form label-placement="left" label-width="120" size="small">
|
||||||
|
<n-form-item label="注入流量(m³/s)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="injectionParams.flowRate"
|
||||||
|
:max="10"
|
||||||
|
:min="0"
|
||||||
|
:step="0.1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="注入速率(m/s)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="injectionParams.injectionRate"
|
||||||
|
:max="50"
|
||||||
|
:min="0"
|
||||||
|
:step="0.5"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="注入压力(MPa)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="injectionParams.pressure"
|
||||||
|
:max="100"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="流体温度(°C)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="injectionParams.temperature"
|
||||||
|
:max="200"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="isSelectingInjectionWell"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startSelectingInjectionWell">
|
||||||
|
{{ isSelectingInjectionWell ? '请点击场景选择注入井...' : '选择注入井' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="injectionWells.length === 0"
|
||||||
|
block
|
||||||
|
type="warning"
|
||||||
|
@click="startInjection">
|
||||||
|
开始注入 ({{ injectionWells.length }})
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="injectionWells.length === 0"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="clearInjectionWells">
|
||||||
|
清除注入井
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 采出井 -->
|
||||||
|
<n-tab-pane name="production" tab="采出井">
|
||||||
|
<n-form label-placement="left" label-width="120" size="small">
|
||||||
|
<n-form-item label="采出流量(m³/h)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="productionParams.flowRate"
|
||||||
|
:max="1000"
|
||||||
|
:min="0"
|
||||||
|
:step="10"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="采出速率(m/s)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="productionParams.productionRate"
|
||||||
|
:max="10"
|
||||||
|
:min="0"
|
||||||
|
:step="0.1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="采出压力(MPa)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="productionParams.pressure"
|
||||||
|
:max="50"
|
||||||
|
:min="0"
|
||||||
|
:step="0.5"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="流体温度(°C)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="productionParams.temperature"
|
||||||
|
:max="150"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="isSelectingProductionWell"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startSelectingProductionWell">
|
||||||
|
{{ isSelectingProductionWell ? '请点击场景选择采出井...' : '选择采出井' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="productionWells.length === 0"
|
||||||
|
block
|
||||||
|
type="success"
|
||||||
|
@click="startProduction">
|
||||||
|
开始采出 ({{ productionWells.length }})
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="productionWells.length === 0"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="clearProductionWells">
|
||||||
|
清除采出井
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NForm, NFormItem, NInputNumber, NSpace, NTabPane, NTabs} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {EventManagerEvents, Viewer} from '@deep/engine';
|
||||||
|
import {InjectionHtmlPanel, ProductionHtmlPanel} from '../../htmlPanel';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let injectionWells: any[] = [];
|
||||||
|
let productionWells: any[] = [];
|
||||||
|
let wellCounter = 0;
|
||||||
|
const isSelectingInjectionWell = ref(false);
|
||||||
|
const isSelectingProductionWell = ref(false);
|
||||||
|
const injectionModals = new Map<any, InjectionHtmlPanel>();
|
||||||
|
const productionModals = new Map<any, ProductionHtmlPanel>();
|
||||||
|
|
||||||
|
// 注入参数
|
||||||
|
const injectionParams = ref({
|
||||||
|
flowRate: 2.5,
|
||||||
|
injectionRate: 10,
|
||||||
|
pressure: 30,
|
||||||
|
temperature: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 采出参数
|
||||||
|
const productionParams = ref({
|
||||||
|
flowRate: 500,
|
||||||
|
productionRate: 5,
|
||||||
|
pressure: 15,
|
||||||
|
temperature: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
setupClickListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置点击事件监听
|
||||||
|
*/
|
||||||
|
const setupClickListener = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, ({data}) => {
|
||||||
|
if (isSelectingInjectionWell.value || isSelectingProductionWell.value || !viewer) return;
|
||||||
|
|
||||||
|
const {intersects} = data;
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
for (const intersection of intersects) {
|
||||||
|
const clickedObject = intersection.object;
|
||||||
|
|
||||||
|
// 检查是否点击了注入井
|
||||||
|
const injectionWell = injectionWells.find(w => w.mesh === clickedObject);
|
||||||
|
if (injectionWell) {
|
||||||
|
let modal = injectionModals.get(injectionWell);
|
||||||
|
if (!modal) {
|
||||||
|
modal = createInjectionModalForWell(injectionWell);
|
||||||
|
}
|
||||||
|
updateInjectionModalInfo(injectionWell, modal);
|
||||||
|
if (modal && intersection.point) {
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.copy(intersection.point);
|
||||||
|
modalObject.position.y += 0.5;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否点击了采出井
|
||||||
|
const productionWell = productionWells.find(w => w.mesh === clickedObject);
|
||||||
|
if (productionWell) {
|
||||||
|
let modal = productionModals.get(productionWell);
|
||||||
|
if (!modal) {
|
||||||
|
modal = createProductionModalForWell(productionWell);
|
||||||
|
}
|
||||||
|
updateProductionModalInfo(productionWell, modal);
|
||||||
|
if (modal && intersection.point) {
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.copy(intersection.point);
|
||||||
|
modalObject.position.y += 0.5;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为注入井创建信息弹窗
|
||||||
|
*/
|
||||||
|
const createInjectionModalForWell = (well: any): InjectionHtmlPanel => {
|
||||||
|
if (!viewer) throw new Error('Viewer not initialized');
|
||||||
|
|
||||||
|
const modal = new InjectionHtmlPanel('css2d');
|
||||||
|
|
||||||
|
modal.onDelete(() => {
|
||||||
|
modal.hide();
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
if (modalObject.parent) {
|
||||||
|
modalObject.parent.remove(modalObject);
|
||||||
|
}
|
||||||
|
injectionModals.delete(well);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wellPosition = well.mesh.position;
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.set(wellPosition.x, wellPosition.y + 1, wellPosition.z);
|
||||||
|
viewer.scene.add(modalObject);
|
||||||
|
modal.show();
|
||||||
|
injectionModals.set(well, modal);
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为采出井创建信息弹窗
|
||||||
|
*/
|
||||||
|
const createProductionModalForWell = (well: any): ProductionHtmlPanel => {
|
||||||
|
if (!viewer) throw new Error('Viewer not initialized');
|
||||||
|
|
||||||
|
const modal = new ProductionHtmlPanel('css2d');
|
||||||
|
|
||||||
|
modal.onDelete(() => {
|
||||||
|
modal.hide();
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
if (modalObject.parent) {
|
||||||
|
modalObject.parent.remove(modalObject);
|
||||||
|
}
|
||||||
|
productionModals.delete(well);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wellPosition = well.mesh.position;
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.set(wellPosition.x, wellPosition.y + 1, wellPosition.z);
|
||||||
|
viewer.scene.add(modalObject);
|
||||||
|
modal.show();
|
||||||
|
productionModals.set(well, modal);
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新注入井信息弹窗内容
|
||||||
|
*/
|
||||||
|
const updateInjectionModalInfo = (well: any, modal: InjectionHtmlPanel) => {
|
||||||
|
modal.updateData({
|
||||||
|
wellName: well.name,
|
||||||
|
flowRate: well.flowRate || injectionParams.value.flowRate,
|
||||||
|
injectionRate: well.injectionRate || injectionParams.value.injectionRate,
|
||||||
|
pressure: well.pressure || injectionParams.value.pressure,
|
||||||
|
volume: well.volume || 0,
|
||||||
|
temperature: well.temperature || injectionParams.value.temperature,
|
||||||
|
status: well.isInjecting ? 'injecting' : 'stopped'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新采出井信息弹窗内容
|
||||||
|
*/
|
||||||
|
const updateProductionModalInfo = (well: any, modal: ProductionHtmlPanel) => {
|
||||||
|
modal.updateData({
|
||||||
|
wellName: well.name,
|
||||||
|
flowRate: well.flowRate || productionParams.value.flowRate,
|
||||||
|
productionRate: well.productionRate || productionParams.value.productionRate,
|
||||||
|
pressure: well.pressure || productionParams.value.pressure,
|
||||||
|
volume: well.volume || 0,
|
||||||
|
temperature: well.temperature || productionParams.value.temperature,
|
||||||
|
status: well.isProducing ? 'producing' : 'stopped'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始选择注入井
|
||||||
|
*/
|
||||||
|
const startSelectingInjectionWell = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
isSelectingInjectionWell.value = true;
|
||||||
|
const canvas = viewer.renderer!.domElement;
|
||||||
|
canvas.addEventListener('click', onInjectionWellClick, {once: true});
|
||||||
|
canvas.style.cursor = 'crosshair';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始选择采出井
|
||||||
|
*/
|
||||||
|
const startSelectingProductionWell = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
isSelectingProductionWell.value = true;
|
||||||
|
const canvas = viewer.renderer!.domElement;
|
||||||
|
canvas.addEventListener('click', onProductionWellClick, {once: true});
|
||||||
|
canvas.style.cursor = 'crosshair';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理注入井点击事件
|
||||||
|
*/
|
||||||
|
const onInjectionWellClick = (event: MouseEvent) => {
|
||||||
|
if (!viewer || !isSelectingInjectionWell.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) {
|
||||||
|
createInjectionWell(intersection.point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.style.cursor = 'default';
|
||||||
|
isSelectingInjectionWell.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理采出井点击事件
|
||||||
|
*/
|
||||||
|
const onProductionWellClick = (event: MouseEvent) => {
|
||||||
|
if (!viewer || !isSelectingProductionWell.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) {
|
||||||
|
createProductionWell(intersection.point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.style.cursor = 'default';
|
||||||
|
isSelectingProductionWell.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建注入井
|
||||||
|
*/
|
||||||
|
const createInjectionWell = (position: THREE.Vector3) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const geometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 16);
|
||||||
|
const material = new THREE.MeshBasicMaterial({color: 0xff6b6b});
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.position.copy(position);
|
||||||
|
mesh.position.y += 0.5;
|
||||||
|
|
||||||
|
wellCounter++;
|
||||||
|
const wellName = `注入井_${wellCounter}`;
|
||||||
|
mesh.name = wellName;
|
||||||
|
|
||||||
|
viewer.scene.add(mesh);
|
||||||
|
|
||||||
|
const well = {
|
||||||
|
mesh,
|
||||||
|
name: wellName,
|
||||||
|
flowRate: injectionParams.value.flowRate,
|
||||||
|
injectionRate: injectionParams.value.injectionRate,
|
||||||
|
pressure: injectionParams.value.pressure,
|
||||||
|
volume: 0,
|
||||||
|
temperature: injectionParams.value.temperature,
|
||||||
|
isInjecting: false
|
||||||
|
};
|
||||||
|
|
||||||
|
injectionWells.push(well);
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建采出井
|
||||||
|
*/
|
||||||
|
const createProductionWell = (position: THREE.Vector3) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const geometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 16);
|
||||||
|
const material = new THREE.MeshBasicMaterial({color: 0x4ecdc4});
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.position.copy(position);
|
||||||
|
mesh.position.y += 0.5;
|
||||||
|
|
||||||
|
wellCounter++;
|
||||||
|
const wellName = `采出井_${wellCounter}`;
|
||||||
|
mesh.name = wellName;
|
||||||
|
|
||||||
|
viewer.scene.add(mesh);
|
||||||
|
|
||||||
|
const well = {
|
||||||
|
mesh,
|
||||||
|
name: wellName,
|
||||||
|
flowRate: productionParams.value.flowRate,
|
||||||
|
productionRate: productionParams.value.productionRate,
|
||||||
|
pressure: productionParams.value.pressure,
|
||||||
|
volume: 0,
|
||||||
|
temperature: productionParams.value.temperature,
|
||||||
|
isProducing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
productionWells.push(well);
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始注入
|
||||||
|
*/
|
||||||
|
const startInjection = () => {
|
||||||
|
injectionWells.forEach(well => {
|
||||||
|
well.isInjecting = true;
|
||||||
|
const modal = injectionModals.get(well);
|
||||||
|
if (modal) {
|
||||||
|
updateInjectionModalInfo(well, modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟注入体积增加
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!well.isInjecting) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
well.volume += well.flowRate * 0.1;
|
||||||
|
const modal = injectionModals.get(well);
|
||||||
|
if (modal) {
|
||||||
|
updateInjectionModalInfo(well, modal);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始采出
|
||||||
|
*/
|
||||||
|
const startProduction = () => {
|
||||||
|
productionWells.forEach(well => {
|
||||||
|
well.isProducing = true;
|
||||||
|
const modal = productionModals.get(well);
|
||||||
|
if (modal) {
|
||||||
|
updateProductionModalInfo(well, modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟采出体积增加
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!well.isProducing) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
well.volume += well.flowRate * 0.1 / 3600; // 转换为秒
|
||||||
|
const modal = productionModals.get(well);
|
||||||
|
if (modal) {
|
||||||
|
updateProductionModalInfo(well, modal);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除注入井
|
||||||
|
*/
|
||||||
|
const clearInjectionWells = () => {
|
||||||
|
if (injectionWells.length > 0) {
|
||||||
|
injectionWells.forEach(well => {
|
||||||
|
const modal = injectionModals.get(well);
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
if (modalObject.parent) {
|
||||||
|
modalObject.parent.remove(modalObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (viewer) {
|
||||||
|
viewer.scene.remove(well.mesh);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
injectionWells = [];
|
||||||
|
injectionModals.clear();
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除采出井
|
||||||
|
*/
|
||||||
|
const clearProductionWells = () => {
|
||||||
|
if (productionWells.length > 0) {
|
||||||
|
productionWells.forEach(well => {
|
||||||
|
const modal = productionModals.get(well);
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
if (modalObject.parent) {
|
||||||
|
modalObject.parent.remove(modalObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (viewer) {
|
||||||
|
viewer.scene.remove(well.mesh);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
productionWells = [];
|
||||||
|
productionModals.clear();
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="fracturing" title="定向造储">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="isRunning"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startFracturing">
|
||||||
|
{{ isRunning ? '定向造储中...' : '开始定向造储' }}
|
||||||
|
</n-button>
|
||||||
|
<n-checkbox v-model:checked="showRuptureSpheres">
|
||||||
|
显示小球
|
||||||
|
</n-checkbox>
|
||||||
|
<n-checkbox v-model:checked="showFractures">
|
||||||
|
显示裂缝
|
||||||
|
</n-checkbox>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="reset">
|
||||||
|
重置
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref, watch} from 'vue';
|
||||||
|
import {NButton, NCheckbox, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {FractureEffect, Viewer} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let effect: FractureEffect | null = null;
|
||||||
|
|
||||||
|
const isRunning = ref(false);
|
||||||
|
const showRuptureSpheres = ref(true);
|
||||||
|
const showFractures = ref(true);
|
||||||
|
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const startFracturing = async () => {
|
||||||
|
if (!viewer || isRunning.value) return;
|
||||||
|
|
||||||
|
// dispose previous
|
||||||
|
if (effect) {
|
||||||
|
effect.dispose();
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = [
|
||||||
|
new THREE.Vector3(1.5, -0.3, 1.5),
|
||||||
|
new THREE.Vector3(0, -0.3, 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
effect = new FractureEffect({
|
||||||
|
points: points,
|
||||||
|
curveType: 'catmullrom',
|
||||||
|
tension: 0.5,
|
||||||
|
spacing: 0.1, // 增大间距
|
||||||
|
bufferRatio: 0.02,
|
||||||
|
closed: false, // 不闭合,形成螺旋
|
||||||
|
spheresVisible: showRuptureSpheres.value,
|
||||||
|
cracksVisible: showFractures.value
|
||||||
|
});
|
||||||
|
|
||||||
|
isRunning.value = true;
|
||||||
|
effect.onProgress((progress) => {
|
||||||
|
if (progress >= 1) isRunning.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await effect.start();
|
||||||
|
// 确保使用 SDK 提供的方法同步裂缝可见性
|
||||||
|
if (effect) {
|
||||||
|
if (showFractures.value) effect.showCracks(); else effect.hideCracks();
|
||||||
|
}
|
||||||
|
isRunning.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (effect) {
|
||||||
|
effect.dispose();
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
isRunning.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (effect) {
|
||||||
|
effect.dispose();
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当复选框切换时,调用 effect 的方法或直接设置可见性
|
||||||
|
watch(showRuptureSpheres, (val) => {
|
||||||
|
if (!effect) return;
|
||||||
|
if (val) effect.showSpheres(); else effect.hideSpheres();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(showFractures, (val) => {
|
||||||
|
if (!effect) return;
|
||||||
|
if (val) effect.showCracks(); else effect.hideCracks();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="deformationFieldData" title="形变场数据">
|
||||||
|
<n-space>
|
||||||
|
<n-button :disabled="hasVolume" type="primary" @click="create">
|
||||||
|
创建
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasVolume" type="error" @click="remove">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {VolumeMesh, VolumeRenderMode, VolumeTool} from '@deep/engine';
|
||||||
|
import * as THREE from "three/webgpu";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasVolume = ref(false);
|
||||||
|
let volume: VolumeMesh | null = null;
|
||||||
|
|
||||||
|
//------ 地热粉尘体数据固定填充参数 开始------
|
||||||
|
/**
|
||||||
|
* 体素值语义枚举。
|
||||||
|
*/
|
||||||
|
enum EVolumeVoxelValue {
|
||||||
|
/**
|
||||||
|
* 空体素值(无颜色区域)。
|
||||||
|
*/
|
||||||
|
Empty = 0,
|
||||||
|
/**
|
||||||
|
* 绿色管道体素值。
|
||||||
|
*/
|
||||||
|
GreenPipe = 160,
|
||||||
|
/**
|
||||||
|
* 红色热点体素值。
|
||||||
|
*/
|
||||||
|
RedHotspot = 245,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管道轴线定义。
|
||||||
|
*/
|
||||||
|
interface IPipeAxis {
|
||||||
|
/**
|
||||||
|
* 轴线起点。
|
||||||
|
*/
|
||||||
|
start: THREE.Vector3;
|
||||||
|
/**
|
||||||
|
* 轴线终点。
|
||||||
|
*/
|
||||||
|
end: THREE.Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体素分辨率。
|
||||||
|
*/
|
||||||
|
const VOLUME_SIZE = 256;
|
||||||
|
/**
|
||||||
|
* 管道半径。
|
||||||
|
*/
|
||||||
|
const PIPE_RADIUS = 0.2;
|
||||||
|
/**
|
||||||
|
* 红色热点半径。
|
||||||
|
*/
|
||||||
|
const RED_HOTSPOT_RADIUS = 0.28;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 两根完整管道轴线(与钻井面板保持一致)。
|
||||||
|
*/
|
||||||
|
const PIPE_AXIS_LIST: IPipeAxis[] = [
|
||||||
|
{
|
||||||
|
start: new THREE.Vector3(1.5, 2.5, 1.5),
|
||||||
|
end: new THREE.Vector3(1.5, 0, 1.5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new THREE.Vector3(0, 2.5, 0),
|
||||||
|
end: new THREE.Vector3(0, 0, 0),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红色热点中心点(按用户指定)。
|
||||||
|
*/
|
||||||
|
const RED_HOTSPOT_POSITION_LIST: THREE.Vector3[] = [
|
||||||
|
new THREE.Vector3(0, 0.5, 0),
|
||||||
|
new THREE.Vector3(1.5019181086420077, 1.1203964954097632, 1.5),
|
||||||
|
];
|
||||||
|
//------ 地热粉尘体数据固定填充参数 结束------
|
||||||
|
|
||||||
|
//------ 地热粉尘体数据固定填充工具函数 开始------
|
||||||
|
/**
|
||||||
|
* 计算点到线段的最短距离。
|
||||||
|
* @param px 点的 x 坐标
|
||||||
|
* @param py 点的 y 坐标
|
||||||
|
* @param pz 点的 z 坐标
|
||||||
|
* @param start 线段起点
|
||||||
|
* @param end 线段终点
|
||||||
|
* @returns 点到线段最短距离
|
||||||
|
*/
|
||||||
|
const distancePointToSegment = (px: number, py: number, pz: number, start: THREE.Vector3, end: THREE.Vector3): number => {
|
||||||
|
const vx = end.x - start.x;
|
||||||
|
const vy = end.y - start.y;
|
||||||
|
const vz = end.z - start.z;
|
||||||
|
const wx = px - start.x;
|
||||||
|
const wy = py - start.y;
|
||||||
|
const wz = pz - start.z;
|
||||||
|
|
||||||
|
const c1 = wx * vx + wy * vy + wz * vz;
|
||||||
|
if (c1 <= 0) {
|
||||||
|
const dx = px - start.x;
|
||||||
|
const dy = py - start.y;
|
||||||
|
const dz = pz - start.z;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
const c2 = vx * vx + vy * vy + vz * vz;
|
||||||
|
if (c2 <= c1) {
|
||||||
|
const dx = px - end.x;
|
||||||
|
const dy = py - end.y;
|
||||||
|
const dz = pz - end.z;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = c1 / c2;
|
||||||
|
const projX = start.x + t * vx;
|
||||||
|
const projY = start.y + t * vy;
|
||||||
|
const projZ = start.z + t * vz;
|
||||||
|
const dx = px - projX;
|
||||||
|
const dy = py - projY;
|
||||||
|
const dz = pz - projZ;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算当前点到最近管道轴线的最短距离。
|
||||||
|
* @param px 点的 x 坐标
|
||||||
|
* @param py 点的 y 坐标
|
||||||
|
* @param pz 点的 z 坐标
|
||||||
|
* @returns 到最近管道轴线的最短距离
|
||||||
|
*/
|
||||||
|
const nearestPipeDistance = (px: number, py: number, pz: number): number => {
|
||||||
|
let minDist = Infinity;
|
||||||
|
for (const pipeAxis of PIPE_AXIS_LIST) {
|
||||||
|
const dist = distancePointToSegment(px, py, pz, pipeAxis.start, pipeAxis.end);
|
||||||
|
if (dist < minDist) minDist = dist;
|
||||||
|
}
|
||||||
|
return minDist;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前体素点是否处于任一红色热点范围,并返回归一化中心接近度(1=热点中心,0=热点边缘)。
|
||||||
|
* 未命中任一热点时返回 null。
|
||||||
|
* @param px 点的 x 坐标
|
||||||
|
* @param py 点的 y 坐标
|
||||||
|
* @param pz 点的 z 坐标
|
||||||
|
* @returns 命中时 [0,1] 归一化接近度,未命中返回 null
|
||||||
|
*/
|
||||||
|
const nearestHotspotFactor = (px: number, py: number, pz: number): number | null => {
|
||||||
|
const radiusSquared = RED_HOTSPOT_RADIUS * RED_HOTSPOT_RADIUS;
|
||||||
|
let best: number | null = null;
|
||||||
|
for (const hotspotPosition of RED_HOTSPOT_POSITION_LIST) {
|
||||||
|
const dx = px - hotspotPosition.x;
|
||||||
|
const dy = py - hotspotPosition.y;
|
||||||
|
const dz = pz - hotspotPosition.z;
|
||||||
|
const distSq = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (distSq <= radiusSquared) {
|
||||||
|
// 越靠近中心值越大
|
||||||
|
const factor = 1 - Math.sqrt(distSq) / RED_HOTSPOT_RADIUS;
|
||||||
|
if (best === null || factor > best) best = factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------ Perlin 噪声实现(固定种子,确保体数据渐变可复现)------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 噪声置换表,使用固定种子 42 初始化(Fisher-Yates 洗牌 + 线性同余随机数)。
|
||||||
|
*/
|
||||||
|
const NOISE_PERM: Uint8Array = (() => {
|
||||||
|
const perm = new Uint8Array(512);
|
||||||
|
const p = new Uint8Array(256);
|
||||||
|
for (let i = 0; i < 256; i++) p[i] = i;
|
||||||
|
let rng = 42;
|
||||||
|
for (let i = 255; i > 0; i--) {
|
||||||
|
rng = (Math.imul(rng, 1664525) + 1013904223) | 0;
|
||||||
|
const j = (rng >>> 0) % (i + 1);
|
||||||
|
const tmp = p[i];
|
||||||
|
p[i] = p[j];
|
||||||
|
p[j] = tmp;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 256; i++) perm[i] = perm[i + 256] = p[i];
|
||||||
|
return perm;
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perlin 噪声五次平滑插值曲线。
|
||||||
|
*/
|
||||||
|
const noiseFade = (t: number): number => t * t * t * (t * (t * 6 - 15) + 10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 线性插值。
|
||||||
|
*/
|
||||||
|
const noiseLerp = (a: number, b: number, t: number): number => a + t * (b - a);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perlin 噪声梯度函数。
|
||||||
|
*/
|
||||||
|
const noiseGrad = (hash: number, x: number, y: number, z: number): number => {
|
||||||
|
const h = hash & 15;
|
||||||
|
const u = h < 8 ? x : y;
|
||||||
|
const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
|
||||||
|
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 三维 Perlin 噪声,返回 [-1, 1] 范围内的值。
|
||||||
|
* @param x 噪声空间坐标 x
|
||||||
|
* @param y 噪声空间坐标 y
|
||||||
|
* @param z 噪声空间坐标 z
|
||||||
|
* @returns 噪声值 [-1, 1]
|
||||||
|
*/
|
||||||
|
const perlinNoise3d = (x: number, y: number, z: number): number => {
|
||||||
|
const X = Math.floor(x) & 255;
|
||||||
|
const Y = Math.floor(y) & 255;
|
||||||
|
const Z = Math.floor(z) & 255;
|
||||||
|
const fx = x - Math.floor(x);
|
||||||
|
const fy = y - Math.floor(y);
|
||||||
|
const fz = z - Math.floor(z);
|
||||||
|
const u = noiseFade(fx);
|
||||||
|
const v = noiseFade(fy);
|
||||||
|
const w = noiseFade(fz);
|
||||||
|
const A = NOISE_PERM[X] + Y;
|
||||||
|
const AA = NOISE_PERM[A] + Z;
|
||||||
|
const AB = NOISE_PERM[A + 1] + Z;
|
||||||
|
const B = NOISE_PERM[X + 1] + Y;
|
||||||
|
const BA = NOISE_PERM[B] + Z;
|
||||||
|
const BB = NOISE_PERM[B + 1] + Z;
|
||||||
|
return noiseLerp(
|
||||||
|
noiseLerp(
|
||||||
|
noiseLerp(noiseGrad(NOISE_PERM[AA], fx, fy, fz), noiseGrad(NOISE_PERM[BA], fx - 1, fy, fz), u),
|
||||||
|
noiseLerp(noiseGrad(NOISE_PERM[AB], fx, fy - 1, fz), noiseGrad(NOISE_PERM[BB], fx - 1, fy - 1, fz), u),
|
||||||
|
v,
|
||||||
|
),
|
||||||
|
noiseLerp(
|
||||||
|
noiseLerp(noiseGrad(NOISE_PERM[AA + 1], fx, fy, fz - 1), noiseGrad(NOISE_PERM[BA + 1], fx - 1, fy, fz - 1), u),
|
||||||
|
noiseLerp(noiseGrad(NOISE_PERM[AB + 1], fx, fy - 1, fz - 1), noiseGrad(NOISE_PERM[BB + 1], fx - 1, fy - 1, fz - 1), u),
|
||||||
|
v,
|
||||||
|
),
|
||||||
|
w,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分形布朗运动(fBm)噪声:叠加多个 octave 增加细节层次,返回归一化 [0, 1] 值。
|
||||||
|
* @param x 噪声空间坐标 x
|
||||||
|
* @param y 噪声空间坐标 y
|
||||||
|
* @param z 噪声空间坐标 z
|
||||||
|
* @param octaves 叠加层数,默认 2 层(兼顾效果与性能)
|
||||||
|
* @returns 归一化噪声值 [0, 1]
|
||||||
|
*/
|
||||||
|
const fbmNoise3d = (x: number, y: number, z: number, octaves = 2): number => {
|
||||||
|
let value = 0;
|
||||||
|
let amplitude = 0.5;
|
||||||
|
let frequency = 1;
|
||||||
|
let maxValue = 0;
|
||||||
|
for (let i = 0; i < octaves; i++) {
|
||||||
|
value += perlinNoise3d(x * frequency, y * frequency, z * frequency) * amplitude;
|
||||||
|
maxValue += amplitude;
|
||||||
|
amplitude *= 0.5;
|
||||||
|
frequency *= 2;
|
||||||
|
}
|
||||||
|
// 归一化到 [0, 1]
|
||||||
|
return (value / maxValue + 1) * 0.5;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成体数据:管道区域强度由 Perlin fBm 噪声在 [0, 255] 全范围随机生成。
|
||||||
|
* 热点区域在噪声基础上叠加中心接近度偏移,使强度整体偏向高区间(对应红色)。
|
||||||
|
* @param outerBox 岩样外包围盒
|
||||||
|
* @returns 体数据数组
|
||||||
|
*/
|
||||||
|
const generatePipeFilledVolumeData = (outerBox: THREE.Box3): Uint8Array => {
|
||||||
|
const volumeData = new Uint8Array(VOLUME_SIZE * VOLUME_SIZE * VOLUME_SIZE);
|
||||||
|
volumeData.fill(EVolumeVoxelValue.Empty);
|
||||||
|
|
||||||
|
// 预计算坐标映射,降低三层循环中的重复计算开销。
|
||||||
|
const mappedXList = new Float32Array(VOLUME_SIZE);
|
||||||
|
const mappedYList = new Float32Array(VOLUME_SIZE);
|
||||||
|
const mappedZList = new Float32Array(VOLUME_SIZE);
|
||||||
|
|
||||||
|
for (let x = 0; x < VOLUME_SIZE; x++) {
|
||||||
|
mappedXList[x] = VolumeTool.remap(x, 0, VOLUME_SIZE - 1, outerBox.min.x, outerBox.max.x);
|
||||||
|
}
|
||||||
|
for (let y = 0; y < VOLUME_SIZE; y++) {
|
||||||
|
mappedYList[y] = VolumeTool.remap(y, 0, VOLUME_SIZE - 1, outerBox.min.y, outerBox.max.y);
|
||||||
|
}
|
||||||
|
for (let z = 0; z < VOLUME_SIZE; z++) {
|
||||||
|
mappedZList[z] = VolumeTool.remap(z, 0, VOLUME_SIZE - 1, outerBox.min.z, outerBox.max.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 噪声空间缩放系数。
|
||||||
|
const NOISE_SCALE = 3.0;
|
||||||
|
|
||||||
|
for (let x = 0; x < VOLUME_SIZE; x++) {
|
||||||
|
const worldX = mappedXList[x];
|
||||||
|
for (let y = 0; y < VOLUME_SIZE; y++) {
|
||||||
|
const worldY = mappedYList[y];
|
||||||
|
for (let z = 0; z < VOLUME_SIZE; z++) {
|
||||||
|
const worldZ = mappedZList[z];
|
||||||
|
const voxelIndex = x + y * VOLUME_SIZE + z * VOLUME_SIZE * VOLUME_SIZE;
|
||||||
|
|
||||||
|
// 管道外保持空体素(强度 0)。
|
||||||
|
const pipeDist = nearestPipeDistance(worldX, worldY, worldZ);
|
||||||
|
if (pipeDist > PIPE_RADIUS) {
|
||||||
|
volumeData[voxelIndex] = EVolumeVoxelValue.Empty;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fBm 噪声 [0, 1],映射到管道强度范围 [10, 255]。
|
||||||
|
const noiseVal = fbmNoise3d(worldX * NOISE_SCALE, worldY * NOISE_SCALE, worldZ * NOISE_SCALE);
|
||||||
|
let voxelValue = 10 + noiseVal * 245;
|
||||||
|
|
||||||
|
// 热点区域:在噪声基础上叠加中心接近度偏移,使强度整体偏向高区间(红色)。
|
||||||
|
const hotspotFactor = nearestHotspotFactor(worldX, worldY, worldZ);
|
||||||
|
if (hotspotFactor !== null) {
|
||||||
|
voxelValue = voxelValue + hotspotFactor * 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeData[voxelIndex] = Math.min(255, Math.round(voxelValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumeData;
|
||||||
|
};
|
||||||
|
//------ 地热粉尘体数据固定填充工具函数 结束------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建形变场体数据并挂载到场景。
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const create = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const box = bus.getRockSample();
|
||||||
|
const outerBox = new THREE.Box3().setFromObject(box);
|
||||||
|
const volumeData = generatePipeFilledVolumeData(outerBox);
|
||||||
|
|
||||||
|
volume = new VolumeMesh(viewer, {
|
||||||
|
name: '形变场数据',
|
||||||
|
size: VOLUME_SIZE,
|
||||||
|
scale: 4.99,
|
||||||
|
data: volumeData,
|
||||||
|
mode: VolumeRenderMode.MaximumIntensityProjection,
|
||||||
|
colorStops: [
|
||||||
|
// 强度 10(step≈0.04)起始为亮绿色,随强度升高过渡至深绿,热点高强度区映射为红色。
|
||||||
|
{color: "#335b33", step: 0.04},
|
||||||
|
{color: "#00ff00", step: 0.62},
|
||||||
|
{color: "#b31414", step: 0.90},
|
||||||
|
{color: "#FF0000", step: 1.00},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
volume.renderOrder = 1
|
||||||
|
// volume.material.depthTest = true
|
||||||
|
volume.material.opacity = 0.5
|
||||||
|
hasVolume.value = true;
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除形变场体数据并更新场景树。
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const remove = () => {
|
||||||
|
if (volume) {
|
||||||
|
volume.dispose();
|
||||||
|
volume = null;
|
||||||
|
}
|
||||||
|
hasVolume.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="GeothermalDrillingAndCementingPanel" title="钻井与固井">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="createFixedDrillingPipe">
|
||||||
|
开始钻进
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {ParametricPipe} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建固定路径的钻进管道
|
||||||
|
*/
|
||||||
|
const createFixedDrillingPipe = () => {
|
||||||
|
createPip()
|
||||||
|
createPip2()
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPip = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const points: THREE.Vector3[] = [
|
||||||
|
new THREE.Vector3(1.5, 2.5, 1.5),
|
||||||
|
new THREE.Vector3(1.5, 0, 1.5),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pipeMaterial = new THREE.MeshBasicNodeMaterial({
|
||||||
|
color: 0x808080,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pip = new ParametricPipe(viewer, {
|
||||||
|
points: points,
|
||||||
|
radius: 0.2,
|
||||||
|
radialSegments: 16,
|
||||||
|
cornerRadius: 0.1,
|
||||||
|
cornerSplit: 5,
|
||||||
|
progress: 0,
|
||||||
|
material: pipeMaterial,
|
||||||
|
capStart: false,
|
||||||
|
capEnd: false,
|
||||||
|
enableDrillingRobot: true,
|
||||||
|
robotColor: 0xff0000,
|
||||||
|
});
|
||||||
|
pip.renderOrder = 1
|
||||||
|
pip.name = `管道`;
|
||||||
|
viewer.scene.add(pip);
|
||||||
|
pip.setCollisionProxyType(true);
|
||||||
|
pip.addCollisionTarget(bus.getRockSample());
|
||||||
|
|
||||||
|
pip.setDuration(2);
|
||||||
|
pip.startAnimation();
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const createPip2 = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const points: THREE.Vector3[] = [
|
||||||
|
new THREE.Vector3(0, 2.5, 0),
|
||||||
|
new THREE.Vector3(0, 0, 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pipeMaterial = new THREE.MeshBasicNodeMaterial({
|
||||||
|
color: 0x808080,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pip = new ParametricPipe(viewer, {
|
||||||
|
points: points,
|
||||||
|
radius: 0.2,
|
||||||
|
radialSegments: 16,
|
||||||
|
cornerRadius: 0.1,
|
||||||
|
cornerSplit: 5,
|
||||||
|
progress: 0,
|
||||||
|
material: pipeMaterial,
|
||||||
|
capStart: false,
|
||||||
|
capEnd: false,
|
||||||
|
enableDrillingRobot: true,
|
||||||
|
robotColor: 0xff0000,
|
||||||
|
});
|
||||||
|
|
||||||
|
pip.name = `管道1`;
|
||||||
|
viewer.scene.add(pip);
|
||||||
|
pip.setCollisionProxyType(true);
|
||||||
|
pip.addCollisionTarget(bus.getRockSample());
|
||||||
|
|
||||||
|
pip.setDuration(2);
|
||||||
|
pip.startAnimation();
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="geothermalInjectionProduction" title="注采">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="warning"
|
||||||
|
@click="createThermalBreakthroughFlowParticles">
|
||||||
|
开始注采
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!isActive"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="disposeThermalBreakthroughFlowParticles">
|
||||||
|
停止注采
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onBeforeUnmount, onMounted, ref} from "vue";
|
||||||
|
import {NButton, NCollapseItem, NSpace} from "naive-ui";
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {FlowParticles} from "@deep/engine";
|
||||||
|
|
||||||
|
const particleCount = ref(2400);
|
||||||
|
const speedMultiplier = ref(1.0);
|
||||||
|
const heatingThreshold = ref(0.5);
|
||||||
|
const particleSize = ref(0.03);
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const isActive = ref(false);
|
||||||
|
|
||||||
|
const syncActiveState = () => {
|
||||||
|
isActive.value = !!bus.getFlowParticles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFlowParticles = () => {
|
||||||
|
const previousFlowParticles = bus.getFlowParticles();
|
||||||
|
if (!previousFlowParticles) return;
|
||||||
|
previousFlowParticles.dispose();
|
||||||
|
bus.setFlowParticles(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createThermalBreakthroughFlowParticles = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const pipe1 = viewer.parametric.findPipe((pipe) => {
|
||||||
|
return pipe.name === "管道";
|
||||||
|
});
|
||||||
|
const pipe2 = viewer.parametric.findPipe((pipe) => {
|
||||||
|
return pipe.name === "管道1";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pipe1 || !pipe2) return;
|
||||||
|
|
||||||
|
clearFlowParticles();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const flowParticles = new FlowParticles(viewer, {
|
||||||
|
startPipe: pipe1,
|
||||||
|
endPipe: pipe2,
|
||||||
|
particleCount: particleCount.value,
|
||||||
|
speedMultiplier: speedMultiplier.value,
|
||||||
|
particleSize: particleSize.value,
|
||||||
|
heatingThreshold: heatingThreshold.value,
|
||||||
|
colorStops: [
|
||||||
|
{color: "#00FF00", step: 0},
|
||||||
|
{color: "#FFFF00", step: 0.4},
|
||||||
|
{color: "#FF0000", step: 0.6},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
flowParticles.flowPoints.renderOrder = 0
|
||||||
|
bus.setFlowParticles(flowParticles);
|
||||||
|
} catch (error) {
|
||||||
|
bus.setFlowParticles(null);
|
||||||
|
console.error("创建热突破流体失败:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncActiveState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const disposeThermalBreakthroughFlowParticles = () => {
|
||||||
|
clearFlowParticles();
|
||||||
|
syncActiveState();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncActiveState();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 该面板卸载时不主动销毁全局流体对象,避免切换折叠面板后状态丢失
|
||||||
|
isActive.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="geothermalPointCloudTemperatureDisplay" title="地热温度点云显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasCloud" size="small" type="primary" @click="create">创建</n-button>
|
||||||
|
<n-button :disabled="!hasCloud" size="small" type="error" @click="remove">移除</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="hasCloud">
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">粒子大小</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="pointSize" :max="0.02" :min="0.001" :step="0.001" @update:value="onPointSizeChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">强度过滤</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="intensityRange" :max="1" :min="0" :step="0.01" range @update:value="onFilterChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { NButton, NCollapseItem, NSlider } from "naive-ui";
|
||||||
|
import { useBus } from "@/hooks";
|
||||||
|
import { PointCloud, PointCloudTool } from "@deep/engine";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地热场景点云可见状态。
|
||||||
|
*/
|
||||||
|
const hasCloud = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云强度过滤范围。
|
||||||
|
*/
|
||||||
|
const intensityRange = ref<[number, number]>([0, 1]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云粒子大小。
|
||||||
|
*/
|
||||||
|
const pointSize = ref<number>(0.01);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景总线实例。
|
||||||
|
*/
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前温度点云实例。
|
||||||
|
*/
|
||||||
|
let cloud: PointCloud | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建地热温度点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const create = (): void => {
|
||||||
|
//------ Geothermal温度点云面板 开始------
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成用于温度点云的点数据。
|
||||||
|
*/
|
||||||
|
const pointCloudData = PointCloudTool.generateNoisePointCloudData({
|
||||||
|
min: [-2.5, -2.5, -2.5],
|
||||||
|
max: [2.5, 2.5, 2.5],
|
||||||
|
x: 170,
|
||||||
|
y: 170,
|
||||||
|
z: 170,
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建点云并绑定颜色映射。
|
||||||
|
*/
|
||||||
|
cloud = new PointCloud(viewer, {
|
||||||
|
name: "地热温度点云",
|
||||||
|
pointCloudData,
|
||||||
|
pointSize: pointSize.value,
|
||||||
|
colorStops: [
|
||||||
|
{color: "#0000FF", step: 0.0},
|
||||||
|
{color: "#00FF00", step: 0.33},
|
||||||
|
{color: "#FFFF00", step: 0.66},
|
||||||
|
{color: "#FF0000", step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建后同步一次面板参数。
|
||||||
|
*/
|
||||||
|
cloud.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
cloud.setPointSize(pointSize.value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发场景树刷新并更新状态。
|
||||||
|
*/
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasCloud.value = true;
|
||||||
|
//------ Geothermal温度点云面板 结束------
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除地热温度点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const remove = (): void => {
|
||||||
|
//------ Geothermal温度点云面板 开始------
|
||||||
|
if (cloud) {
|
||||||
|
/**
|
||||||
|
* 释放当前点云资源。
|
||||||
|
*/
|
||||||
|
cloud.dispose();
|
||||||
|
cloud = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCloud.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
//------ Geothermal温度点云面板 结束------
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据滑块范围过滤点云强度。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onFilterChange = (): void => {
|
||||||
|
//------ Geothermal温度点云面板 开始------
|
||||||
|
cloud?.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
//------ Geothermal温度点云面板 结束------
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据滑块更新点云粒子大小。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onPointSizeChange = (): void => {
|
||||||
|
//------ Geothermal温度点云面板 开始------
|
||||||
|
cloud?.setPointSize(pointSize.value);
|
||||||
|
//------ Geothermal温度点云面板 结束------
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,518 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="developmentMining" title="开拓采准回采">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button style="width: 100%" type="primary" @click="handleDevelopment">
|
||||||
|
钻孔
|
||||||
|
</n-button>
|
||||||
|
<n-button style="width: 100%" type="primary" @click="handlePreparation">
|
||||||
|
开拓/采准
|
||||||
|
</n-button>
|
||||||
|
<n-button style="width: 100%" type="primary" @click="handleStoping">
|
||||||
|
回采
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {CSGOperationType, MiningRobot, ParametricPipe, Viewer} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let verticalPipes: ParametricPipe[] = []; // 开拓管道
|
||||||
|
let horizontalPipe: ParametricPipe | null = null; // 采准管道
|
||||||
|
let connectionPipes: ParametricPipe[] = []; // 回采连接管道
|
||||||
|
let miningRobot: MiningRobot | null = null; // 回采机器人
|
||||||
|
let currentRowIndex = ref(0); // 当前挖掘行索引
|
||||||
|
let currentLayerIndex = ref(0); // 当前挖掘层索引
|
||||||
|
|
||||||
|
// 配置3个管道的起始点位(沿Z轴分布)
|
||||||
|
const pipeConfigs = [
|
||||||
|
{
|
||||||
|
startPoint: new THREE.Vector3(-0.11, 2.5, -1.5),
|
||||||
|
depth: 2.3,
|
||||||
|
endPoint: null,
|
||||||
|
direction: 'down',
|
||||||
|
type: 'intake',
|
||||||
|
label: '进风井'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: new THREE.Vector3(-0.11, 2.5, 0),
|
||||||
|
depth: 2,
|
||||||
|
endPoint: null,
|
||||||
|
direction: 'down',
|
||||||
|
type: 'filling',
|
||||||
|
label: '充填井'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: new THREE.Vector3(0.2233053054589904, 2.5, 1.5),
|
||||||
|
depth: 2.8,
|
||||||
|
endPoint: null,
|
||||||
|
direction: 'down',
|
||||||
|
type: 'exhaust',
|
||||||
|
label: '回风井'
|
||||||
|
},
|
||||||
|
{startPoint: new THREE.Vector3(-0.11, 0, -2.505), depth: 1.1, endPoint: null, direction: 'z-negative'},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDevelopment = () => {
|
||||||
|
console.log('开拓');
|
||||||
|
clearVerticalPipes();
|
||||||
|
createVerticalPipes();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreparation = () => {
|
||||||
|
console.log('采准');
|
||||||
|
|
||||||
|
// 创建水平管道
|
||||||
|
createHorizontalPipe();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStoping = () => {
|
||||||
|
console.log('回采');
|
||||||
|
// 在场景中查找名为"矿体2"的对象
|
||||||
|
let oreMesh = viewer?.scene.getObjectByName("矿体2") as THREE.Mesh;
|
||||||
|
oreMesh.material.side = THREE.DoubleSide
|
||||||
|
// 计算矿体的包围盒
|
||||||
|
const boundingBox = new THREE.Box3().setFromObject(oreMesh);
|
||||||
|
const size = new THREE.Vector3();
|
||||||
|
boundingBox.getSize(size);
|
||||||
|
const center = new THREE.Vector3();
|
||||||
|
boundingBox.getCenter(center);
|
||||||
|
|
||||||
|
console.log('矿体包围盒:', {
|
||||||
|
min: boundingBox.min,
|
||||||
|
max: boundingBox.max,
|
||||||
|
size,
|
||||||
|
center
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建机器人
|
||||||
|
if (miningRobot) {
|
||||||
|
miningRobot.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
miningRobot = new MiningRobot({
|
||||||
|
size: 0.075,
|
||||||
|
color: 0xff6600,
|
||||||
|
widthRatio: 1.5,
|
||||||
|
heightRatio: 1.2,
|
||||||
|
depthRatio: 2
|
||||||
|
});
|
||||||
|
miningRobot.group.position.set(0, 0, -0.55);
|
||||||
|
// 添加机器人到场景
|
||||||
|
viewer.scene.add(miningRobot.group);
|
||||||
|
|
||||||
|
miningRobot.group.visible = true;
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
// 添加矿体作为 CSG 目标对象(要被切割头挖掘的对象)
|
||||||
|
miningRobot.addCSGTarget(oreMesh, CSGOperationType.SUBTRACTION);
|
||||||
|
|
||||||
|
// 获取切割头的尺寸和偏移(用于计算挖掘路径)
|
||||||
|
const cuttingHeadSize = miningRobot.options.size * 1.2;
|
||||||
|
const rowSpacing = cuttingHeadSize; // Z 方向间距,严格等于切割头尺寸
|
||||||
|
const layerHeight = cuttingHeadSize; // Y 方向间距,严格等于切割头尺寸
|
||||||
|
|
||||||
|
// 计算切割头相对于机器人组的偏移
|
||||||
|
const baseSize = miningRobot.options.size;
|
||||||
|
const depth = baseSize * miningRobot.options.depthRatio;
|
||||||
|
const connectorLength = depth * 0.4;
|
||||||
|
const cuttingHeadOffset = depth / 2 + connectorLength + cuttingHeadSize / 2; // 切割头在 Z 轴正方向的偏移
|
||||||
|
|
||||||
|
// 计算挖掘路径(这些是切割头的目标位置)
|
||||||
|
const miningPaths: Array<{ start: THREE.Vector3, end: THREE.Vector3 }> = [];
|
||||||
|
|
||||||
|
// 从 Z 负方向开始,每个 Z 层从下往上挖
|
||||||
|
const startY = boundingBox.min.y + cuttingHeadSize / 2;
|
||||||
|
const endY = boundingBox.max.y - cuttingHeadSize / 2;
|
||||||
|
const startZ = boundingBox.min.z + cuttingHeadSize / 2;
|
||||||
|
const endZ = boundingBox.max.z - cuttingHeadSize / 2;
|
||||||
|
const startX = boundingBox.min.x + cuttingHeadSize / 2;
|
||||||
|
const endX = boundingBox.max.x - cuttingHeadSize / 2;
|
||||||
|
|
||||||
|
let currentZ = startZ;
|
||||||
|
let zLayerIndex = 0;
|
||||||
|
|
||||||
|
// 遍历每个 Z 层
|
||||||
|
while (currentZ <= endZ) {
|
||||||
|
let currentY = startY;
|
||||||
|
let rowIndex = 0;
|
||||||
|
|
||||||
|
// 在当前 Z 层,从下往上挖
|
||||||
|
while (currentY <= endY) {
|
||||||
|
// 交替方向挖掘(之字形):从 X 负方向挖到正方向,再从正方向挖回负方向
|
||||||
|
const isEvenRow = rowIndex % 2 === 0;
|
||||||
|
const pathStartX = isEvenRow ? startX : endX;
|
||||||
|
const pathEndX = isEvenRow ? endX : startX;
|
||||||
|
|
||||||
|
// 机器人组的位置 = 切割头目标位置 - 切割头偏移
|
||||||
|
miningPaths.push({
|
||||||
|
start: new THREE.Vector3(pathStartX, currentY, currentZ - cuttingHeadOffset),
|
||||||
|
end: new THREE.Vector3(pathEndX, currentY, currentZ - cuttingHeadOffset)
|
||||||
|
});
|
||||||
|
|
||||||
|
currentY += layerHeight;
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentZ += rowSpacing;
|
||||||
|
zLayerIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`生成了 ${miningPaths.length} 条挖掘路径`);
|
||||||
|
|
||||||
|
// 开始挖掘第一条路径
|
||||||
|
currentRowIndex.value = 0;
|
||||||
|
currentLayerIndex.value = 0;
|
||||||
|
|
||||||
|
const startNextPath = () => {
|
||||||
|
if (currentRowIndex.value >= miningPaths.length) {
|
||||||
|
console.log('挖掘完成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = miningPaths[currentRowIndex.value];
|
||||||
|
if (!path) {
|
||||||
|
console.warn('路径不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始挖掘路径 ${currentRowIndex.value + 1}/${miningPaths.length}`);
|
||||||
|
|
||||||
|
miningRobot!.startMining(
|
||||||
|
viewer!,
|
||||||
|
path.start,
|
||||||
|
path.end,
|
||||||
|
{
|
||||||
|
duration: 1,
|
||||||
|
csgFrequency: 0.5,
|
||||||
|
onProgress: (_progress) => {
|
||||||
|
// 可以在这里更新进度UI
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
console.log(`路径 ${currentRowIndex.value + 1} 完成`);
|
||||||
|
currentRowIndex.value++;
|
||||||
|
|
||||||
|
// 延迟一点再开始下一条路径
|
||||||
|
setTimeout(() => {
|
||||||
|
startNextPath();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始第一条路径
|
||||||
|
startNextPath();
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建3个垂直向下的管道
|
||||||
|
*/
|
||||||
|
const createVerticalPipes = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const currentViewer = viewer;
|
||||||
|
|
||||||
|
// 前3个垂直管道配置
|
||||||
|
const verticalConfigs = pipeConfigs.slice(0, 3);
|
||||||
|
|
||||||
|
verticalConfigs.forEach((config, index) => {
|
||||||
|
const {startPoint, depth, type, label} = config;
|
||||||
|
const points: THREE.Vector3[] = [];
|
||||||
|
// if (index === 1) {
|
||||||
|
|
||||||
|
const topPoint = new THREE.Vector3(startPoint.x, startPoint.y, startPoint.z);
|
||||||
|
const bottomPoint = new THREE.Vector3(
|
||||||
|
startPoint.x,
|
||||||
|
startPoint.y - 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: 0x808080,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pipe = new ParametricPipe(currentViewer, {
|
||||||
|
points: points,
|
||||||
|
radius: 0.1,
|
||||||
|
radialSegments: 16,
|
||||||
|
cornerRadius: 0.1,
|
||||||
|
cornerSplit: 5,
|
||||||
|
progress: 0,
|
||||||
|
material: pipeMaterial,
|
||||||
|
capStart: false,
|
||||||
|
capEnd: false,
|
||||||
|
enableDrillingRobot: true,
|
||||||
|
robotColor: 0xff0000,
|
||||||
|
// debugCollisionProxy: true,
|
||||||
|
metadata: {
|
||||||
|
drillingType: type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (index === 1) {
|
||||||
|
pipe.setCollisionProxyType(false);
|
||||||
|
pipe.addCollisionTarget(bus.getSection(), CSGOperationType.HOLLOW_SUBTRACTION, -1);
|
||||||
|
} else if (index === 2) {
|
||||||
|
pipe.setCollisionProxyType(false);
|
||||||
|
const mesh = viewer?.scene.getObjectByName("地质层") as THREE.Mesh;
|
||||||
|
pipe.addCollisionTarget(mesh, CSGOperationType.HOLLOW_SUBTRACTION, -1);
|
||||||
|
}else {
|
||||||
|
pipe.setCollisionProxyType(true);
|
||||||
|
}
|
||||||
|
currentViewer.scene.add(pipe);
|
||||||
|
pipe.name = label
|
||||||
|
|
||||||
|
// 启用CSG碰撞
|
||||||
|
pipe.addCollisionTarget(bus.getRockSample());
|
||||||
|
|
||||||
|
pipe.setDuration(3);
|
||||||
|
pipe.startAnimation();
|
||||||
|
|
||||||
|
verticalPipes.push(pipe);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建水平方向的管道(沿Z轴)
|
||||||
|
*/
|
||||||
|
const createHorizontalPipe = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const currentViewer = viewer;
|
||||||
|
|
||||||
|
// 第4个水平管道配置
|
||||||
|
const config = pipeConfigs[3];
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const {startPoint, depth} = config;
|
||||||
|
const points: THREE.Vector3[] = [];
|
||||||
|
|
||||||
|
// 沿Z轴正方向的管道
|
||||||
|
const segments = 10;
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const t = i / segments;
|
||||||
|
const z = startPoint.z + depth * t;
|
||||||
|
points.push(new THREE.Vector3(startPoint.x, startPoint.y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeMaterial = new THREE.MeshBasicNodeMaterial({
|
||||||
|
color: 0x808080,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pipe = new ParametricPipe(currentViewer, {
|
||||||
|
points: points,
|
||||||
|
radius: 0.25,
|
||||||
|
radialSegments: 16,
|
||||||
|
cornerRadius: 0.1,
|
||||||
|
cornerSplit: 5,
|
||||||
|
progress: 0,
|
||||||
|
material: pipeMaterial,
|
||||||
|
capStart: false,
|
||||||
|
capEnd: false,
|
||||||
|
enableDrillingRobot: true,
|
||||||
|
robotColor: 0xff0000,
|
||||||
|
});
|
||||||
|
currentViewer.scene.add(pipe);
|
||||||
|
pipe.name = `回采管道_4`;
|
||||||
|
|
||||||
|
// 启用CSG碰撞
|
||||||
|
pipe.setCollisionProxyType(true); // true = 静态
|
||||||
|
pipe.addCollisionTarget(bus.getRockSample());
|
||||||
|
|
||||||
|
// 监听动画完成 - 使用定时器检查进度
|
||||||
|
const checkAnimationComplete = setInterval(() => {
|
||||||
|
if (pipe.options.progress >= 1) {
|
||||||
|
clearInterval(checkAnimationComplete);
|
||||||
|
console.log('水平管道动画完成,创建连接管道');
|
||||||
|
createConnectionPipes();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 点击后立即开始动画
|
||||||
|
pipe.setDuration(3);
|
||||||
|
pipe.startAnimation();
|
||||||
|
|
||||||
|
horizontalPipe = pipe;
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建3个连接管道
|
||||||
|
*/
|
||||||
|
const createConnectionPipes = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const meshTarget = viewer.scene.getObjectByName("矿体1") as THREE.Mesh
|
||||||
|
const meshTarget2 = viewer.scene.getObjectByName("地质层") as THREE.Mesh
|
||||||
|
|
||||||
|
const currentViewer = viewer;
|
||||||
|
const config = pipeConfigs[3];
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
// 获取水平管道的终点
|
||||||
|
const endPoint = new THREE.Vector3(
|
||||||
|
config.startPoint.x,
|
||||||
|
config.startPoint.y,
|
||||||
|
config.startPoint.z + config.depth
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const Points = [
|
||||||
|
new THREE.Vector3(0.2233053054589904, -0.37226677965409577, -0.4178163281883125 - 0.4),
|
||||||
|
new THREE.Vector3(0.2233053054589904 - 0.3, -0.37226677965409577, -0.4178163281883125 - 0.4),
|
||||||
|
new THREE.Vector3(0.2233053054589904 - 0.6, -0.37226677965409577, -0.4178163281883125 - 0.4),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3个目标点,x坐标依次递减0.3
|
||||||
|
const targetPoints = [
|
||||||
|
new THREE.Vector3(0.2233053054589904, -0.37226677965409577, -0.4178163281883125),
|
||||||
|
new THREE.Vector3(0.2233053054589904 - 0.3, -0.37226677965409577, -0.4178163281883125),
|
||||||
|
new THREE.Vector3(0.2233053054589904 - 0.6, -0.37226677965409577, -0.4178163281883125),
|
||||||
|
];
|
||||||
|
|
||||||
|
targetPoints.forEach((targetPoint, index) => {
|
||||||
|
// 计算向外的偏移方向(根据index决定左右,中间为0)
|
||||||
|
const lateralOffset = index === 0 ? 0.3 : index === 1 ? 0 : -0.3;
|
||||||
|
|
||||||
|
// 起点:从水平管道侧面衍生,向外偏移
|
||||||
|
const startPoint = new THREE.Vector3(
|
||||||
|
endPoint.x + lateralOffset * 0.4,
|
||||||
|
endPoint.y,
|
||||||
|
endPoint.z - 0.1
|
||||||
|
);
|
||||||
|
|
||||||
|
// 平行段终点
|
||||||
|
const parallelDistance = 0.2;
|
||||||
|
const parallelEndPoint = new THREE.Vector3(
|
||||||
|
startPoint.x,
|
||||||
|
startPoint.y - 0.05,
|
||||||
|
startPoint.z + parallelDistance
|
||||||
|
);
|
||||||
|
|
||||||
|
// 控制点:用于创建曲线
|
||||||
|
const controlPoint = new THREE.Vector3(
|
||||||
|
parallelEndPoint.x + lateralOffset * 0.5,
|
||||||
|
parallelEndPoint.y - 0.5,
|
||||||
|
parallelEndPoint.z
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取对应的中间点
|
||||||
|
const middlePoint = Points[index];
|
||||||
|
if (!middlePoint) return;
|
||||||
|
|
||||||
|
let points: THREE.Vector3[];
|
||||||
|
|
||||||
|
// 第一个管道(index === 0)一直延伸到 z = 2.4
|
||||||
|
if (index === 0) {
|
||||||
|
// 最后平行于 Z 轴延伸到 z = 2.4
|
||||||
|
const finalPoint = new THREE.Vector3(
|
||||||
|
middlePoint.x,
|
||||||
|
middlePoint.y,
|
||||||
|
2.4
|
||||||
|
);
|
||||||
|
const baseStart = new THREE.Vector3(
|
||||||
|
startPoint.x,
|
||||||
|
startPoint.y - 0.05, -2.5
|
||||||
|
);
|
||||||
|
points = [ startPoint, parallelEndPoint, controlPoint, middlePoint, finalPoint];
|
||||||
|
} else {
|
||||||
|
// 其他管道:从中间点延伸到目标点
|
||||||
|
points = [startPoint, parallelEndPoint, controlPoint, middlePoint, targetPoint];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeMaterial = new THREE.MeshBasicNodeMaterial({
|
||||||
|
color: 0x808080,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pipe = new ParametricPipe(currentViewer, {
|
||||||
|
points: points,
|
||||||
|
radius: 0.1,
|
||||||
|
radialSegments: 20,
|
||||||
|
cornerRadius: 1,
|
||||||
|
cornerSplit: 30,
|
||||||
|
progress: 0,
|
||||||
|
material: pipeMaterial,
|
||||||
|
capStart: false,
|
||||||
|
capEnd: false,
|
||||||
|
enableDrillingRobot: true,
|
||||||
|
robotColor: 0xff0000,
|
||||||
|
robotCylinderLengthRatio: 1.5,
|
||||||
|
robotConeLengthRatio: 1,
|
||||||
|
collisionCheckProgressStep: 0.06,
|
||||||
|
collisionProxyHeight: 0.4,
|
||||||
|
// debugCollisionProxy: true
|
||||||
|
});
|
||||||
|
pipe.renderOrder = 1
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
pipe.setCollisionProxyType(false);
|
||||||
|
pipe.addCollisionTarget(bus.getSection(), CSGOperationType.HOLLOW_SUBTRACTION, -1);
|
||||||
|
pipe.addCollisionTarget(meshTarget, CSGOperationType.HOLLOW_SUBTRACTION, -1);
|
||||||
|
pipe.addCollisionTarget(meshTarget2, CSGOperationType.SUBTRACTION, -1);
|
||||||
|
|
||||||
|
bus.setTemperatureLine(pipe)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
currentViewer.scene.add(pipe);
|
||||||
|
pipe.name = `连接管道_${index + 1}`;
|
||||||
|
|
||||||
|
// 立即开始动画
|
||||||
|
pipe.setDuration(3);
|
||||||
|
pipe.startAnimation();
|
||||||
|
|
||||||
|
connectionPipes.push(pipe);
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除垂直管道
|
||||||
|
*/
|
||||||
|
const clearVerticalPipes = () => {
|
||||||
|
verticalPipes.forEach(pipe => {
|
||||||
|
pipe.dispose();
|
||||||
|
});
|
||||||
|
verticalPipes = [];
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
328
packages/demo/src/panels/GoldMineScene/DrillingPanel.vue
Normal file
328
packages/demo/src/panels/GoldMineScene/DrillingPanel.vue
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="exploration-mining" title="开拓采准回采">
|
||||||
|
<n-form label-placement="left" label-width="140" size="small">
|
||||||
|
<n-form-item label="移动速度">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="miningParams.speed"
|
||||||
|
:max="1"
|
||||||
|
:min="0.01"
|
||||||
|
:step="0.01"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="挖掘距离">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="miningParams.distance"
|
||||||
|
:max="20"
|
||||||
|
:min="1"
|
||||||
|
:step="0.5"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="isMining"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startMining">
|
||||||
|
{{ isMining ? '挖掘中...' : '矿体回采' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="robots.length === 0"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="reset">
|
||||||
|
重置 ({{ robots.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 {MiningRobot, Viewer} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let robots: MiningRobot[] = [];
|
||||||
|
let robotCounter = 0;
|
||||||
|
const isMining = ref(false);
|
||||||
|
|
||||||
|
// 颜色选项
|
||||||
|
const colorOptions = [
|
||||||
|
{label: '橙色', value: 0xffa500},
|
||||||
|
{label: '黄色', value: 0xffff00},
|
||||||
|
{label: '绿色', value: 0x00ff00},
|
||||||
|
{label: '蓝色', value: 0x0000ff},
|
||||||
|
{label: '红色', value: 0xff0000},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 回采参数
|
||||||
|
const miningParams = ref({
|
||||||
|
color: 0xffa500,
|
||||||
|
speed: 0.05,
|
||||||
|
distance: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找岩样对象
|
||||||
|
*/
|
||||||
|
const findRockSample = (): THREE.Object3D | null => {
|
||||||
|
if (!viewer) return null;
|
||||||
|
|
||||||
|
let rockSample: THREE.Object3D | null = null;
|
||||||
|
viewer.scene.traverse((object) => {
|
||||||
|
if (object.name && object.name.includes('岩样')) {
|
||||||
|
rockSample = object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rockSample;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始挖掘
|
||||||
|
*/
|
||||||
|
const startMining = () => {
|
||||||
|
if (!viewer || isMining.value) return;
|
||||||
|
|
||||||
|
const rockSample = findRockSample();
|
||||||
|
if (!rockSample) {
|
||||||
|
console.warn('未找到岩样对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMining.value = true;
|
||||||
|
|
||||||
|
// 在绝对位置 (5, 5, 5) 创建机器人
|
||||||
|
const startPosition = new THREE.Vector3(5, 5, 5);
|
||||||
|
|
||||||
|
// 目标位置:沿着负Z轴方向移动
|
||||||
|
const endPosition = new THREE.Vector3(
|
||||||
|
startPosition.x,
|
||||||
|
startPosition.y,
|
||||||
|
startPosition.z - miningParams.value.distance
|
||||||
|
);
|
||||||
|
|
||||||
|
createAndAnimateMiningRobot(startPosition, endPosition, rockSample);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并动画化回采机器人
|
||||||
|
*/
|
||||||
|
const createAndAnimateMiningRobot = (
|
||||||
|
startPosition: THREE.Vector3,
|
||||||
|
endPosition: THREE.Vector3,
|
||||||
|
rockSample: THREE.Object3D
|
||||||
|
) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const robot = new MiningRobot({
|
||||||
|
size: 1,
|
||||||
|
color: miningParams.value.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
robotCounter++;
|
||||||
|
robot.group.name = `回采机器人_${robotCounter}`;
|
||||||
|
|
||||||
|
// 添加到场景
|
||||||
|
viewer.scene.add(robot.group);
|
||||||
|
|
||||||
|
robots.push(robot);
|
||||||
|
|
||||||
|
// 开始挖掘动画
|
||||||
|
robot.startMining(startPosition, endPosition, rockSample, {
|
||||||
|
speed: miningParams.value.speed,
|
||||||
|
cutterSize: 0.5,
|
||||||
|
cutterSegments: 16,
|
||||||
|
csgInterval: 0.1,
|
||||||
|
onComplete: () => {
|
||||||
|
isMining.value = false;
|
||||||
|
console.log('挖掘完成');
|
||||||
|
},
|
||||||
|
onProgress: (progress) => {
|
||||||
|
// 可以在这里添加进度显示逻辑
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
if (robots.length > 0) {
|
||||||
|
robots.forEach(robot => {
|
||||||
|
robot.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
robots = [];
|
||||||
|
robotCounter = 0;
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,660 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="ventilation-filling" title="通风充填">
|
||||||
|
<n-form label-placement="left" label-width="140" size="small">
|
||||||
|
<n-form-item label="充填结束时间(秒)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="fillingParams.endTime"
|
||||||
|
:max="50"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item>
|
||||||
|
<n-button
|
||||||
|
:disabled="fillingStatus.isRunning"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startFilling">
|
||||||
|
{{ fillingStatus.isRunning ? `充填中... ${fillingStatus.progress}%` : '开始充填' }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="送风结束时间(秒)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="airSupplyParams.endTime"
|
||||||
|
:max="50"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item>
|
||||||
|
<n-button
|
||||||
|
:disabled="airSupplyStatus.isRunning"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startAirSupply">
|
||||||
|
{{ airSupplyStatus.isRunning ? `送风中... ${airSupplyStatus.progress}%` : '开始送风' }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="回风结束时间(秒)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="returnAirParams.endTime"
|
||||||
|
:max="50"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item>
|
||||||
|
<n-button
|
||||||
|
:disabled="returnAirStatus.isRunning"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startReturnAir">
|
||||||
|
{{ returnAirStatus.isRunning ? `回风中... ${returnAirStatus.progress}%` : '开始回风' }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-button
|
||||||
|
:disabled="!hasActiveProcess"
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="resetAll">
|
||||||
|
重置所有
|
||||||
|
</n-button>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NForm, NFormItem, NInputNumber} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {CssType, EventManagerEvents, ParametricPipe, Viewer} from '@deep/engine';
|
||||||
|
import {AirSupplyHtmlPanel, FillingHtmlPanel, ReturnAirHtmlPanel} from '@/htmlPanel';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let fillingPipe: ParametricPipe | null = null;
|
||||||
|
let airSupplyPipe: ParametricPipe | null = null;
|
||||||
|
let returnAirPipe: ParametricPipe | null = null;
|
||||||
|
|
||||||
|
// 保存原始材质属性
|
||||||
|
interface OriginalMaterialState {
|
||||||
|
transparent: boolean;
|
||||||
|
color?: THREE.Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fillingOriginalState: OriginalMaterialState | null = null;
|
||||||
|
let airSupplyOriginalState: OriginalMaterialState | null = null;
|
||||||
|
let returnAirOriginalState: OriginalMaterialState | null = null;
|
||||||
|
|
||||||
|
// HTML 面板引用
|
||||||
|
let fillingHtmlPanel: FillingHtmlPanel | null = null;
|
||||||
|
let airSupplyHtmlPanel: AirSupplyHtmlPanel | null = null;
|
||||||
|
let returnAirHtmlPanel: ReturnAirHtmlPanel | null = null;
|
||||||
|
|
||||||
|
// 充填参数
|
||||||
|
const fillingParams = ref({
|
||||||
|
endTime: 5,
|
||||||
|
material: '尾砂胶结',
|
||||||
|
fillingRate: 80,
|
||||||
|
pressure: 1.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 送风参数
|
||||||
|
const airSupplyParams = ref({
|
||||||
|
endTime: 5,
|
||||||
|
pressure: 1.2, // 压力 (MPa)
|
||||||
|
flowRate: 150, // 流量 (m³/min)
|
||||||
|
temperature: 25, // 温度 (°C)
|
||||||
|
humidity: 60, // 湿度 (%)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回风参数
|
||||||
|
const returnAirParams = ref({
|
||||||
|
endTime: 5,
|
||||||
|
pressure: 0.8, // 压力 (MPa)
|
||||||
|
flowRate: 140, // 流量 (m³/min)
|
||||||
|
temperature: 28, // 温度 (°C)
|
||||||
|
humidity: 65, // 湿度 (%)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 充填状态
|
||||||
|
const fillingStatus = ref({
|
||||||
|
isRunning: false,
|
||||||
|
progress: 0,
|
||||||
|
startTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 送风状态
|
||||||
|
const airSupplyStatus = ref({
|
||||||
|
isRunning: false,
|
||||||
|
progress: 0,
|
||||||
|
startTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回风状态
|
||||||
|
const returnAirStatus = ref({
|
||||||
|
isRunning: false,
|
||||||
|
progress: 0,
|
||||||
|
startTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定时器引用
|
||||||
|
let fillingTimer: number | null = null;
|
||||||
|
let airSupplyTimer: number | null = null;
|
||||||
|
let returnAirTimer: number | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
setupSelectionListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置选择监听器
|
||||||
|
*/
|
||||||
|
const setupSelectionListener = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 监听物体选择事件
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, ({data}) => {
|
||||||
|
const {intersects} = data;
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
// 只处理第一个拾取到的物体
|
||||||
|
const intersection = intersects[0];
|
||||||
|
const clickedObject = intersection.object;
|
||||||
|
const raycastPosition = intersection.point; // 射线拾取的位置
|
||||||
|
|
||||||
|
// 查找选中对象对应的管道
|
||||||
|
const pipe = viewer!.parametric.pipes.find(p => p === clickedObject || p.uuid === clickedObject.uuid);
|
||||||
|
if (!pipe || !pipe.options.metadata?.drillingType) return;
|
||||||
|
|
||||||
|
const drillingType = pipe.options.metadata.drillingType;
|
||||||
|
|
||||||
|
// 根据钻进类型创建对应的弹窗,使用射线位置
|
||||||
|
if (drillingType === 'filling') {
|
||||||
|
createFillingPanel(pipe, raycastPosition);
|
||||||
|
} else if (drillingType === 'intake') {
|
||||||
|
createAirSupplyPanel(pipe, raycastPosition);
|
||||||
|
} else if (drillingType === 'exhaust') {
|
||||||
|
createReturnAirPanel(pipe, raycastPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建充填面板
|
||||||
|
*/
|
||||||
|
const createFillingPanel = (_pipe: ParametricPipe, position: THREE.Vector3) => {
|
||||||
|
// 如果已存在面板,先移除
|
||||||
|
if (fillingHtmlPanel) {
|
||||||
|
viewer?.scene.remove(fillingHtmlPanel.getCssObject());
|
||||||
|
fillingHtmlPanel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTML 面板
|
||||||
|
fillingHtmlPanel = new FillingHtmlPanel(CssType.CSS2D);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
fillingHtmlPanel.onDelete(() => {
|
||||||
|
if (fillingHtmlPanel) {
|
||||||
|
viewer?.scene.remove(fillingHtmlPanel.getCssObject());
|
||||||
|
fillingHtmlPanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
fillingHtmlPanel.updateData({
|
||||||
|
progress: fillingStatus.value.progress,
|
||||||
|
currentTime: (Date.now() - fillingStatus.value.startTime) / 1000,
|
||||||
|
endTime: fillingParams.value.endTime,
|
||||||
|
material: fillingParams.value.material,
|
||||||
|
fillingRate: fillingParams.value.fillingRate,
|
||||||
|
pressure: fillingParams.value.pressure,
|
||||||
|
status: fillingStatus.value.isRunning ? 'running' : 'stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
fillingHtmlPanel.updatePosition(new THREE.Vector3(position.x, position.y + 1, position.z));
|
||||||
|
fillingHtmlPanel.show();
|
||||||
|
viewer?.scene.add(fillingHtmlPanel.getCssObject());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建送风面板
|
||||||
|
*/
|
||||||
|
const createAirSupplyPanel = (_pipe: ParametricPipe, position: THREE.Vector3) => {
|
||||||
|
// 如果已存在面板,先移除
|
||||||
|
if (airSupplyHtmlPanel) {
|
||||||
|
viewer?.scene.remove(airSupplyHtmlPanel.getCssObject());
|
||||||
|
airSupplyHtmlPanel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTML 面板
|
||||||
|
airSupplyHtmlPanel = new AirSupplyHtmlPanel(CssType.CSS2D);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
airSupplyHtmlPanel.onDelete(() => {
|
||||||
|
if (airSupplyHtmlPanel) {
|
||||||
|
viewer?.scene.remove(airSupplyHtmlPanel.getCssObject());
|
||||||
|
airSupplyHtmlPanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
airSupplyHtmlPanel.updateData({
|
||||||
|
progress: airSupplyStatus.value.progress,
|
||||||
|
currentTime: (Date.now() - airSupplyStatus.value.startTime) / 1000,
|
||||||
|
endTime: airSupplyParams.value.endTime,
|
||||||
|
pressure: airSupplyParams.value.pressure,
|
||||||
|
flowRate: airSupplyParams.value.flowRate,
|
||||||
|
temperature: airSupplyParams.value.temperature,
|
||||||
|
humidity: airSupplyParams.value.humidity,
|
||||||
|
status: airSupplyStatus.value.isRunning ? 'running' : 'stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
airSupplyHtmlPanel.updatePosition(new THREE.Vector3(position.x, position.y + 1, position.z));
|
||||||
|
airSupplyHtmlPanel.show();
|
||||||
|
viewer?.scene.add(airSupplyHtmlPanel.getCssObject());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建回风面板
|
||||||
|
*/
|
||||||
|
const createReturnAirPanel = (_pipe: ParametricPipe, position: THREE.Vector3) => {
|
||||||
|
// 如果已存在面板,先移除
|
||||||
|
if (returnAirHtmlPanel) {
|
||||||
|
viewer?.scene.remove(returnAirHtmlPanel.getCssObject());
|
||||||
|
returnAirHtmlPanel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTML 面板
|
||||||
|
returnAirHtmlPanel = new ReturnAirHtmlPanel(CssType.CSS2D);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
returnAirHtmlPanel.onDelete(() => {
|
||||||
|
if (returnAirHtmlPanel) {
|
||||||
|
viewer?.scene.remove(returnAirHtmlPanel.getCssObject());
|
||||||
|
returnAirHtmlPanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
returnAirHtmlPanel.updateData({
|
||||||
|
progress: returnAirStatus.value.progress,
|
||||||
|
currentTime: (Date.now() - returnAirStatus.value.startTime) / 1000,
|
||||||
|
endTime: returnAirParams.value.endTime,
|
||||||
|
pressure: returnAirParams.value.pressure,
|
||||||
|
flowRate: returnAirParams.value.flowRate,
|
||||||
|
temperature: returnAirParams.value.temperature,
|
||||||
|
humidity: returnAirParams.value.humidity,
|
||||||
|
status: returnAirStatus.value.isRunning ? 'running' : 'stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
returnAirHtmlPanel.updatePosition(new THREE.Vector3(position.x, position.y + 1, position.z));
|
||||||
|
returnAirHtmlPanel.show();
|
||||||
|
viewer?.scene.add(returnAirHtmlPanel.getCssObject());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新充填面板内容
|
||||||
|
*/
|
||||||
|
const updateFillingPanel = () => {
|
||||||
|
if (!fillingHtmlPanel) return;
|
||||||
|
|
||||||
|
fillingHtmlPanel.updateData({
|
||||||
|
progress: fillingStatus.value.progress,
|
||||||
|
currentTime: (Date.now() - fillingStatus.value.startTime) / 1000,
|
||||||
|
endTime: fillingParams.value.endTime,
|
||||||
|
material: fillingParams.value.material,
|
||||||
|
fillingRate: fillingParams.value.fillingRate,
|
||||||
|
pressure: fillingParams.value.pressure,
|
||||||
|
status: fillingStatus.value.isRunning ? 'running' : 'completed'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新送风面板内容
|
||||||
|
*/
|
||||||
|
const updateAirSupplyPanel = () => {
|
||||||
|
if (!airSupplyHtmlPanel) return;
|
||||||
|
|
||||||
|
airSupplyHtmlPanel.updateData({
|
||||||
|
progress: airSupplyStatus.value.progress,
|
||||||
|
currentTime: (Date.now() - airSupplyStatus.value.startTime) / 1000,
|
||||||
|
endTime: airSupplyParams.value.endTime,
|
||||||
|
pressure: airSupplyParams.value.pressure,
|
||||||
|
flowRate: airSupplyParams.value.flowRate,
|
||||||
|
temperature: airSupplyParams.value.temperature,
|
||||||
|
humidity: airSupplyParams.value.humidity,
|
||||||
|
status: airSupplyStatus.value.isRunning ? 'running' : 'completed'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新回风面板内容
|
||||||
|
*/
|
||||||
|
const updateReturnAirPanel = () => {
|
||||||
|
if (!returnAirHtmlPanel) return;
|
||||||
|
|
||||||
|
returnAirHtmlPanel.updateData({
|
||||||
|
progress: returnAirStatus.value.progress,
|
||||||
|
currentTime: (Date.now() - returnAirStatus.value.startTime) / 1000,
|
||||||
|
endTime: returnAirParams.value.endTime,
|
||||||
|
pressure: returnAirParams.value.pressure,
|
||||||
|
flowRate: returnAirParams.value.flowRate,
|
||||||
|
temperature: returnAirParams.value.temperature,
|
||||||
|
humidity: returnAirParams.value.humidity,
|
||||||
|
status: returnAirStatus.value.isRunning ? 'running' : 'completed'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找第一个充填类型的管道
|
||||||
|
*/
|
||||||
|
const findFirstFillingPipe = (): ParametricPipe | null => {
|
||||||
|
if (!viewer) return null;
|
||||||
|
return viewer.parametric.findPipeByMetadata('drillingType', 'filling');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找第一个进风类型的管道
|
||||||
|
*/
|
||||||
|
const findFirstIntakePipe = (): ParametricPipe | null => {
|
||||||
|
if (!viewer) return null;
|
||||||
|
return viewer.parametric.findPipeByMetadata('drillingType', 'intake');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找第一个回风类型的管道
|
||||||
|
*/
|
||||||
|
const findFirstExhaustPipe = (): ParametricPipe | null => {
|
||||||
|
if (!viewer) return null;
|
||||||
|
return viewer.parametric.findPipeByMetadata('drillingType', 'exhaust');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算是否有活动进程
|
||||||
|
const hasActiveProcess = computed(() => {
|
||||||
|
return fillingStatus.value.isRunning ||
|
||||||
|
airSupplyStatus.value.isRunning ||
|
||||||
|
returnAirStatus.value.isRunning;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始充填
|
||||||
|
*/
|
||||||
|
const startFilling = async () => {
|
||||||
|
if (fillingStatus.value.isRunning) return;
|
||||||
|
|
||||||
|
// 查找充填管道
|
||||||
|
fillingPipe = findFirstFillingPipe();
|
||||||
|
if (!fillingPipe) {
|
||||||
|
console.warn('未找到充填类型的管道');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存原始材质属性
|
||||||
|
fillingOriginalState = {
|
||||||
|
transparent: fillingPipe.material.transparent,
|
||||||
|
color: (fillingPipe.material as any).color?.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
fillingStatus.value.progress = 0;
|
||||||
|
fillingStatus.value.startTime = Date.now();
|
||||||
|
fillingStatus.value.isRunning = true;
|
||||||
|
|
||||||
|
// 设置材质颜色和透明度
|
||||||
|
fillingPipe.material.transparent = true;
|
||||||
|
|
||||||
|
// 加载纹理
|
||||||
|
const texture = await viewer!.resources.loadTexture('/texture/flow/arrow.png');
|
||||||
|
fillingPipe.setColorNode(texture, 0x00bfff); // 深天蓝色 - 充填
|
||||||
|
fillingPipe.setScrollAxis('x');
|
||||||
|
fillingPipe.setScrollDirection(-1);
|
||||||
|
fillingPipe.setScrollSpeed(0.02);
|
||||||
|
fillingPipe.startTextureAnimation(fillingParams.value.endTime);
|
||||||
|
fillingPipe.emitter.once('textureAnimationComplete').then(stopFilling);
|
||||||
|
|
||||||
|
// 仅用于 UI 进度刷新
|
||||||
|
const totalTime = fillingParams.value.endTime * 1000;
|
||||||
|
fillingTimer = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - fillingStatus.value.startTime;
|
||||||
|
fillingStatus.value.progress = Math.min(Math.round((elapsed / totalTime) * 100), 100);
|
||||||
|
updateFillingPanel();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止充填
|
||||||
|
*/
|
||||||
|
const stopFilling = () => {
|
||||||
|
if (fillingTimer !== null) {
|
||||||
|
clearInterval(fillingTimer);
|
||||||
|
fillingTimer = null;
|
||||||
|
}
|
||||||
|
fillingStatus.value.isRunning = false;
|
||||||
|
if (fillingStatus.value.progress >= 100) {
|
||||||
|
fillingStatus.value.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillingPipe) {
|
||||||
|
// 若动画仍在运行(手动重置场景),先停止
|
||||||
|
if (fillingPipe.textureAnimating) {
|
||||||
|
fillingPipe.stopTextureAnimation();
|
||||||
|
}
|
||||||
|
fillingPipe.removeTexture();
|
||||||
|
|
||||||
|
if (fillingOriginalState) {
|
||||||
|
fillingPipe.material.transparent = fillingOriginalState.transparent;
|
||||||
|
if (fillingOriginalState.color && (fillingPipe.material as any).color) {
|
||||||
|
(fillingPipe.material as any).color.copy(fillingOriginalState.color);
|
||||||
|
}
|
||||||
|
fillingPipe.material.needsUpdate = true;
|
||||||
|
fillingOriginalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillingPipe = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始送风
|
||||||
|
*/
|
||||||
|
const startAirSupply = async () => {
|
||||||
|
if (airSupplyStatus.value.isRunning) return;
|
||||||
|
|
||||||
|
// 查找进风管道
|
||||||
|
airSupplyPipe = findFirstIntakePipe();
|
||||||
|
if (!airSupplyPipe) {
|
||||||
|
console.warn('未找到进风类型的管道');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存原始材质属性
|
||||||
|
airSupplyOriginalState = {
|
||||||
|
transparent: airSupplyPipe.material.transparent,
|
||||||
|
color: (airSupplyPipe.material as any).color?.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
airSupplyStatus.value.progress = 0;
|
||||||
|
airSupplyStatus.value.startTime = Date.now();
|
||||||
|
airSupplyStatus.value.isRunning = true;
|
||||||
|
|
||||||
|
// 设置材质透明度
|
||||||
|
airSupplyPipe.material.transparent = true;
|
||||||
|
|
||||||
|
// 加载纹理
|
||||||
|
const texture = await viewer!.resources.loadTexture('/texture/flow/arrow.png');
|
||||||
|
airSupplyPipe.setColorNode(texture, 0x00ff00); // 绿色 - 送风
|
||||||
|
airSupplyPipe.setScrollAxis('x');
|
||||||
|
airSupplyPipe.setScrollDirection(-1);
|
||||||
|
airSupplyPipe.setScrollSpeed(0.02);
|
||||||
|
airSupplyPipe.startTextureAnimation(airSupplyParams.value.endTime);
|
||||||
|
airSupplyPipe.emitter.once('textureAnimationComplete').then(stopAirSupply);
|
||||||
|
|
||||||
|
const totalTime = airSupplyParams.value.endTime * 1000;
|
||||||
|
airSupplyTimer = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - airSupplyStatus.value.startTime;
|
||||||
|
airSupplyStatus.value.progress = Math.min(Math.round((elapsed / totalTime) * 100), 100);
|
||||||
|
updateAirSupplyPanel();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止送风
|
||||||
|
*/
|
||||||
|
const stopAirSupply = () => {
|
||||||
|
if (airSupplyTimer !== null) {
|
||||||
|
clearInterval(airSupplyTimer);
|
||||||
|
airSupplyTimer = null;
|
||||||
|
}
|
||||||
|
airSupplyStatus.value.isRunning = false;
|
||||||
|
if (airSupplyStatus.value.progress >= 100) {
|
||||||
|
airSupplyStatus.value.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (airSupplyPipe) {
|
||||||
|
if (airSupplyPipe.textureAnimating) {
|
||||||
|
airSupplyPipe.stopTextureAnimation();
|
||||||
|
}
|
||||||
|
airSupplyPipe.removeTexture();
|
||||||
|
|
||||||
|
if (airSupplyOriginalState) {
|
||||||
|
airSupplyPipe.material.transparent = airSupplyOriginalState.transparent;
|
||||||
|
if (airSupplyOriginalState.color && (airSupplyPipe.material as any).color) {
|
||||||
|
(airSupplyPipe.material as any).color.copy(airSupplyOriginalState.color);
|
||||||
|
}
|
||||||
|
airSupplyPipe.material.needsUpdate = true;
|
||||||
|
airSupplyOriginalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
airSupplyPipe = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始回风
|
||||||
|
*/
|
||||||
|
const startReturnAir = async () => {
|
||||||
|
if (returnAirStatus.value.isRunning) return;
|
||||||
|
|
||||||
|
// 查找回风管道
|
||||||
|
returnAirPipe = findFirstExhaustPipe();
|
||||||
|
if (!returnAirPipe) {
|
||||||
|
console.warn('未找到回风类型的管道');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存原始材质属性
|
||||||
|
returnAirOriginalState = {
|
||||||
|
transparent: returnAirPipe.material.transparent,
|
||||||
|
color: (returnAirPipe.material as any).color?.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
returnAirStatus.value.progress = 0;
|
||||||
|
returnAirStatus.value.startTime = Date.now();
|
||||||
|
returnAirStatus.value.isRunning = true;
|
||||||
|
|
||||||
|
// 设置材质透明度
|
||||||
|
returnAirPipe.material.transparent = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 加载纹理
|
||||||
|
const texture = await viewer!.resources.loadTexture('/texture/flow/arrow1.png');
|
||||||
|
returnAirPipe.setColorNode(texture, 0xffa500); // 橙色 - 回风
|
||||||
|
returnAirPipe.setScrollAxis('x'); // Y 轴方向(从下到上)
|
||||||
|
returnAirPipe.setScrollDirection(1); // 正向(向上)
|
||||||
|
returnAirPipe.setScrollSpeed(0.02);
|
||||||
|
returnAirPipe.startTextureAnimation(returnAirParams.value.endTime);
|
||||||
|
returnAirPipe.emitter.once('textureAnimationComplete').then(stopReturnAir);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载箭头纹理失败:', error);
|
||||||
|
returnAirStatus.value.isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = returnAirParams.value.endTime * 1000;
|
||||||
|
returnAirTimer = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - returnAirStatus.value.startTime;
|
||||||
|
returnAirStatus.value.progress = Math.min(Math.round((elapsed / totalTime) * 100), 100);
|
||||||
|
updateReturnAirPanel();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止回风
|
||||||
|
*/
|
||||||
|
const stopReturnAir = () => {
|
||||||
|
if (returnAirTimer !== null) {
|
||||||
|
clearInterval(returnAirTimer);
|
||||||
|
returnAirTimer = null;
|
||||||
|
}
|
||||||
|
returnAirStatus.value.isRunning = false;
|
||||||
|
if (returnAirStatus.value.progress >= 100) {
|
||||||
|
returnAirStatus.value.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnAirPipe) {
|
||||||
|
if (returnAirPipe.textureAnimating) {
|
||||||
|
returnAirPipe.stopTextureAnimation();
|
||||||
|
}
|
||||||
|
returnAirPipe.removeTexture();
|
||||||
|
|
||||||
|
if (returnAirOriginalState) {
|
||||||
|
returnAirPipe.material.transparent = returnAirOriginalState.transparent;
|
||||||
|
if (returnAirOriginalState.color && (returnAirPipe.material as any).color) {
|
||||||
|
(returnAirPipe.material as any).color.copy(returnAirOriginalState.color);
|
||||||
|
}
|
||||||
|
returnAirPipe.material.needsUpdate = true;
|
||||||
|
returnAirOriginalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
returnAirPipe = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有
|
||||||
|
*/
|
||||||
|
const resetAll = () => {
|
||||||
|
stopFilling();
|
||||||
|
stopAirSupply();
|
||||||
|
stopReturnAir();
|
||||||
|
|
||||||
|
// 移除所有 HTML 面板
|
||||||
|
if (fillingHtmlPanel) {
|
||||||
|
viewer?.scene.remove(fillingHtmlPanel.getCssObject());
|
||||||
|
fillingHtmlPanel = null;
|
||||||
|
}
|
||||||
|
if (airSupplyHtmlPanel) {
|
||||||
|
viewer?.scene.remove(airSupplyHtmlPanel.getCssObject());
|
||||||
|
airSupplyHtmlPanel = null;
|
||||||
|
}
|
||||||
|
if (returnAirHtmlPanel) {
|
||||||
|
viewer?.scene.remove(returnAirHtmlPanel.getCssObject());
|
||||||
|
returnAirHtmlPanel = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器和面板
|
||||||
|
onUnmounted(() => {
|
||||||
|
resetAll();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="DrillingAndCementingPanel" title="钻井与固井">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="createFixedDrillingPipe">
|
||||||
|
开始钻进
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {ParametricPipe} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建固定路径的钻进管道
|
||||||
|
*/
|
||||||
|
const createFixedDrillingPipe = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const points: THREE.Vector3[] = [
|
||||||
|
new THREE.Vector3(1.3, 2.5, 1.5),
|
||||||
|
new THREE.Vector3(1.3, 0.2, 1.5),
|
||||||
|
new THREE.Vector3(-0.8, 0.2, -0.8),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pipeMaterial = new THREE.MeshBasicNodeMaterial({
|
||||||
|
color: 0x808080,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pip = new ParametricPipe(viewer, {
|
||||||
|
points: points,
|
||||||
|
radius: 0.15,
|
||||||
|
radialSegments: 16,
|
||||||
|
cornerRadius: 0.3,
|
||||||
|
cornerSplit: 5,
|
||||||
|
progress: 0,
|
||||||
|
material: pipeMaterial,
|
||||||
|
capStart: false,
|
||||||
|
capEnd: false,
|
||||||
|
enableDrillingRobot: true,
|
||||||
|
robotColor: 0xff0000,
|
||||||
|
});
|
||||||
|
|
||||||
|
pip.name = `管道`;
|
||||||
|
viewer.scene.add(pip);
|
||||||
|
pip.setCollisionProxyType(true);
|
||||||
|
pip.addCollisionTarget(bus.getRockSample());
|
||||||
|
|
||||||
|
pip.setDuration(3);
|
||||||
|
pip.startAnimation();
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
297
packages/demo/src/panels/OilGasScene/FluidApplicationPanel.vue
Normal file
297
packages/demo/src/panels/OilGasScene/FluidApplicationPanel.vue
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="fluid" title="流体施加">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="fluidStatus.isRunning"
|
||||||
|
type="primary"
|
||||||
|
@click="startSelectingPipe"
|
||||||
|
>
|
||||||
|
开始施加
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!fluidStatus.isRunning"
|
||||||
|
type="error"
|
||||||
|
@click="stopFluidApplication"
|
||||||
|
>
|
||||||
|
取消施加
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {CssType, EventManagerEvents, ParametricPipe, Viewer} from '@deep/engine';
|
||||||
|
import {InjectionHtmlPanel} from '@/htmlPanel';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
|
||||||
|
interface FluidApplication {
|
||||||
|
id: number;
|
||||||
|
position: THREE.Vector3;
|
||||||
|
pipeObject: THREE.Object3D;
|
||||||
|
flowRate: number;
|
||||||
|
pressure: number;
|
||||||
|
volume: number;
|
||||||
|
modal: InjectionHtmlPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FluidStatus {
|
||||||
|
progress: number;
|
||||||
|
startTime: number;
|
||||||
|
isRunning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let applications: FluidApplication[] = [];
|
||||||
|
let applicationCounter = 0;
|
||||||
|
let fluidPipe: any = null;
|
||||||
|
let fluidOriginalState: any = null;
|
||||||
|
let fluidTimer: number | null = null;
|
||||||
|
const fluidStatus = ref<FluidStatus>({
|
||||||
|
progress: 0,
|
||||||
|
startTime: 0,
|
||||||
|
isRunning: false
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
startApplicationUpdates();
|
||||||
|
setupRaycastListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (viewer) {
|
||||||
|
viewer.events.off(EventManagerEvents.RAYCAST_PICK, onRaycastPick);
|
||||||
|
}
|
||||||
|
stopFluidApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置射线拾取监听器
|
||||||
|
*/
|
||||||
|
const setupRaycastListener = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, onRaycastPick);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理射线拾取事件 - 点击管道时弹出面板
|
||||||
|
*/
|
||||||
|
const onRaycastPick = (event: any) => {
|
||||||
|
const {intersects} = event;
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
const intersection = intersects[0];
|
||||||
|
if (intersection && intersection.object && intersection.object.name === '管道') {
|
||||||
|
createFluidApplication(intersection.point, intersection.object);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找第一个管道对象
|
||||||
|
*/
|
||||||
|
const findFirstPipe = (): ParametricPipe => {
|
||||||
|
if (!viewer) return null;
|
||||||
|
return viewer.parametric.findPipe((pipe: any) => {
|
||||||
|
return pipe.name === '管道'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始流体施加 - 启动纹理动画
|
||||||
|
*/
|
||||||
|
const startSelectingPipe = async () => {
|
||||||
|
if (fluidStatus.value.isRunning) return;
|
||||||
|
|
||||||
|
// 查找管道
|
||||||
|
const pipe = findFirstPipe();
|
||||||
|
if (!pipe) {
|
||||||
|
console.warn('未找到名为"管道"的对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管道是否正在使用
|
||||||
|
if (pipe.userData.pipeStatus) {
|
||||||
|
console.warn('管道正在进行其他操作');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fluidPipe = pipe
|
||||||
|
// 标记管道状态为流体施加
|
||||||
|
fluidPipe.userData.pipeStatus = 'fluid';
|
||||||
|
|
||||||
|
// 保存原始材质属性
|
||||||
|
fluidOriginalState = {
|
||||||
|
transparent: fluidPipe.material.transparent,
|
||||||
|
color: (fluidPipe.material as any).color?.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
fluidStatus.value.progress = 0;
|
||||||
|
fluidStatus.value.startTime = Date.now();
|
||||||
|
fluidStatus.value.isRunning = true;
|
||||||
|
|
||||||
|
// 设置材质透明度
|
||||||
|
fluidPipe.material.transparent = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 加载纹理并开始动画
|
||||||
|
const texture = await viewer!.resources.loadTexture('/texture/flow/arrow.png');
|
||||||
|
fluidPipe.setColorNode(texture, 0x00aaff); // 蓝色 - 流体
|
||||||
|
fluidPipe.setScrollAxis('x');
|
||||||
|
fluidPipe.setScrollDirection(-1);
|
||||||
|
fluidPipe.setScrollSpeed(0.02);
|
||||||
|
fluidPipe.startTextureAnimation(10); // 10秒动画
|
||||||
|
fluidPipe.emitter.once('textureAnimationComplete').then(stopFluidApplication);
|
||||||
|
|
||||||
|
// 仅用于 UI 进度刷新
|
||||||
|
const totalTime = 10 * 1000;
|
||||||
|
fluidTimer = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - fluidStatus.value.startTime;
|
||||||
|
fluidStatus.value.progress = Math.min(Math.round((elapsed / totalTime) * 100), 100);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载箭头纹理失败:', error);
|
||||||
|
stopFluidApplication();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止流体施加
|
||||||
|
*/
|
||||||
|
const stopFluidApplication = () => {
|
||||||
|
if (fluidTimer !== null) {
|
||||||
|
clearInterval(fluidTimer);
|
||||||
|
fluidTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fluidStatus.value.isRunning = false;
|
||||||
|
if (fluidStatus.value.progress >= 100) {
|
||||||
|
fluidStatus.value.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fluidPipe) {
|
||||||
|
if (fluidPipe.textureAnimating) {
|
||||||
|
fluidPipe.stopTextureAnimation();
|
||||||
|
}
|
||||||
|
fluidPipe.removeTexture();
|
||||||
|
|
||||||
|
if (fluidOriginalState) {
|
||||||
|
fluidPipe.material.transparent = fluidOriginalState.transparent;
|
||||||
|
if (fluidOriginalState.color && (fluidPipe.material as any).color) {
|
||||||
|
(fluidPipe.material as any).color.copy(fluidOriginalState.color);
|
||||||
|
}
|
||||||
|
fluidPipe.material.needsUpdate = true;
|
||||||
|
fluidOriginalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除管道状态标记
|
||||||
|
if (fluidPipe.userData.pipeStatus === 'fluid') {
|
||||||
|
delete fluidPipe.userData.pipeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
fluidPipe = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建流体施加点
|
||||||
|
*/
|
||||||
|
const createFluidApplication = (position: THREE.Vector3, pipeObject: THREE.Object3D) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
applicationCounter++;
|
||||||
|
|
||||||
|
// 创建信息面板
|
||||||
|
const modal = new InjectionHtmlPanel(CssType.CSS2D);
|
||||||
|
|
||||||
|
modal.onDelete(() => {
|
||||||
|
const index = applications.findIndex(app => app.id === applicationCounter);
|
||||||
|
if (index !== -1) {
|
||||||
|
removeApplication(applications[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalObject = modal.getCssObject();
|
||||||
|
modalObject.position.set(position.x, position.y + 0.5, position.z);
|
||||||
|
viewer.scene.add(modalObject);
|
||||||
|
|
||||||
|
const application: FluidApplication = {
|
||||||
|
id: applicationCounter,
|
||||||
|
position: position.clone(),
|
||||||
|
pipeObject: pipeObject,
|
||||||
|
flowRate: 1.0,
|
||||||
|
pressure: 10.0,
|
||||||
|
volume: 0,
|
||||||
|
modal: modal,
|
||||||
|
};
|
||||||
|
|
||||||
|
applications.push(application);
|
||||||
|
updateApplicationModalInfo(application);
|
||||||
|
modal.show();
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新施加信息面板
|
||||||
|
*/
|
||||||
|
const updateApplicationModalInfo = (application: FluidApplication) => {
|
||||||
|
const wellName = `流体施加点_${application.id}`;
|
||||||
|
|
||||||
|
application.modal.updateData({
|
||||||
|
wellName: wellName,
|
||||||
|
flowRate: application.flowRate,
|
||||||
|
injectionRate: 1.0,
|
||||||
|
pressure: application.pressure,
|
||||||
|
volume: application.volume,
|
||||||
|
temperature: 25,
|
||||||
|
status: 'injecting',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动施加更新循环
|
||||||
|
*/
|
||||||
|
const startApplicationUpdates = () => {
|
||||||
|
setInterval(() => {
|
||||||
|
applications.forEach(application => {
|
||||||
|
// 更新累计体积
|
||||||
|
application.volume += application.flowRate * 0.1;
|
||||||
|
updateApplicationModalInfo(application);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除单个施加
|
||||||
|
*/
|
||||||
|
const removeApplication = (application: FluidApplication) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 移除面板
|
||||||
|
application.modal.hide();
|
||||||
|
const modalObject = application.modal.getCssObject();
|
||||||
|
if (modalObject.parent) {
|
||||||
|
modalObject.parent.remove(modalObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数组中移除
|
||||||
|
const index = applications.indexOf(application);
|
||||||
|
if (index !== -1) {
|
||||||
|
applications.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
167
packages/demo/src/panels/OilGasScene/FracturingPanel.vue
Normal file
167
packages/demo/src/panels/OilGasScene/FracturingPanel.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="fracturing" title="压裂">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="isRunning"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startFracturing">
|
||||||
|
{{ isRunning ? '压裂中...' : '开始压裂' }}
|
||||||
|
</n-button>
|
||||||
|
<n-checkbox v-model:checked="showRuptureSpheres">
|
||||||
|
显示小球
|
||||||
|
</n-checkbox>
|
||||||
|
<n-checkbox v-model:checked="showFractures">
|
||||||
|
显示裂缝
|
||||||
|
</n-checkbox>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="reset">
|
||||||
|
重置
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref, watch} from 'vue';
|
||||||
|
import {NButton, NCheckbox, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {FractureEffect, Viewer} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let effect: FractureEffect | null = null;
|
||||||
|
|
||||||
|
const isRunning = ref(false);
|
||||||
|
const showRuptureSpheres = ref(true);
|
||||||
|
const showFractures = ref(true);
|
||||||
|
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成一圈螺旋压裂点(在终点位置)
|
||||||
|
*/
|
||||||
|
const generateSpiralPoints = (): THREE.Vector3[] => {
|
||||||
|
const startPoint = new THREE.Vector3(1.5, 0, 1.5);
|
||||||
|
const endPoint = new THREE.Vector3(-0.8, 0, -0.8);
|
||||||
|
|
||||||
|
const points: THREE.Vector3[] = [];
|
||||||
|
const totalTurns = 1; // 一圈
|
||||||
|
const pointsPerTurn = 20; // 每圈20个点
|
||||||
|
const totalPoints = pointsPerTurn;
|
||||||
|
|
||||||
|
// 计算线的方向
|
||||||
|
const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize();
|
||||||
|
|
||||||
|
// 计算垂直于方向的两个正交向量
|
||||||
|
const perpendicular1 = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||||
|
const perpendicular2 = new THREE.Vector3().crossVectors(direction, perpendicular1).normalize();
|
||||||
|
|
||||||
|
// 螺旋参数
|
||||||
|
const radius = 0.4;
|
||||||
|
const spiralHeight = 0.5; // 螺旋高度
|
||||||
|
const backwardOffset = 0.3; // 向后偏移距离
|
||||||
|
|
||||||
|
// 计算螺旋起始点(从终点向后偏移)
|
||||||
|
const spiralStart = endPoint.clone().add(direction.clone().multiplyScalar(-backwardOffset));
|
||||||
|
|
||||||
|
for (let i = 0; i <= totalPoints; i++) {
|
||||||
|
const t = i / totalPoints;
|
||||||
|
|
||||||
|
// 螺旋角度
|
||||||
|
const angle = t * totalTurns * Math.PI * 2;
|
||||||
|
|
||||||
|
// 沿着方向的高度变化(确保不超出终点)
|
||||||
|
const heightOffset = direction.clone().multiplyScalar(t * spiralHeight);
|
||||||
|
|
||||||
|
// 在起始点位置生成螺旋
|
||||||
|
const offset = new THREE.Vector3()
|
||||||
|
.addScaledVector(perpendicular1, Math.cos(angle) * radius)
|
||||||
|
.addScaledVector(perpendicular2, Math.sin(angle) * radius);
|
||||||
|
|
||||||
|
const point = spiralStart.clone().add(offset).add(heightOffset);
|
||||||
|
|
||||||
|
// 确保不超出终点
|
||||||
|
const toEnd = new THREE.Vector3().subVectors(endPoint, point);
|
||||||
|
if (toEnd.dot(direction) < 0) {
|
||||||
|
// 如果超出终点,则限制在终点位置
|
||||||
|
points.push(endPoint.clone().add(offset));
|
||||||
|
} else {
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startFracturing = async () => {
|
||||||
|
if (!viewer || isRunning.value) return;
|
||||||
|
|
||||||
|
// dispose previous
|
||||||
|
if (effect) {
|
||||||
|
effect.dispose();
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = generateSpiralPoints();
|
||||||
|
|
||||||
|
effect = new FractureEffect({
|
||||||
|
points: points,
|
||||||
|
curveType: 'catmullrom',
|
||||||
|
tension: 0.5,
|
||||||
|
spacing: 0.2, // 增大间距
|
||||||
|
bufferRatio: 0.02,
|
||||||
|
closed: false, // 不闭合,形成螺旋
|
||||||
|
spheresVisible: showRuptureSpheres.value,
|
||||||
|
cracksVisible: showFractures.value
|
||||||
|
});
|
||||||
|
|
||||||
|
isRunning.value = true;
|
||||||
|
effect.onProgress((progress) => {
|
||||||
|
if (progress >= 1) isRunning.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await effect.start();
|
||||||
|
// 确保使用 SDK 提供的方法同步裂缝可见性
|
||||||
|
if (effect) {
|
||||||
|
if (showFractures.value) effect.showCracks(); else effect.hideCracks();
|
||||||
|
}
|
||||||
|
isRunning.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (effect) {
|
||||||
|
effect.dispose();
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
isRunning.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (effect) {
|
||||||
|
effect.dispose();
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当复选框切换时,调用 effect 的方法或直接设置可见性
|
||||||
|
watch(showRuptureSpheres, (val) => {
|
||||||
|
if (!effect) return;
|
||||||
|
if (val) effect.showSpheres(); else effect.hideSpheres();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(showFractures, (val) => {
|
||||||
|
if (!effect) return;
|
||||||
|
if (val) effect.showCracks(); else effect.hideCracks();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,296 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="injection-production" title="注采管理">
|
||||||
|
<n-space :size="12" vertical>
|
||||||
|
<!-- 注入控制 -->
|
||||||
|
<div>
|
||||||
|
<div>注入控制</div>
|
||||||
|
<n-button
|
||||||
|
:disabled="injectionStatus.isRunning"
|
||||||
|
style="width: 100%; margin-bottom: 8px;"
|
||||||
|
type="primary"
|
||||||
|
@click="startInjection"
|
||||||
|
>
|
||||||
|
开始注入
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!injectionStatus.isRunning"
|
||||||
|
style="width: 100%;"
|
||||||
|
type="error"
|
||||||
|
@click="stopInjection"
|
||||||
|
>
|
||||||
|
取消注入
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 采出控制 -->
|
||||||
|
<div>
|
||||||
|
<div>采出控制</div>
|
||||||
|
<n-button
|
||||||
|
:disabled="productionStatus.isRunning"
|
||||||
|
style="width: 100%; margin-bottom: 8px;"
|
||||||
|
type="info"
|
||||||
|
@click="startProduction"
|
||||||
|
>
|
||||||
|
开始采出
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="!productionStatus.isRunning"
|
||||||
|
style="width: 100%;"
|
||||||
|
type="error"
|
||||||
|
@click="stopProduction"
|
||||||
|
>
|
||||||
|
取消采出
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
import {Viewer} from '@deep/engine';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
|
||||||
|
interface Status {
|
||||||
|
progress: number;
|
||||||
|
startTime: number;
|
||||||
|
isRunning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let injectionPipe: any = null;
|
||||||
|
let injectionOriginalState: any = null;
|
||||||
|
let injectionTimer: number | null = null;
|
||||||
|
const injectionStatus = ref<Status>({
|
||||||
|
progress: 0,
|
||||||
|
startTime: 0,
|
||||||
|
isRunning: false
|
||||||
|
});
|
||||||
|
|
||||||
|
let productionPipe: any = null;
|
||||||
|
let productionOriginalState: any = null;
|
||||||
|
let productionTimer: number | null = null;
|
||||||
|
const productionStatus = ref<Status>({
|
||||||
|
progress: 0,
|
||||||
|
startTime: 0,
|
||||||
|
isRunning: false
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopInjection();
|
||||||
|
stopProduction();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找第一个管道对象
|
||||||
|
*/
|
||||||
|
const findFirstPipe = (): any => {
|
||||||
|
if (!viewer) return null;
|
||||||
|
return viewer.parametric.findPipe((pipe: any) => pipe.name === '管道');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始注入
|
||||||
|
*/
|
||||||
|
const startInjection = async () => {
|
||||||
|
if (injectionStatus.value.isRunning) return;
|
||||||
|
|
||||||
|
const pipe = findFirstPipe();
|
||||||
|
if (!pipe) {
|
||||||
|
console.warn('未找到名为"管道"的对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管道是否正在使用
|
||||||
|
if (pipe.userData.pipeStatus) {
|
||||||
|
console.warn('管道正在进行其他操作');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectionPipe = pipe;
|
||||||
|
|
||||||
|
// 标记管道状态为注入
|
||||||
|
injectionPipe.userData.pipeStatus = 'injection';
|
||||||
|
|
||||||
|
injectionOriginalState = {
|
||||||
|
transparent: injectionPipe.material.transparent,
|
||||||
|
color: (injectionPipe.material as any).color?.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
injectionStatus.value.progress = 0;
|
||||||
|
injectionStatus.value.startTime = Date.now();
|
||||||
|
injectionStatus.value.isRunning = true;
|
||||||
|
|
||||||
|
injectionPipe.material.transparent = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
// const texture = await viewer!.resources.loadTexture('/texture/flow/arrow.png');
|
||||||
|
// injectionPipe.setColorNode(texture, 0x00ff00); // 绿色 - 注入
|
||||||
|
|
||||||
|
const texture = await viewer!.resources.loadTexture('/texture/flow/water.jpg');
|
||||||
|
injectionPipe.textureRepeatY = 1
|
||||||
|
injectionPipe.setColorNode(texture); // 绿色 - 注入
|
||||||
|
|
||||||
|
injectionPipe.setScrollAxis('x');
|
||||||
|
injectionPipe.setScrollDirection(-1);
|
||||||
|
injectionPipe.setScrollSpeed(0.005);
|
||||||
|
injectionPipe.startTextureAnimation(10);
|
||||||
|
injectionPipe.emitter.once('textureAnimationComplete').then(stopInjection);
|
||||||
|
|
||||||
|
const totalTime = 10 * 1000;
|
||||||
|
injectionTimer = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - injectionStatus.value.startTime;
|
||||||
|
injectionStatus.value.progress = Math.min(Math.round((elapsed / totalTime) * 100), 100);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载注入纹理失败:', error);
|
||||||
|
stopInjection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止注入
|
||||||
|
*/
|
||||||
|
const stopInjection = () => {
|
||||||
|
if (injectionTimer !== null) {
|
||||||
|
clearInterval(injectionTimer);
|
||||||
|
injectionTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectionStatus.value.isRunning = false;
|
||||||
|
if (injectionStatus.value.progress >= 100) {
|
||||||
|
injectionStatus.value.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (injectionPipe) {
|
||||||
|
if (injectionPipe.textureAnimating) {
|
||||||
|
injectionPipe.stopTextureAnimation();
|
||||||
|
}
|
||||||
|
injectionPipe.removeTexture();
|
||||||
|
|
||||||
|
if (injectionOriginalState) {
|
||||||
|
injectionPipe.material.transparent = injectionOriginalState.transparent;
|
||||||
|
if (injectionOriginalState.color && (injectionPipe.material as any).color) {
|
||||||
|
(injectionPipe.material as any).color.copy(injectionOriginalState.color);
|
||||||
|
}
|
||||||
|
injectionPipe.material.needsUpdate = true;
|
||||||
|
injectionOriginalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除管道状态标记
|
||||||
|
if (injectionPipe.userData.pipeStatus === 'injection') {
|
||||||
|
delete injectionPipe.userData.pipeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectionPipe = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始采出
|
||||||
|
*/
|
||||||
|
const startProduction = async () => {
|
||||||
|
if (productionStatus.value.isRunning) return;
|
||||||
|
|
||||||
|
const pipe = findFirstPipe();
|
||||||
|
if (!pipe) {
|
||||||
|
console.warn('未找到名为"管道"的对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管道是否正在使用
|
||||||
|
if (pipe.userData.pipeStatus) {
|
||||||
|
console.warn('管道正在进行其他操作');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
productionPipe = pipe;
|
||||||
|
|
||||||
|
// 标记管道状态为采出
|
||||||
|
productionPipe.userData.pipeStatus = 'production';
|
||||||
|
|
||||||
|
productionOriginalState = {
|
||||||
|
transparent: productionPipe.material.transparent,
|
||||||
|
color: (productionPipe.material as any).color?.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
productionStatus.value.progress = 0;
|
||||||
|
productionStatus.value.startTime = Date.now();
|
||||||
|
productionStatus.value.isRunning = true;
|
||||||
|
|
||||||
|
productionPipe.material.transparent = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const texture = await viewer!.resources.loadTexture('/texture/flow/arrow1.png');
|
||||||
|
productionPipe.setColorNode(texture, 0xff0000); // 红色 - 采出
|
||||||
|
productionPipe.setScrollAxis('x');
|
||||||
|
productionPipe.setScrollDirection(1);
|
||||||
|
productionPipe.setScrollSpeed(0.02);
|
||||||
|
productionPipe.startTextureAnimation(10);
|
||||||
|
productionPipe.emitter.once('textureAnimationComplete').then(stopProduction);
|
||||||
|
|
||||||
|
const totalTime = 10 * 1000;
|
||||||
|
productionTimer = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - productionStatus.value.startTime;
|
||||||
|
productionStatus.value.progress = Math.min(Math.round((elapsed / totalTime) * 100), 100);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载采出纹理失败:', error);
|
||||||
|
stopProduction();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止采出
|
||||||
|
*/
|
||||||
|
const stopProduction = () => {
|
||||||
|
if (productionTimer !== null) {
|
||||||
|
clearInterval(productionTimer);
|
||||||
|
productionTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
productionStatus.value.isRunning = false;
|
||||||
|
if (productionStatus.value.progress >= 100) {
|
||||||
|
productionStatus.value.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productionPipe) {
|
||||||
|
if (productionPipe.textureAnimating) {
|
||||||
|
productionPipe.stopTextureAnimation();
|
||||||
|
}
|
||||||
|
productionPipe.removeTexture();
|
||||||
|
|
||||||
|
if (productionOriginalState) {
|
||||||
|
productionPipe.material.transparent = productionOriginalState.transparent;
|
||||||
|
if (productionOriginalState.color && (productionPipe.material as any).color) {
|
||||||
|
(productionPipe.material as any).color.copy(productionOriginalState.color);
|
||||||
|
}
|
||||||
|
productionPipe.material.needsUpdate = true;
|
||||||
|
productionOriginalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除管道状态标记
|
||||||
|
if (productionPipe.userData.pipeStatus === 'production') {
|
||||||
|
delete productionPipe.userData.pipeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
productionPipe = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
73
packages/demo/src/panels/ScenePanelContainer.vue
Normal file
73
packages/demo/src/panels/ScenePanelContainer.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="containerStyle" class="scene-panel-container">
|
||||||
|
<n-scrollbar :style="{ height: scrollbarHeight }">
|
||||||
|
<n-collapse :default-expanded-names="defaultExpandedNames" :display-directive="'show'">
|
||||||
|
<slot name="panels"></slot>
|
||||||
|
</n-collapse>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {NCollapse, NScrollbar} from 'naive-ui';
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
top?: string;
|
||||||
|
left?: string;
|
||||||
|
right?: string;
|
||||||
|
bottom?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
position?: Position;
|
||||||
|
width?: string;
|
||||||
|
defaultExpandedNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
position: () => ({top: '410px', left: '10px'}),
|
||||||
|
width: '320px',
|
||||||
|
defaultExpandedNames: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerStyle = computed(() => {
|
||||||
|
const style: Record<string, string> = {
|
||||||
|
width: props.width
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.position) {
|
||||||
|
if (props.position.top) style.top = props.position.top;
|
||||||
|
if (props.position.left) style.left = props.position.left;
|
||||||
|
if (props.position.right) style.right = props.position.right;
|
||||||
|
if (props.position.bottom) style.bottom = props.position.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollbarHeight = computed(() => {
|
||||||
|
if (props.position?.top) {
|
||||||
|
return `calc(96vh - ${props.position.top})`;
|
||||||
|
}
|
||||||
|
return 'auto';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scene-panel-container {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #aeb0b2;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-panel-container :deep(.n-scrollbar) {
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
261
packages/demo/src/panels/TunnelScene/DisturbanceLoadingPanel.vue
Normal file
261
packages/demo/src/panels/TunnelScene/DisturbanceLoadingPanel.vue
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="disturbance-loading" title="扰动加载">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button :disabled="isExcited" block type="primary" @click="triggerClosedStress">
|
||||||
|
{{ isExcited ? '激发中...' : '封闭应力激发' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!isExcited" block type="default" @click="resetClosedStress">
|
||||||
|
重置激发状态
|
||||||
|
</n-button>
|
||||||
|
<n-button block type="info" @click="showDisturbanceLabel">
|
||||||
|
扰动加载标签
|
||||||
|
</n-button>
|
||||||
|
<n-divider style="margin: 8px 0">扰动波参数</n-divider>
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<div>
|
||||||
|
<n-text>频率 (Hz): {{ waveFrequency }}</n-text>
|
||||||
|
<n-slider v-model:value="waveFrequency" :max="10" :min="1" :step="1"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<n-text>距离 (m): {{ waveDistance }}</n-text>
|
||||||
|
<n-slider v-model:value="waveDistance" :max="5" :min="0.5" :step="0.5"/>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
<n-button block type="warning" @click="startDisturbanceAnimation">
|
||||||
|
扰动状态动画
|
||||||
|
</n-button>
|
||||||
|
<n-button block type="error" @click="removeDisturbanceAnimation">
|
||||||
|
扰动状态动画移除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, onUnmounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NDivider, NSlider, NSpace, NText} from 'naive-ui';
|
||||||
|
import {BusEvents, useBus} from '@/hooks';
|
||||||
|
import {CssType, EventManagerEvents, ParametricBox, SeismicWave} from '@deep/engine';
|
||||||
|
import {DisturbanceLabelHtmlPanel, StressWaveDeviceHtmlPanel} from '@/htmlPanel';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
const isExcited = ref(false);
|
||||||
|
const waveFrequency = ref(1); // Hz
|
||||||
|
const waveDistance = ref(2.0); // m
|
||||||
|
let deviceBox: ParametricBox | null = null;
|
||||||
|
let devicePanel: StressWaveDeviceHtmlPanel | null = null;
|
||||||
|
let disturbanceLabelPanel: DisturbanceLabelHtmlPanel | null = null;
|
||||||
|
let seismicWave: SeismicWave | null = null;
|
||||||
|
let exciteTimer: number | null = null;
|
||||||
|
let disturbanceWave: SeismicWave | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
initStressWaveDevice();
|
||||||
|
setupClickListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (exciteTimer !== null) {
|
||||||
|
clearTimeout(exciteTimer);
|
||||||
|
exciteTimer = null;
|
||||||
|
}
|
||||||
|
seismicWave?.stop();
|
||||||
|
seismicWave?.dispose();
|
||||||
|
seismicWave = null;
|
||||||
|
removeDisturbanceAnimation();
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (deviceBox && viewer) {
|
||||||
|
viewer.scene.remove(deviceBox);
|
||||||
|
deviceBox.geometry.dispose();
|
||||||
|
deviceBox = null;
|
||||||
|
}
|
||||||
|
if (devicePanel) {
|
||||||
|
const obj = devicePanel.getCssObject();
|
||||||
|
obj.parent?.remove(obj);
|
||||||
|
devicePanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function initStressWaveDevice() {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
deviceBox = new ParametricBox({
|
||||||
|
width: 0.3,
|
||||||
|
height: 0.3,
|
||||||
|
depth: 0.3,
|
||||||
|
material: new THREE.MeshBasicMaterial({
|
||||||
|
color: new THREE.Color("#00ffff"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
deviceBox.name = '内部应力波激发装置';
|
||||||
|
deviceBox.position.set(2, 2, 2);
|
||||||
|
viewer.scene.add(deviceBox);
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupClickListener() {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, ({data}) => {
|
||||||
|
const {intersects} = data;
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
const intersection = intersects[0];
|
||||||
|
const clicked = intersection.object;
|
||||||
|
|
||||||
|
if (clicked !== deviceBox && clicked?.parent !== deviceBox) return;
|
||||||
|
|
||||||
|
if (devicePanel) return;
|
||||||
|
|
||||||
|
const pos = deviceBox!.position;
|
||||||
|
devicePanel = new StressWaveDeviceHtmlPanel(
|
||||||
|
{
|
||||||
|
position: {x: pos.x, y: pos.y, z: pos.z},
|
||||||
|
excitationEnergy: 128.5,
|
||||||
|
},
|
||||||
|
CssType.CSS2D
|
||||||
|
);
|
||||||
|
|
||||||
|
devicePanel.onDelete(() => {
|
||||||
|
if (devicePanel) {
|
||||||
|
devicePanel.hide();
|
||||||
|
const obj = devicePanel.getCssObject();
|
||||||
|
obj.parent?.remove(obj);
|
||||||
|
devicePanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const panelObj = devicePanel.getCssObject();
|
||||||
|
panelObj.position.set(pos.x, pos.y + 0.4, pos.z);
|
||||||
|
viewer.scene.add(panelObj);
|
||||||
|
devicePanel.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerClosedStress() {
|
||||||
|
if (!deviceBox || isExcited.value) return;
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
isExcited.value = true;
|
||||||
|
|
||||||
|
// 将 box 改为激发色(橙红发光)
|
||||||
|
const mat = deviceBox.material as THREE.MeshStandardNodeMaterial;
|
||||||
|
mat.color.set(0xff3300);
|
||||||
|
mat.emissiveIntensity = 0.8;
|
||||||
|
|
||||||
|
// 创建地震波
|
||||||
|
seismicWave = new SeismicWave({
|
||||||
|
emitterPositions: [deviceBox.position.clone()],
|
||||||
|
minRadius: 0.1,
|
||||||
|
maxRadius: 1.2,
|
||||||
|
waveDuration: 1800,
|
||||||
|
spawnInterval: 500,
|
||||||
|
opacity: 0.35,
|
||||||
|
color: new THREE.Color(0xff6600),
|
||||||
|
shape: 'sphere',
|
||||||
|
autoStart: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2s 后停止并变灰
|
||||||
|
exciteTimer = window.setTimeout(() => {
|
||||||
|
seismicWave?.stop();
|
||||||
|
seismicWave?.dispose();
|
||||||
|
seismicWave = null;
|
||||||
|
exciteTimer = null;
|
||||||
|
|
||||||
|
if (deviceBox) {
|
||||||
|
const m = deviceBox.material as THREE.MeshStandardNodeMaterial;
|
||||||
|
m.color.set(0x888888);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetClosedStress() {
|
||||||
|
if (exciteTimer !== null) {
|
||||||
|
clearTimeout(exciteTimer);
|
||||||
|
exciteTimer = null;
|
||||||
|
}
|
||||||
|
seismicWave?.stop();
|
||||||
|
seismicWave?.dispose();
|
||||||
|
seismicWave = null;
|
||||||
|
|
||||||
|
if (deviceBox) {
|
||||||
|
const mat = deviceBox.material as THREE.MeshStandardNodeMaterial;
|
||||||
|
mat.color.set("#00ffff");
|
||||||
|
}
|
||||||
|
isExcited.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDisturbanceLabel() {
|
||||||
|
if (disturbanceLabelPanel) return;
|
||||||
|
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
disturbanceLabelPanel = new DisturbanceLabelHtmlPanel(
|
||||||
|
{
|
||||||
|
waveFunction: 'A·sin(2πft)',
|
||||||
|
frequency: 50.0,
|
||||||
|
amplitude: 0.25,
|
||||||
|
count: 10,
|
||||||
|
interval: 2.0,
|
||||||
|
},
|
||||||
|
CssType.CSS2D
|
||||||
|
);
|
||||||
|
|
||||||
|
disturbanceLabelPanel.onDelete(() => {
|
||||||
|
if (disturbanceLabelPanel) {
|
||||||
|
disturbanceLabelPanel.hide();
|
||||||
|
const obj = disturbanceLabelPanel.getCssObject();
|
||||||
|
obj.parent?.remove(obj);
|
||||||
|
disturbanceLabelPanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const panelObj = disturbanceLabelPanel.getCssObject();
|
||||||
|
panelObj.position.set(0, 1.5, 0);
|
||||||
|
viewer.scene.add(panelObj);
|
||||||
|
disturbanceLabelPanel.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDisturbanceAnimation() {
|
||||||
|
if (disturbanceWave) return;
|
||||||
|
|
||||||
|
const positions = [-1.5, 0, 1.5].map(z => new THREE.Vector3(0, -2.5, z));
|
||||||
|
|
||||||
|
// 根据频率计算生成间隔: interval = 1000 / frequency (ms)
|
||||||
|
const spawnInterval = Math.max(100, 1000 / waveFrequency.value);
|
||||||
|
|
||||||
|
// 使用距离参数作为最大半径
|
||||||
|
const maxRadius = waveDistance.value;
|
||||||
|
|
||||||
|
disturbanceWave = new SeismicWave({
|
||||||
|
emitterPositions: positions,
|
||||||
|
minRadius: 0.1,
|
||||||
|
maxRadius: maxRadius,
|
||||||
|
waveDuration: 2000,
|
||||||
|
spawnInterval: spawnInterval,
|
||||||
|
opacity: 0.3,
|
||||||
|
color: new THREE.Color(0x00fcff),
|
||||||
|
shape: 'hemisphere',
|
||||||
|
autoStart: true,
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDisturbanceAnimation() {
|
||||||
|
disturbanceWave?.stop();
|
||||||
|
disturbanceWave?.dispose();
|
||||||
|
disturbanceWave = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="FieldDataPanel" title="场数据">
|
||||||
|
<StressDisplayPanel/>
|
||||||
|
<StrainDisplayPanel/>
|
||||||
|
<TemperatureDisplayPanel/>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NCollapseItem} from 'naive-ui';
|
||||||
|
import {StrainDisplayPanel, StressDisplayPanel, TemperatureDisplayPanel} from "@/disasterFormationPanel/TunnelScene";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,297 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="strainDisplay" title="试样内部应变显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<!-- 创建/移除按钮 -->
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasVolume" size="small" type="primary" @click="create">
|
||||||
|
创建
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasVolume" size="small" type="error" @click="remove">
|
||||||
|
移除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 体渲染控制 -->
|
||||||
|
<template v-if="hasVolume">
|
||||||
|
<!-- 使用平滑过渡 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">使用平滑过渡</div>
|
||||||
|
<n-switch v-model:value="useSmoothing" @update:value="onSmoothingChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 范围 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">范围</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="range" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密度阈值 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">密度阈值</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="threshold" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 不透明度 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">不透明度</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="opacity" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 光线步进次数 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">光线步进次数</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="steps" :max="1000" :min="0" :step="1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 渲染模式 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">渲染模式</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="mode"
|
||||||
|
:options="modeOptions"
|
||||||
|
size="small"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref, watch} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSelect, NSlider, NSwitch} from 'naive-ui';
|
||||||
|
import {BusEvents, useBus} from '@/hooks';
|
||||||
|
import {VolumeMesh, VolumeRenderMode, VolumeTool} from '@deep/engine';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasVolume = ref(false);
|
||||||
|
let volume: VolumeMesh | null = null;
|
||||||
|
let unsubscribeX: (() => void) | null = null;
|
||||||
|
let unsubscribeY: (() => void) | null = null;
|
||||||
|
let unsubscribeZ: (() => void) | null = null;
|
||||||
|
let unsubscribeClippingMode: (() => void) | null = null;
|
||||||
|
|
||||||
|
const subscribeClipPlanes = (clippingManager: any, clippingMode: string | null) => {
|
||||||
|
if (unsubscribeX) {
|
||||||
|
unsubscribeX();
|
||||||
|
unsubscribeX = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeY) {
|
||||||
|
unsubscribeY();
|
||||||
|
unsubscribeY = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeZ) {
|
||||||
|
unsubscribeZ();
|
||||||
|
unsubscribeZ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clippingMode === 'three') {
|
||||||
|
const planeX = clippingManager.getPlane('x');
|
||||||
|
const planeY = clippingManager.getPlane('y');
|
||||||
|
const planeZ = clippingManager.getPlane('z');
|
||||||
|
if (planeX) unsubscribeX = planeX.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneX) volume.clipPlaneX.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
if (planeY) unsubscribeY = planeY.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneY) volume.clipPlaneY.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
if (planeZ) unsubscribeZ = planeZ.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneZ) volume.clipPlaneZ.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
} else if (clippingMode === 'single') {
|
||||||
|
const planeSingle = clippingManager.getPlane('single');
|
||||||
|
if (planeSingle) unsubscribeX = planeSingle.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlane) volume.clipPlane.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 体渲染控制参数
|
||||||
|
const useSmoothing = ref(false);
|
||||||
|
const range = ref(0.5);
|
||||||
|
const threshold = ref(0.5);
|
||||||
|
const opacity = ref(0.5);
|
||||||
|
const steps = ref(200);
|
||||||
|
const mode = ref(0);
|
||||||
|
|
||||||
|
// 渲染模式选项
|
||||||
|
const modeOptions = [
|
||||||
|
{label: '发射吸收模型', value: 0},
|
||||||
|
{label: '最小强度投影', value: 1},
|
||||||
|
{label: '最大强度投影', value: 2}
|
||||||
|
];
|
||||||
|
|
||||||
|
const create = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const data = VolumeTool.generateVolumeData({size: 256, rangeMin: 10, rangeMax: 50});
|
||||||
|
|
||||||
|
volume = new VolumeMesh(viewer, {
|
||||||
|
name: "应变场",
|
||||||
|
size: 256,
|
||||||
|
data,
|
||||||
|
mode: VolumeRenderMode.EmissionAbsorptionModel,
|
||||||
|
clipMode: bus.clippingMode === 'single' ? 1 : 0,
|
||||||
|
scale: 4.99,
|
||||||
|
colorStops: [
|
||||||
|
{color: '#00FFFF', step: 0.2},
|
||||||
|
{color: '#0080FF', step: 0.5},
|
||||||
|
{color: '#8000FF', step: 0.7},
|
||||||
|
{color: '#FF00FF', step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
volume.renderOrder = 1
|
||||||
|
// 如果加载面面板已打开,则启用体材质深度测试
|
||||||
|
if (bus.getLoadSurfaceExists && bus.getLoadSurfaceExists()) {
|
||||||
|
const mats = Array.isArray(volume.material) ? volume.material : [volume.material];
|
||||||
|
mats.forEach((mat: any) => {
|
||||||
|
if (mat) mat.depthTest = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化控制参数
|
||||||
|
if (volume.range) range.value = volume.range.value;
|
||||||
|
if (volume.threshold) threshold.value = volume.threshold.value;
|
||||||
|
if (volume.opacity) opacity.value = volume.opacity.value;
|
||||||
|
if (volume.steps) steps.value = volume.steps.value;
|
||||||
|
if (volume.mode) mode.value = volume.mode.value;
|
||||||
|
if (volume.useSmoothing) useSmoothing.value = volume.useSmoothing.value === 1;
|
||||||
|
|
||||||
|
// 如果剖切已激活,立即订阅对应平面
|
||||||
|
if (bus.clippingMode !== null) {
|
||||||
|
subscribeClipPlanes(viewer.clipping, bus.clippingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听剖切模式变化,更新 clipMode 并重新订阅平面
|
||||||
|
unsubscribeClippingMode = bus.on(BusEvents.CLIPPING_MODE_CHANGED, ({data}) => {
|
||||||
|
if (!volume) return;
|
||||||
|
if (data === null) {
|
||||||
|
volume.clipMode.value = 0;
|
||||||
|
volume.clipPlane.value.set(0, 0, 0, 1);
|
||||||
|
volume.clipPlaneX.value.set(-1, 0, 0, -2.5);
|
||||||
|
volume.clipPlaneY.value.set(0, -1, 0, -2.5);
|
||||||
|
volume.clipPlaneZ.value.set(0, 0, -1, -2.5);
|
||||||
|
subscribeClipPlanes(viewer.clipping, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
volume.clipMode.value = data === 'single' ? 1 : 0;
|
||||||
|
subscribeClipPlanes(viewer.clipping, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasVolume.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
if (volume) {
|
||||||
|
volume.dispose();
|
||||||
|
volume = null;
|
||||||
|
}
|
||||||
|
// 取消事件监听
|
||||||
|
if (unsubscribeX) {
|
||||||
|
unsubscribeX();
|
||||||
|
unsubscribeX = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeY) {
|
||||||
|
unsubscribeY();
|
||||||
|
unsubscribeY = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeZ) {
|
||||||
|
unsubscribeZ();
|
||||||
|
unsubscribeZ = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeClippingMode) {
|
||||||
|
unsubscribeClippingMode();
|
||||||
|
unsubscribeClippingMode = null;
|
||||||
|
}
|
||||||
|
hasVolume.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unsubscribeX) unsubscribeX();
|
||||||
|
if (unsubscribeY) unsubscribeY();
|
||||||
|
if (unsubscribeZ) unsubscribeZ();
|
||||||
|
if (unsubscribeClippingMode) unsubscribeClippingMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSmoothingChange = (value: boolean) => {
|
||||||
|
if (volume && volume.useSmoothing) {
|
||||||
|
volume.useSmoothing.value = value ? 1 : 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听参数变化并更新体渲染
|
||||||
|
watch(range, (value) => {
|
||||||
|
if (volume && volume.range) {
|
||||||
|
volume.range.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(threshold, (value) => {
|
||||||
|
if (volume && volume.threshold) {
|
||||||
|
volume.threshold.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(opacity, (value) => {
|
||||||
|
if (volume && volume.opacity) {
|
||||||
|
volume.material.opacity = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(steps, (value) => {
|
||||||
|
if (volume && volume.steps) {
|
||||||
|
volume.steps.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(mode, (value) => {
|
||||||
|
if (volume && volume.mode) {
|
||||||
|
volume.mode.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="stressDisplay" title="试样内部应力显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<!-- //------ 体数据来源切换 开始------ -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">体数据来源</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="dataSource"
|
||||||
|
:options="dataSourceOptions"
|
||||||
|
size="small"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- //------ 体数据来源切换 结束------ -->
|
||||||
|
|
||||||
|
<!-- 创建/移除按钮 -->
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasVolume" size="small" type="primary" @click="create">
|
||||||
|
创建
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasVolume" size="small" type="error" @click="remove">
|
||||||
|
移除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 体渲染控制 -->
|
||||||
|
<template v-if="hasVolume">
|
||||||
|
<!-- 使用平滑过渡 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">使用平滑过渡</div>
|
||||||
|
<n-switch v-model:value="useSmoothing" @update:value="onSmoothingChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 范围 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">范围</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="range" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密度阈值 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">密度阈值</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="threshold" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 不透明度 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">不透明度</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="opacity" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 光线步进次数 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">光线步进次数</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="steps" :max="1000" :min="0" :step="1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 渲染模式 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">渲染模式</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="mode"
|
||||||
|
:options="modeOptions"
|
||||||
|
size="small"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, onUnmounted, ref, watch} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSelect, NSlider, NSwitch} from 'naive-ui';
|
||||||
|
import {BusEvents, useBus} from '@/hooks';
|
||||||
|
import {VolumeMesh, VolumeRenderMode, VolumeTool} from '@deep/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应力体数据来源枚举
|
||||||
|
*/
|
||||||
|
enum StressVolumeDataSource {
|
||||||
|
/**
|
||||||
|
* 随机生成体数据
|
||||||
|
*/
|
||||||
|
RandomGenerated = 0,
|
||||||
|
/**
|
||||||
|
* 从 RAW 文件加载体数据
|
||||||
|
*/
|
||||||
|
RawFile = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应力场 RAW 文件路径
|
||||||
|
*/
|
||||||
|
const STRESS_RAW_DATA_PATH = "/model/raw/head256x256x256_17.raw";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应力场体素尺寸
|
||||||
|
*/
|
||||||
|
const STRESS_VOLUME_SIZE = 256;
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasVolume = ref(false);
|
||||||
|
let volume: VolumeMesh | null = null;
|
||||||
|
let unsubscribeX: (() => void) | null = null;
|
||||||
|
let unsubscribeY: (() => void) | null = null;
|
||||||
|
let unsubscribeZ: (() => void) | null = null;
|
||||||
|
let unsubscribeClippingMode: (() => void) | null = null;
|
||||||
|
|
||||||
|
const subscribeClipPlanes = (clippingManager: any, clippingMode: string | null) => {
|
||||||
|
if (unsubscribeX) {
|
||||||
|
unsubscribeX();
|
||||||
|
unsubscribeX = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeY) {
|
||||||
|
unsubscribeY();
|
||||||
|
unsubscribeY = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeZ) {
|
||||||
|
unsubscribeZ();
|
||||||
|
unsubscribeZ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clippingMode === 'three') {
|
||||||
|
const planeX = clippingManager.getPlane('x');
|
||||||
|
const planeY = clippingManager.getPlane('y');
|
||||||
|
const planeZ = clippingManager.getPlane('z');
|
||||||
|
if (planeX) unsubscribeX = planeX.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneX) volume.clipPlaneX.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
if (planeY) unsubscribeY = planeY.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneY) volume.clipPlaneY.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
if (planeZ) unsubscribeZ = planeZ.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneZ) volume.clipPlaneZ.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
} else if (clippingMode === 'single') {
|
||||||
|
const planeSingle = clippingManager.getPlane('single');
|
||||||
|
if (planeSingle) unsubscribeX = planeSingle.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlane) volume.clipPlane.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 体渲染控制参数
|
||||||
|
const useSmoothing = ref(false);
|
||||||
|
const range = ref(0.5);
|
||||||
|
const threshold = ref(0.5);
|
||||||
|
const opacity = ref(0.5);
|
||||||
|
const steps = ref(200);
|
||||||
|
const mode = ref(0);
|
||||||
|
|
||||||
|
//------ 体数据来源切换 开始------
|
||||||
|
/**
|
||||||
|
* 当前体数据来源
|
||||||
|
*/
|
||||||
|
const dataSource = ref<StressVolumeDataSource>(StressVolumeDataSource.RawFile);
|
||||||
|
// const dataSource = ref<StressVolumeDataSource>(StressVolumeDataSource.RandomGenerated);
|
||||||
|
/**
|
||||||
|
* 体数据来源下拉选项
|
||||||
|
*/
|
||||||
|
const dataSourceOptions = [
|
||||||
|
{label: '随机生成', value: StressVolumeDataSource.RandomGenerated},
|
||||||
|
{label: 'RAW 文件', value: StressVolumeDataSource.RawFile},
|
||||||
|
];
|
||||||
|
//------ 体数据来源切换 结束------
|
||||||
|
|
||||||
|
// 渲染模式选项
|
||||||
|
const modeOptions = [
|
||||||
|
{label: '发射吸收模型', value: 0},
|
||||||
|
{label: '最小强度投影', value: 1},
|
||||||
|
{label: '最大强度投影', value: 2}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印体数据统计信息
|
||||||
|
* @param data - 体数据字节数组
|
||||||
|
*/
|
||||||
|
const logVolumeDataStats = (data: Uint8Array): void => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
console.warn("应力体数据为空,无法统计最小值、最大值和众数。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let minValue = data[0];
|
||||||
|
let maxValue = data[0];
|
||||||
|
let mostFrequentValue = data[0];
|
||||||
|
let mostFrequentCount = 0;
|
||||||
|
const counts = new Uint32Array(256);
|
||||||
|
|
||||||
|
for (const value of data) {
|
||||||
|
if (value < minValue) {
|
||||||
|
minValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > maxValue) {
|
||||||
|
maxValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
counts[value] += 1;
|
||||||
|
if (counts[value] > mostFrequentCount) {
|
||||||
|
mostFrequentCount = counts[value];
|
||||||
|
mostFrequentValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("应力体数据统计", {
|
||||||
|
minValue,
|
||||||
|
maxValue,
|
||||||
|
mostFrequentValue,
|
||||||
|
mostFrequentCount,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
void create();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//------ 体数据来源切换 开始------
|
||||||
|
/**
|
||||||
|
* 获取应力体数据
|
||||||
|
* @param selectedDataSource - 体数据来源
|
||||||
|
* @returns 体渲染所需的体素数据
|
||||||
|
*/
|
||||||
|
const getStressVolumeData = async (selectedDataSource: StressVolumeDataSource): Promise<Uint8Array> => {
|
||||||
|
// 保留旧逻辑:默认使用随机体数据
|
||||||
|
if (selectedDataSource === StressVolumeDataSource.RandomGenerated) {
|
||||||
|
return VolumeTool.generateVolumeData({size: STRESS_VOLUME_SIZE, rangeMin: 0, rangeMax: 50});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增逻辑:从 RAW 文件加载体数据
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const rawData = await viewer.resources.loadRawData(STRESS_RAW_DATA_PATH);
|
||||||
|
const volumeData = new Uint8Array(rawData);
|
||||||
|
logVolumeDataStats(volumeData);
|
||||||
|
return volumeData;
|
||||||
|
};
|
||||||
|
//------ 体数据来源切换 结束------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建应力体渲染
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
const create = async (): Promise<void> => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
let data: Uint8Array;
|
||||||
|
try {
|
||||||
|
// 按用户选择的数据来源获取体数据
|
||||||
|
data = await getStressVolumeData(dataSource.value);
|
||||||
|
console.log(data)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// RAW 文件加载失败时回退随机数据,保证旧逻辑可用
|
||||||
|
console.error("应力体数据加载失败,已回退到随机数据:", error);
|
||||||
|
data = VolumeTool.generateVolumeData({size: STRESS_VOLUME_SIZE, rangeMin: 10, rangeMax: 50});
|
||||||
|
}
|
||||||
|
|
||||||
|
volume = new VolumeMesh(viewer, {
|
||||||
|
name: "应力场",
|
||||||
|
size: STRESS_VOLUME_SIZE,
|
||||||
|
data,
|
||||||
|
mode: VolumeRenderMode.EmissionAbsorptionModel,
|
||||||
|
clipMode: bus.clippingMode === 'single' ? 1 : 0,
|
||||||
|
scale: 4.99,
|
||||||
|
colorStops: [
|
||||||
|
{color: '#0000FF', step: 0.2},
|
||||||
|
{color: '#00FF00', step: 0.5},
|
||||||
|
{color: '#FFFF00', step: 0.7},
|
||||||
|
{color: '#FF0000', step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
volume.renderOrder = 1
|
||||||
|
// 如果加载面面板已打开,则启用体材质深度测试
|
||||||
|
let loadSurfaceExists = bus.getLoadSurfaceExists()
|
||||||
|
if (loadSurfaceExists) {
|
||||||
|
const mats = Array.isArray(volume.material) ? volume.material : [volume.material];
|
||||||
|
mats.forEach((mat: any) => {
|
||||||
|
if (mat) mat.depthTest = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 初始化控制参数
|
||||||
|
if (volume.range) range.value = volume.range.value;
|
||||||
|
if (volume.threshold) threshold.value = volume.threshold.value;
|
||||||
|
if (volume.opacity) opacity.value = volume.opacity.value;
|
||||||
|
if (volume.steps) steps.value = volume.steps.value;
|
||||||
|
if (volume.mode) mode.value = volume.mode.value;
|
||||||
|
if (volume.useSmoothing) useSmoothing.value = volume.useSmoothing.value === 1;
|
||||||
|
|
||||||
|
// 如果剖切已激活,立即订阅对应平面
|
||||||
|
if (bus.clippingMode !== null) {
|
||||||
|
subscribeClipPlanes(viewer.clipping, bus.clippingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听剖切模式变化,更新 clipMode 并重新订阅平面
|
||||||
|
unsubscribeClippingMode = bus.on(BusEvents.CLIPPING_MODE_CHANGED, ({data}) => {
|
||||||
|
if (!volume) return;
|
||||||
|
if (data === null) {
|
||||||
|
// 剖切结束,还原所有 clipPlane 到默认值
|
||||||
|
volume.clipMode.value = 0;
|
||||||
|
volume.clipPlane.value.set(0, 0, 0, 1);
|
||||||
|
volume.clipPlaneX.value.set(-1, 0, 0, -2.5);
|
||||||
|
volume.clipPlaneY.value.set(0, -1, 0, -2.5);
|
||||||
|
volume.clipPlaneZ.value.set(0, 0, -1, -2.5);
|
||||||
|
subscribeClipPlanes(viewer.clipping, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
volume.clipMode.value = data === 'single' ? 1 : 0;
|
||||||
|
subscribeClipPlanes(viewer.clipping, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasVolume.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
if (volume) {
|
||||||
|
volume.dispose();
|
||||||
|
volume = null;
|
||||||
|
}
|
||||||
|
// 取消事件监听
|
||||||
|
if (unsubscribeX) {
|
||||||
|
unsubscribeX();
|
||||||
|
unsubscribeX = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeY) {
|
||||||
|
unsubscribeY();
|
||||||
|
unsubscribeY = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeZ) {
|
||||||
|
unsubscribeZ();
|
||||||
|
unsubscribeZ = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeClippingMode) {
|
||||||
|
unsubscribeClippingMode();
|
||||||
|
unsubscribeClippingMode = null;
|
||||||
|
}
|
||||||
|
hasVolume.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unsubscribeX) unsubscribeX();
|
||||||
|
if (unsubscribeY) unsubscribeY();
|
||||||
|
if (unsubscribeZ) unsubscribeZ();
|
||||||
|
if (unsubscribeClippingMode) unsubscribeClippingMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSmoothingChange = (value: boolean) => {
|
||||||
|
if (volume && volume.useSmoothing) {
|
||||||
|
volume.useSmoothing.value = value ? 1 : 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听参数变化并更新体渲染
|
||||||
|
watch(range, (value) => {
|
||||||
|
if (volume && volume.range) {
|
||||||
|
volume.range.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(threshold, (value) => {
|
||||||
|
if (volume && volume.threshold) {
|
||||||
|
volume.threshold.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(opacity, (value) => {
|
||||||
|
if (volume && volume.opacity) {
|
||||||
|
volume.material.opacity = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(steps, (value) => {
|
||||||
|
if (volume && volume.steps) {
|
||||||
|
volume.steps.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(mode, (value) => {
|
||||||
|
if (volume && volume.mode) {
|
||||||
|
volume.mode.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,297 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="temperatureDisplay" title="试样内部温度显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<!-- 创建/移除按钮 -->
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasVolume" size="small" type="primary" @click="create">
|
||||||
|
创建
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="!hasVolume" size="small" type="error" @click="remove">
|
||||||
|
移除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 体渲染控制 -->
|
||||||
|
<template v-if="hasVolume">
|
||||||
|
<!-- 使用平滑过渡 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">使用平滑过渡</div>
|
||||||
|
<n-switch v-model:value="useSmoothing" @update:value="onSmoothingChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 范围 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">范围</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="range" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密度阈值 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">密度阈值</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="threshold" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 不透明度 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">不透明度</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="opacity" :max="1" :min="0" :step="0.01"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 光线步进次数 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">光线步进次数</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="steps" :max="1000" :min="0" :step="1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 渲染模式 -->
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">渲染模式</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="mode"
|
||||||
|
:options="modeOptions"
|
||||||
|
size="small"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref, watch} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSelect, NSlider, NSwitch} from 'naive-ui';
|
||||||
|
import {BusEvents, useBus} from '@/hooks';
|
||||||
|
import {VolumeMesh, VolumeRenderMode, VolumeTool} from '@deep/engine';
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const hasVolume = ref(false);
|
||||||
|
let volume: VolumeMesh | null = null;
|
||||||
|
let unsubscribeX: (() => void) | null = null;
|
||||||
|
let unsubscribeY: (() => void) | null = null;
|
||||||
|
let unsubscribeZ: (() => void) | null = null;
|
||||||
|
let unsubscribeClippingMode: (() => void) | null = null;
|
||||||
|
|
||||||
|
const subscribeClipPlanes = (clippingManager: any, clippingMode: string | null) => {
|
||||||
|
if (unsubscribeX) {
|
||||||
|
unsubscribeX();
|
||||||
|
unsubscribeX = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeY) {
|
||||||
|
unsubscribeY();
|
||||||
|
unsubscribeY = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeZ) {
|
||||||
|
unsubscribeZ();
|
||||||
|
unsubscribeZ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clippingMode === 'three') {
|
||||||
|
const planeX = clippingManager.getPlane('x');
|
||||||
|
const planeY = clippingManager.getPlane('y');
|
||||||
|
const planeZ = clippingManager.getPlane('z');
|
||||||
|
if (planeX) unsubscribeX = planeX.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneX) volume.clipPlaneX.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
if (planeY) unsubscribeY = planeY.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneY) volume.clipPlaneY.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
if (planeZ) unsubscribeZ = planeZ.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlaneZ) volume.clipPlaneZ.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
} else if (clippingMode === 'single') {
|
||||||
|
const planeSingle = clippingManager.getPlane('single');
|
||||||
|
if (planeSingle) unsubscribeX = planeSingle.emitter.on('move', ({data}) => {
|
||||||
|
if (volume?.clipPlane) volume.clipPlane.value.set(data.normal.x, data.normal.y, data.normal.z, -data.constant);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 体渲染控制参数
|
||||||
|
const useSmoothing = ref(false);
|
||||||
|
const range = ref(0.5);
|
||||||
|
const threshold = ref(0.5);
|
||||||
|
const opacity = ref(0.5);
|
||||||
|
const steps = ref(200);
|
||||||
|
const mode = ref(0);
|
||||||
|
|
||||||
|
// 渲染模式选项
|
||||||
|
const modeOptions = [
|
||||||
|
{label: '发射吸收模型', value: 0},
|
||||||
|
{label: '最小强度投影', value: 1},
|
||||||
|
{label: '最大强度投影', value: 2}
|
||||||
|
];
|
||||||
|
|
||||||
|
const create = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const data = VolumeTool.generateVolumeData({size: 256, rangeMin: 10, rangeMax: 50});
|
||||||
|
|
||||||
|
volume = new VolumeMesh(viewer, {
|
||||||
|
name: "温度场",
|
||||||
|
size: 256,
|
||||||
|
data,
|
||||||
|
mode: VolumeRenderMode.EmissionAbsorptionModel,
|
||||||
|
clipMode: bus.clippingMode === 'single' ? 1 : 0,
|
||||||
|
scale: 4.99,
|
||||||
|
colorStops: [
|
||||||
|
{color: '#c418d1', step: 0.0},
|
||||||
|
{color: '#6ca616', step: 0.3},
|
||||||
|
{color: '#ff5b00', step: 0.5},
|
||||||
|
{color: '#ca7800', step: 0.7},
|
||||||
|
{color: '#00c6ff', step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
volume.renderOrder = 1
|
||||||
|
// 如果加载面面板已打开,则启用体材质深度测试
|
||||||
|
if (bus.getLoadSurfaceExists && bus.getLoadSurfaceExists()) {
|
||||||
|
const mats = Array.isArray(volume.material) ? volume.material : [volume.material];
|
||||||
|
mats.forEach((mat: any) => {
|
||||||
|
if (mat) mat.depthTest = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化控制参数
|
||||||
|
if (volume.range) range.value = volume.range.value;
|
||||||
|
if (volume.threshold) threshold.value = volume.threshold.value;
|
||||||
|
if (volume.opacity) opacity.value = volume.opacity.value;
|
||||||
|
if (volume.steps) steps.value = volume.steps.value;
|
||||||
|
if (volume.mode) mode.value = volume.mode.value;
|
||||||
|
if (volume.useSmoothing) useSmoothing.value = volume.useSmoothing.value === 1;
|
||||||
|
|
||||||
|
// 如果剖切已激活,立即订阅对应平面
|
||||||
|
if (bus.clippingMode !== null) {
|
||||||
|
subscribeClipPlanes(viewer.clipping, bus.clippingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听剖切模式变化,更新 clipMode 并重新订阅平面
|
||||||
|
unsubscribeClippingMode = bus.on(BusEvents.CLIPPING_MODE_CHANGED, ({data}) => {
|
||||||
|
if (!volume) return;
|
||||||
|
if (data === null) {
|
||||||
|
volume.clipMode.value = 0;
|
||||||
|
volume.clipPlane.value.set(0, 0, 0, 1);
|
||||||
|
volume.clipPlaneX.value.set(-1, 0, 0, -2.5);
|
||||||
|
volume.clipPlaneY.value.set(0, -1, 0, -2.5);
|
||||||
|
volume.clipPlaneZ.value.set(0, 0, -1, -2.5);
|
||||||
|
subscribeClipPlanes(viewer.clipping, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
volume.clipMode.value = data === 'single' ? 1 : 0;
|
||||||
|
subscribeClipPlanes(viewer.clipping, data);
|
||||||
|
});
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasVolume.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
if (volume) {
|
||||||
|
volume.dispose();
|
||||||
|
volume = null;
|
||||||
|
}
|
||||||
|
// 取消事件监听
|
||||||
|
if (unsubscribeX) {
|
||||||
|
unsubscribeX();
|
||||||
|
unsubscribeX = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeY) {
|
||||||
|
unsubscribeY();
|
||||||
|
unsubscribeY = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeZ) {
|
||||||
|
unsubscribeZ();
|
||||||
|
unsubscribeZ = null;
|
||||||
|
}
|
||||||
|
if (unsubscribeClippingMode) {
|
||||||
|
unsubscribeClippingMode();
|
||||||
|
unsubscribeClippingMode = null;
|
||||||
|
}
|
||||||
|
hasVolume.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unsubscribeX) unsubscribeX();
|
||||||
|
if (unsubscribeY) unsubscribeY();
|
||||||
|
if (unsubscribeZ) unsubscribeZ();
|
||||||
|
if (unsubscribeClippingMode) unsubscribeClippingMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSmoothingChange = (value: boolean) => {
|
||||||
|
if (volume && volume.useSmoothing) {
|
||||||
|
volume.useSmoothing.value = value ? 1 : 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听参数变化并更新体渲染
|
||||||
|
watch(range, (value) => {
|
||||||
|
if (volume && volume.range) {
|
||||||
|
volume.range.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(threshold, (value) => {
|
||||||
|
if (volume && volume.threshold) {
|
||||||
|
volume.threshold.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(opacity, (value) => {
|
||||||
|
if (volume && volume.opacity) {
|
||||||
|
volume.material.opacity = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(steps, (value) => {
|
||||||
|
if (volume && volume.steps) {
|
||||||
|
volume.steps.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(mode, (value) => {
|
||||||
|
if (volume && volume.mode) {
|
||||||
|
volume.mode.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
368
packages/demo/src/panels/TunnelScene/LoadSurface.vue
Normal file
368
packages/demo/src/panels/TunnelScene/LoadSurface.vue
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="temperature" title="加载面">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="showLoadSurface">
|
||||||
|
显示加载面
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="warning"
|
||||||
|
@click="closeLoadSurface">
|
||||||
|
关闭加载面
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {BusEvents, useBus} from "@/hooks";
|
||||||
|
import {CubePanel, LabelPosition,} from "@deep/engine";
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import type {ISinglePanelOptions} from "@deep/engine/src/mesh/CubePanel";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const panels = new Map<string, CubePanel>();
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.on(BusEvents.SCENE_TREE_UPDATED, () => {
|
||||||
|
initLoadSurface()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// 预处理函数:提前创建面板,避免显示时卡顿
|
||||||
|
function initLoadSurface() {
|
||||||
|
if (isInitialized.value) return;
|
||||||
|
|
||||||
|
const viewer = bus.viewer;
|
||||||
|
if (!viewer) return;
|
||||||
|
// 使用 CubePanel 的静态方法获取模型包围盒六个面的中心点位置
|
||||||
|
// const faceCenters = CubePanel.getModelFaceCenters(bus.getRockSample());
|
||||||
|
const faceCenters = {
|
||||||
|
"YF": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 2.5
|
||||||
|
},
|
||||||
|
"BEI": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": -2.5
|
||||||
|
},
|
||||||
|
"ZUO": {
|
||||||
|
"x": -2.5,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"YOU": {
|
||||||
|
"x": 2.5,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"SHANG": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 2.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"XIA": {
|
||||||
|
"x": 0,
|
||||||
|
"y": -2.5,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let panelSize = 5 * 1.1; // 增大面板大小,远离中间正方体
|
||||||
|
|
||||||
|
// 偏移常量
|
||||||
|
const OFFSET_CONSTANT = 5;
|
||||||
|
|
||||||
|
// 面板数据配置
|
||||||
|
const panelConfigs = {
|
||||||
|
YF: {
|
||||||
|
data: Array(5).fill(null).map(() => Array(5).fill(null).map(() => ({
|
||||||
|
color: new THREE.Color(0xcccccc),
|
||||||
|
text: '',
|
||||||
|
}))),
|
||||||
|
title: 'YF',
|
||||||
|
position: faceCenters?.YF ? new THREE.Vector3(faceCenters.YF.x, faceCenters.YF.y, faceCenters.YF.z + OFFSET_CONSTANT) : new THREE.Vector3(0, 0, modelSize.z * 0.8 + OFFSET_CONSTANT),
|
||||||
|
rotation: new THREE.Euler(0, 0, 0),
|
||||||
|
smallTextConfig: {
|
||||||
|
position: LabelPosition.RIGHT,
|
||||||
|
smallText1: [
|
||||||
|
{text: '1排:', offset: 30, cubeIndex: 0, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '2排:', offset: 30, cubeIndex: 1, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '3排:', offset: 30, cubeIndex: 2, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '4排:', offset: 30, cubeIndex: 3, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '5排:', offset: 30, cubeIndex: 4, fontSize: 60, color: '#000000'}
|
||||||
|
],
|
||||||
|
smallText2: [
|
||||||
|
{text: '7.5MPa', offset: 150, cubeIndex: 0, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '7.8MPa', offset: 150, cubeIndex: 1, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.6MPa', offset: 150, cubeIndex: 2, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.8MPa', offset: 150, cubeIndex: 3, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '9.2MPa', offset: 150, cubeIndex: 4, fontSize: 60, color: '#666666'}
|
||||||
|
],
|
||||||
|
rotation: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
YB: {
|
||||||
|
data: Array(5).fill(null).map(() => Array(5).fill(null).map(() => ({
|
||||||
|
color: new THREE.Color(0xcccccc),
|
||||||
|
text: '',
|
||||||
|
}))),
|
||||||
|
title: 'YB',
|
||||||
|
position: faceCenters?.BEI ? new THREE.Vector3(faceCenters.BEI.x, faceCenters.BEI.y, faceCenters.BEI.z - OFFSET_CONSTANT) : new THREE.Vector3(0, 0, -modelSize.z * 0.8 - OFFSET_CONSTANT),
|
||||||
|
rotation: new THREE.Euler(0, Math.PI, 0),
|
||||||
|
smallTextConfig: {
|
||||||
|
position: LabelPosition.RIGHT,
|
||||||
|
smallText1: [
|
||||||
|
{text: '1排:', offset: 30, cubeIndex: 0, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '2排:', offset: 30, cubeIndex: 1, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '3排:', offset: 30, cubeIndex: 2, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '4排:', offset: 30, cubeIndex: 3, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '5排:', offset: 30, cubeIndex: 4, fontSize: 60, color: '#000000'}
|
||||||
|
],
|
||||||
|
smallText2: [
|
||||||
|
{text: '7.5MPa', offset: 150, cubeIndex: 0, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '7.8MPa', offset: 150, cubeIndex: 1, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.6MPa', offset: 150, cubeIndex: 2, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.8MPa', offset: 150, cubeIndex: 3, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '9.2MPa', offset: 150, cubeIndex: 4, fontSize: 60, color: '#666666'}
|
||||||
|
],
|
||||||
|
rotation: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ZU: {
|
||||||
|
data: Array(5).fill(null).map(() => Array(5).fill(null).map(() => ({
|
||||||
|
color: new THREE.Color(0xcccccc),
|
||||||
|
text: '',
|
||||||
|
}))),
|
||||||
|
title: 'ZU',
|
||||||
|
position: faceCenters?.SHANG ? new THREE.Vector3(faceCenters.SHANG.x, faceCenters.SHANG.y + OFFSET_CONSTANT, faceCenters.SHANG.z) : new THREE.Vector3(0, modelSize.y * 0.8 + OFFSET_CONSTANT, 0),
|
||||||
|
rotation: new THREE.Euler(-Math.PI / 2, 0, 0),
|
||||||
|
smallTextConfig: {
|
||||||
|
position: LabelPosition.RIGHT,
|
||||||
|
smallText1: [
|
||||||
|
{text: '1排:', offset: 30, cubeIndex: 0, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '2排:', offset: 30, cubeIndex: 1, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '3排:', offset: 30, cubeIndex: 2, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '4排:', offset: 30, cubeIndex: 3, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '5排:', offset: 30, cubeIndex: 4, fontSize: 60, color: '#000000'}
|
||||||
|
],
|
||||||
|
smallText2: [
|
||||||
|
{text: '7.5MPa', offset: 150, cubeIndex: 0, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '7.8MPa', offset: 150, cubeIndex: 1, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.6MPa', offset: 150, cubeIndex: 2, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.8MPa', offset: 150, cubeIndex: 3, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '9.2MPa', offset: 150, cubeIndex: 4, fontSize: 60, color: '#666666'}
|
||||||
|
],
|
||||||
|
rotation: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ZD: {
|
||||||
|
data: Array(5).fill(null).map(() => Array(5).fill(null).map(() => ({
|
||||||
|
color: new THREE.Color(0xcccccc),
|
||||||
|
text: '',
|
||||||
|
}))),
|
||||||
|
title: 'ZD',
|
||||||
|
position: faceCenters?.XIA ? new THREE.Vector3(faceCenters.XIA.x, faceCenters.XIA.y - OFFSET_CONSTANT, faceCenters.XIA.z) : new THREE.Vector3(0, -modelSize.y * 0.8 - OFFSET_CONSTANT, 0),
|
||||||
|
rotation: new THREE.Euler(Math.PI / 2, 0, 0),
|
||||||
|
smallTextConfig: {
|
||||||
|
position: LabelPosition.RIGHT,
|
||||||
|
smallText1: [
|
||||||
|
{text: '3排:', offset: 30, cubeIndex: 2, fontSize: 60, color: '#000000'}
|
||||||
|
],
|
||||||
|
smallText2: [
|
||||||
|
{text: '8.6MPa', offset: 150, cubeIndex: 2, fontSize: 60, color: '#666666'}
|
||||||
|
],
|
||||||
|
rotation: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
XL: {
|
||||||
|
data: Array(5).fill(null).map(() => Array(5).fill(null).map(() => ({
|
||||||
|
color: new THREE.Color(0xcccccc),
|
||||||
|
text: '',
|
||||||
|
}))),
|
||||||
|
title: 'XL',
|
||||||
|
position: faceCenters?.ZUO ? new THREE.Vector3(faceCenters.ZUO.x - OFFSET_CONSTANT, faceCenters.ZUO.y, faceCenters.ZUO.z) : new THREE.Vector3(-modelSize.x * 0.8 - OFFSET_CONSTANT, 0, 0),
|
||||||
|
rotation: new THREE.Euler(0, -Math.PI / 2, 0),
|
||||||
|
smallTextConfig: {
|
||||||
|
position: LabelPosition.RIGHT,
|
||||||
|
smallText1: [
|
||||||
|
{text: '1排:', offset: 30, cubeIndex: 0, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '2排:', offset: 30, cubeIndex: 1, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '3排:', offset: 30, cubeIndex: 2, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '4排:', offset: 30, cubeIndex: 3, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '5排:', offset: 30, cubeIndex: 4, fontSize: 60, color: '#000000'}
|
||||||
|
],
|
||||||
|
smallText2: [
|
||||||
|
{text: '7.5MPa', offset: 150, cubeIndex: 0, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '7.8MPa', offset: 150, cubeIndex: 1, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.6MPa', offset: 150, cubeIndex: 2, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.8MPa', offset: 150, cubeIndex: 3, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '9.2MPa', offset: 150, cubeIndex: 4, fontSize: 60, color: '#666666'}
|
||||||
|
],
|
||||||
|
rotation: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
XR: {
|
||||||
|
data: Array(5).fill(null).map(() => Array(5).fill(null).map(() => ({
|
||||||
|
color: new THREE.Color(0xcccccc),
|
||||||
|
text: '',
|
||||||
|
}))),
|
||||||
|
title: 'XR',
|
||||||
|
position: faceCenters?.YOU ? new THREE.Vector3(faceCenters.YOU.x + OFFSET_CONSTANT, faceCenters.YOU.y, faceCenters.YOU.z) : new THREE.Vector3(modelSize.x * 0.8 + OFFSET_CONSTANT, 0, 0),
|
||||||
|
rotation: new THREE.Euler(0, Math.PI / 2, 0),
|
||||||
|
smallTextConfig: {
|
||||||
|
position: LabelPosition.RIGHT,
|
||||||
|
smallText1: [
|
||||||
|
{text: '1排:', offset: 30, cubeIndex: 0, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '2排:', offset: 30, cubeIndex: 1, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '3排:', offset: 30, cubeIndex: 2, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '4排:', offset: 30, cubeIndex: 3, fontSize: 60, color: '#000000'},
|
||||||
|
{text: '5排:', offset: 30, cubeIndex: 4, fontSize: 60, color: '#000000'}
|
||||||
|
],
|
||||||
|
smallText2: [
|
||||||
|
{text: '7.5MPa', offset: 150, cubeIndex: 0, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '7.8MPa', offset: 150, cubeIndex: 1, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.6MPa', offset: 150, cubeIndex: 2, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '8.8MPa', offset: 150, cubeIndex: 3, fontSize: 60, color: '#666666'},
|
||||||
|
{text: '9.2MPa', offset: 150, cubeIndex: 4, fontSize: 60, color: '#666666'}
|
||||||
|
],
|
||||||
|
rotation: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置面板数据
|
||||||
|
// YF 面板数据
|
||||||
|
panelConfigs.YF.data[2][2] = {color: new THREE.Color(0x4488ff), text: '8.6'};
|
||||||
|
|
||||||
|
// YB 面板数据
|
||||||
|
panelConfigs.YB.data[1][1] = {color: new THREE.Color(0xff8844), text: '7.2'};
|
||||||
|
panelConfigs.YB.data[1][2] = {color: new THREE.Color(0xff4444), text: '8.6'};
|
||||||
|
panelConfigs.YB.data[1][3] = {color: new THREE.Color(0x4488ff), text: '7.6'};
|
||||||
|
panelConfigs.YB.data[2][1] = {color: new THREE.Color(0x4488ff), text: '8.7'};
|
||||||
|
panelConfigs.YB.data[2][2] = {color: new THREE.Color(0x8844ff), text: '7.9'};
|
||||||
|
panelConfigs.YB.data[3][1] = {color: new THREE.Color(0x44ff88), text: '8.1'};
|
||||||
|
|
||||||
|
// ZU 面板数据
|
||||||
|
panelConfigs.ZU.data[1][2] = {color: new THREE.Color(0x4488ff), text: '7.4'};
|
||||||
|
|
||||||
|
// ZD 面板数据
|
||||||
|
panelConfigs.ZD.data[2][2] = {color: new THREE.Color(0x4488ff), text: '8.0'};
|
||||||
|
panelConfigs.ZD.data[3][2] = {color: new THREE.Color(0x4488ff), text: '7.4'};
|
||||||
|
|
||||||
|
// XL 面板数据
|
||||||
|
panelConfigs.XL.data[2][1] = {color: new THREE.Color(0x4488ff), text: '8.0'};
|
||||||
|
panelConfigs.XL.data[3][1] = {color: new THREE.Color(0x4488ff), text: '8.3'};
|
||||||
|
|
||||||
|
// XR 面板数据
|
||||||
|
panelConfigs.XR.data[1][1] = {color: new THREE.Color(0x4488ff), text: '7.6'};
|
||||||
|
panelConfigs.XR.data[1][2] = {color: new THREE.Color(0x8844ff), text: '7.9'};
|
||||||
|
panelConfigs.XR.data[1][3] = {color: new THREE.Color(0x4488ff), text: '7.6'};
|
||||||
|
panelConfigs.XR.data[2][2] = {color: new THREE.Color(0x4488ff), text: '8.3'};
|
||||||
|
panelConfigs.XR.data[3][1] = {color: new THREE.Color(0xff4444), text: '9.0'};
|
||||||
|
|
||||||
|
// 创建所有面板但不添加到场景
|
||||||
|
Object.entries(panelConfigs).forEach(([key, config]) => {
|
||||||
|
const panelOptions: ISinglePanelOptions = {
|
||||||
|
name: key,
|
||||||
|
size: panelSize,
|
||||||
|
cubeSize: 0.3,
|
||||||
|
cubeGap: 0.05,
|
||||||
|
position: config.position,
|
||||||
|
rotation: config.rotation,
|
||||||
|
initialData: config.data,
|
||||||
|
titleConfig: {
|
||||||
|
text: config.title,
|
||||||
|
position: LabelPosition.TOP,
|
||||||
|
rotation: 0,
|
||||||
|
fontSize: 1,
|
||||||
|
color: '#000000',
|
||||||
|
offset: 1
|
||||||
|
},
|
||||||
|
smallTextConfig: config.smallTextConfig,
|
||||||
|
// ZD 面只显示第2列(索引为2)
|
||||||
|
visibleColumns: key === 'ZD' ? [2] : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const panel = new CubePanel(panelOptions);
|
||||||
|
panel.group.visible = false; // 初始隐藏
|
||||||
|
viewer.scene.add(panel.group);
|
||||||
|
panels.set(key, panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
isInitialized.value = true;
|
||||||
|
// 标记为已创建加载面
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载面
|
||||||
|
function showLoadSurface() {
|
||||||
|
// 如果未初始化,先初始化
|
||||||
|
if (!isInitialized.value) {
|
||||||
|
initLoadSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewer = bus.viewer;
|
||||||
|
// 打开所有面板
|
||||||
|
panels.forEach((panel) => {
|
||||||
|
panel.group.visible = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打开深度测试:针对所有体数据网格
|
||||||
|
if (viewer) {
|
||||||
|
const vomeshes = viewer.getAllVolumeMesh();
|
||||||
|
console.log(vomeshes)
|
||||||
|
vomeshes.forEach((m: any) => {
|
||||||
|
const mats = Array.isArray(m.material) ? m.material : [m.material];
|
||||||
|
mats.forEach((mat: any) => {
|
||||||
|
if (mat) {
|
||||||
|
|
||||||
|
mat.depthTest = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记加载面已存在并显示
|
||||||
|
bus.setLoadSurfaceExists(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏加载面
|
||||||
|
function closeLoadSurface() {
|
||||||
|
// 只隐藏,不销毁
|
||||||
|
panels.forEach((panel) => {
|
||||||
|
panel.group.visible = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭深度测试
|
||||||
|
const viewer = bus.viewer;
|
||||||
|
if (viewer) {
|
||||||
|
const vomeshes = viewer.getAllVolumeMesh();
|
||||||
|
vomeshes.forEach((m: any) => {
|
||||||
|
const mats = Array.isArray(m.material) ? m.material : [m.material];
|
||||||
|
mats.forEach((mat: any) => {
|
||||||
|
if (mat) mat.depthTest = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记加载面已关闭/不存在
|
||||||
|
bus.setLoadSurfaceExists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时预处理
|
||||||
|
onMounted(() => {
|
||||||
|
// 可以在这里调用 initLoadSurface() 进行预加载
|
||||||
|
// initLoadSurface();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="PointCloudFieldDataPanel" title="点云场数据">
|
||||||
|
<PointCloudStressDisplayPanel/>
|
||||||
|
<PointCloudStrainDisplayPanel/>
|
||||||
|
<PointCloudTemperatureDisplayPanel/>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NCollapseItem} from 'naive-ui';
|
||||||
|
import PointCloudStressDisplayPanel from './PointCloudStressDisplayPanel.vue';
|
||||||
|
import PointCloudStrainDisplayPanel from './PointCloudStrainDisplayPanel.vue';
|
||||||
|
import PointCloudTemperatureDisplayPanel from './PointCloudTemperatureDisplayPanel.vue';
|
||||||
|
</script>
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="pointCloudStrainDisplay" title="试样内部应变点云显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasCloud" size="small" type="primary" @click="create">创建</n-button>
|
||||||
|
<n-button :disabled="!hasCloud" size="small" type="error" @click="remove">移除</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="hasCloud">
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">粒子大小</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="pointSize" :max="0.02" :min="0.001" :step="0.001" @update:value="onPointSizeChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">强度过滤</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="intensityRange" :max="1" :min="0" :step="0.01" range @update:value="onFilterChange"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from "vue";
|
||||||
|
import {NButton, NCollapseItem, NSlider} from "naive-ui";
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {PointCloud, PointCloudTool} from "@deep/engine";
|
||||||
|
|
||||||
|
//------ 点云面板升级 开始------
|
||||||
|
/**
|
||||||
|
* 点云可见状态。
|
||||||
|
*/
|
||||||
|
const hasCloud = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云强度过滤范围。
|
||||||
|
*/
|
||||||
|
const intensityRange = ref<[number, number]>([0, 1]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云粒子大小。
|
||||||
|
*/
|
||||||
|
const pointSize = ref<number>(0.01);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景总线实例。
|
||||||
|
*/
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前应变点云实例。
|
||||||
|
*/
|
||||||
|
let cloud: PointCloud | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建应变点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const create = (): void => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const pointCloudData = PointCloudTool.generateNoisePointCloudData({
|
||||||
|
min: [-2.5, -2.5, -2.5],
|
||||||
|
max: [2.5, 2.5, 2.5],
|
||||||
|
size: 170,
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
cloud = new PointCloud(viewer, {
|
||||||
|
name: "应变点云场",
|
||||||
|
pointCloudData,
|
||||||
|
pointSize: pointSize.value,
|
||||||
|
colorStops: [
|
||||||
|
{color: "#0000FF", step: 0.0},
|
||||||
|
{color: "#00FF00", step: 0.33},
|
||||||
|
{color: "#FFFF00", step: 0.66},
|
||||||
|
{color: "#FF0000", step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建后立即同步一次过滤范围与粒子大小。
|
||||||
|
cloud.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
cloud.setPointSize(pointSize.value);
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasCloud.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除应变点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const remove = (): void => {
|
||||||
|
if (cloud) {
|
||||||
|
cloud.dispose();
|
||||||
|
cloud = null;
|
||||||
|
}
|
||||||
|
hasCloud.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应强度过滤范围变更。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onFilterChange = (): void => {
|
||||||
|
cloud?.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应粒子大小变更。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onPointSizeChange = (): void => {
|
||||||
|
cloud?.setPointSize(pointSize.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件销毁时清理点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
onUnmounted((): void => {
|
||||||
|
remove();
|
||||||
|
});
|
||||||
|
//------ 点云面板升级 结束------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="pointCloudStressDisplay" title="试样内部应力点云显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasCloud" size="small" type="primary" @click="create">创建</n-button>
|
||||||
|
<n-button :disabled="!hasCloud" size="small" type="error" @click="remove">移除</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="hasCloud">
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">粒子大小</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="pointSize" :max="0.02" :min="0.001" :step="0.001" @update:value="onPointSizeChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">强度过滤</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="intensityRange" :max="1" :min="0" :step="0.01" range @update:value="onFilterChange"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from "vue";
|
||||||
|
import {NButton, NCollapseItem, NSlider} from "naive-ui";
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {PointCloud, PointCloudTool} from "@deep/engine";
|
||||||
|
|
||||||
|
//------ 点云面板升级 开始------
|
||||||
|
/**
|
||||||
|
* 点云可见状态。
|
||||||
|
*/
|
||||||
|
const hasCloud = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云强度过滤范围。
|
||||||
|
*/
|
||||||
|
const intensityRange = ref<[number, number]>([0, 1]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云粒子大小。
|
||||||
|
*/
|
||||||
|
const pointSize = ref<number>(0.01);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景总线实例。
|
||||||
|
*/
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前应力点云实例。
|
||||||
|
*/
|
||||||
|
let cloud: PointCloud | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建应力点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const create = (): void => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const pointCloudData = PointCloudTool.generateNoisePointCloudData({
|
||||||
|
min: [-2.5, -2.5, -2.5],
|
||||||
|
max: [2.5, 2.5, 2.5],
|
||||||
|
size: 170,
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
cloud = new PointCloud(viewer, {
|
||||||
|
name: "应力点云场",
|
||||||
|
pointCloudData,
|
||||||
|
pointSize: pointSize.value,
|
||||||
|
colorStops: [
|
||||||
|
{color: "#0000FF", step: 0.0},
|
||||||
|
{color: "#00FF00", step: 0.33},
|
||||||
|
{color: "#FFFF00", step: 0.66},
|
||||||
|
{color: "#FF0000", step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建后立即同步一次过滤范围与粒子大小。
|
||||||
|
cloud.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
cloud.setPointSize(pointSize.value);
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasCloud.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除应力点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const remove = (): void => {
|
||||||
|
if (cloud) {
|
||||||
|
cloud.dispose();
|
||||||
|
cloud = null;
|
||||||
|
}
|
||||||
|
hasCloud.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应强度过滤范围变更。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onFilterChange = (): void => {
|
||||||
|
cloud?.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应粒子大小变更。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onPointSizeChange = (): void => {
|
||||||
|
cloud?.setPointSize(pointSize.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件销毁时清理点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
onUnmounted((): void => {
|
||||||
|
remove();
|
||||||
|
});
|
||||||
|
//------ 点云面板升级 结束------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="pointCloudTemperatureDisplay" title="地热温度点云显示">
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button :disabled="hasCloud" size="small" type="primary" @click="create">创建</n-button>
|
||||||
|
<n-button :disabled="!hasCloud" size="small" type="error" @click="remove">移除</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="hasCloud">
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">粒子大小</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="pointSize" :max="0.02" :min="0.001" :step="0.001" @update:value="onPointSizeChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">强度过滤</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<n-slider v-model:value="intensityRange" :max="1" :min="0" :step="0.01" range @update:value="onFilterChange"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onUnmounted, ref} from "vue";
|
||||||
|
import {NButton, NCollapseItem, NSlider} from "naive-ui";
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {PointCloud, PointCloudTool} from "@deep/engine";
|
||||||
|
|
||||||
|
//------ 点云面板升级 开始------
|
||||||
|
/**
|
||||||
|
* 点云可见状态。
|
||||||
|
*/
|
||||||
|
const hasCloud = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云强度过滤范围。
|
||||||
|
*/
|
||||||
|
const intensityRange = ref<[number, number]>([0, 1]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点云粒子大小。
|
||||||
|
*/
|
||||||
|
const pointSize = ref<number>(0.01);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景总线实例。
|
||||||
|
*/
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前温度点云实例。
|
||||||
|
*/
|
||||||
|
let cloud: PointCloud | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建温度点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const create = (): void => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const pointCloudData = PointCloudTool.generateNoisePointCloudData({
|
||||||
|
min: [-2.5, -2.5, -2.5],
|
||||||
|
max: [2.5, 2.5, 2.5],
|
||||||
|
size: 170,
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
cloud = new PointCloud(viewer, {
|
||||||
|
name: '温度点云场',
|
||||||
|
pointCloudData,
|
||||||
|
pointSize: pointSize.value,
|
||||||
|
colorStops: [
|
||||||
|
{color: "#c418d1", step: 0.0},
|
||||||
|
{color: "#6ca616", step: 0.3},
|
||||||
|
{color: "#ff5b00", step: 0.5},
|
||||||
|
{color: "#ca7800", step: 0.7},
|
||||||
|
{color: "#00c6ff", step: 1.0},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建后立即同步一次过滤范围与粒子大小。
|
||||||
|
cloud.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
cloud.setPointSize(pointSize.value);
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
hasCloud.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除温度点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const remove = (): void => {
|
||||||
|
if (cloud) {
|
||||||
|
cloud.dispose();
|
||||||
|
cloud = null;
|
||||||
|
}
|
||||||
|
hasCloud.value = false;
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应强度过滤范围变更。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onFilterChange = (): void => {
|
||||||
|
cloud?.filterByIntensity(intensityRange.value[0], intensityRange.value[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应粒子大小变更。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onPointSizeChange = (): void => {
|
||||||
|
cloud?.setPointSize(pointSize.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件销毁时清理点云。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
onUnmounted((): void => {
|
||||||
|
remove();
|
||||||
|
});
|
||||||
|
//------ 点云面板升级 结束------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
415
packages/demo/src/panels/TunnelScene/RobotExcavationPanel.vue
Normal file
415
packages/demo/src/panels/TunnelScene/RobotExcavationPanel.vue
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="robot-excavation" title="机器人开挖">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
:disabled="isAnimating"
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startExcavation">
|
||||||
|
{{ isAnimating ? "开挖中..." : "开始开挖" }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
:disabled="!excavationTunnel"
|
||||||
|
block
|
||||||
|
type="info"
|
||||||
|
@click="toggleExcavationMaterialMode">
|
||||||
|
{{ excavationMaterialMode === ExcavationMaterialMode.Normal ? "切换线框材质" : "切换普通材质" }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
:disabled="!excavationTunnel"
|
||||||
|
block
|
||||||
|
type="warning"
|
||||||
|
@click="resetExcavation">
|
||||||
|
重置
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
|
import {NButton, NCollapseItem, NSpace} from "naive-ui";
|
||||||
|
import {
|
||||||
|
createRock7Material,
|
||||||
|
createRock8Material,
|
||||||
|
CSGOperationType,
|
||||||
|
type IArchOptions,
|
||||||
|
ParametricArch,
|
||||||
|
Tool,
|
||||||
|
Viewer
|
||||||
|
} from "@deep/engine";
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {BusEvents} from "@/hooks/Bus.ts";
|
||||||
|
import * as THREE from "three/webgpu";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
|
||||||
|
// 开挖隧道对象
|
||||||
|
let excavationTunnel: ParametricArch | null = null;
|
||||||
|
|
||||||
|
// 动画状态
|
||||||
|
const isAnimating = ref(false);
|
||||||
|
const animationProgress = ref(0);
|
||||||
|
let progressInterval: number | null = null;
|
||||||
|
|
||||||
|
// 隧道参数
|
||||||
|
const tunnelParams = ref({
|
||||||
|
width: 0.8,
|
||||||
|
height: 0.4,
|
||||||
|
thickness: 0.06,
|
||||||
|
curveSegments: 32,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动画参数
|
||||||
|
const animationParams = ref({
|
||||||
|
duration: 9,
|
||||||
|
delay: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const straightPathPoints1 = [
|
||||||
|
{
|
||||||
|
x: -0.13032569816002015,
|
||||||
|
y: -0.038,
|
||||||
|
z: 2.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: -0.13032569816002015,
|
||||||
|
y: -0.036827218370504344,
|
||||||
|
z: 2.4,
|
||||||
|
},
|
||||||
|
{x: 0.3, y: -0.035, z: 1},
|
||||||
|
{x: -0.13032569816002015, y: -0.036827218370504344, z: -2},
|
||||||
|
];
|
||||||
|
|
||||||
|
//------ 开挖隧道材质切换 开始------
|
||||||
|
/**
|
||||||
|
* 开挖隧道材质显示模式
|
||||||
|
*/
|
||||||
|
enum ExcavationMaterialMode {
|
||||||
|
/**
|
||||||
|
* 普通材质
|
||||||
|
*/
|
||||||
|
Normal = "normal",
|
||||||
|
/**
|
||||||
|
* 线框材质
|
||||||
|
*/
|
||||||
|
Wireframe = "wireframe",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隧道材质显示模式
|
||||||
|
*/
|
||||||
|
const excavationMaterialMode = ref<ExcavationMaterialMode>(ExcavationMaterialMode.Normal);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存开挖隧道创建时的原始材质
|
||||||
|
* key 为网格 uuid
|
||||||
|
*/
|
||||||
|
const excavationOriginalMaterialMap = new Map<string, THREE.Material | THREE.Material[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存由原始材质克隆得到的线框材质
|
||||||
|
* key 为网格 uuid
|
||||||
|
*/
|
||||||
|
const excavationWireframeMaterialMap = new Map<string, THREE.Material | THREE.Material[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 克隆单个材质并启用线框显示
|
||||||
|
* @param material 原始材质
|
||||||
|
* @returns 线框材质
|
||||||
|
*/
|
||||||
|
const cloneWireframeMaterial = (material: THREE.Material): THREE.Material => {
|
||||||
|
// 克隆原始材质,保留颜色与贴图等参数
|
||||||
|
const wireframeMaterial = material.clone();
|
||||||
|
|
||||||
|
// 仅在材质支持 wireframe 属性时开启线框
|
||||||
|
if ("wireframe" in wireframeMaterial) {
|
||||||
|
Reflect.set(wireframeMaterial, "wireframe", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wireframeMaterial;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由原始材质生成对应线框材质
|
||||||
|
* @param material 原始材质或材质数组
|
||||||
|
* @returns 线框材质或材质数组
|
||||||
|
*/
|
||||||
|
const buildWireframeMaterial = (material: THREE.Material | THREE.Material[]): THREE.Material | THREE.Material[] => {
|
||||||
|
if (Array.isArray(material)) {
|
||||||
|
// 多材质网格:逐项克隆并开启线框
|
||||||
|
return material.map((item) => cloneWireframeMaterial(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloneWireframeMaterial(material);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放线框材质资源
|
||||||
|
*/
|
||||||
|
const disposeWireframeMaterials = (): void => {
|
||||||
|
excavationWireframeMaterialMap.forEach((material) => {
|
||||||
|
if (Array.isArray(material)) {
|
||||||
|
// 材质数组需要逐项释放
|
||||||
|
material.forEach((item) => item.dispose());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
material.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
excavationWireframeMaterialMap.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置材质缓存与模式
|
||||||
|
*/
|
||||||
|
const resetMaterialCache = (): void => {
|
||||||
|
disposeWireframeMaterials();
|
||||||
|
excavationOriginalMaterialMap.clear();
|
||||||
|
excavationMaterialMode.value = ExcavationMaterialMode.Normal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存隧道创建时原始材质,并提前生成线框材质
|
||||||
|
* @param tunnel 开挖隧道对象
|
||||||
|
*/
|
||||||
|
const cacheExcavationMaterials = (tunnel: ParametricArch): void => {
|
||||||
|
resetMaterialCache();
|
||||||
|
|
||||||
|
tunnel.traverse((object3D) => {
|
||||||
|
if (!(object3D instanceof THREE.Mesh)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录创建时材质,作为“普通材质”来源
|
||||||
|
excavationOriginalMaterialMap.set(object3D.uuid, object3D.material);
|
||||||
|
|
||||||
|
// 预生成线框材质,避免切换时重复克隆
|
||||||
|
const wireframeMaterial = buildWireframeMaterial(object3D.material);
|
||||||
|
excavationWireframeMaterialMap.set(object3D.uuid, wireframeMaterial);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按指定模式应用开挖隧道材质
|
||||||
|
* @param mode 目标显示模式
|
||||||
|
*/
|
||||||
|
const applyExcavationMaterialMode = (mode: ExcavationMaterialMode): void => {
|
||||||
|
if (!excavationTunnel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
excavationTunnel.traverse((object3D) => {
|
||||||
|
if (!(object3D instanceof THREE.Mesh)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMaterial =
|
||||||
|
mode === ExcavationMaterialMode.Normal
|
||||||
|
? excavationOriginalMaterialMap.get(object3D.uuid)
|
||||||
|
: excavationWireframeMaterialMap.get(object3D.uuid);
|
||||||
|
|
||||||
|
// 找不到缓存则跳过,避免错误覆盖
|
||||||
|
if (!targetMaterial) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object3D.material = targetMaterial;
|
||||||
|
});
|
||||||
|
|
||||||
|
excavationMaterialMode.value = mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换开挖隧道材质模式
|
||||||
|
*/
|
||||||
|
const toggleExcavationMaterialMode = (): void => {
|
||||||
|
if (!excavationTunnel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMode =
|
||||||
|
excavationMaterialMode.value === ExcavationMaterialMode.Normal
|
||||||
|
? ExcavationMaterialMode.Wireframe
|
||||||
|
: ExcavationMaterialMode.Normal;
|
||||||
|
|
||||||
|
applyExcavationMaterialMode(targetMode);
|
||||||
|
};
|
||||||
|
//------ 开挖隧道材质切换 结束------
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始开挖动画
|
||||||
|
*/
|
||||||
|
const startExcavation = () => {
|
||||||
|
if (!viewer) {
|
||||||
|
console.error("Viewer 未初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除之前的开挖隧道
|
||||||
|
if (excavationTunnel) {
|
||||||
|
viewer.scene.remove(excavationTunnel);
|
||||||
|
excavationTunnel.dispose();
|
||||||
|
excavationTunnel = null;
|
||||||
|
//------ 开挖隧道材质切换 开始------
|
||||||
|
resetMaterialCache();
|
||||||
|
//------ 开挖隧道材质切换 结束------
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建参数化城门洞配置
|
||||||
|
const geometrySettings: IArchOptions = {
|
||||||
|
material: createRock8Material(),
|
||||||
|
width: tunnelParams.value.width,
|
||||||
|
height: tunnelParams.value.height,
|
||||||
|
depth: -4.5,
|
||||||
|
thickness: tunnelParams.value.thickness,
|
||||||
|
curveSegments: tunnelParams.value.curveSegments,
|
||||||
|
steps: 8,
|
||||||
|
hasHole: false,
|
||||||
|
// bottomEnabled: true,
|
||||||
|
// 底部
|
||||||
|
topEnabled: true,
|
||||||
|
followMeshUrl: "/model/机器人.glb",
|
||||||
|
followMeshOffset: new THREE.Vector3(0, -0.2, 0.1),
|
||||||
|
// extrudePathPoints: straightPathPoints,
|
||||||
|
extrudePathPoints: straightPathPoints1,
|
||||||
|
enableCSGOperation: true,
|
||||||
|
animate: true,
|
||||||
|
// animate: false,
|
||||||
|
// debugCollisionProxy: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
excavationTunnel = new ParametricArch(viewer, geometrySettings);
|
||||||
|
Tool.defineMaterialOpacityMapping(excavationTunnel);
|
||||||
|
excavationTunnel.name = "机器人开挖隧道";
|
||||||
|
|
||||||
|
let defaultExcavationTunnel = new ParametricArch(viewer, {
|
||||||
|
...geometrySettings,
|
||||||
|
animate:false
|
||||||
|
});
|
||||||
|
defaultExcavationTunnel.name = "机器人开挖隧道碰撞体";
|
||||||
|
defaultExcavationTunnel.visible = false;
|
||||||
|
Tool.setExcludeAll(defaultExcavationTunnel)
|
||||||
|
viewer.rangeCullingManager.add(defaultExcavationTunnel);
|
||||||
|
|
||||||
|
// excavationTunnel.addCollisionTarget(bus.getRockSample());
|
||||||
|
excavationTunnel.addCollisionTarget(bus.getSection(),CSGOperationType.HOLLOW_SUBTRACTION, -1);
|
||||||
|
|
||||||
|
excavationTunnel.renderOrder = 0;
|
||||||
|
|
||||||
|
viewer.rangeCullingManager.add(excavationTunnel);
|
||||||
|
cacheExcavationMaterials(excavationTunnel);
|
||||||
|
applyExcavationMaterialMode(ExcavationMaterialMode.Normal);
|
||||||
|
|
||||||
|
// 配置动画参数
|
||||||
|
excavationTunnel.setAnimationDuration(animationParams.value.duration * 1000);
|
||||||
|
excavationTunnel.setAnimationDelay(animationParams.value.delay * 1000);
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
excavationTunnel.startAnimation();
|
||||||
|
isAnimating.value = true;
|
||||||
|
animationProgress.value = 0;
|
||||||
|
|
||||||
|
// 启动进度监控
|
||||||
|
startProgressMonitoring();
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置开挖动画
|
||||||
|
*/
|
||||||
|
const resetExcavation = () => {
|
||||||
|
if (!excavationTunnel) return;
|
||||||
|
|
||||||
|
excavationTunnel.resetAnimation();
|
||||||
|
isAnimating.value = false;
|
||||||
|
animationProgress.value = 0;
|
||||||
|
stopProgressMonitoring();
|
||||||
|
|
||||||
|
//------ 开挖隧道材质切换 开始------
|
||||||
|
applyExcavationMaterialMode(ExcavationMaterialMode.Normal);
|
||||||
|
//------ 开挖隧道材质切换 结束------
|
||||||
|
|
||||||
|
viewer?.rangeCullingManager.remove(excavationTunnel);
|
||||||
|
excavationTunnel.dispose();
|
||||||
|
excavationTunnel = null;
|
||||||
|
|
||||||
|
//------ 开挖隧道材质切换 开始------
|
||||||
|
resetMaterialCache();
|
||||||
|
//------ 开挖隧道材质切换 结束------
|
||||||
|
|
||||||
|
console.log("机器人开挖动画已重置");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动进度监控
|
||||||
|
*/
|
||||||
|
const startProgressMonitoring = () => {
|
||||||
|
stopProgressMonitoring();
|
||||||
|
|
||||||
|
const totalDuration = (animationParams.value.duration + animationParams.value.delay) * 1000;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
progressInterval = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(100, Math.floor((elapsed / totalDuration) * 100));
|
||||||
|
animationProgress.value = progress;
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
isAnimating.value = false;
|
||||||
|
stopProgressMonitoring();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止进度监控
|
||||||
|
*/
|
||||||
|
const stopProgressMonitoring = () => {
|
||||||
|
if (progressInterval !== null) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
const cleanup = () => {
|
||||||
|
stopProgressMonitoring();
|
||||||
|
|
||||||
|
if (excavationTunnel && viewer) {
|
||||||
|
//------ 开挖隧道材质切换 开始------
|
||||||
|
applyExcavationMaterialMode(ExcavationMaterialMode.Normal);
|
||||||
|
//------ 开挖隧道材质切换 结束------
|
||||||
|
|
||||||
|
viewer.scene.remove(excavationTunnel);
|
||||||
|
excavationTunnel.dispose();
|
||||||
|
excavationTunnel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//------ 开挖隧道材质切换 开始------
|
||||||
|
resetMaterialCache();
|
||||||
|
//------ 开挖隧道材质切换 结束------
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
218
packages/demo/src/panels/TunnelScene/StressApplicationPanel.vue
Normal file
218
packages/demo/src/panels/TunnelScene/StressApplicationPanel.vue
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="stress" title="封闭应力施加">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="startStressApplication">
|
||||||
|
开始施加
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="resetStressApplication">
|
||||||
|
重置
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref} from 'vue';
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {CssType, EventManagerEvents, ParametricCylinder, Viewer} from '@deep/engine';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
import {type IStressData, StressApplicationHtmlPanel} from "../../htmlPanel";
|
||||||
|
import {BusEvents} from "../../hooks/Bus.ts";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
const stressCylinders = ref<ParametricCylinder[]>([]);
|
||||||
|
const stressPanels = new Map<ParametricCylinder, StressApplicationHtmlPanel>();
|
||||||
|
let stressCounter = 0;
|
||||||
|
|
||||||
|
// 应力参数
|
||||||
|
const stressParams = ref({
|
||||||
|
stressValue: 10.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 固定的圆柱尺寸
|
||||||
|
const CYLINDER_RADIUS = 0.15;
|
||||||
|
const CYLINDER_HEIGHT = 0.3;
|
||||||
|
|
||||||
|
// 定义初始应力施加点的位置
|
||||||
|
const initialStressPositions = [
|
||||||
|
{x: -1.5, y: 0, z: 0},
|
||||||
|
{x: 1.5, y: 0, z: 0},
|
||||||
|
{x: 0, y: 0, z: -1.5},
|
||||||
|
{x: 0, y: 0, z: 1.5},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bus.once(BusEvents.VIEWER_INITIALIZED).then(() => {
|
||||||
|
viewer = bus.getViewer();
|
||||||
|
initializeStressCylinders();
|
||||||
|
setupClickListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化应力施加圆柱模型
|
||||||
|
*/
|
||||||
|
const initializeStressCylinders = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
initialStressPositions.forEach((pos, index) => {
|
||||||
|
createStressCylinder(new THREE.Vector3(pos.x, pos.y, pos.z), index + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建应力施加圆柱
|
||||||
|
*/
|
||||||
|
const createStressCylinder = (position: THREE.Vector3, index: number) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const material = new THREE.MeshStandardNodeMaterial({
|
||||||
|
color: 0xff6b6b,
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 0.3,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cylinder = new ParametricCylinder({
|
||||||
|
radiusTop: CYLINDER_RADIUS,
|
||||||
|
radiusBottom: CYLINDER_RADIUS,
|
||||||
|
height: CYLINDER_HEIGHT,
|
||||||
|
//------ 圆柱上下不闭合 开始------
|
||||||
|
openEnded: true,
|
||||||
|
//------ 圆柱上下不闭合 结束------
|
||||||
|
//------ 圆柱细分优化 开始------
|
||||||
|
radialSegments: 96,
|
||||||
|
//------ 圆柱细分优化 结束------
|
||||||
|
material: material,
|
||||||
|
});
|
||||||
|
|
||||||
|
stressCounter++;
|
||||||
|
cylinder.name = `应力施加点_${stressCounter}`;
|
||||||
|
cylinder.position.copy(position);
|
||||||
|
|
||||||
|
// 存储应力数据到 userData
|
||||||
|
const stressData: IStressData = {
|
||||||
|
name: cylinder.name,
|
||||||
|
stressValue: stressParams.value.stressValue,
|
||||||
|
direction: {x: 0, y: -1, z: 0}, // 默认向下
|
||||||
|
activationTime: 5.0,
|
||||||
|
status: 'inactive',
|
||||||
|
};
|
||||||
|
cylinder.userData.stressData = stressData;
|
||||||
|
|
||||||
|
viewer.scene.add(cylinder);
|
||||||
|
stressCylinders.value.push(cylinder);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置点击事件监听
|
||||||
|
*/
|
||||||
|
const setupClickListener = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, ({data}) => {
|
||||||
|
const {intersects} = data;
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
for (const intersection of intersects) {
|
||||||
|
const clickedObject = intersection.object;
|
||||||
|
const cylinder = stressCylinders.value.find(c => c.mesh === clickedObject);
|
||||||
|
|
||||||
|
if (cylinder) {
|
||||||
|
showStressPanel(cylinder, intersection.point);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示应力施加面板
|
||||||
|
*/
|
||||||
|
const showStressPanel = (cylinder: ParametricCylinder, clickPoint: THREE.Vector3) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 如果已有面板,先移除
|
||||||
|
let panel = stressPanels.get(cylinder);
|
||||||
|
if (panel) {
|
||||||
|
panel.hide();
|
||||||
|
const panelObject = panel.getCssObject();
|
||||||
|
if (panelObject.parent) {
|
||||||
|
panelObject.parent.remove(panelObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取应力数据
|
||||||
|
const stressData: IStressData = cylinder.mesh.userData.stressData || {
|
||||||
|
name: cylinder.mesh.name,
|
||||||
|
stressValue: stressParams.value.stressValue,
|
||||||
|
direction: {x: 0, y: -1, z: 0},
|
||||||
|
activationTime: 5.0,
|
||||||
|
status: 'inactive',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建新面板
|
||||||
|
panel = new StressApplicationHtmlPanel(stressData, CssType.CSS2D);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
panel.onDelete(() => {
|
||||||
|
if (panel) {
|
||||||
|
panel.hide();
|
||||||
|
const panelObject = panel.getCssObject();
|
||||||
|
if (panelObject.parent) {
|
||||||
|
panelObject.parent.remove(panelObject);
|
||||||
|
}
|
||||||
|
stressPanels.delete(cylinder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置面板位置
|
||||||
|
const panelObject = panel.getCssObject();
|
||||||
|
panelObject.position.copy(clickPoint);
|
||||||
|
panelObject.position.y += 0.5;
|
||||||
|
|
||||||
|
// 添加到场景
|
||||||
|
viewer.scene.add(panelObject);
|
||||||
|
panel.show();
|
||||||
|
stressPanels.set(cylinder, panel);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始施加应力
|
||||||
|
*/
|
||||||
|
const startStressApplication = () => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
stressCylinders.value.forEach((cylinder) => {
|
||||||
|
const position = cylinder.mesh.position.clone();
|
||||||
|
showStressPanel(cylinder, position);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const resetStressApplication = () => {
|
||||||
|
// 移除所有应力面板
|
||||||
|
stressPanels.forEach((panel) => {
|
||||||
|
panel.hide();
|
||||||
|
const panelObject = panel.getCssObject();
|
||||||
|
if (panelObject.parent) {
|
||||||
|
panelObject.parent.remove(panelObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stressPanels.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="temperature" title="温度施加">
|
||||||
|
<n-space style="width: 100%" vertical>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="showTemperaturePanels">
|
||||||
|
显示温度面板
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="warning"
|
||||||
|
@click="closeTemperaturePanels">
|
||||||
|
关闭温度面板
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {NButton, NCollapseItem, NSpace} from 'naive-ui';
|
||||||
|
import {useBus} from "@/hooks";
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
function showTemperaturePanels() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTemperaturePanels() {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
242
packages/demo/src/panels/base/ClippingPanel.vue
Normal file
242
packages/demo/src/panels/base/ClippingPanel.vue
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<n-collapse-item name="clipping" title="剖切控制">
|
||||||
|
<div class="panel-section">
|
||||||
|
<h4>选择要剖切的对象</h4>
|
||||||
|
<div class="scene-tree-container">
|
||||||
|
<n-tree-select
|
||||||
|
v-model:value="selectedKeys"
|
||||||
|
:options="treeData"
|
||||||
|
key-field="uuid"
|
||||||
|
label-field="name"
|
||||||
|
multiple
|
||||||
|
placeholder="选择要剖切的对象"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<n-button
|
||||||
|
:disabled="selectedKeys.length === 0"
|
||||||
|
type="default"
|
||||||
|
@click="handleClearSelection"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="selectedKeys.length === 0 || activeMode === 'single'"
|
||||||
|
:type="activeMode === 'three' ? 'primary' : 'default'"
|
||||||
|
@click="handleThreePlaneClipping"
|
||||||
|
>
|
||||||
|
三面剖切
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:disabled="selectedKeys.length === 0 || activeMode === 'three'"
|
||||||
|
:type="activeMode === 'single' ? 'primary' : 'default'"
|
||||||
|
@click="handleSinglePlaneClipping"
|
||||||
|
>
|
||||||
|
单面剖切
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 单面模式操作 -->
|
||||||
|
<!-- <template v-if="activeMode === 'single'">-->
|
||||||
|
<!-- <div class="control-row">-->
|
||||||
|
<!-- <div class="control-label">操作模式</div>-->
|
||||||
|
<!-- <n-select-->
|
||||||
|
<!-- v-model:value="singleTransformMode"-->
|
||||||
|
<!-- :options="transformModeOptions"-->
|
||||||
|
<!-- size="small"-->
|
||||||
|
<!-- style="width: 120px"-->
|
||||||
|
<!-- @update:value="onSingleTransformModeChange"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="tsx" setup>
|
||||||
|
import {onMounted, ref, watch} from 'vue';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {NButton, NCollapseItem, NSelect, NTreeSelect} from 'naive-ui';
|
||||||
|
import {useBus} from '@/hooks';
|
||||||
|
import {BusEvents} from '@/hooks/Bus.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单面剖切变换空间枚举
|
||||||
|
*/
|
||||||
|
enum SinglePlaneTransformSpace {
|
||||||
|
/**
|
||||||
|
* 局部空间
|
||||||
|
*/
|
||||||
|
Local = "local",
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
const treeData = ref<any[]>([]);
|
||||||
|
const selectedKeys = ref<Array<string>>([]);
|
||||||
|
const isClippingInitialized = ref(false);
|
||||||
|
const activeMode = ref<'three' | 'single' | null>(null);
|
||||||
|
const singleTransformMode = ref<'translate' | 'rotate'>('translate');
|
||||||
|
|
||||||
|
const transformModeOptions = [
|
||||||
|
{label: '移动', value: 'translate'},
|
||||||
|
{label: '旋转', value: 'rotate'},
|
||||||
|
];
|
||||||
|
|
||||||
|
//------ 单面剖切局部空间 开始------
|
||||||
|
/**
|
||||||
|
* 应用单面剖切局部空间设置
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const applySinglePlaneLocalSpace = (): void => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
viewer.clipping.getPlane("single")?.setTransformSpace(SinglePlaneTransformSpace.Local);
|
||||||
|
};
|
||||||
|
//------ 单面剖切局部空间 结束------
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateTreeData();
|
||||||
|
|
||||||
|
bus.on(BusEvents.SCENE_TREE_UPDATED, () => {
|
||||||
|
updateTreeData();
|
||||||
|
selectedKeys.value = getAllTreeKeys(treeData.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(selectedKeys, (newKeys, oldKeys) => {
|
||||||
|
if (!isClippingInitialized.value) return;
|
||||||
|
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
if (newKeys.length === 0) {
|
||||||
|
viewer.clipping.clearClippingGroups();
|
||||||
|
isClippingInitialized.value = false;
|
||||||
|
activeMode.value = null;
|
||||||
|
bus.setClippingMode(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysToRemove = oldKeys.filter(key => !newKeys.includes(key));
|
||||||
|
const keysToAdd = newKeys.filter(key => !oldKeys.includes(key));
|
||||||
|
|
||||||
|
if (keysToRemove.length > 0) viewer.clipping.removeClippingObjectsByUuid(keysToRemove);
|
||||||
|
if (keysToAdd.length > 0) viewer.clipping.addClippingObjectsByUuid(keysToAdd);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTreeData = () => {
|
||||||
|
if (bus.viewer) {
|
||||||
|
const sceneData = bus.viewer.serializeSceneFiltered();
|
||||||
|
treeData.value = sceneData;
|
||||||
|
if (selectedKeys.value.length === 0) {
|
||||||
|
selectedKeys.value = getAllTreeKeys(sceneData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllTreeKeys = (nodes: any[]): string[] => {
|
||||||
|
return nodes.filter(n => n.uuid).map(n => n.uuid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThreePlaneClipping = () => {
|
||||||
|
if (!selectedKeys.value.length) return;
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
viewer.clipping.removePlane('single');
|
||||||
|
viewer.clipping.addClippingObjectsByUuid(selectedKeys.value);
|
||||||
|
if (!viewer.clipping.getPlane('x')) {
|
||||||
|
viewer.clipping.addDefaultPlanes();
|
||||||
|
}
|
||||||
|
viewer.clipping.autoPlanePosition();
|
||||||
|
activeMode.value = 'three';
|
||||||
|
isClippingInitialized.value = true;
|
||||||
|
bus.setClippingMode('three');
|
||||||
|
viewer.clipping.startClipping();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSinglePlaneClipping = () => {
|
||||||
|
if (!selectedKeys.value.length) return;
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
viewer.clipping.removePlane('x');
|
||||||
|
viewer.clipping.removePlane('y');
|
||||||
|
viewer.clipping.removePlane('z');
|
||||||
|
viewer.clipping.addClippingObjectsByUuid(selectedKeys.value);
|
||||||
|
const plane = viewer.clipping.addPlane('single', {
|
||||||
|
normal: new THREE.Vector3(1, 0, 0),
|
||||||
|
constant: 8,
|
||||||
|
enableTransformControls: true,
|
||||||
|
transformMode: singleTransformMode.value,
|
||||||
|
});
|
||||||
|
// 直接创建 helper,不走 autoPlanePosition
|
||||||
|
const worldPosition = new THREE.Vector3(-6, 0, 0);
|
||||||
|
plane.createHelper(worldPosition, 5, viewer.clipping.planeHelperGroup);
|
||||||
|
|
||||||
|
plane.setTransformSpace(SinglePlaneTransformSpace.Local);
|
||||||
|
|
||||||
|
// 启用附加的旋转控制手柄
|
||||||
|
plane.showRotateHandles(true);
|
||||||
|
|
||||||
|
viewer.clipping.scene.add(viewer.clipping.planeHelperGroup);
|
||||||
|
plane.emitter.emit('move', {
|
||||||
|
position: worldPosition,
|
||||||
|
constant: plane.plane.constant,
|
||||||
|
normal: plane.plane.normal.clone(),
|
||||||
|
});
|
||||||
|
activeMode.value = 'single';
|
||||||
|
isClippingInitialized.value = true;
|
||||||
|
bus.setClippingMode('single');
|
||||||
|
viewer.clipping.startClipping();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSingleTransformModeChange = (mode: 'translate' | 'rotate') => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
viewer.clipping.getPlane('single')?.setTransformMode(mode);
|
||||||
|
//------ 单面剖切局部空间 开始------
|
||||||
|
// 模式切换后再次确保局部空间设置不丢失
|
||||||
|
applySinglePlaneLocalSpace();
|
||||||
|
//------ 单面剖切局部空间 结束------
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
selectedKeys.value = getAllTreeKeys(treeData.value);
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
viewer.clipping.clearClippingGroups();
|
||||||
|
activeMode.value = null;
|
||||||
|
isClippingInitialized.value = false;
|
||||||
|
bus.setClippingMode(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({updateTreeData});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.panel-section h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 1px solid #eaeaea;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-tree-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
64
packages/demo/src/router/index.ts
Normal file
64
packages/demo/src/router/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
|
import TunnelScene from '../views/TunnelScene.vue'
|
||||||
|
import GoldMineScene from '../views/GoldMineScene.vue'
|
||||||
|
import OilGasScene from '../views/OilGasScene.vue'
|
||||||
|
import GeothermalScene from '../views/GeothermalScene.vue'
|
||||||
|
import HomePage from "@/views/HomePage.vue";
|
||||||
|
|
||||||
|
// 导出路由配置数组
|
||||||
|
export const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomePage,
|
||||||
|
meta: {
|
||||||
|
title: '首页'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenes/tunnel',
|
||||||
|
name: 'tunnel-scene',
|
||||||
|
component: TunnelScene,
|
||||||
|
meta: {
|
||||||
|
title: '隧道场景'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenes/gold-mine',
|
||||||
|
name: 'gold-mine-scene',
|
||||||
|
component: GoldMineScene,
|
||||||
|
meta: {
|
||||||
|
title: '金属矿场景'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenes/oil-gas',
|
||||||
|
name: 'oil-gas-scene',
|
||||||
|
component: OilGasScene,
|
||||||
|
meta: {
|
||||||
|
title: '油气场景'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenes/geothermal',
|
||||||
|
name: 'geothermal-scene',
|
||||||
|
component: GeothermalScene,
|
||||||
|
meta: {
|
||||||
|
title: '地热场景'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由切换后设置页面标题
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// 从路由元信息中获取标题,如果没有则使用默认标题
|
||||||
|
const title = to.meta.title as string || 'Deep Engine Demo';
|
||||||
|
document.title = title;
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
15
packages/demo/src/style/style.css
Normal file
15
packages/demo/src/style/style.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
html, body, #app {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#viewer {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
73
packages/demo/src/utils/ModelUtils.ts
Normal file
73
packages/demo/src/utils/ModelUtils.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型工具类
|
||||||
|
* 提供模型相关的工具函数
|
||||||
|
*/
|
||||||
|
export class ModelUtils {
|
||||||
|
/**
|
||||||
|
* 查找并修改"断层"模型的材质为双面
|
||||||
|
* @param object 要搜索的根对象
|
||||||
|
*/
|
||||||
|
public static findAndModifyFaultModel(object: THREE.Object3D): void {
|
||||||
|
object.traverse((child) => {
|
||||||
|
if (child.name === "断层") {
|
||||||
|
// 遍历断层模型的所有子对象,修改材质为双面
|
||||||
|
child.traverse((faultChild) => {
|
||||||
|
if (faultChild instanceof THREE.Mesh) {
|
||||||
|
if (Array.isArray(faultChild.material)) {
|
||||||
|
// 处理材质数组
|
||||||
|
faultChild.material.forEach((material) => {
|
||||||
|
if (material instanceof THREE.Material) {
|
||||||
|
material.side = THREE.DoubleSide;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (faultChild.material instanceof THREE.Material) {
|
||||||
|
// 处理单个材质
|
||||||
|
faultChild.material.side = THREE.DoubleSide;
|
||||||
|
faultChild.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
console.log("已修改断层模型的材质为双面:", faultChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找名字为"试样"的mesh并修改指定材质的深度测试和深度缓冲为false
|
||||||
|
* 因为fbx模型不标准,内部模型透明后, 会导致排序后消失
|
||||||
|
* @param object 要搜索的根对象
|
||||||
|
* @param objectName
|
||||||
|
* @param materialName
|
||||||
|
*/
|
||||||
|
public static findAndModifySampleMaterial(object: THREE.Object3D, objectName: string = "试样", materialName?: string): void {
|
||||||
|
object.traverse((child) => {
|
||||||
|
if (child.name === objectName && child instanceof THREE.Mesh) {
|
||||||
|
console.log(`找到${objectName}模型:`, child);
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
// 处理材质数组
|
||||||
|
child.material.forEach((material, index) => {
|
||||||
|
if (!materialName || material.name === materialName) {
|
||||||
|
material.visible = false;
|
||||||
|
material.depthTest = false;
|
||||||
|
material.depthWrite = false;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
console.log(`已修改${objectName}模型材质的深度测试和深度缓冲为false:`, material);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (child.material instanceof THREE.Material) {
|
||||||
|
// 处理单个材质
|
||||||
|
if (!materialName || child.material.name === materialName) {
|
||||||
|
child.material.visible = false;
|
||||||
|
child.material.depthTest = false;
|
||||||
|
child.material.depthWrite = false;
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
console.log(`已修改${objectName}模型材质的深度测试和深度缓冲为false:`, child.material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
109
packages/demo/src/views/GeothermalScene.vue
Normal file
109
packages/demo/src/views/GeothermalScene.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useBus, useDebug, useRockSample, useSection} from "@/hooks";
|
||||||
|
import {BusEvents} from "../hooks/Bus.ts";
|
||||||
|
import {Tool, Viewer, ViewerEvents} from "@deep/engine";
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {color} from 'three/tsl';
|
||||||
|
import ScenePanelContainer from "../panels/ScenePanelContainer.vue";
|
||||||
|
import ClippingPanel from "../panels/base/ClippingPanel.vue";
|
||||||
|
import {GeothermalDisasterFormationPanel} from "@/disasterFormationPanel";
|
||||||
|
import SceneTree from "@/components/SceneTree.vue";
|
||||||
|
import Toolbar from "@/components/Toolbar.vue";
|
||||||
|
import GeothermalDrillingAndCementingPanel from "@/panels/GeothermalScene/GeothermalDrillingAndCementingPanel.vue";
|
||||||
|
import DirectionalReservoir from "@/panels/GeothermalScene/DirectionalReservoir.vue";
|
||||||
|
import GeothermalInjectionProductionPanel from "@/panels/GeothermalScene/GeothermalInjectionProductionPanel.vue";
|
||||||
|
//------ Geothermal温度点云面板 开始------
|
||||||
|
import GeothermalPointCloudTemperatureDisplayPanel from "@/panels/GeothermalScene/PointCloud/PointCloudTemperatureDisplayPanel.vue";
|
||||||
|
import DustConcentrationPanel from "@/panels/GeothermalScene/DustConcentrationPanel.vue";
|
||||||
|
//------ Geothermal温度点云面板 结束------
|
||||||
|
|
||||||
|
const initCameraState = {
|
||||||
|
"position": {
|
||||||
|
"x": -6.91226017256851,
|
||||||
|
"y": 2.3541103505941274,
|
||||||
|
"z": 4.997129647842356
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"x": -0.04230009083232398,
|
||||||
|
"y": -0.15589621886333271,
|
||||||
|
"z": 0.34832661850080066
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewer = new Viewer("viewer", {
|
||||||
|
initCameraState: initCameraState
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将 viewer 实例存储到 window 和 bus 中
|
||||||
|
window.viewer = viewer;
|
||||||
|
|
||||||
|
viewer.emitter.once(ViewerEvents.INIT).then(() => {
|
||||||
|
bus.setViewer(viewer);
|
||||||
|
|
||||||
|
const box = useRockSample(bus);
|
||||||
|
viewer.rangeCullingManager.init(box)
|
||||||
|
|
||||||
|
// 背景
|
||||||
|
viewer.scene.backgroundNode = color("#aaaaaa");
|
||||||
|
|
||||||
|
// 环境光
|
||||||
|
// const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||||
|
// viewer.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
|
viewer.scene.add(directionalLight);
|
||||||
|
|
||||||
|
// 设置调试面板
|
||||||
|
useDebug(bus);
|
||||||
|
|
||||||
|
|
||||||
|
// 通知其他组件 viewer 已初始化
|
||||||
|
bus.emit(BusEvents.VIEWER_INITIALIZED, viewer);
|
||||||
|
|
||||||
|
// 加载场景特定内容
|
||||||
|
useSection(bus, {
|
||||||
|
position: {x: 0, y: -0.10656223429426581, z: -0.9157208558221872},
|
||||||
|
rotation: {x: 2.5247932959349972, y: 1.5700981950940986, z: -2.811899957888065}
|
||||||
|
});
|
||||||
|
viewer.resources.loadGLTF("/model/地热.glb").then((model) => {
|
||||||
|
viewer.scene.add(model);
|
||||||
|
// 更新场景树
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
Tool.setRenderOrder(viewer.scene, 1, [{name: "岩样", renderOrder: 2}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 地热场景
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="viewer"></div>
|
||||||
|
<Toolbar/>
|
||||||
|
<SceneTree ref="sceneTreeRef"/>
|
||||||
|
<ScenePanelContainer
|
||||||
|
:default-expanded-names="[]"
|
||||||
|
width="320px"
|
||||||
|
>
|
||||||
|
<template #panels>
|
||||||
|
<ClippingPanel/>
|
||||||
|
<GeothermalDrillingAndCementingPanel/>
|
||||||
|
<DirectionalReservoir/>
|
||||||
|
<GeothermalInjectionProductionPanel/>
|
||||||
|
<DustConcentrationPanel/>
|
||||||
|
<!-- ------ Geothermal温度点云面板 开始------ -->
|
||||||
|
<GeothermalPointCloudTemperatureDisplayPanel/>
|
||||||
|
<!-- ------ Geothermal温度点云面板 结束------ -->
|
||||||
|
<GeothermalDisasterFormationPanel/>
|
||||||
|
</template>
|
||||||
|
</ScenePanelContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
101
packages/demo/src/views/GoldMineScene.vue
Normal file
101
packages/demo/src/views/GoldMineScene.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import ScenePanelContainer from '../panels/ScenePanelContainer.vue';
|
||||||
|
import ClippingPanel from '../panels/base/ClippingPanel.vue';
|
||||||
|
import {useBus, useDebug} from "@/hooks";
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {BusEvents} from "@/hooks/Bus.ts";
|
||||||
|
import {Tool, Viewer, ViewerEvents} from "@deep/engine";
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {color} from 'three/tsl';
|
||||||
|
import DrillingPanel from '../panels/GoldMineScene/DrillingPanel.vue'
|
||||||
|
import VentilationFillingPanel from '../panels/GoldMineScene/VentilationFillingPanel.vue';
|
||||||
|
import {GoldMineDisasterFormationPanel} from "@/disasterFormationPanel";
|
||||||
|
import SceneTree from "@/components/SceneTree.vue";
|
||||||
|
import Toolbar from "@/components/Toolbar.vue";
|
||||||
|
import DevelopmentMiningPanel from "@/panels/GoldMineScene/DevelopmentMiningPanel.vue";
|
||||||
|
|
||||||
|
const initCameraState = {
|
||||||
|
"position": {
|
||||||
|
"x": -6.91226017256851,
|
||||||
|
"y": 2.3541103505941274,
|
||||||
|
"z": 4.997129647842356
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"x": -0.04230009083232398,
|
||||||
|
"y": -0.15589621886333271,
|
||||||
|
"z": 0.34832661850080066
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const drillingPanelRef = ref<InstanceType<typeof DrillingPanel> | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewer = new Viewer("viewer", {
|
||||||
|
initCameraState: initCameraState
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将 viewer 实例存储到 window 和 bus 中
|
||||||
|
window.viewer = viewer;
|
||||||
|
|
||||||
|
viewer.emitter.once(ViewerEvents.INIT).then(() => {
|
||||||
|
bus.setViewer(viewer);
|
||||||
|
|
||||||
|
// 背景
|
||||||
|
viewer.scene.backgroundNode = color("#aaaaaa");
|
||||||
|
|
||||||
|
// 环境光
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||||
|
viewer.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
|
viewer.scene.add(directionalLight);
|
||||||
|
|
||||||
|
// 设置调试面板
|
||||||
|
useDebug(bus);
|
||||||
|
|
||||||
|
bus.emit(BusEvents.VIEWER_INITIALIZED, viewer);
|
||||||
|
|
||||||
|
|
||||||
|
viewer.resources.loadGLTF("/model/金属矿.glb").then((model) => {
|
||||||
|
viewer.scene.add(model);
|
||||||
|
|
||||||
|
const box = model.getObjectByName("岩样") as THREE.Mesh
|
||||||
|
bus.setRockSample(box);
|
||||||
|
box.material.opacity = 0.6;
|
||||||
|
box.material.transparent = true
|
||||||
|
viewer.rangeCullingManager.init(box)
|
||||||
|
const section = model.getObjectByName("断面")!;
|
||||||
|
bus.setSection(section);
|
||||||
|
|
||||||
|
Tool.setRenderOrder(viewer.scene, 1, [{name: "岩样", renderOrder: 2}]);
|
||||||
|
// 更新场景树,显示新添加的模型
|
||||||
|
bus.triggerSceneTreeUpdate()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// 金属矿场景
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="viewer"></div>
|
||||||
|
<Toolbar/>
|
||||||
|
<SceneTree ref="sceneTreeRef"/>
|
||||||
|
<ScenePanelContainer
|
||||||
|
:default-expanded-names="[]"
|
||||||
|
width="320px"
|
||||||
|
>
|
||||||
|
<template #panels>
|
||||||
|
<ClippingPanel/>
|
||||||
|
<DrillingPanel ref="drillingPanelRef"/>
|
||||||
|
<DevelopmentMiningPanel/>
|
||||||
|
<VentilationFillingPanel/>
|
||||||
|
<GoldMineDisasterFormationPanel/>
|
||||||
|
</template>
|
||||||
|
</ScenePanelContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
52
packages/demo/src/views/HomePage.vue
Normal file
52
packages/demo/src/views/HomePage.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {NButton, NSpace} from 'naive-ui';
|
||||||
|
import {useRouter} from 'vue-router';
|
||||||
|
import {routes} from "@/router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleButtonClick = (path: string) => {
|
||||||
|
window.open(path, '_blank');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="home-container-wrapper">
|
||||||
|
<div class="home-container">
|
||||||
|
<n-space>
|
||||||
|
<template v-for="route in routes" :key="route.name">
|
||||||
|
<n-button
|
||||||
|
v-if="route.path !== '/'"
|
||||||
|
@click="handleButtonClick(route.path)"
|
||||||
|
>
|
||||||
|
{{ route.meta?.title || route.name }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-container-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-container {
|
||||||
|
padding: 10px;
|
||||||
|
background: #5cb1b1;
|
||||||
|
width: 500px;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
packages/demo/src/views/OilGasScene.vue
Normal file
104
packages/demo/src/views/OilGasScene.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useBus, useDebug, useRockSample, useSection} from "@/hooks";
|
||||||
|
import {BusEvents} from "../hooks/Bus.ts";
|
||||||
|
import {Tool, Viewer, ViewerEvents} from "@deep/engine";
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {color} from 'three/tsl';
|
||||||
|
import ScenePanelContainer from "../panels/ScenePanelContainer.vue";
|
||||||
|
import ClippingPanel from "../panels/base/ClippingPanel.vue";
|
||||||
|
import DrillingAndCementingPanel from "../panels/OilGasScene/DrillingAndCementingPanel.vue";
|
||||||
|
import FluidApplicationPanel from "../panels/OilGasScene/FluidApplicationPanel.vue";
|
||||||
|
import InjectionProductionPanel from "../panels/OilGasScene/InjectionProductionPanel.vue";
|
||||||
|
import FracturingPanel from "../panels/OilGasScene/FracturingPanel.vue";
|
||||||
|
import {OilGasDisasterFormationPanel} from "@/disasterFormationPanel";
|
||||||
|
import SceneTree from "@/components/SceneTree.vue";
|
||||||
|
import Toolbar from "@/components/Toolbar.vue";
|
||||||
|
|
||||||
|
const initCameraState = {
|
||||||
|
"position": {
|
||||||
|
"x": -6.91226017256851,
|
||||||
|
"y": 2.3541103505941274,
|
||||||
|
"z": 4.997129647842356
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"x": -0.04230009083232398,
|
||||||
|
"y": -0.15589621886333271,
|
||||||
|
"z": 0.34832661850080066
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewer = new Viewer("viewer", {
|
||||||
|
initCameraState: initCameraState
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将 viewer 实例存储到 window 和 bus 中
|
||||||
|
window.viewer = viewer;
|
||||||
|
|
||||||
|
viewer.emitter.once(ViewerEvents.INIT).then(() => {
|
||||||
|
bus.setViewer(viewer);
|
||||||
|
|
||||||
|
const box = useRockSample(bus);
|
||||||
|
viewer.rangeCullingManager.init(box)
|
||||||
|
|
||||||
|
// 背景
|
||||||
|
viewer.scene.backgroundNode = color("#aaaaaa");
|
||||||
|
|
||||||
|
// 环境光
|
||||||
|
// const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||||
|
// viewer.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
|
viewer.scene.add(directionalLight);
|
||||||
|
|
||||||
|
// 设置调试面板
|
||||||
|
useDebug(bus);
|
||||||
|
|
||||||
|
|
||||||
|
// 通知其他组件 viewer 已初始化
|
||||||
|
bus.emit(BusEvents.VIEWER_INITIALIZED, viewer);
|
||||||
|
|
||||||
|
// 加载场景特定内容
|
||||||
|
useSection(bus, {
|
||||||
|
position: {x: 0, y: -0.10656223429426581, z: -0.9157208558221872},
|
||||||
|
rotation: {x: 2.5247932959349972, y: 1.5700981950940986, z: -2.811899957888065}
|
||||||
|
});
|
||||||
|
viewer.resources.loadGLTF("/model/深埋油气.glb").then((model) => {
|
||||||
|
viewer.scene.add(model);
|
||||||
|
// 更新场景树
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
Tool.setRenderOrder(viewer.scene, 1, [{name: "岩样", renderOrder: 2}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 油气场景
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="viewer"></div>
|
||||||
|
<Toolbar/>
|
||||||
|
<SceneTree ref="sceneTreeRef"/>
|
||||||
|
<ScenePanelContainer
|
||||||
|
:default-expanded-names="[]"
|
||||||
|
width="320px"
|
||||||
|
>
|
||||||
|
<template #panels>
|
||||||
|
<ClippingPanel/>
|
||||||
|
|
||||||
|
<DrillingAndCementingPanel/>
|
||||||
|
<FluidApplicationPanel/>
|
||||||
|
<InjectionProductionPanel/>
|
||||||
|
<FracturingPanel/>
|
||||||
|
<OilGasDisasterFormationPanel/>
|
||||||
|
</template>
|
||||||
|
</ScenePanelContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
430
packages/demo/src/views/TunnelScene.vue
Normal file
430
packages/demo/src/views/TunnelScene.vue
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useBus, useDebug, useRockSample, useSection} from "@/hooks";
|
||||||
|
import {
|
||||||
|
createRock2Material,
|
||||||
|
CSGOperationType,
|
||||||
|
CssType,
|
||||||
|
EventManagerEvents,
|
||||||
|
ParametricArch,
|
||||||
|
Tool,
|
||||||
|
Viewer,
|
||||||
|
ViewerEvents
|
||||||
|
} from "@deep/engine";
|
||||||
|
import {RockSampleHtmlPanel, SensorHtmlPanel, TunnelHtmlPanel} from '@/htmlPanel';
|
||||||
|
import * as THREE from 'three/webgpu';
|
||||||
|
import {color} from 'three/tsl';
|
||||||
|
import ScenePanelContainer from "../panels/ScenePanelContainer.vue";
|
||||||
|
import ClippingPanel from "../panels/base/ClippingPanel.vue";
|
||||||
|
import RobotExcavationPanel from "@/panels/TunnelScene/RobotExcavationPanel.vue";
|
||||||
|
import {TunnelDisasterFormationPanel} from "@/disasterFormationPanel";
|
||||||
|
import SceneTree from "@/components/SceneTree.vue";
|
||||||
|
import Toolbar from "@/components/Toolbar.vue";
|
||||||
|
import LoadSurface from "@/panels/TunnelScene/LoadSurface.vue";
|
||||||
|
import FieldDataPanel from "@/panels/TunnelScene/FieldData/FieldDataPanel.vue";
|
||||||
|
import DisturbanceLoadingPanel from "@/panels/TunnelScene/DisturbanceLoadingPanel.vue";
|
||||||
|
import PointCloudFieldDataPanel from "@/panels/TunnelScene/PointCloud/PointCloudFieldDataPanel.vue";
|
||||||
|
|
||||||
|
const initCameraState = {
|
||||||
|
"position": {
|
||||||
|
"x": -6.91226017256851,
|
||||||
|
"y": 2.3541103505941274,
|
||||||
|
"z": 4.997129647842356
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"x": -0.04230009083232398,
|
||||||
|
"y": -0.15589621886333271,
|
||||||
|
"z": 0.34832661850080066
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelNameMappings = [
|
||||||
|
{name: 'Object119', displayName: '检测传感器1'},
|
||||||
|
{name: 'Object120', displayName: '检测传感器2'},
|
||||||
|
{name: 'Object121', displayName: '检测传感器3'}
|
||||||
|
]
|
||||||
|
|
||||||
|
const bus = useBus();
|
||||||
|
let rockSamplePanel: RockSampleHtmlPanel | null = null;
|
||||||
|
let sensorPanel1: SensorHtmlPanel | null = null;
|
||||||
|
let sensorPanel2: SensorHtmlPanel | null = null;
|
||||||
|
let sensorPanel3: SensorHtmlPanel | null = null;
|
||||||
|
let tunnelPanel: TunnelHtmlPanel | null = null;
|
||||||
|
|
||||||
|
|
||||||
|
let arch: ParametricArch | ParametricArch | null = null;
|
||||||
|
|
||||||
|
// 路径点定义
|
||||||
|
const straightPathPoint3 = [
|
||||||
|
{x: 0, y: 0, z: 0},
|
||||||
|
{x: 0, y: 0, z: -4.4}
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewer = new Viewer("viewer", {
|
||||||
|
initCameraState: initCameraState,
|
||||||
|
modelNameMappings: modelNameMappings
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将 viewer 实例存储到 window 和 bus 中
|
||||||
|
window.viewer = viewer;
|
||||||
|
|
||||||
|
viewer.emitter.once(ViewerEvents.INIT).then(() => {
|
||||||
|
bus.setViewer(viewer);
|
||||||
|
|
||||||
|
// 初始化场景
|
||||||
|
const box = useRockSample(bus);
|
||||||
|
viewer.rangeCullingManager.init(box)
|
||||||
|
// 背景
|
||||||
|
viewer.scene.backgroundNode = color("#aaaaaa");
|
||||||
|
|
||||||
|
|
||||||
|
// 环境光
|
||||||
|
// const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||||
|
// viewer.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
|
viewer.scene.add(directionalLight);
|
||||||
|
|
||||||
|
// 设置调试面板
|
||||||
|
useDebug(bus);
|
||||||
|
|
||||||
|
// 加载场景特定内容
|
||||||
|
useSection(bus, {
|
||||||
|
position: {x: 0, y: -0.10656223429426581, z: -0.9157208558221872},
|
||||||
|
rotation: {x: 2.5247932959349972, y: 1.5700981950940986, z: -2.811899957888065}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewer.resources.loadGLTF("/model/隧道_归为到中心5x5_移除外壳1.glb").then((model) => {
|
||||||
|
viewer.scene.add(model);
|
||||||
|
|
||||||
|
// 更新场景树
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
|
||||||
|
Tool.setRenderOrder(viewer.scene, 1, [{name: "岩样", renderOrder: 2}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
initArch();
|
||||||
|
setupRockSampleClickListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const initArch = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
const box = bus.getRockSample();
|
||||||
|
const section = bus.getSection()
|
||||||
|
// 城门洞几何参数
|
||||||
|
const geometrySettings = {
|
||||||
|
material: createRock2Material({}, false),
|
||||||
|
"width": 0.8,
|
||||||
|
"height": 0.4,
|
||||||
|
"depth": -4.4,
|
||||||
|
"thickness": 0.06,
|
||||||
|
"curveSegments": 24,
|
||||||
|
"steps": 8,
|
||||||
|
"hasHole": false,
|
||||||
|
"bottomEnabled": false, // 不生成底部面
|
||||||
|
"topEnabled": false, // 不生成顶部面
|
||||||
|
// extrudePathPoints: straightPathPoints,
|
||||||
|
// extrudePathPoints: straightPathPoints2,
|
||||||
|
extrudePathPoints: straightPathPoint3
|
||||||
|
};
|
||||||
|
arch = new ParametricArch(viewer, geometrySettings);
|
||||||
|
|
||||||
|
Tool.defineMaterialOpacityMapping(arch);
|
||||||
|
|
||||||
|
|
||||||
|
arch.position.set(
|
||||||
|
-0.13032569816002015,
|
||||||
|
-0.036827218370504344,
|
||||||
|
2.5
|
||||||
|
);
|
||||||
|
// arch.visible = false
|
||||||
|
arch.name = "参数化城门洞"
|
||||||
|
arch.addCollisionTarget(box, CSGOperationType.HOLLOW_SUBTRACTION);
|
||||||
|
arch.addCollisionTarget(section, CSGOperationType.HOLLOW_SUBTRACTION);
|
||||||
|
arch.subtractMesh()
|
||||||
|
viewer.scene.add(arch);
|
||||||
|
bus.triggerSceneTreeUpdate();
|
||||||
|
// 设置 userData 用于弹窗显示
|
||||||
|
arch.userData = {
|
||||||
|
tunnelType: '城门洞隧道',
|
||||||
|
depth: Math.abs(geometrySettings.depth),
|
||||||
|
radius: geometrySettings.width / 2
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示岩样弹窗
|
||||||
|
*/
|
||||||
|
const showRockSamplePanel = (clickPoint: THREE.Vector3) => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
// 如果已有弹窗,直接返回,不重复创建或移动
|
||||||
|
if (rockSamplePanel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建岩样数据(示例数据)
|
||||||
|
const sampleData = {
|
||||||
|
materialName: '铁矿石(Fe2O3)',
|
||||||
|
meshSize: '200目',
|
||||||
|
ratio: '60%',
|
||||||
|
uniaxialCompressiveStrength: 150,
|
||||||
|
tensileStrength: 20,
|
||||||
|
elasticModulus: 150,
|
||||||
|
brittlenessIndex: 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建岩样弹窗
|
||||||
|
rockSamplePanel = new RockSampleHtmlPanel(CssType.CSS2D, sampleData);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
rockSamplePanel.onDelete(() => {
|
||||||
|
if (rockSamplePanel) {
|
||||||
|
rockSamplePanel.hide();
|
||||||
|
const panelObject = rockSamplePanel.getCssObject();
|
||||||
|
if (panelObject.parent) {
|
||||||
|
panelObject.parent.remove(panelObject);
|
||||||
|
}
|
||||||
|
rockSamplePanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置弹窗位置
|
||||||
|
const panelObject = rockSamplePanel.getCssObject();
|
||||||
|
panelObject.position.copy(clickPoint);
|
||||||
|
panelObject.position.y += 0.5; // 向上偏移
|
||||||
|
|
||||||
|
// 添加到场景
|
||||||
|
viewer.scene.add(panelObject);
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
rockSamplePanel.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示城门洞隧道弹窗
|
||||||
|
*/
|
||||||
|
const showTunnelPanel = (clickedObject: THREE.Object3D, clickPoint: THREE.Vector3) => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
// 如果已有弹窗,直接返回,不重复创建或移动
|
||||||
|
if (tunnelPanel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 mesh 的 userData 获取隧道数据
|
||||||
|
const archOptions = clickedObject.userData.archOptions || {};
|
||||||
|
const tunnelData = {
|
||||||
|
type: archOptions.tunnelType || '城门洞隧道',
|
||||||
|
position: {
|
||||||
|
x: parseFloat(clickPoint.x.toFixed(2)),
|
||||||
|
y: parseFloat(clickPoint.y.toFixed(2)),
|
||||||
|
z: parseFloat(clickPoint.z.toFixed(2))
|
||||||
|
},
|
||||||
|
depth: archOptions.depth || 4.4,
|
||||||
|
radius: archOptions.radius || 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建隧道弹窗
|
||||||
|
tunnelPanel = new TunnelHtmlPanel(tunnelData, CssType.CSS2D);
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
tunnelPanel.onDelete(() => {
|
||||||
|
if (tunnelPanel) {
|
||||||
|
tunnelPanel.hide();
|
||||||
|
const panelObject = tunnelPanel.getCssObject();
|
||||||
|
if (panelObject.parent) {
|
||||||
|
panelObject.parent.remove(panelObject);
|
||||||
|
}
|
||||||
|
tunnelPanel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置弹窗位置
|
||||||
|
const panelObject = tunnelPanel.getCssObject();
|
||||||
|
panelObject.position.copy(clickPoint);
|
||||||
|
panelObject.position.y += 0.5; // 向上偏移
|
||||||
|
|
||||||
|
// 添加到场景
|
||||||
|
viewer.scene.add(panelObject);
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
tunnelPanel.show();
|
||||||
|
|
||||||
|
console.log('点击了城门洞隧道,位置:', clickPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示传感器弹窗
|
||||||
|
*/
|
||||||
|
const showSensorPanel = (sensorName: string, clickPoint: THREE.Vector3) => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
|
||||||
|
// 根据传感器名称确定使用哪个面板变量
|
||||||
|
const channel = parseInt(sensorName.replace('检测传感器', ''));
|
||||||
|
let currentPanel: SensorHtmlPanel | null = null;
|
||||||
|
|
||||||
|
if (channel === 1) {
|
||||||
|
currentPanel = sensorPanel1;
|
||||||
|
} else if (channel === 2) {
|
||||||
|
currentPanel = sensorPanel2;
|
||||||
|
} else if (channel === 3) {
|
||||||
|
currentPanel = sensorPanel3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果该传感器已有弹窗,直接返回,不重复创建或移动
|
||||||
|
if (currentPanel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建传感器数据(示例数据)
|
||||||
|
const sensorData = {
|
||||||
|
type: sensorName,
|
||||||
|
channel: channel,
|
||||||
|
position: {
|
||||||
|
x: clickPoint.x,
|
||||||
|
y: clickPoint.y,
|
||||||
|
z: clickPoint.z
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建传感器弹窗
|
||||||
|
const newPanel = new SensorHtmlPanel(sensorData, CssType.CSS2D);
|
||||||
|
|
||||||
|
// 根据通道号保存到对应的变量
|
||||||
|
if (channel === 1) {
|
||||||
|
sensorPanel1 = newPanel;
|
||||||
|
} else if (channel === 2) {
|
||||||
|
sensorPanel2 = newPanel;
|
||||||
|
} else if (channel === 3) {
|
||||||
|
sensorPanel3 = newPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置删除回调
|
||||||
|
newPanel.onDelete(() => {
|
||||||
|
newPanel.hide();
|
||||||
|
const panelObject = newPanel.getCssObject();
|
||||||
|
if (panelObject.parent) {
|
||||||
|
panelObject.parent.remove(panelObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空对应的面板变量
|
||||||
|
if (channel === 1) {
|
||||||
|
sensorPanel1 = null;
|
||||||
|
} else if (channel === 2) {
|
||||||
|
sensorPanel2 = null;
|
||||||
|
} else if (channel === 3) {
|
||||||
|
sensorPanel3 = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置弹窗位置
|
||||||
|
const panelObject = newPanel.getCssObject();
|
||||||
|
panelObject.position.copy(clickPoint);
|
||||||
|
panelObject.position.y += 0.5; // 向上偏移
|
||||||
|
|
||||||
|
// 添加到场景
|
||||||
|
viewer.scene.add(panelObject);
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
newPanel.show();
|
||||||
|
|
||||||
|
console.log(`点击了${sensorName},位置:`, clickPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查对象是否匹配指定名称(支持原始名称和显示名称)
|
||||||
|
*/
|
||||||
|
const isObjectMatch = (obj: THREE.Object3D, targetName: string): boolean => {
|
||||||
|
// 检查原始名称
|
||||||
|
if (obj.name && obj.name.includes(targetName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查显示名称(如果有映射)
|
||||||
|
if (obj.userData && obj.userData.hasMappedName && obj.userData.displayName) {
|
||||||
|
return obj.userData.displayName.includes(targetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置岩样点击事件监听
|
||||||
|
*/
|
||||||
|
const setupRockSampleClickListener = () => {
|
||||||
|
const viewer = bus.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 使用 viewer 的事件系统监听射线拾取事件
|
||||||
|
viewer.events.on(EventManagerEvents.RAYCAST_PICK, ({data}) => {
|
||||||
|
const {intersects} = data;
|
||||||
|
|
||||||
|
if (!intersects || intersects.length === 0) return;
|
||||||
|
|
||||||
|
// 只取第一个相交对象
|
||||||
|
const intersection = intersects[0];
|
||||||
|
const clickedObject = intersection.object;
|
||||||
|
const clickPoint = intersection.point;
|
||||||
|
|
||||||
|
// 检查是否点击了岩样
|
||||||
|
if (isObjectMatch(clickedObject, '岩样')) {
|
||||||
|
console.log('点击了岩样:', clickedObject.name);
|
||||||
|
showRockSamplePanel(clickPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否点击了检测传感器
|
||||||
|
if (isObjectMatch(clickedObject, '检测传感器1')) {
|
||||||
|
showSensorPanel('检测传感器1', clickPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObjectMatch(clickedObject, '检测传感器2')) {
|
||||||
|
showSensorPanel('检测传感器2', clickPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObjectMatch(clickedObject, '检测传感器3')) {
|
||||||
|
showSensorPanel('检测传感器3', clickPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否点击了城门洞隧道
|
||||||
|
if (isObjectMatch(clickedObject, '参数化城门洞')) {
|
||||||
|
showTunnelPanel(clickedObject, clickPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隧道场景
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="viewer"></div>
|
||||||
|
<Toolbar/>
|
||||||
|
<SceneTree ref="sceneTreeRef"/>
|
||||||
|
<ScenePanelContainer
|
||||||
|
:default-expanded-names="[]"
|
||||||
|
width="320px"
|
||||||
|
>
|
||||||
|
<template #panels>
|
||||||
|
<ClippingPanel/>
|
||||||
|
<RobotExcavationPanel/>
|
||||||
|
<LoadSurface/>
|
||||||
|
<FieldDataPanel/>
|
||||||
|
<PointCloudFieldDataPanel/>
|
||||||
|
<DisturbanceLoadingPanel/>
|
||||||
|
<TunnelDisasterFormationPanel/>
|
||||||
|
<!-- <StressApplicationPanel />-->
|
||||||
|
<!-- <TemperatureApplicationPanel />-->
|
||||||
|
</template>
|
||||||
|
</ScenePanelContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
26
packages/demo/src/vite-env.d.ts
vendored
Normal file
26
packages/demo/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
import type {Bus} from "./hooks";
|
||||||
|
import type {Viewer} from "@deep/engine";
|
||||||
|
|
||||||
|
// 扩展window对象,添加viewer属性
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
viewer: Viewer | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
bus: Bus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
19
packages/demo/tsconfig.app.json
Normal file
19
packages/demo/tsconfig.app.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
},
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user