From 27dcba2dff946e14469225b4f23e7f563f816eb9 Mon Sep 17 00:00:00 2001 From: luoyifan Date: Mon, 9 Jun 2025 16:24:24 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=A0=87=E7=AD=BE=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=20LabelManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/engine/Viewport.ts | 6 +- src/core/manager/InstancePointManager.ts | 30 +++++ src/core/manager/LabelManager.ts | 207 +++++++++++++++++++++++++++++++ src/core/manager/LineSegmentManager.ts | 16 +++ src/core/manager/ResourceManager.ts | 120 ++++++++++++++++++ src/modules/measure/MeasureRenderer.ts | 141 ++++++++------------- 6 files changed, 429 insertions(+), 91 deletions(-) create mode 100644 src/core/manager/InstancePointManager.ts create mode 100644 src/core/manager/LabelManager.ts create mode 100644 src/core/manager/LineSegmentManager.ts create mode 100644 src/core/manager/ResourceManager.ts diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts index c6bd7e4..4448b35 100644 --- a/src/core/engine/Viewport.ts +++ b/src/core/engine/Viewport.ts @@ -22,6 +22,8 @@ import EventBus from '@/runtime/EventBus.ts' import Constract from '@/core/Constract.ts' import DragControl from '@/core/controls/DragControl.ts' import type { PropertySetter } from '@/core/base/PropertyTypes.ts' +import LabelManager from '@/core/manager/LabelManager.ts' +import ResourceManager from '@/core/manager/ResourceManager.ts' /** * 视窗对象 @@ -40,11 +42,13 @@ export default class Viewport { selectInspect = new SelectInspect() mouseMoveInspect = new MouseMoveInspect() dragControl = new DragControl() + labelManager = new LabelManager() tools: IControls[] = [ markRaw(this.selectInspect), markRaw(this.mouseMoveInspect), - markRaw(this.dragControl) + markRaw(this.dragControl), + markRaw(this.labelManager) ] // 状态管理器 diff --git a/src/core/manager/InstancePointManager.ts b/src/core/manager/InstancePointManager.ts new file mode 100644 index 0000000..1a7e458 --- /dev/null +++ b/src/core/manager/InstancePointManager.ts @@ -0,0 +1,30 @@ +import * as THREE from 'three' +import type IControls from '@/core/controls/IControls.ts' +import type Viewport from '@/core/engine/Viewport.ts' + +export default class InstancePointManager implements IControls { + viewport: Viewport + mesh: THREE.InstancedMesh + dummy: THREE.Object3D = new THREE.Object3D() + count: number = 0 + maxInstances: number = 100000 + + init(viewport: Viewport): void { + this.viewport = viewport + + const geometry = new THREE.PlaneGeometry(0.25, 0.25) + const material = new THREE.MeshBasicMaterial({ + color: 0xFFFF99, + transparent: true, + depthWrite: false, + side: THREE.DoubleSide + }) + + this.mesh = new THREE.InstancedMesh(geometry, material, this.maxInstances) + this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) + viewport.scene.add(this.mesh) + } + + dispose() { + } +} diff --git a/src/core/manager/LabelManager.ts b/src/core/manager/LabelManager.ts new file mode 100644 index 0000000..570bbbc --- /dev/null +++ b/src/core/manager/LabelManager.ts @@ -0,0 +1,207 @@ +import * as THREE from 'three' +import type IControls from '@/core/controls/IControls.ts' +import type Viewport from '@/core/engine/Viewport.ts' +import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' +import { Text } from 'troika-three-text' +import SimSunTTF from '@/assets/fonts/simsunb.ttf' + +export interface LabelOption { + /** + * 标签组件名称 + */ + name?: string + /** + * 是否使用 HTML 标签 + */ + useHtmlLabel: boolean + /** + * ex: css='14px', text=0.4 + */ + fontSize: number | string + /** + * ex: '#ffffff' + */ + color: string + /** + * ex: css='5px 8px', text=0.2 + */ + padding?: number | string + text?: string +} + +/** + * 标签管理器 + */ +export default class LabelManager implements IControls { + viewport: Viewport + private labelMap: Map = new Map() + + init(viewport: Viewport): void { + this.viewport = viewport + } + + createOrUpdateLabelByDistance(parentObj: THREE.Object3D, startPos: THREE.Vector3, endPos: THREE.Vector3, option: LabelOption): Text | CSS2DObject { + let labelObj = this.labelMap.get(parentObj.userData.labelObjectId) + if (!labelObj) { + labelObj = this.createLabel(parentObj, option) + } + + const position = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5) + labelObj.position.set(position.x, position.y + 0.3, position.z - 0.2) + + // 计算距离 + const distance = startPos.distanceTo(endPos) + const text = distance.toFixed(2) + ' m' + this.updateLabel(parentObj, text) + + return labelObj + } + + createLabel(parentObj: THREE.Object3D, option: LabelOption): Text | CSS2DObject { + const labelObj = this.createLabelObject(option) + parentObj.userData.labelObjectId = labelObj.uuid + + this.viewport.scene.add(labelObj) + + if (labelObj instanceof CSS2DObject) { + labelObj.element.innerHTML = option.text + + } else if (labelObj instanceof Text) { + // 让文本朝向摄像机 + labelObj.quaternion.copy(this.viewport.camera.quaternion) + labelObj.text = option.text + labelObj.sync() + } + + return labelObj + } + + + updateLabel(parentObj: THREE.Object3D, text: string) { + const labelObj = this.labelMap.get(parentObj.userData.labelObjectId) + if (labelObj) { + if (labelObj instanceof CSS2DObject) { + labelObj.element.innerHTML = text + + } else if (labelObj instanceof Text) { + labelObj.text = text + labelObj.sync() + } + } else { + console.warn('Label not found for parent object:', parentObj) + } + } + + removeLabel(parentObj: THREE.Object3D) { + if (parentObj?.userData?.labelObjectId) { + const labelObj = this.labelMap.get(parentObj.userData.labelObjectId) + this.labelMap.delete(labelObj.uuid) + this.viewport.scene.remove(labelObj) + labelObj.dispose() + parentObj.userData.labelObjectId = undefined + } + } + + removeById(id: string): void { + const labelObj = this.labelMap.get(id) + if (labelObj) { + this.viewport.scene.remove(labelObj) + this.labelMap.delete(id) + labelObj.dispose() + } + } + + private createLabelObject(option: LabelOption): Text | CSS2DObject { + if (option.useHtmlLabel) { + const div = document.createElement('div') + div.className = 'css2dObjectLabel' + div.innerHTML = option.text + div.style.padding = (option.padding as string) + div.style.color = option.color + div.style.fontSize = (option.fontSize as string) + div.style.position = 'absolute' + div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)' + div.style.borderRadius = '12px' + div.style.top = '0px' + div.style.left = '0px' + // div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件 + const obj = new CSS2DObject(div) + obj.uuid = system.createUUID() + obj.name = option.name + this.labelMap.set(obj.uuid, obj) + return obj + + } else { + const label = new Text() + label.uuid = system.createUUID() + label.text = option.text + label.font = SimSunTTF + label.fontSize = (option.fontSize as number) + label.color = option.color + label.opacity = 0.8 + label.padding = (option.padding as number) + label.anchorX = 'center' + label.anchorY = 'middle' + label.depthOffset = 1 + label.backgroundColor = '#000000' // 黑色背景 + label.backgroundOpacity = 0.6 // 背景半透明 + label.padding = 0.2 // 内边距 + label.material.depthTest = false + label.name = option.name + + label.sync() + this.labelMap.set(label.uuid, label) + return label + } + } + + dispose(): void { + // 清理资源 + this.viewport = undefined + } + + + // /** + // * 手动创建标签示例 + // */ + // createLabel(text: string): Text | CSS2DObject { + // if (this.useHtmlLabel) { + // const div = document.createElement('div') + // div.className = 'css2dObjectLabel' + // div.innerHTML = text + // div.style.padding = '5px 8px' + // div.style.color = '#fff' + // div.style.fontSize = '14px' + // div.style.position = 'absolute' + // div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)' + // div.style.borderRadius = '12px' + // div.style.top = '0px' + // div.style.left = '0px' + // // div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件 + // const obj = new CSS2DObject(div) + // obj.name = MeasureRenderer.LABEL_NAME + // return obj + // + // } else { + // const label = new Text() + // label.text = text + // label.font = SimSunTTF + // label.fontSize = 0.4 + // label.color = '#333333' + // label.opacity = 0.8 + // label.padding = 0.2 + // label.anchorX = 'center' + // label.anchorY = 'middle' + // label.depthOffset = 1 + // label.backgroundColor = '#000000' // 黑色背景 + // label.backgroundOpacity = 0.6 // 背景半透明 + // label.padding = 0.2 // 内边距 + // label.material.depthTest = false + // label.name = MeasureRenderer.LABEL_NAME + // + // label.sync() + // return label + // } + // } +} + diff --git a/src/core/manager/LineSegmentManager.ts b/src/core/manager/LineSegmentManager.ts new file mode 100644 index 0000000..e1659d8 --- /dev/null +++ b/src/core/manager/LineSegmentManager.ts @@ -0,0 +1,16 @@ +import * as THREE from 'three' +import type IControls from '@/core/controls/IControls.ts' +import type Viewport from '@/core/engine/Viewport.ts' + +export default class LineSegmentManager implements IControls { + viewport: Viewport + + init(viewport: Viewport): void { + this.viewport = viewport + } + + dispose(): void { + // 清理资源 + this.viewport = undefined + } +} diff --git a/src/core/manager/ResourceManager.ts b/src/core/manager/ResourceManager.ts new file mode 100644 index 0000000..ac4554d --- /dev/null +++ b/src/core/manager/ResourceManager.ts @@ -0,0 +1,120 @@ +import * as THREE from 'three' +import type Viewport from '@/core/engine/Viewport.ts' + +/** + * 资源管理器 + * 管理 Three.js 中的几何体、材质、纹理和网格等资源 + * 提供创建、获取和销毁资源的方法 + */ +export default class ResourceManager { + public static readonly instance: ResourceManager = new ResourceManager() + + viewport: Viewport + geometries: Map = new Map() + materials: Map = new Map() + textures: Map = new Map() + meshes: Map = new Map() + + createGeometry(key: string, geometry: THREE.BufferGeometry) { + this.geometries.set(key, geometry) + return geometry + } + + createMaterial(key: string, material: THREE.Material) { + this.materials.set(key, material) + return material + } + + createTexture(key: string, texture: THREE.Texture) { + this.textures.set(key, texture) + return texture + } + + createMesh(key: string, mesh: THREE.Mesh) { + this.meshes.set(key, mesh) + return mesh + } + + getGeometry(key: string) { + return this.geometries.get(key) + } + + getMaterial(key: string) { + return this.materials.get(key) + } + + getTexture(key: string) { + return this.textures.get(key) + } + + getMesh(key: string) { + return this.meshes.get(key) + } + + disposeGeometry(key: string) { + const geometry = this.geometries.get(key) + if (geometry) { + geometry.dispose() + this.geometries.delete(key) + } + } + + disposeMaterial(key: string) { + const material = this.materials.get(key) + if (material) { + material.dispose() + this.materials.delete(key) + } + } + + disposeTexture(key: string) { + const texture = this.textures.get(key) + if (texture) { + texture.dispose() + this.textures.delete(key) + } + } + + disposeMesh(key: string) { + const mesh = this.meshes.get(key) + if (mesh) { + if (mesh.geometry) { + mesh.geometry.dispose() + } + if (mesh.material) { + if (Array.isArray(mesh.material)) { + mesh.material.forEach(mat => mat.dispose()) + } else { + mesh.material.dispose() + } + } + this.meshes.delete(key) + } + } + + dispose(): void { + // 清理资源 + this.geometries.forEach(geometry => geometry.dispose()) + this.materials.forEach(material => material.dispose()) + this.textures.forEach(texture => texture.dispose()) + this.meshes.forEach(mesh => { + if (mesh.geometry) { + mesh.geometry.dispose() + } + if (mesh.material) { + if (Array.isArray(mesh.material)) { + mesh.material.forEach(mat => mat.dispose()) + } else { + mesh.material.dispose() + } + } + }) + + this.geometries.clear() + this.materials.clear() + this.textures.clear() + this.meshes.clear() + + this.viewport = undefined + } +} diff --git a/src/modules/measure/MeasureRenderer.ts b/src/modules/measure/MeasureRenderer.ts index 0bfd928..acdfc7a 100644 --- a/src/modules/measure/MeasureRenderer.ts +++ b/src/modules/measure/MeasureRenderer.ts @@ -4,10 +4,6 @@ import { getLineId } from '@/core/ModelUtils.ts' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' import { Line2 } from 'three/examples/jsm/lines/Line2.js' -import { numberToString } from '@/utils/webutils.ts' -import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' -import { Text } from 'troika-three-text' -import SimSunTTF from '@/assets/fonts/simsunb.ttf' import Constract from '@/core/Constract.ts' /** @@ -114,92 +110,57 @@ export default class MeasureRenderer extends BaseRenderer { // // this.group.add(...objects) // } - // - // afterCreateOrUpdateLine(start: ItemJson, end: ItemJson, type: LinkType, option: RendererCudOption, object: THREE.Object3D) { - // super.afterCreateOrUpdateLine(start, end, type, option, object) - // - // const startPoint = this.tempViewport?.entityManager.findObjectById(start.id) - // const endPoint = this.tempViewport?.entityManager.findObjectById(end.id) - // - // const p0 = startPoint.position - // const p1 = endPoint.position - // - // const dist = p0.distanceTo(p1) - // const label = numberToString(dist) + ' m' - // - // const position = new THREE.Vector3().addVectors(p0, p1).multiplyScalar(0.5) - // let labelObj: Text | CSS2DObject | undefined = object.userData.labelObj - // if (!labelObj || !labelObj.parent) { - // labelObj = this.createLabel(label) - // this.group.add(labelObj) - // object.userData.labelObj = labelObj - // } - // - // labelObj.position.set(position.x, position.y + 0.3, position.z - 0.2) - // - // if (this.useHtmlLabel) { - // labelObj.element.innerHTML = label - // - // } else { - // // 让文本朝向摄像机 - // labelObj.quaternion.copy(this.tempViewport.camera.quaternion) - // labelObj.text = label - // labelObj.sync() - // } - // } - // - // afterDeleteLine(start: ItemJson, end: ItemJson, type: LinkType, option: RendererCudOption, object: THREE.Object3D) { - // super.afterDeleteLine(start, end, type, option, object) - // - // // 删除标签 - // const labelObj = object.userData?.labelObj - // if (labelObj && labelObj.parent) { - // labelObj.parent.remove(labelObj) - // } - // } - // - // /** - // * 创建标签 - // */ - // createLabel(text: string): Text | CSS2DObject { - // if (this.useHtmlLabel) { - // const div = document.createElement('div') - // div.className = 'css2dObjectLabel' - // div.innerHTML = text - // div.style.padding = '5px 8px' - // div.style.color = '#fff' - // div.style.fontSize = '14px' - // div.style.position = 'absolute' - // div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)' - // div.style.borderRadius = '12px' - // div.style.top = '0px' - // div.style.left = '0px' - // // div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件 - // const obj = new CSS2DObject(div) - // obj.name = MeasureRenderer.LABEL_NAME - // return obj - // - // } else { - // const label = new Text() - // label.text = text - // label.font = SimSunTTF - // label.fontSize = 0.4 - // label.color = '#333333' - // label.opacity = 0.8 - // label.padding = 0.2 - // label.anchorX = 'center' - // label.anchorY = 'middle' - // label.depthOffset = 1 - // label.backgroundColor = '#000000' // 黑色背景 - // label.backgroundOpacity = 0.6 // 背景半透明 - // label.padding = 0.2 // 内边距 - // label.material.depthTest = false - // label.name = MeasureRenderer.LABEL_NAME - // - // label.sync() - // return label - // } - // } + + afterCreateOrUpdateLine(start: ItemJson, end: ItemJson, type: LinkType, option: RendererCudOption, object: THREE.Object3D) { + super.afterCreateOrUpdateLine(start, end, type, option, object) + + const startPoint = this.tempViewport?.entityManager.findObjectById(start.id) + const endPoint = this.tempViewport?.entityManager.findObjectById(end.id) + + this.tempViewport.labelManager.createOrUpdateLabelByDistance(object, startPoint.position, endPoint.position, { + useHtmlLabel: this.useHtmlLabel, + fontSize: 0.4, + color: '#333333' + }) + + // const p0 = startPoint.position + // const p1 = endPoint.position + // + // const dist = p0.distanceTo(p1) + // const label = numberToString(dist) + ' m' + // + // const position = new THREE.Vector3().addVectors(p0, p1).multiplyScalar(0.5) + // let labelObj: Text | CSS2DObject | undefined = object.userData.labelObj + // if (!labelObj || !labelObj.parent) { + // labelObj = this.createLabel(label) + // this.appendToScene(labelObj) + // object.userData.labelObj = labelObj + // } + // + // labelObj.position.set(position.x, position.y + 0.3, position.z - 0.2) + // + // if (this.useHtmlLabel) { + // labelObj.element.innerHTML = label + // + // } else { + // // 让文本朝向摄像机 + // labelObj.quaternion.copy(this.tempViewport.camera.quaternion) + // labelObj.text = label + // labelObj.sync() + // } + } + + afterDeleteLine(start: ItemJson, end: ItemJson, type: LinkType, option: RendererCudOption, object: THREE.Object3D) { + super.afterDeleteLine(start, end, type, option, object) + + this.tempViewport.labelManager.removeLabel(object) + // 删除标签 + // const labelObj = object.userData?.labelObj + // if (labelObj && labelObj.parent) { + // labelObj.parent.remove(labelObj) + // } + } + dispose() { super.dispose()