feat(all): init commit

This commit is contained in:
plum 2026-04-19 18:46:28 +08:00
commit 5d7f479765
479 changed files with 178500 additions and 0 deletions

View 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
View 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/

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
# 解决pnpm add时优先在本地查找依赖
link-workspace-packages=true

11
README.md Normal file
View 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
View 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
View 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
View File

13
packages/demo/index.html Normal file
View 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>

View 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
View 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>

View 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

View 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>

View 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>

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -0,0 +1,6 @@
export * from './PrintModelModal';
export * from './SensorModal';
export * from './TunnelModal';
export * from './DrillingModal';
export * from './SingleSensorModal';
export * from './RuptureEventModal';

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,2 @@
export {default as CasingDamageDisasterPanel} from "./CasingDamageDisasterPanel.vue"
export {default as WellboreInstabilityDisasterPanel} from "./WellboreInstabilityDisasterPanel.vue"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View 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"

View 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;
}
}

View File

@ -0,0 +1,5 @@
export * from './Bus';
export * from './useBus';
export * from './useDebug';
export * from './useRockSample';
export * from './useSection';

View 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;
}

View 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);
});
}

View 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;
};

View 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;
};

View 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'};
}
}
}

View 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'};
}
}
}

View 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'};
}
}
}

View 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'};
}
}
}

View 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'};
}
}
}

View 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'};
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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);
}
}

View 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
View 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'))

View 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>

View File

@ -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>

View File

@ -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: [
// 10step0.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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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),
];
// 3x0.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) => {
// index0
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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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 22
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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

View 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;
}

View 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);
}
}
}
});
}
}

View 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>

View 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>

View 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>

View 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>

View 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
View 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 {}

View 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