import _ from 'lodash' import PointPng from '@/assets/images/logo.png' import * as THREE from 'three' import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' import Viewport from '@/designer/Viewport.ts' import { numberToString, getUnitString } from '@/utils/webutils' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { Vector3 } from 'three' let pdFn, pmFn, puFn, kdFn /** * 用于在 threejs 中创建一系列的 object_for_measure 点,并标记他的距离 */ export default class RulerTool implements ITool { static OBJ_NAME = 'object_for_measure' static LABEL_NAME = 'label_for_measure' static MAX_DISTANCE = 500 //当相交物体的距离太远时,忽略它 viewport: Viewport // 当前测绘内容组 group: THREE.Group isCompleted = false mouseMoved = false canvas: HTMLCanvasElement // 用户在测量时绘制的线的当前实例 protected polyline?: THREE.Line // 用于存储临时点 protected tempPointMarker?: THREE.Mesh // 用于存储临时线条,用于在鼠标移动时绘制线条/区域/角度 protected tempLine?: THREE.Line // 用于在鼠标移动时存储临时标签,只有测量距离时才有 protected tempLabel?: CSS2DObject // 存储点 protected pointArray: THREE.Vector3[] = [] //保存上次点击时间,以便检测双击事件 protected lastClickTime: number = 0 static LINE_MATERIAL = new THREE.LineBasicMaterial({ color: 0xE63C17, linewidth: 2, opacity: 0.9, transparent: true, side: THREE.DoubleSide, depthWrite: false, depthTest: false }) init(viewport: Viewport) { this.viewport = viewport const viewerDom = this.viewport.viewerDom this.canvas = $(viewerDom).children('canvas')[0] as HTMLCanvasElement pdFn = this.mousedown.bind(this) this.canvas.addEventListener('pointerdown', pdFn) pmFn = this.mousemove.bind(this) this.canvas.addEventListener('pointermove', pmFn) puFn = this.mouseup.bind(this) this.canvas.addEventListener('pointerup', puFn) // 初始化group this.group = new THREE.Group() this.group.name = `${RulerTool.OBJ_NAME}_group` this.group.userData = { mode: this.viewport.state.cursorMode } this.viewport.scene.add(this.group) // 测量距离、面积和角度需要折线 this.polyline = this.createLine() this.group.add(this.polyline) this.isCompleted = false this.viewport.viewerDom.style.cursor = 'crosshair' // 当次绘制点 this.pointArray = [] system.msg('进入鼠标测距模式') } destory(): void { system.msg('退出鼠标测距模式') const viewerDom = this.viewport.viewerDom this.isCompleted = true viewerDom.style.cursor = '' this.canvas.removeEventListener('pointerdown', pdFn) pdFn = undefined this.canvas.removeEventListener('pointermove', pmFn) pmFn = undefined this.canvas.removeEventListener('pointerup', puFn) puFn = undefined this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) this.tempLine && this.viewport.scene.remove(this.tempLine) this.tempLabel && this.viewport.scene.remove(this.tempLabel) this.tempPointMarker = undefined this.tempLine = undefined this.tempLabel = undefined } mousedown = () => { this.mouseMoved = false } // 鼠标移动,创建对应的临时点与线 mousemove = (e: MouseEvent) => { if (this.isCompleted) return this.mouseMoved = true const point = this.getClosestIntersection(e) if (!point) { return } // 在鼠标移动时绘制临时点 if (this.tempPointMarker) { this.tempPointMarker.position.set(point.x, point.y, point.z) } else { this.tempPointMarker = this.createPointMarker(point) this.viewport.scene.add(this.tempPointMarker) } // 移动时绘制临时线 if (this.pointArray.length > 0) { const p0 = this.pointArray[this.pointArray.length - 1] // 获取最后一个点 const line = this.tempLine || this.createLine() const geom = line.geometry const startPoint = this.pointArray[0] const lastPoint = this.pointArray[this.pointArray.length - 1] if (this.viewport.state.cursorMode === 'RulerArea') { geom.setFromPoints([lastPoint, point, startPoint]) } else { geom.setFromPoints([lastPoint, point]) } if (this.viewport.state.cursorMode === 'Ruler') { const dist = p0.distanceTo(point) const label = `${numberToString(dist)} ${getUnitString(this.viewport.state.cursorMode)}` const position = new THREE.Vector3((point.x + p0.x) / 2, (point.y + p0.y) / 2, (point.z + p0.z) / 2) this.addOrUpdateTempLabel(label, position) } // tempLine 只需添加到场景一次 if (!this.tempLine) { this.viewport.scene.add(line) this.tempLine = line } } // this.viewport.dispatchSignal('sceneGraphChanged') } mouseup = (e: MouseEvent) => { // 如果mouseMoved是true,那么它可能在移动,而不是点击 if (!this.mouseMoved) { // 右键点击表示完成绘图操作 if (e.button === 2) { this.viewport.state.cursorMode = 'normal' } else if (e.button === 0) { // 左键点击表示添加点 this.onMouseClicked(e) } } } onMouseClicked = (e: MouseEvent) => { if (this.isCompleted) { return } const point = this.getClosestIntersection(e) if (!point) { return } // 双击触发两次点击事件,我们需要避免这里的第二次点击 const now = Date.now() if (this.lastClickTime && (now - this.lastClickTime < 10)) return this.lastClickTime = now this.pointArray.push(point) const count = this.pointArray.length const marker = this.createPointMarker(point) marker.userData.point = point marker.userData.pointIndex = count - 1 this.group.add(marker) // 把点加入拖拽控制器 this.viewport.dragControl.setDragObjects([marker], 'push') if (this.polyline) { this.polyline.geometry.setFromPoints(this.pointArray) if (this.tempLabel && count > 1) { const p0 = this.pointArray[count - 2] this.tempLabel.position.set((p0.x + point.x) / 2, (p0.y + point.y) / 2, (p0.z + point.z) / 2) this.group.add(this.tempLabel) // 创建距离测量线时,此处的 临时label 将作为正式的使用,不在this.clearTemp()中清除,故置为undefined this.tempLabel = undefined } } // this.redrawComplete() // this.viewport.dispatchSignal('sceneGraphChanged') } /** * Creates THREE.Line */ private createLine(): THREE.Line { const geom = new THREE.BufferGeometry() const obj = new THREE.Line(geom, RulerTool.LINE_MATERIAL) obj.frustumCulled = false obj.name = RulerTool.OBJ_NAME obj.userData = { type: 'line' } return obj } /** * 创建点标记 */ createPointMarker(position?: THREE.Vector3): THREE.Mesh { const p = position const scale = 0.25 const tt = new THREE.BoxGeometry(1, 1, 1) const t2 = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) const obj = new THREE.Mesh(tt, t2) obj.scale.set(scale, 0.1, scale) if (p) { obj.position.set(p.x, p.y, p.z) } obj.name = RulerTool.OBJ_NAME obj.userData = { mode: this.viewport.state.cursorMode, type: 'measure-marker' } return obj } /** * 获取按下对应三维位置 */ getClosestIntersection(e: MouseEvent) { const _point = new THREE.Vector2() _point.x = e.offsetX / this.viewport.renderer.domElement.offsetWidth _point.y = e.offsetY / this.viewport.renderer.domElement.offsetHeight const intersects = this.viewport.getIntersects(_point) if (intersects && intersects.length > 2) { if (intersects.length > 0 && intersects[0].distance < RulerTool.MAX_DISTANCE) { return new Vector3( intersects[0].point.x, 0.1, intersects[1].point.z ) } } return null } /** * 添加或更新临时标签和位置 */ addOrUpdateTempLabel(label: string, position: THREE.Vector3) { if (!this.tempLabel) { this.tempLabel = this.createLabel(label) console.log('addOrUpdateTempLabel', label, position) this.viewport.scene.add(this.tempLabel) } this.tempLabel.position.set(position.x, position.y, position.z) this.tempLabel.element.innerHTML = label } /** * 创建标签 */ createLabel(text: string): CSS2DObject { 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 = RulerTool.LABEL_NAME obj.userData = { type: 'label' } return obj } // 重绘完成 redrawComplete() { if (!this.tempPointMarker) return const point = this.tempPointMarker.userData.point this.pointArray[this.tempPointMarker.userData.pointIndex] = point const count = this.pointArray.length if (this.polyline) { this.polyline.geometry.setFromPoints(this.pointArray) // 如果是距离测量,则清除group中已有的label,再重新创建 if (this.viewport.state.cursorMode === 'Ruler' && count > 1) { this.clearCurrentLabel() // 绘制label for (let i = 0; i < count - 1; i++) { const p0 = this.pointArray[i] const p1 = this.pointArray[i + 1] if (!p0 || !p1) continue const dist = p0.distanceTo(p1) const label = `${numberToString(dist)} ${getUnitString(this.viewport.state.cursorMode)}` const position = new THREE.Vector3((p0.x + p1.x) / 2, (p0.y + p1.y) / 2, (p0.z + p1.z) / 2) const labelObj = this.createLabel(label) labelObj.position.set(position.x, position.y, position.z) labelObj.element.innerHTML = label this.group.add(labelObj) } } } // this.destory() } /** * 清除当前group label */ clearCurrentLabel() { for (let i = this.group.children.length - 1; i >= 0; i--) { const c = this.group.children[i] if (c.userData.type === 'label') { this.group.remove(c) } } } }