import * as THREE from 'three' import type Viewport from '@/core/engine/Viewport' import type { InteractionOption } from '@/core/manager/InteractionManager.ts' import { getRenderer } from '@/core/manager/ModuleManager.ts' import { Line2 } from 'three/examples/jsm/lines/Line2' import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial' import { numberToString } from '@/utils/webutils.ts' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' let pdFn, pmFn, puFn /** * 基本交互控制器基类 * 定义了在建模编辑器中物流单元如何响应鼠标和键盘操作 */ export default abstract class BaseInteraction { viewport!: Viewport canvas: HTMLCanvasElement viewerDom: HTMLElement // 交互选项 option: InteractionOption // 临时标记 tempPointMarker?: THREE.Mesh tempLine: Line2 | undefined = undefined tempLabel: CSS2DObject | undefined = undefined // 用于判断, 按下之后有没有移动 mouseOnlyClick = false // 上次鼠标移动位置 lastMovePosition: THREE.Vector3 | undefined = undefined // 保存上次点击时间,以便检测双击事件 lastClickTime: number = 0 readonly itemTypeName: string // 连线起点 linkStartPointId: string linkStartPointObject: THREE.Object3D dragOption: DragOption | undefined dragOriginPosition: THREE.Vector3 | undefined dragItem: ItemJson | undefined templineMaterial = new LineMaterial({ color: 0xE63C17, // 主颜色 linewidth: 2, // 实际可用的线宽 vertexColors: true, // 启用顶点颜色 dashed: false, alphaToCoverage: true }) /** * 物品是否"单点", 不允许连线 */ get isSinglePointMode(): boolean { return false } constructor(itemTypeName: string) { this.itemTypeName = itemTypeName } createPointOfItem(catchPoint: ItemJson, point: THREE.Vector3): ItemJson { const renderer = getRenderer(this.itemTypeName) const defaultScale = renderer.defaultScale const defaultRotation = renderer.defaultRotation _.extend(catchPoint, { id: system.createUUID(), t: this.itemTypeName, v: true, tf: [ [point.x, point.y, point.z], [defaultRotation.x, defaultRotation.y, defaultRotation.z], [defaultScale.x, defaultScale.y, defaultScale.z] ], dt: { in: [] as string[], out: [] as string[], center: [] as string[] } }) return catchPoint } /** * 拖拽点开始 */ dragPointStart(viewport: Viewport, dragOption: DragOption) { this.viewport = viewport this.dragOption = dragOption this.dragOriginPosition = dragOption.object.position.clone() // 找到 itemJson const itemJson = _.find(this.viewport.stateManager.vdata.items, (item) => item.id === dragOption.entityId) if (!itemJson) { system.showErrorDialog('Not found for entityId:' + dragOption.entityId) return false } this.dragItem = itemJson return true } /** * 拖拽点移动 */ dragPointMove(viewport: Viewport, e: MouseEvent) { if (this.viewport !== viewport) return } /** * 拖拽点完成 */ dragPointComplete(viewport: Viewport, e: MouseEvent) { if (this.viewport !== viewport) return // 获取当前鼠标所在位置 if (!CurrentMouseInfo || isNaN(CurrentMouseInfo.x) || isNaN(CurrentMouseInfo.z) || !this.dragItem?.tf?.[0]) { return } // 提交状态管理器 const stateManager = this.viewport.stateManager stateManager.beginStateUpdate({ createFromInteraction: true }) this.dragItem.tf[0][0] = CurrentMouseInfo.x this.dragItem.tf[0][2] = CurrentMouseInfo.z stateManager.endStateUpdate() this.viewport = undefined this.dragOption = undefined this.dragItem = undefined return true } /** * 开始交互 */ start(viewport: Viewport, option: InteractionOption = {}) { this.stop() this.viewport = viewport this.option = option this.viewerDom = this.viewport.viewerDom this.canvas = this.viewport.renderer.domElement if (option?.startPoint) { this.linkStartPointId = option.startPoint } else { this.linkStartPointId = undefined } if (this.linkStartPointId) { if (this.isSinglePointMode) { // 单点模式不需要起始点 this.linkStartPointId = undefined this.linkStartPointObject = undefined } else { this.linkStartPointObject = this.viewport.entityManager.findObjectsById(this.linkStartPointId)?.[0] } } 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) } /** * 停止交互 */ stop() { if (this.canvas) { this.canvas.removeEventListener('pointerdown', pdFn) pdFn = undefined this.canvas.removeEventListener('pointermove', pmFn) pmFn = undefined this.canvas.removeEventListener('pointerup', puFn) puFn = undefined } this.linkStartPointId = undefined this.linkStartPointObject = undefined // 清空所有临时点 this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) this.tempPointMarker = undefined this.tempLine && this.viewport.scene.remove(this.tempLine) this.tempLine = undefined this.tempLabel && this.viewport.scene.remove(this.tempLabel) this.tempLabel = undefined this.viewerDom = undefined this.canvas = undefined } mousedown() { this.mouseOnlyClick = true } mousemove(e: MouseEvent): THREE.Vector3 | undefined { this.mouseOnlyClick = false // 当前鼠标所在的点 const point = this.viewport.getClosestIntersection(e) if (!point) { return } // 如果按下了 shift 键,则 point 的位置必须位于 startPoint 正上下方,或者正左右方 if (this.linkStartPointObject && e.shiftKey) { const startPos = this.linkStartPointObject.position const dx = Math.abs(point.x - startPos.x) const dz = Math.abs(point.z - startPos.z) if (dx > dz) { point.z = startPos.z } else { point.x = startPos.x } } // 移动时绘制临时线 if (this.linkStartPointObject) { // 获取最后一个点 const p0 = this.linkStartPointObject.position const dist = p0.distanceTo(point) const label = `${numberToString(dist)} m` const position = new THREE.Vector3().addVectors(p0, point).multiplyScalar(0.5) if (!this.tempLine) { this.tempLine = this.createTempLine() this.viewport.scene.add(this.tempLine) } if (!this.tempLabel) { this.tempLabel = this.createTempLabel(label) this.viewport.scene.add(this.tempLabel) } this.tempLine.geometry.setFromPoints([p0, point]) this.tempLabel.position.set(position.x, position.y, position.z) this.tempLabel.element.innerHTML = label } this.lastMovePosition = point // 在鼠标移动时绘制临时点 if (this.tempPointMarker) { this.tempPointMarker.position.set(point.x, point.y, point.z) } else { this.tempPointMarker = this.createTempPointMarker(point) this.viewport.scene.add(this.tempPointMarker) } // this.viewport.dispatchSignal('sceneGraphChanged') return point } /** * 创建测量线 */ createTempLine(): Line2 { const geom = new LineGeometry() const obj = new Line2(geom, this.templineMaterial) obj.frustumCulled = false return obj } /** * 鼠标松开事件 */ mouseup(e: MouseEvent) { if (this.mouseOnlyClick) { if (e.button === 2) { // 右键点击, 完成绘图操作 this.viewport.interactionManager.exitInteraction() } else if (e.button === 0) { // 左键点击, 添加点 this.onMouseClicked(e) } } } onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined { // 获取鼠标点击位置的三维坐标 const point = this.lastMovePosition if (!point) { return } // 双击触发两次点击事件,我们需要避免这里的第二次点击 const now = Date.now() if (this.lastClickTime && (now - this.lastClickTime < 50)) { return } this.lastClickTime = now // 如果正式的点命中到同类型的节点上,则不添加新的点,只牵线到该点 let catchPoint: ItemJson | null = this.viewport.stateManager.findItemByPosition(point, this.itemTypeName) if (this.isSinglePointMode) { // 单点模式,直接添加点 if (catchPoint) { // 如果已经有点了,则不再添加 system.msg('Point already exists at this position.') return } // 则添加一个新的点 const stateManager = this.viewport.stateManager stateManager.beginStateUpdate({ createFromInteraction: true }) catchPoint = {} as ItemJson catchPoint = this.createPointOfItem(catchPoint, point) stateManager.vdata.items.push(catchPoint) stateManager.endStateUpdate() return } let from: ItemJson | undefined = undefined if (this.linkStartPointId) { from = this.viewport.stateManager.findItemById(this.linkStartPointId) if (!from) { system.showErrorDialog(`Cannot find state item: ${this.linkStartPointId}`) return } } if (catchPoint) { // 连线到目标点 if (this.linkStartPointId === catchPoint.id) { // 自己连接自己,忽略 system.msg('Cannot link to itself.') return } // 关联2个点 if (this.linkStartPointId && from) { catchPoint.dt.center.push(this.linkStartPointId) from.dt.center.push(catchPoint.id) } // 提交状态管理器 const stateManager = this.viewport.stateManager stateManager.beginStateUpdate({ createFromInteraction: true }) catchPoint.dt.center.push(this.linkStartPointId) from.dt.center.push(catchPoint.id) stateManager.endStateUpdate() } else { // 添加正式点 catchPoint = {} as ItemJson catchPoint = this.createPointOfItem(catchPoint, point) // 提交状态管理器 const stateManager = this.viewport.stateManager stateManager.beginStateUpdate({ createFromInteraction: true }) // 关联2个点 stateManager.vdata.items.push(catchPoint) if (from) { catchPoint.dt.center.push(this.linkStartPointId) from.dt.center.push(catchPoint.id) } else { stateManager.vdata.items.push(catchPoint) } stateManager.endStateUpdate() } // 把点加入拖拽控制器 // this.viewport.dragControl.setDragObjects(marker, 'push') // 更新起始点为新添加的点 this.linkStartPointId = catchPoint.id this.linkStartPointObject = this.viewport.entityManager.findObjectsById(catchPoint.id)?.[0] // 删除临时点 this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) this.tempPointMarker = undefined return point } /** * 创建临时点标记 */ createTempPointMarker(position?: THREE.Vector3): THREE.Mesh { const p = position const renderer = getRenderer(this.itemTypeName) const scale = renderer.defaultScale const rotation = renderer.defaultRotation const geometry = new THREE.BoxGeometry(1, 1, 1) const material = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) const obj = new THREE.Mesh(geometry, material) obj.scale.set(scale.x, scale.y, scale.x) obj.rotation.set( THREE.MathUtils.degToRad(rotation.x), THREE.MathUtils.degToRad(rotation.y), THREE.MathUtils.degToRad(rotation.z) ) if (p) { obj.position.set(p.x, p.y, p.z) } return obj } /** * 创建标签 */ createTempLabel(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) return obj } } export interface DragOption { object: THREE.Object3D entityId: string }