8 changed files with 929 additions and 123 deletions
@ -0,0 +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 { |
||||
|
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<string, number> = new Map() |
||||
|
|
||||
|
/** |
||||
|
* 创建点实例 |
||||
|
* @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) |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
} |
||||
@ -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<string, Text | CSS2DObject> = 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
|
||||
|
// }
|
||||
|
// }
|
||||
|
} |
||||
|
|
||||
@ -0,0 +1,269 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
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 { |
||||
|
private readonly viewport: Viewport |
||||
|
private readonly lineGeometry: LineSegmentsGeometry |
||||
|
private readonly lineMaterial: LineMaterial |
||||
|
private readonly lineSegments: LineSegments2 |
||||
|
private readonly segments: Map<string, LineSegmentData> = new Map() |
||||
|
public needsUpdate: boolean = false |
||||
|
private colorArray: Float32Array | null = null |
||||
|
private positionArray: Float32Array | null = null |
||||
|
|
||||
|
/** |
||||
|
* 创建或更新线段 |
||||
|
* @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() { |
||||
|
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; |
||||
|
} |
||||
@ -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<string, THREE.BufferGeometry> = new Map() |
||||
|
materials: Map<string, THREE.Material> = new Map() |
||||
|
textures: Map<string, THREE.Texture> = new Map() |
||||
|
meshes: Map<string, THREE.Mesh> = 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 |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue