From 27dcba2dff946e14469225b4f23e7f563f816eb9 Mon Sep 17 00:00:00 2001 From: luoyifan Date: Mon, 9 Jun 2025 16:24:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E7=AE=A1=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() From ca12e043fb444dbbf4b3c79a22e169eff2addad0 Mon Sep 17 00:00:00 2001 From: luoyifan Date: Mon, 9 Jun 2025 18:15:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E7=BA=BF=E6=AE=B5=20/=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E7=82=B9=E4=BD=8D=E7=AE=A1=E7=90=86.=20Insta?= =?UTF-8?q?ncePointManager=20/=20LineSegmentManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/Constract.ts | 4 +- src/core/base/BaseRenderer.ts | 45 ++++++ src/core/engine/Viewport.ts | 44 +++-- src/core/manager/InstancePointManager.ts | 183 ++++++++++++++++++--- src/core/manager/LineSegmentManager.ts | 267 ++++++++++++++++++++++++++++++- src/modules/measure/MeasureRenderer.ts | 41 +++-- 6 files changed, 526 insertions(+), 58 deletions(-) diff --git a/src/core/Constract.ts b/src/core/Constract.ts index e691545..0f4e6c7 100644 --- a/src/core/Constract.ts +++ b/src/core/Constract.ts @@ -31,6 +31,8 @@ const Constract = Object.freeze({ HEIGHT_RACK: 0, HEIGHT_WAY: 0.01, HEIGHT_WAY_LABEL: 0.03, - HEIGHT_WAY_LINE: 0.02 + HEIGHT_WAY_LINE: 0.02, + + MAX_MEASURE_INSTANCES: 1000, }) export default Constract diff --git a/src/core/base/BaseRenderer.ts b/src/core/base/BaseRenderer.ts index 3008069..9f43815 100644 --- a/src/core/base/BaseRenderer.ts +++ b/src/core/base/BaseRenderer.ts @@ -2,6 +2,9 @@ import type Viewport from '@/core/engine/Viewport' import * as THREE from 'three' import { getLineId } from '@/core/ModelUtils.ts' import { Line2 } from 'three/examples/jsm/lines/Line2' +import InstancePointManager from '@/core/manager/InstancePointManager.ts' +import Constract from '@/core/Constract.ts' +import type LineSegmentManager from '@/core/manager/LineSegmentManager.ts' /** * 基本渲染器基类 @@ -27,6 +30,48 @@ export default abstract class BaseRenderer { return Promise.resolve() } + get pointManager(): InstancePointManager { + if (!this.tempViewport) { + throw new Error('tempViewport is not set. Please call beginRendererUpdate first.') + } + let pointManager = this.tempViewport.pointManagerMap.get(this.itemTypeName) + if (!pointManager) { + pointManager = this.createPointManager() + if (pointManager) { + this.tempViewport.pointManagerMap.set(this.itemTypeName, pointManager) + } + } + return pointManager + } + + get lineSegmentManager(): LineSegmentManager { + if (!this.tempViewport) { + throw new Error('tempViewport is not set. Please call beginRendererUpdate first.') + } + let lineSegmentManager = this.tempViewport.lineSegmentManagerMap.get(this.itemTypeName) + if (!lineSegmentManager) { + lineSegmentManager = this.createLineSegmentManager() + if (lineSegmentManager) { + this.tempViewport.lineSegmentManagerMap.set(this.itemTypeName, lineSegmentManager) + } + } + return lineSegmentManager + } + + /** + * 创建点管理器 + */ + createPointManager(): InstancePointManager { + return null + } + + /** + * 创建线段管理器 + */ + createLineSegmentManager(): LineSegmentManager { + return null + } + /** * 开始更新 * @param viewport 当前视口 diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts index 4448b35..7bf6da6 100644 --- a/src/core/engine/Viewport.ts +++ b/src/core/engine/Viewport.ts @@ -23,7 +23,8 @@ 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' +import type InstancePointManager from '@/core/manager/InstancePointManager.ts' +import type LineSegmentManager from '@/core/manager/LineSegmentManager.ts' /** * 视窗对象 @@ -60,6 +61,12 @@ export default class Viewport { // 交互管理器 interactionManager = new InteractionManager() + // 点实例管理器 moduleName -> InstancePointManager + pointManagerMap: Map = new Map() + + // 线段实例管理器 moduleName -> InstancePointManager + lineSegmentManagerMap: Map = new Map() + // 监听窗口大小变化 resizeObserver?: ResizeObserver @@ -331,23 +338,23 @@ export default class Viewport { */ animate() { this.animationFrameId = requestAnimationFrame(this.animate.bind(this)) - this.renderView() - if (window['lineMaterial']) { - this.offset -= 0.002 - window['lineMaterial'].dashOffset = this.offset + for (const lineSegmentManager of this.lineSegmentManagerMap.values()) { + if (lineSegmentManager.needsUpdate) { + lineSegmentManager.updateGeometry() + } } - } - /** - * 渲染视图 - */ - renderView() { this.statsControls?.update() this.renderer?.render(this.scene.scene, this.camera) this.css2DRenderer.render(this.scene.scene, this.camera) this.css3DRenderer.render(this.scene.scene, this.camera) + + // if (window['lineMaterial']) { + // this.offset -= 0.002 + // window['lineMaterial'].dashOffset = this.offset + // } } /** @@ -463,6 +470,23 @@ export default class Viewport { dispose() { this.state.isReady = false + if (this.pointManagerMap.size > 0) { + this.pointManagerMap.forEach((manager) => { + if (manager.dispose) { + manager.dispose() + } + }) + this.pointManagerMap.clear() + } + if (this.lineSegmentManagerMap.size > 0) { + this.lineSegmentManagerMap.forEach((manager) => { + if (manager.dispose) { + manager.dispose() + } + }) + this.lineSegmentManagerMap.clear() + } + if (this.tools) { for (const tool of this.tools) { if (tool.dispose) { diff --git a/src/core/manager/InstancePointManager.ts b/src/core/manager/InstancePointManager.ts index 1a7e458..62c6fd3 100644 --- a/src/core/manager/InstancePointManager.ts +++ b/src/core/manager/InstancePointManager.ts @@ -1,30 +1,177 @@ import * as THREE from 'three' import type IControls from '@/core/controls/IControls.ts' import type Viewport from '@/core/engine/Viewport.ts' +import { Vector3 } from 'three/src/math/Vector3' +import { Euler } from 'three/src/math/Euler' -export default class InstancePointManager implements IControls { - viewport: Viewport - mesh: THREE.InstancedMesh - dummy: THREE.Object3D = new THREE.Object3D() - count: number = 0 - maxInstances: number = 100000 +export default class InstancePointManager { + private readonly viewport: Viewport + private readonly instancedMesh: THREE.InstancedMesh + private readonly freeIndices: number[] = [] + private readonly maxInstanceCount: number + private readonly geometry: THREE.BufferGeometry + private readonly material: THREE.Material + private readonly dummy: THREE.Object3D = new THREE.Object3D() + // itemId -> instanceId + private instanceData: Map = new Map() - init(viewport: Viewport): void { + /** + * 创建点实例 + * @param item 点数据 + * @returns 分配的实例ID (如果失败返回-1) + */ + createPoint(item: ItemJson): number { + if (this.freeIndices.length === 0) { + system.showErrorDialog('InstancePointManager is full') + return -1 + } + + const instanceId = this.freeIndices.pop()! + this.instanceData.set(item.id, instanceId) + + this.updatePoint(item) + return instanceId + } + + /** + * 更新点实例 + * @param item 点数据 + * @param option 更新选项 + */ + updatePoint(item: ItemJson, option: { + position?: Vector3 + rotation?: Vector3 + scale?: Vector3 + } = {}): void { + const instanceId = this.instanceData.get(item.id) + if (instanceId === undefined) return + + let [position, rotation, scale] = item.tf + if (option.position) { + position = option.position.toArray() + } + if (option.rotation) { + rotation = option.rotation.toArray() + } + if (option.scale) { + scale = option.scale.toArray() + } + + this.dummy.position.set(position[0], position[1], position[2]) + this.dummy.rotation.set( + THREE.MathUtils.degToRad(rotation[0]), + THREE.MathUtils.degToRad(rotation[1]), + THREE.MathUtils.degToRad(rotation[2]) + ) + this.dummy.scale.set(scale[0], scale[1], scale[2]) + + if (item.v === false) { + this.dummy.scale.set(0, 0, 0) + } + this.dummy.updateMatrix() + + this.instancedMesh.setMatrixAt(instanceId, this.dummy.matrix) + this.instancedMesh.instanceMatrix.needsUpdate = true + } + + /** + * 删除点实例 + * @param id 点ID + */ + deletePoint(id: string): void { + const instanceId = this.instanceData.get(id) + if (instanceId === undefined) return + + // 隐藏实例 + this.dummy.scale.set(0, 0, 0) + this.dummy.updateMatrix() + this.instancedMesh.setMatrixAt(instanceId, this.dummy.matrix) + this.instancedMesh.instanceMatrix.needsUpdate = true + + // 回收索引 + this.freeIndices.push(instanceId) + this.instanceData.delete(id) + } + + /** + * 获取点实例的世界位置 + * @param id 点ID + * @param target 目标向量 + */ + getWorldPosition(id: string, target: THREE.Vector3): void { + const instanceId = this.instanceData.get(id) + if (instanceId === undefined) return + + const matrix = new THREE.Matrix4() + this.instancedMesh.getMatrixAt(instanceId, matrix) + target.setFromMatrixPosition(matrix) + } + + // 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) + // } + + + public static create( + viewport: Viewport, + geometry: THREE.BufferGeometry, + material: THREE.Material, + maxInstances: number = 1000 + ): InstancePointManager { + return new InstancePointManager(viewport, geometry, material, maxInstances) + } + + constructor( + viewport: Viewport, + geometry: THREE.BufferGeometry, + material: THREE.Material, + maxInstances: number = 1000 + ) { this.viewport = viewport + this.geometry = geometry + this.material = material + this.maxInstanceCount = maxInstances + + this.instancedMesh = new THREE.InstancedMesh(geometry, material, maxInstances) + this.instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) + viewport.scene.add(this.instancedMesh) - 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) + this.dummy.scale.set(0, 0, 0) + for (let i = 0; i < maxInstances; i++) { + this.dummy.updateMatrix() + this.instancedMesh.setMatrixAt(i, this.dummy.matrix) + this.freeIndices.push(i) + } + + this.instancedMesh.instanceMatrix.needsUpdate = true } dispose() { + this.viewport.scene.remove(this.instancedMesh) + this.instancedMesh.geometry.dispose() + + if (this.instancedMesh.material) { + if (Array.isArray(this.instancedMesh.material)) { + this.instancedMesh.material.forEach(mat => mat.dispose()) + } else { + this.instancedMesh.material.dispose() + } + } + + this.instancedMesh.dispose() + this.instanceData.clear() + this.freeIndices.length = 0 } } diff --git a/src/core/manager/LineSegmentManager.ts b/src/core/manager/LineSegmentManager.ts index e1659d8..86137b2 100644 --- a/src/core/manager/LineSegmentManager.ts +++ b/src/core/manager/LineSegmentManager.ts @@ -1,16 +1,269 @@ import * as THREE from 'three' -import type IControls from '@/core/controls/IControls.ts' import type Viewport from '@/core/engine/Viewport.ts' +import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry' +import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2' +import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial' -export default class LineSegmentManager implements IControls { - viewport: Viewport +/** + * 线段管理器 + */ +export default class LineSegmentManager { + private readonly viewport: Viewport + private readonly lineGeometry: LineSegmentsGeometry + private readonly lineMaterial: LineMaterial + private readonly lineSegments: LineSegments2 + private readonly segments: Map = new Map() + public needsUpdate: boolean = false + private colorArray: Float32Array | null = null + private positionArray: Float32Array | null = null - init(viewport: Viewport): void { + /** + * 创建或更新线段 + * @param key 线段唯一标识 + * @param start 起点 + * @param end 终点 + * @param color 线段颜色 (可选) + * @param userData 自定义数据 (可选) + */ + createLine(key: string, start: THREE.Vector3Like, end: THREE.Vector3Like, color?: THREE.Color | number | string, userData?: any): void { + const startVec = start instanceof THREE.Vector3 + ? start.clone() + : new THREE.Vector3(start[0], start[1], start[2]) + + const endVec = end instanceof THREE.Vector3 + ? end.clone() + : new THREE.Vector3(end[0], end[1], end[2]) + + let colorObj: THREE.Color | undefined + if (color instanceof THREE.Color) { + colorObj = color + } else if (typeof color === 'number') { + colorObj = new THREE.Color(color) + } else if (typeof color === 'string') { + colorObj = new THREE.Color(color) + } + + if (this.segments.has(key)) { + // 更新现有线段 + const segment = this.segments.get(key)! + segment.start.copy(startVec) + segment.end.copy(endVec) + if (colorObj) segment.color = colorObj + if (userData) segment.userData = userData + } else { + // 创建新线段 + this.segments.set(key, { + key, + start: startVec, + end: endVec, + color: colorObj, + visible: true, + userData + }) + } + + this.needsUpdate = true + } + + /** + * 更新线段位置 + * @param key 线段唯一标识 + * @param start 新起点 + * @param end 新终点 + */ + updateLinePosition(key: string, start: THREE.Vector3Like, end: THREE.Vector3Like): void { + const segment = this.segments.get(key) + if (!segment) return + + if (start instanceof THREE.Vector3) { + segment.start.copy(start) + } else { + segment.start.set(start[0], start[1], start[2]) + } + + if (end instanceof THREE.Vector3) { + segment.end.copy(end) + } else { + segment.end.set(end[0], end[1], end[2]) + } + + this.needsUpdate = true + } + + /** + * 更新线段颜色 + * @param key 线段唯一标识 + * @param color 新颜色 + */ + updateLineColor(key: string, color: THREE.Color | number | string): void { + const segment = this.segments.get(key) + if (!segment) return + + if (color instanceof THREE.Color) { + segment.color = color + } else if (typeof color === 'number') { + segment.color = new THREE.Color(color) + } else if (typeof color === 'string') { + segment.color = new THREE.Color(color) + } else { + segment.color = undefined + } + + this.needsUpdate = true + } + + /** + * 设置线段可见性 + * @param key 线段唯一标识 + * @param visible 是否可见 + */ + setLineVisible(key: string, visible: boolean): void { + const segment = this.segments.get(key) + if (segment && segment.visible !== visible) { + segment.visible = visible + this.needsUpdate = true + } + } + + /** + * 删除线段 + * @param key 线段唯一标识 + */ + deleteLine(key: string): void { + if (this.segments.delete(key)) { + this.needsUpdate = true + } + } + + /** + * 获取线段数据 + * @param key 线段唯一标识 + */ + getLineData(key: string): LineSegmentData | undefined { + return this.segments.get(key) + } + + /** + * 更新几何体数据 (应在渲染循环中调用) + */ + updateGeometry(): void { + if (!this.needsUpdate) return + + const segmentCount = this.segments.size + const positionCount = segmentCount * 6 // 每条线段2个点,每个点3个分量 + const colorCount = segmentCount * 6 // 每个顶点3个颜色分量 + + // 初始化或调整数组大小 + if (!this.positionArray || this.positionArray.length !== positionCount) { + this.positionArray = new Float32Array(positionCount) + } + + if (!this.colorArray || this.colorArray.length !== colorCount) { + this.colorArray = new Float32Array(colorCount) + } + + // 填充数据 + let positionIndex = 0 + let colorIndex = 0 + + const defaultColor = new THREE.Color(this.lineMaterial.color) + + this.segments.forEach(segment => { + if (!segment.visible) { + // 对于不可见的线段,跳过位置设置但保留颜色为黑色 + positionIndex += 6 + + // 设置颜色为黑色(透明) + this.colorArray![colorIndex++] = 0 + this.colorArray![colorIndex++] = 0 + this.colorArray![colorIndex++] = 0 + + this.colorArray![colorIndex++] = 0 + this.colorArray![colorIndex++] = 0 + this.colorArray![colorIndex++] = 0 + return + } + + // 设置起点位置 + this.positionArray![positionIndex++] = segment.start.x + this.positionArray![positionIndex++] = segment.start.y + this.positionArray![positionIndex++] = segment.start.z + + // 设置终点位置 + this.positionArray![positionIndex++] = segment.end.x + this.positionArray![positionIndex++] = segment.end.y + this.positionArray![positionIndex++] = segment.end.z + + // 设置起点颜色 + const startColor = segment.color || defaultColor + this.colorArray![colorIndex++] = startColor.r + this.colorArray![colorIndex++] = startColor.g + this.colorArray![colorIndex++] = startColor.b + + // 设置终点颜色 + const endColor = segment.color || defaultColor + this.colorArray![colorIndex++] = endColor.r + this.colorArray![colorIndex++] = endColor.g + this.colorArray![colorIndex++] = endColor.b + }) + + // 更新几何体 + this.lineGeometry.setPositions(this.positionArray) + this.lineGeometry.setColors(this.colorArray) + + // 设置实例计数为可见线段数量 + const visibleCount = Array.from(this.segments.values()).filter(s => s.visible).length + this.lineGeometry.instanceCount = visibleCount + + this.needsUpdate = false + } + + /** + * 创建线段管理器实例 + */ + public static create(viewport: Viewport, lineMaterial: LineMaterial): LineSegmentManager { + return new LineSegmentManager(viewport, lineMaterial) + } + + private constructor(viewport: Viewport, lineMaterial: LineMaterial) { this.viewport = viewport + this.lineMaterial = lineMaterial + + this.lineGeometry = new LineSegmentsGeometry() + + // 创建线段的渲染对象 + this.lineSegments = new LineSegments2(this.lineGeometry, this.lineMaterial) + this.lineSegments.name = 'batch_line_segments' + this.lineSegments.frustumCulled = false + this.viewport.scene.add(this.lineSegments) } - dispose(): void { - // 清理资源 - this.viewport = undefined + dispose() { + this.viewport.scene.remove(this.lineSegments) + this.lineGeometry.dispose() + + if (this.lineMaterial) { + if (Array.isArray(this.lineMaterial)) { + this.lineMaterial.forEach(mat => mat.dispose()) + } else { + this.lineMaterial.dispose() + } + } + + this.segments.clear() + this.positionArray = null + this.colorArray = null } } + +/** + * 线段数据接口 + */ +interface LineSegmentData { + key: string; + start: THREE.Vector3; + end: THREE.Vector3; + color?: THREE.Color; + visible?: boolean; + userData?: any; +} diff --git a/src/modules/measure/MeasureRenderer.ts b/src/modules/measure/MeasureRenderer.ts index acdfc7a..56eadf9 100644 --- a/src/modules/measure/MeasureRenderer.ts +++ b/src/modules/measure/MeasureRenderer.ts @@ -5,6 +5,8 @@ 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 Constract from '@/core/Constract.ts' +import InstancePointManager from '@/core/manager/InstancePointManager.ts' +import LineSegmentManager from '@/core/manager/LineSegmentManager.ts' /** * 辅助测量工具渲染器 @@ -22,34 +24,29 @@ export default class MeasureRenderer extends BaseRenderer { public useHtmlLabel = false - pointMaterial: THREE.Material - lineMaterial: LineMaterial - + pointMaterial: THREE.Material = new THREE.MeshBasicMaterial({ + color: 0xFFFF99, + transparent: true, + depthWrite: false, + side: THREE.DoubleSide + }) + pointGeometry = new THREE.PlaneGeometry(0.25, 0.25) + lineMaterial: LineMaterial = new LineMaterial({ + color: 0xFF8C00, + linewidth: 1, + vertexColors: false, + dashed: false + }) readonly defulePositionY = Constract.HEIGHT_MEASURE readonly defaultScale: THREE.Vector3 = new THREE.Vector3(0.1, 0.1, 0.1) readonly defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0) - constructor(itemTypeName: string) { - super(itemTypeName) + createPointManager(): InstancePointManager { + return InstancePointManager.create(this.tempViewport, this.pointGeometry, this.pointMaterial, Constract.MAX_MEASURE_INSTANCES) } - async init() { - this.pointMaterial = new THREE.SpriteMaterial({ - color: 0xFFFF99, // 0x303133, - transparent: true, - side: THREE.DoubleSide, - opacity: 1, - sizeAttenuation: true - }) - this.lineMaterial = new LineMaterial({ - color: 0xFF8C00, - linewidth: 1, - vertexColors: false, - dashed: false - }) - return Promise.all([ - super.init() - ]) + createLineSegmentManager(): LineSegmentManager { + return LineSegmentManager.create(this.tempViewport, this.lineMaterial) } /**