From a6507a815e297bbcf2f2e9b2d43f75246468ee28 Mon Sep 17 00:00:00 2001 From: luoyifan Date: Tue, 27 May 2025 18:41:58 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D.=20ItemTypeBaseTool?= =?UTF-8?q?box=20->=20Toolbox=20=20=20=20=20=20ItemTypeLineToolbox=20->=20?= =?UTF-8?q?ToolboxLine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ItemTypeBase -> ItemType ItemTypeLineBase -> ItemTypeLine --- src/designer/Viewport.ts | 6 +- src/model/itemType/ItemType.ts | 76 +++++++ src/model/itemType/ItemTypeBase.ts | 77 -------- src/model/itemType/ItemTypeBaseToolbox.ts | 286 --------------------------- src/model/itemType/ItemTypeDefine.ts | 4 +- src/model/itemType/ItemTypeLine.ts | 238 ++++++++++++++++++++++ src/model/itemType/ItemTypeLineBase.ts | 238 ---------------------- src/model/itemType/ItemTypeLineToolbox.ts | 74 ------- src/model/itemType/Toolbox.ts | 286 +++++++++++++++++++++++++++ src/model/itemType/ToolboxLine.ts | 71 +++++++ src/model/itemType/measure/Measure.ts | 12 +- src/model/itemType/measure/MeasureToolbox.ts | 8 +- 12 files changed, 686 insertions(+), 690 deletions(-) create mode 100644 src/model/itemType/ItemType.ts delete mode 100644 src/model/itemType/ItemTypeBase.ts delete mode 100644 src/model/itemType/ItemTypeBaseToolbox.ts create mode 100644 src/model/itemType/ItemTypeLine.ts delete mode 100644 src/model/itemType/ItemTypeLineBase.ts delete mode 100644 src/model/itemType/ItemTypeLineToolbox.ts create mode 100644 src/model/itemType/Toolbox.ts create mode 100644 src/model/itemType/ToolboxLine.ts diff --git a/src/designer/Viewport.ts b/src/designer/Viewport.ts index 04ddaa0..38cff37 100644 --- a/src/designer/Viewport.ts +++ b/src/designer/Viewport.ts @@ -13,7 +13,7 @@ import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' import { getAllItemTypes } from '@/runtime/DefineItemType.ts' import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' -import type ItemTypeBaseToolbox from '@/model/itemType/ItemTypeBaseToolbox.ts' +import type Toolbox from '@/model/itemType/Toolbox.ts' /** * 编辑器对象 @@ -33,11 +33,11 @@ export default class Viewport { dragControl: EsDragControls animationFrameId: any = null - currentTool: ItemTypeBaseToolbox | null = null + currentTool: Toolbox | null = null tools: ITool[] = [ new MouseMoveInspect() ] - toolbox: Record = {} + toolbox: Record = {} /** * 监听窗口大小变化 diff --git a/src/model/itemType/ItemType.ts b/src/model/itemType/ItemType.ts new file mode 100644 index 0000000..aa8dfeb --- /dev/null +++ b/src/model/itemType/ItemType.ts @@ -0,0 +1,76 @@ +import * as THREE from 'three' +import type WorldModel from '@/model/WorldModel.ts' +import type { ItemJson, ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' +import type Viewport from '@/designer/Viewport.ts' +import type Toolbox from '@/model/itemType/Toolbox.ts' + +export default abstract class ItemType { + + /** + * 所有点的数组 + */ + pointArray: THREE.Object3D[] = [] + + name: string + option: ItemTypeDefineOption + worldModel: WorldModel + + public init(worldModel: WorldModel) { + this.worldModel = worldModel + + // 初始化方法,子类可以重写 + return Promise.resolve() + } + + abstract loadFromJson(item: ItemJson): undefined | THREE.Object3D + + abstract createToolbox(viewport: Viewport): Toolbox + + abstract getDefaultScale(): THREE.Vector3 + + abstract getDefaultRotation(): THREE.Vector3 + + abstract createPoint(position: THREE.Vector3, itemJson: ItemJson): THREE.Object3D + + beforeLoad(): THREE.Object3D[] { + return [] + } + + afterLoadPoint(point: THREE.Object3D): void { + } + + afterUpdatePoint(point: THREE.Object3D, updatedLines: THREE.Object3D[]): void { + } + + afterLoadGroup(group: THREE.Group): void { + } + + afterLoadComplete(objects: THREE.Object3D[]): THREE.Object3D[] { + return [] + } + + /** + * 添加到 scene 后的回调 + */ + afterAddScene(scene: THREE.Scene, objects: THREE.Object3D[]): void { + } + + /** + * 添加到 viewport 后的回调 + */ + afterAddViewport(viewport: Viewport): void { + viewport.dragControl.setDragObjects(this.pointArray, 'push') + const toolbox = this.createToolbox(viewport) + viewport.toolbox[this.name] = toolbox + } + + /** + * EsDragControls 开始拖拽某个点时的回调 + */ + abstract dragPointStart(viewport: Viewport, point: THREE.Mesh) + + /** + * EsDragControls 拖拽完成(放开鼠标)时的回调 + */ + abstract dragPointComplete(viewport: Viewport) +} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeBase.ts b/src/model/itemType/ItemTypeBase.ts deleted file mode 100644 index e921386..0000000 --- a/src/model/itemType/ItemTypeBase.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { type Object3D } from 'three' -import type WorldModel from '@/model/WorldModel.ts' -import type { ItemJson, ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' -import * as THREE from 'three' -import type Viewport from '@/designer/Viewport.ts' -import type ItemTypeBaseToolbox from '@/model/itemType/ItemTypeBaseToolbox.ts' - -export default abstract class ItemTypeBase { - - /** - * 所有点的数组 - */ - pointArray: THREE.Object3D[] = [] - - name: string - option: ItemTypeDefineOption - worldModel: WorldModel - - public init(worldModel: WorldModel) { - this.worldModel = worldModel - - // 初始化方法,子类可以重写 - return Promise.resolve() - } - - abstract loadFromJson(item: ItemJson): undefined | THREE.Object3D - - abstract createToolbox(viewport: Viewport): ItemTypeBaseToolbox - - abstract getDefaultScale(): THREE.Vector3 - - abstract getDefaultRotation(): THREE.Vector3 - - abstract createPoint(position: THREE.Vector3, itemJson: ItemJson): THREE.Object3D - - beforeLoad(): THREE.Object3D[] { - return [] - } - - afterLoadPoint(point: THREE.Object3D): void { - } - - afterUpdatePoint(point: THREE.Object3D, updatedLines: THREE.Object3D[]): void { - } - - afterLoadGroup(group: THREE.Group): void { - } - - afterLoadComplete(objects: THREE.Object3D[]): THREE.Object3D[] { - return [] - } - - /** - * 添加到 scene 后的回调 - */ - afterAddScene(scene: THREE.Scene, objects: THREE.Object3D[]): void { - } - - /** - * 添加到 viewport 后的回调 - */ - afterAddViewport(viewport: Viewport): void { - viewport.dragControl.setDragObjects(this.pointArray, 'push') - const toolbox = this.createToolbox(viewport) - viewport.toolbox[this.name] = toolbox - } - - /** - * EsDragControls 开始拖拽某个点时的回调 - */ - abstract dragPointStart(viewport: Viewport, point: THREE.Mesh) - - /** - * EsDragControls 拖拽完成(放开鼠标)时的回调 - */ - abstract dragPointComplete(viewport: Viewport) -} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeBaseToolbox.ts b/src/model/itemType/ItemTypeBaseToolbox.ts deleted file mode 100644 index eb14d7d..0000000 --- a/src/model/itemType/ItemTypeBaseToolbox.ts +++ /dev/null @@ -1,286 +0,0 @@ -import * as THREE from 'three' -import type Viewport from '@/designer/Viewport.ts' -import type ItemTypeBase from '@/model/itemType/ItemTypeBase.ts' -import type { ItemJson } from '@/model/itemType/ItemTypeDefine.ts' - -let pdFn, pmFn, puFn - -/** - * 单元类型工具箱 - */ -export default abstract class ItemTypeBaseToolbox { - /** - * 测量工具所在的视图窗口,从这里可以取到 所有 Three.js 相关的对象. - * 比如: - * - viewport.scene 场景 - * - viewport.renderer 渲染器 - * - viewport.controls 控制器 - * - viewport.camera 摄像机 - * - viewport.raycaster 射线投射器 - * - viewport.dragControl 拖拽控制器 - * - viewport.measure 测量工具 - */ - viewport: Viewport - - /** - * 是否完成工具箱操作 - */ - isCompleted = false - - /** - * 是否鼠标移动事件 - */ - mouseMoved = false - - /** - * 当前鼠标所在的画布, 对应 viewport.renderer.domElement - */ - canvas: HTMLCanvasElement - - /** - * 用于存储临时点 - */ - tempPointMarker?: THREE.Mesh - - /** - * 测量起始点 - */ - startPoint?: THREE.Object3D = undefined - - /** - * 保存上次点击时间,以便检测双击事件 - * @protected - */ - lastClickTime: number = 0 - - /** - * 上次鼠标移动位置 - */ - lastMovePosition: THREE.Vector3 | undefined = undefined - - _itemType: any - - get itemType(): ItemTypeBase { - return this._itemType - } - - mode: CursorMode - - static TMP_TYPE = '_TMP' - - /** - * 获取临时点的名称 - */ - abstract getTempPointName(): string - - addToScene(object: THREE.Object3D) { - this.viewport.scene.add(object) - } - - /** - * 单元工具初始化 - */ - init(viewport: any, itemType: ItemTypeBase): void { - this.viewport = viewport - this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement - this._itemType = itemType - } - - - /** - * 测量工具开始, 监听鼠标事件, 变量初始化等 - */ - start(startPoint?: THREE.Object3D) { - 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) - - this.isCompleted = false - this.viewport.viewerDom.style.cursor = 'crosshair' - - this.mode = this.viewport.state.cursorMode - this.startPoint = startPoint - - system.msg('新建 [' + this.itemType.name + '] 模式') - } - - - /** - * 停止测量模式, 清除所有临时点、线、标签. 停止所有鼠标事件监听 - */ - stop(): 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.tempPointMarker = undefined - } - - - /** - * 创建临时点标记 - */ - createTempPointMarker(position?: THREE.Vector3): THREE.Mesh { - const p = position - const scale = this.itemType.getDefaultScale() - const rotation = this.itemType.getDefaultRotation() - - 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.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) - } - - obj.name = this.getTempPointName() - obj.userData = { - mode: this.mode, - type: ItemTypeBaseToolbox.TMP_TYPE - } - return obj - } - - /** - * 鼠标按下事件 - */ - mousedown() { - this.mouseMoved = false - } - - /** - * 鼠标移动,创建对应的临时点与线 - */ - mousemove(e: MouseEvent): THREE.Vector3 | undefined { - if (this.isCompleted) return - - this.mouseMoved = true - - // 当前鼠标所在的点 - const point = this.viewport.getClosestIntersection(e) - if (!point) { - return - } - - 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 - } - - /** - * 鼠标松开事件 - */ - 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): THREE.Vector3 | undefined { - if (this.isCompleted) { - return - } - - // 获取鼠标点击位置的三维坐标 - const point = this.lastMovePosition - if (!point) { - return - } - - // 双击触发两次点击事件,我们需要避免这里的第二次点击 - const now = Date.now() - if (this.lastClickTime && (now - this.lastClickTime < 50)) { - return - } - this.lastClickTime = now - - const defaultScale = this.itemType.getDefaultScale() - const defaultRotation = this.itemType.getDefaultRotation() - - // 添加正式点 - const itemJson = { - t: this.itemType.name, - a: 'ln', - tf: [ - [point.x, point.y, point.z], - [defaultRotation.x, defaultRotation.y, defaultRotation.z], - [defaultScale.x, defaultScale.y, defaultScale.z] - ], - dt: { - link: [] as string[] - } - } as ItemJson - const marker = this.itemType.createPoint(point, itemJson) - this.addToScene(marker) - - // 把点加入拖拽控制器 - this.viewport.dragControl.setDragObjects([marker], 'push') - - if (this.startPoint) { - this.afterAddPoint(this.startPoint, marker) - } - - // 更新起始点为新添加的点 - this.startPoint = marker - - // 删除临时线 - this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) - this.tempPointMarker = undefined - - return point - } - - /** - * 当用户点击某个点,从临时点转换为正式点后调用 - * 子类可以重写此方法来处理添加点后的逻辑 - */ - afterAddPoint(startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { - // 默认实现不做任何操作 - // 子类可以重写此方法来处理添加点后的逻辑 - } - - /** - * 销毁测量工具, 当视图窗口被销毁时调用 - */ - destory() { - } -} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeDefine.ts b/src/model/itemType/ItemTypeDefine.ts index 44d9d7b..fcfa569 100644 --- a/src/model/itemType/ItemTypeDefine.ts +++ b/src/model/itemType/ItemTypeDefine.ts @@ -1,4 +1,4 @@ -import type ItemTypeBase from '@/model/itemType/ItemTypeBase.ts' +import type ItemType from '@/model/itemType/ItemType.ts' export type ActionType = /** @@ -22,7 +22,7 @@ export interface ItemTypeDefineOption { name: string label: string actionType: ActionType - clazz: ItemTypeBase + clazz: ItemType } /** diff --git a/src/model/itemType/ItemTypeLine.ts b/src/model/itemType/ItemTypeLine.ts new file mode 100644 index 0000000..1237957 --- /dev/null +++ b/src/model/itemType/ItemTypeLine.ts @@ -0,0 +1,238 @@ +import * as THREE from 'three' +import ItemType from '@/model/itemType/ItemType.ts' +import type { ItemJson } from '@/model/itemType/ItemTypeDefine.ts' +import type WorldModel from '@/model/WorldModel.ts' +import type Viewport from '@/designer/Viewport.ts' +import { findObject3DByCondition, findObject3DById } from '@/model/ModelUtils.ts' + +let pmFn + +/** + * ILineType 接口定义了线类型的基本方法 + * 用于创建点和线, 以及处理拖拽事件, 以及交互事件 + */ +export default abstract class ItemTypeLine extends ItemType { + private relationPoints: THREE.Mesh[] = [] + private dragViewport: Viewport | undefined + private dragPoint: THREE.Mesh | undefined + + abstract createPointBasic(position: THREE.Vector3): THREE.Object3D + + abstract createLineBasic(): THREE.Line + + public init(worldModel: WorldModel) { + return super.init(worldModel).then(() => { + }) + } + + afterCreateLine(line: THREE.Line, startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { + } + + afterUpdateLine(line: THREE.Line, startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { + } + + createLine(scene: THREE.Scene, startPoint: THREE.Object3D, endPoint: THREE.Object3D): THREE.Line { + const line = this.createLineBasic() + const geom = line.geometry + geom.setFromPoints([startPoint.position, endPoint.position]) + + if (!line.userData) { + line.userData = {} + } + line.userData.lineStartId = startPoint.uuid + line.userData.lineEndId = endPoint.uuid + + if (startPoint.parent) { + startPoint.parent.add(line) + } else { + scene.add(line) + } + + if (!startPoint.userData.lines) { + startPoint.userData.lines = [] + } + startPoint.userData.lines.push(line.uuid) + + if (!endPoint.userData.lines) { + endPoint.userData.lines = [] + } + endPoint.userData.lines.push(line.uuid) + + this.afterCreateLine(line, startPoint, endPoint) + + return line + } + + /** + * 所有点数据加载完成后,添加进场景之后,需要根据 link 数组创建连接线 + */ + afterAddScene(scene: THREE.Scene, objects: THREE.Object3D[]) { + super.afterAddScene(scene, objects) + + // 为所有的 pointArray 连接线 + for (let i = 0; i < this.pointArray.length; i++) { + const startPoint = this.pointArray[i] + + // 找到这个元素的 userData.link 数组 + const linkArray: string[] = startPoint.userData.link || [] + + for (let j = 0; j < linkArray.length; j++) { + const linkId = linkArray[j] + // 在 pointArray 中查找对应的点 + const endPoint = findObject3DById(scene, linkId) + if (!endPoint) { + console.warn('not found link point uuid=${}', linkId) + continue + } + + const line = this.createLine(scene, startPoint, endPoint) + } + } + } + + createPoint(position: THREE.Vector3, item: ItemJson): THREE.Object3D { + const point = this.createPointBasic(position) + point.name = item.name + point.uuid = item.id || THREE.MathUtils.generateUUID() + point.userData = _.cloneDeep(item.dt) || {} + point.userData.type = item.t + point.userData.actionType = item.a + point.userData.label = item.l + point.userData.color = item.c + + point.rotation.set( + THREE.MathUtils.degToRad(item.tf[1][0]), + THREE.MathUtils.degToRad(item.tf[1][1]), + THREE.MathUtils.degToRad(item.tf[1][2]) + ) + + point.scale.set(item.tf[2][0], item.tf[2][1], item.tf[2][2]) + this.pointArray.push(point) + + this.afterLoadPoint(point) + return point + } + + /** + * 从 Json 某个 item 加载数据 + */ + override loadFromJson(item: ItemJson): undefined | THREE.Object3D { + if (item.a === 'gp') { + // gp 是为了分组而存在的 + const group = new THREE.Group() + group.name = item.name + group.uuid = item.id || THREE.MathUtils.generateUUID() + group.userData = _.cloneDeep(item.dt) || {} + group.userData.type = item.t + group.userData.actionType = item.a + group.userData.label = item.l + group.userData.color = item.c + + this.afterLoadGroup(group) + return group + } + + // 其他情况都是 ln + else if (item.a === 'ln') { + const position = new THREE.Vector3( + item.tf[0][0], + item.tf[0][1], + item.tf[0][2] + ) + + return this.createPoint(position, item) + } + + console.error('ItemTypeLineBase.loadFromJson: Unsupported', item) + } + + + /** + * 被 DragControls 拖拽时触发这个方法 + */ + dragPointStart(viewport: Viewport, point: THREE.Mesh) { + console.log('dragPoint:', point) + this.dragViewport = viewport + this.dragPoint = point + const canvas = viewport.renderer.domElement + + // 收集相关联的线段和标签 + const relationPoints = new Set() + + // 找到与 point 有关联的节点, 无论是 a->b 还是 b->a 都需要收集 + findObject3DByCondition(viewport.scene, (targetObj: THREE.Object3D) => { + if (targetObj.uuid === point.uuid) { + return false + } + + // 无论 a->b 还是 b->a 都需要收集 + if (_.includes(targetObj.userData?.link, point.uuid) || _.includes(point.userData?.link, targetObj.uuid)) { + relationPoints.add(targetObj) + } + }) + + //@ts-ignore + this.relationPoints = Array.from(relationPoints) + + // 监听move事件 + pmFn = this.redrawMousemove.bind(this) + canvas.addEventListener('pointermove', pmFn) + } + + /** + * DragControls 拖拽过程中鼠标移动时触发这个方法 + */ + redrawMousemove(e: MouseEvent) { + if (!this.dragViewport || !this.dragPoint) return + + const updateLines: THREE.Object3D[] = [] + // 更新所有相关线段 + _.forEach(this.relationPoints, (targetPoint, idx) => { + if (targetPoint.uuid === this.dragPoint.uuid) { + return + } + + // 判断谁是起点,谁是终点 + let startPoint: THREE.Object3D + let endPoint: THREE.Object3D + if (_.includes(targetPoint.userData?.link, this.dragPoint.uuid)) { + startPoint = targetPoint + endPoint = this.dragPoint + + } else { + startPoint = this.dragPoint + endPoint = targetPoint + } + + // 找到 startPoint 与 this.dragPoint 之间的线段 + const line = findObject3DByCondition(this.dragViewport.scene, (obj) => { + return obj.userData.lineStartId === startPoint.uuid && obj.userData.lineEndId === endPoint.uuid + })[0] as THREE.Line + + if (!line) { + // line = this.createLine(this.dragViewport.scene, startPoint, endPoint) + console.warn('Line not found between points:', startPoint.uuid, endPoint.uuid) + debugger + } + + // 更新线段几何体 + const geometry = line.geometry as THREE.BufferGeometry + geometry.setFromPoints([startPoint.position, endPoint.position]) + geometry.attributes.position.needsUpdate = true + this.afterUpdateLine(line, startPoint, endPoint) + + updateLines.push(line) + }) + + this.afterUpdatePoint(this.dragPoint, updateLines) + } + + /** + * DragControls 拖拽完成后触发这个方法 + */ + dragPointComplete(viewport: Viewport) { + // 删除鼠标事件监听 + viewport.renderer.domElement.removeEventListener('pointermove', pmFn) + pmFn = undefined + } +} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeLineBase.ts b/src/model/itemType/ItemTypeLineBase.ts deleted file mode 100644 index 2f49dd6..0000000 --- a/src/model/itemType/ItemTypeLineBase.ts +++ /dev/null @@ -1,238 +0,0 @@ -import * as THREE from 'three' -import ItemTypeBase from '@/model/itemType/ItemTypeBase.ts' -import type { ItemJson } from '@/model/itemType/ItemTypeDefine.ts' -import type WorldModel from '@/model/WorldModel.ts' -import type Viewport from '@/designer/Viewport.ts' -import { findObject3DByCondition, findObject3DById } from '@/model/ModelUtils.ts' - -let pmFn - -/** - * ILineType 接口定义了线类型的基本方法 - * 用于创建点和线, 以及处理拖拽事件, 以及交互事件 - */ -export default abstract class ItemTypeLineBase extends ItemTypeBase { - private relationPoints: THREE.Mesh[] = [] - private dragViewport: Viewport | undefined - private dragPoint: THREE.Mesh | undefined - - abstract createPointBasic(position: THREE.Vector3): THREE.Object3D - - abstract createLineBasic(): THREE.Line - - public init(worldModel: WorldModel) { - return super.init(worldModel).then(() => { - }) - } - - afterCreateLine(line: THREE.Line, startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { - } - - afterUpdateLine(line: THREE.Line, startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { - } - - createLine(scene: THREE.Scene, startPoint: THREE.Object3D, endPoint: THREE.Object3D): THREE.Line { - const line = this.createLineBasic() - const geom = line.geometry - geom.setFromPoints([startPoint.position, endPoint.position]) - - if (!line.userData) { - line.userData = {} - } - line.userData.lineStartId = startPoint.uuid - line.userData.lineEndId = endPoint.uuid - - if (startPoint.parent) { - startPoint.parent.add(line) - } else { - scene.add(line) - } - - if (!startPoint.userData.lines) { - startPoint.userData.lines = [] - } - startPoint.userData.lines.push(line.uuid) - - if (!endPoint.userData.lines) { - endPoint.userData.lines = [] - } - endPoint.userData.lines.push(line.uuid) - - this.afterCreateLine(line, startPoint, endPoint) - - return line - } - - /** - * 所有点数据加载完成后,添加进场景之后,需要根据 link 数组创建连接线 - */ - afterAddScene(scene: THREE.Scene, objects: THREE.Object3D[]) { - super.afterAddScene(scene, objects) - - // 为所有的 pointArray 连接线 - for (let i = 0; i < this.pointArray.length; i++) { - const startPoint = this.pointArray[i] - - // 找到这个元素的 userData.link 数组 - const linkArray: string[] = startPoint.userData.link || [] - - for (let j = 0; j < linkArray.length; j++) { - const linkId = linkArray[j] - // 在 pointArray 中查找对应的点 - const endPoint = findObject3DById(scene, linkId) - if (!endPoint) { - console.warn('not found link point uuid=${}', linkId) - continue - } - - const line = this.createLine(scene, startPoint, endPoint) - } - } - } - - createPoint(position: THREE.Vector3, item: ItemJson): THREE.Object3D { - const point = this.createPointBasic(position) - point.name = item.name - point.uuid = item.id || THREE.MathUtils.generateUUID() - point.userData = _.cloneDeep(item.dt) || {} - point.userData.type = item.t - point.userData.actionType = item.a - point.userData.label = item.l - point.userData.color = item.c - - point.rotation.set( - THREE.MathUtils.degToRad(item.tf[1][0]), - THREE.MathUtils.degToRad(item.tf[1][1]), - THREE.MathUtils.degToRad(item.tf[1][2]) - ) - - point.scale.set(item.tf[2][0], item.tf[2][1], item.tf[2][2]) - this.pointArray.push(point) - - this.afterLoadPoint(point) - return point - } - - /** - * 从 Json 某个 item 加载数据 - */ - override loadFromJson(item: ItemJson): undefined | THREE.Object3D { - if (item.a === 'gp') { - // gp 是为了分组而存在的 - const group = new THREE.Group() - group.name = item.name - group.uuid = item.id || THREE.MathUtils.generateUUID() - group.userData = _.cloneDeep(item.dt) || {} - group.userData.type = item.t - group.userData.actionType = item.a - group.userData.label = item.l - group.userData.color = item.c - - this.afterLoadGroup(group) - return group - } - - // 其他情况都是 ln - else if (item.a === 'ln') { - const position = new THREE.Vector3( - item.tf[0][0], - item.tf[0][1], - item.tf[0][2] - ) - - return this.createPoint(position, item) - } - - console.error('ItemTypeLineBase.loadFromJson: Unsupported', item) - } - - - /** - * 被 DragControls 拖拽时触发这个方法 - */ - dragPointStart(viewport: Viewport, point: THREE.Mesh) { - console.log('dragPoint:', point) - this.dragViewport = viewport - this.dragPoint = point - const canvas = viewport.renderer.domElement - - // 收集相关联的线段和标签 - const relationPoints = new Set() - - // 找到与 point 有关联的节点, 无论是 a->b 还是 b->a 都需要收集 - findObject3DByCondition(viewport.scene, (targetObj: THREE.Object3D) => { - if (targetObj.uuid === point.uuid) { - return false - } - - // 无论 a->b 还是 b->a 都需要收集 - if (_.includes(targetObj.userData?.link, point.uuid) || _.includes(point.userData?.link, targetObj.uuid)) { - relationPoints.add(targetObj) - } - }) - - //@ts-ignore - this.relationPoints = Array.from(relationPoints) - - // 监听move事件 - pmFn = this.redrawMousemove.bind(this) - canvas.addEventListener('pointermove', pmFn) - } - - /** - * DragControls 拖拽过程中鼠标移动时触发这个方法 - */ - redrawMousemove(e: MouseEvent) { - if (!this.dragViewport || !this.dragPoint) return - - const updateLines: THREE.Object3D[] = [] - // 更新所有相关线段 - _.forEach(this.relationPoints, (targetPoint, idx) => { - if (targetPoint.uuid === this.dragPoint.uuid) { - return - } - - // 判断谁是起点,谁是终点 - let startPoint: THREE.Object3D - let endPoint: THREE.Object3D - if (_.includes(targetPoint.userData?.link, this.dragPoint.uuid)) { - startPoint = targetPoint - endPoint = this.dragPoint - - } else { - startPoint = this.dragPoint - endPoint = targetPoint - } - - // 找到 startPoint 与 this.dragPoint 之间的线段 - const line = findObject3DByCondition(this.dragViewport.scene, (obj) => { - return obj.userData.lineStartId === startPoint.uuid && obj.userData.lineEndId === endPoint.uuid - })[0] as THREE.Line - - if (!line) { - // line = this.createLine(this.dragViewport.scene, startPoint, endPoint) - console.warn('Line not found between points:', startPoint.uuid, endPoint.uuid) - debugger - } - - // 更新线段几何体 - const geometry = line.geometry as THREE.BufferGeometry - geometry.setFromPoints([startPoint.position, endPoint.position]) - geometry.attributes.position.needsUpdate = true - this.afterUpdateLine(line, startPoint, endPoint) - - updateLines.push(line) - }) - - this.afterUpdatePoint(this.dragPoint, updateLines) - } - - /** - * DragControls 拖拽完成后触发这个方法 - */ - dragPointComplete(viewport: Viewport) { - // 删除鼠标事件监听 - viewport.renderer.domElement.removeEventListener('pointermove', pmFn) - pmFn = undefined - } -} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeLineToolbox.ts b/src/model/itemType/ItemTypeLineToolbox.ts deleted file mode 100644 index c6d783e..0000000 --- a/src/model/itemType/ItemTypeLineToolbox.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as THREE from 'three' -import type { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' -import ItemTypeBaseToolbox from '@/model/itemType/ItemTypeBaseToolbox.ts' -import type ItemTypeLineBase from '@/model/itemType/ItemTypeLineBase.ts' - -let pdFn, pmFn, puFn - -/** - * 线条工具箱 - */ -export default class ItemTypeLineToolbox extends ItemTypeBaseToolbox { - /** - * 临时线条 - */ - tempLine?: THREE.Line - - get itemType(): ItemTypeLineBase { - return this._itemType - } - - getTempPointName(): string { - return '_measure_temp_point' - } - - afterMoveTemplateLine(line: THREE.Line, startPoint: THREE.Object3D, endPoint: THREE.Object3D) { - } - - stop() { - super.stop() - - this.tempLine && this.viewport.scene.remove(this.tempLine) - this.tempLine = undefined - } - - afterAddPoint(startPoint: THREE.Object3D, point: THREE.Object3D) { - // 如果起始点存在,则将新点添加到起始点的链接中 - startPoint.userData.link.push(point.uuid) - this.itemType.createLine(this.viewport.scene, this.startPoint, point) - } - - mousemove(e: MouseEvent): THREE.Vector3 | undefined { - const point = super.mousemove(e) - if (!point) { - return - } - - // 移动时绘制临时线 - if (this.startPoint) { - // 获取最后一个点 - if (!this.tempLine) { - this.tempLine = this.itemType.createLineBasic() - this.viewport.scene.add(this.tempLine) - } - - const p0 = this.startPoint.position - const line = this.tempLine - const geom = line.geometry - geom.setFromPoints([p0, point]) - - this.afterMoveTemplateLine(line, this.startPoint, this.tempPointMarker) - } - } - - onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined { - const r = super.onMouseClicked(e) - if (!r) { - return - } - - this.tempLine && this.viewport.scene.remove(this.tempLine) - this.tempLine = undefined - return r - } -} \ No newline at end of file diff --git a/src/model/itemType/Toolbox.ts b/src/model/itemType/Toolbox.ts new file mode 100644 index 0000000..89e0c0f --- /dev/null +++ b/src/model/itemType/Toolbox.ts @@ -0,0 +1,286 @@ +import * as THREE from 'three' +import type Viewport from '@/designer/Viewport.ts' +import type ItemType from '@/model/itemType/ItemType.ts' +import type { ItemJson } from '@/model/itemType/ItemTypeDefine.ts' + +let pdFn, pmFn, puFn + +/** + * 单元类型工具箱 + */ +export default abstract class Toolbox { + /** + * 测量工具所在的视图窗口,从这里可以取到 所有 Three.js 相关的对象. + * 比如: + * - viewport.scene 场景 + * - viewport.renderer 渲染器 + * - viewport.controls 控制器 + * - viewport.camera 摄像机 + * - viewport.raycaster 射线投射器 + * - viewport.dragControl 拖拽控制器 + * - viewport.measure 测量工具 + */ + viewport: Viewport + + /** + * 是否完成工具箱操作 + */ + isCompleted = false + + /** + * 是否鼠标移动事件 + */ + mouseMoved = false + + /** + * 当前鼠标所在的画布, 对应 viewport.renderer.domElement + */ + canvas: HTMLCanvasElement + + /** + * 用于存储临时点 + */ + tempPointMarker?: THREE.Mesh + + /** + * 测量起始点 + */ + startPoint?: THREE.Object3D = undefined + + /** + * 保存上次点击时间,以便检测双击事件 + * @protected + */ + lastClickTime: number = 0 + + /** + * 上次鼠标移动位置 + */ + lastMovePosition: THREE.Vector3 | undefined = undefined + + _itemType: any + + get itemType(): ItemType { + return this._itemType + } + + mode: CursorMode + + static TMP_TYPE = '_TMP' + + /** + * 获取临时点的名称 + */ + abstract getTempPointName(): string + + addToScene(object: THREE.Object3D) { + this.viewport.scene.add(object) + } + + /** + * 单元工具初始化 + */ + init(viewport: any, itemType: ItemType): void { + this.viewport = viewport + this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement + this._itemType = itemType + } + + + /** + * 测量工具开始, 监听鼠标事件, 变量初始化等 + */ + start(startPoint?: THREE.Object3D) { + 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) + + this.isCompleted = false + this.viewport.viewerDom.style.cursor = 'crosshair' + + this.mode = this.viewport.state.cursorMode + this.startPoint = startPoint + + system.msg('新建 [' + this.itemType.name + '] 模式') + } + + + /** + * 停止测量模式, 清除所有临时点、线、标签. 停止所有鼠标事件监听 + */ + stop(): 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.tempPointMarker = undefined + } + + + /** + * 创建临时点标记 + */ + createTempPointMarker(position?: THREE.Vector3): THREE.Mesh { + const p = position + const scale = this.itemType.getDefaultScale() + const rotation = this.itemType.getDefaultRotation() + + 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.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) + } + + obj.name = this.getTempPointName() + obj.userData = { + mode: this.mode, + type: Toolbox.TMP_TYPE + } + return obj + } + + /** + * 鼠标按下事件 + */ + mousedown() { + this.mouseMoved = false + } + + /** + * 鼠标移动,创建对应的临时点与线 + */ + mousemove(e: MouseEvent): THREE.Vector3 | undefined { + if (this.isCompleted) return + + this.mouseMoved = true + + // 当前鼠标所在的点 + const point = this.viewport.getClosestIntersection(e) + if (!point) { + return + } + + 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 + } + + /** + * 鼠标松开事件 + */ + 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): THREE.Vector3 | undefined { + if (this.isCompleted) { + return + } + + // 获取鼠标点击位置的三维坐标 + const point = this.lastMovePosition + if (!point) { + return + } + + // 双击触发两次点击事件,我们需要避免这里的第二次点击 + const now = Date.now() + if (this.lastClickTime && (now - this.lastClickTime < 50)) { + return + } + this.lastClickTime = now + + const defaultScale = this.itemType.getDefaultScale() + const defaultRotation = this.itemType.getDefaultRotation() + + // 添加正式点 + const itemJson = { + t: this.itemType.name, + a: 'ln', + tf: [ + [point.x, point.y, point.z], + [defaultRotation.x, defaultRotation.y, defaultRotation.z], + [defaultScale.x, defaultScale.y, defaultScale.z] + ], + dt: { + link: [] as string[] + } + } as ItemJson + const marker = this.itemType.createPoint(point, itemJson) + this.addToScene(marker) + + // 把点加入拖拽控制器 + this.viewport.dragControl.setDragObjects([marker], 'push') + + if (this.startPoint) { + this.afterAddPoint(this.startPoint, marker) + } + + // 更新起始点为新添加的点 + this.startPoint = marker + + // 删除临时线 + this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) + this.tempPointMarker = undefined + + return point + } + + /** + * 当用户点击某个点,从临时点转换为正式点后调用 + * 子类可以重写此方法来处理添加点后的逻辑 + */ + afterAddPoint(startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { + // 默认实现不做任何操作 + // 子类可以重写此方法来处理添加点后的逻辑 + } + + /** + * 销毁测量工具, 当视图窗口被销毁时调用 + */ + destory() { + } +} \ No newline at end of file diff --git a/src/model/itemType/ToolboxLine.ts b/src/model/itemType/ToolboxLine.ts new file mode 100644 index 0000000..a399568 --- /dev/null +++ b/src/model/itemType/ToolboxLine.ts @@ -0,0 +1,71 @@ +import * as THREE from 'three' +import Toolbox from '@/model/itemType/Toolbox.ts' +import type ItemTypeLine from '@/model/itemType/ItemTypeLine.ts' + +/** + * 线条工具箱 + */ +export default class ToolboxLine extends Toolbox { + /** + * 临时线条 + */ + tempLine?: THREE.Line + + get itemType(): ItemTypeLine { + return this._itemType + } + + getTempPointName(): string { + return '_measure_temp_point' + } + + afterMoveTemplateLine(line: THREE.Line, startPoint: THREE.Object3D, endPoint: THREE.Object3D) { + } + + stop() { + super.stop() + + this.tempLine && this.viewport.scene.remove(this.tempLine) + this.tempLine = undefined + } + + afterAddPoint(startPoint: THREE.Object3D, point: THREE.Object3D) { + // 如果起始点存在,则将新点添加到起始点的链接中 + startPoint.userData.link.push(point.uuid) + this.itemType.createLine(this.viewport.scene, this.startPoint, point) + } + + mousemove(e: MouseEvent): THREE.Vector3 | undefined { + const point = super.mousemove(e) + if (!point) { + return + } + + // 移动时绘制临时线 + if (this.startPoint) { + // 获取最后一个点 + if (!this.tempLine) { + this.tempLine = this.itemType.createLineBasic() + this.viewport.scene.add(this.tempLine) + } + + const p0 = this.startPoint.position + const line = this.tempLine + const geom = line.geometry + geom.setFromPoints([p0, point]) + + this.afterMoveTemplateLine(line, this.startPoint, this.tempPointMarker) + } + } + + onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined { + const r = super.onMouseClicked(e) + if (!r) { + return + } + + this.tempLine && this.viewport.scene.remove(this.tempLine) + this.tempLine = undefined + return r + } +} \ No newline at end of file diff --git a/src/model/itemType/measure/Measure.ts b/src/model/itemType/measure/Measure.ts index 91ca50a..38fd467 100644 --- a/src/model/itemType/measure/Measure.ts +++ b/src/model/itemType/measure/Measure.ts @@ -1,16 +1,16 @@ import * as THREE from 'three' import { Material } from 'three' -import ItemTypeLineBase from '@/model/itemType/ItemTypeLineBase.ts' +import ItemTypeLine from '@/model/itemType/ItemTypeLine.ts' import WorldModel from '@/model/WorldModel.ts' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { numberToString } from '@/utils/webutils.ts' import { findObject3DById } from '@/model/ModelUtils.ts' import Viewport from '@/designer/Viewport.ts' -import ItemTypeLineToolbox from '@/model/itemType/ItemTypeLineToolbox.ts' +import ToolboxLine from '@/model/itemType/ToolboxLine.ts' import MeasureToolbox from '@/model/itemType/measure/MeasureToolbox.ts' -import ItemTypeBaseToolbox from '@/model/itemType/ItemTypeBaseToolbox.ts' +import Toolbox from '@/model/itemType/Toolbox.ts' -export default class Measure extends ItemTypeLineBase { +export default class Measure extends ItemTypeLine { /** * 当前测绘内容组, 所有测量点、线、标签都在这个组中. 但不包括临时点、线 */ @@ -59,7 +59,7 @@ export default class Measure extends ItemTypeLineBase { return [] } - createToolbox(viewport: Viewport): ItemTypeLineToolbox { + createToolbox(viewport: Viewport): ToolboxLine { const toolbox = new MeasureToolbox(this.group) toolbox.init(viewport, this) return toolbox @@ -178,7 +178,7 @@ export default class Measure extends ItemTypeLineBase { const obj = new CSS2DObject(div) obj.name = MeasureToolbox.TMP_LABEL_NAME obj.userData = { - type: ItemTypeBaseToolbox.TMP_TYPE + type: Toolbox.TMP_TYPE } return obj } diff --git a/src/model/itemType/measure/MeasureToolbox.ts b/src/model/itemType/measure/MeasureToolbox.ts index 59394a6..aa5d135 100644 --- a/src/model/itemType/measure/MeasureToolbox.ts +++ b/src/model/itemType/measure/MeasureToolbox.ts @@ -1,14 +1,14 @@ import * as THREE from 'three' -import ItemTypeLineToolbox from '@/model/itemType/ItemTypeLineToolbox.ts' +import ToolboxLine from '@/model/itemType/ToolboxLine.ts' import { numberToString } from '@/utils/webutils.ts' import type Measure from './Measure.ts' import type { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' -import ItemTypeBaseToolbox from '@/model/itemType/ItemTypeBaseToolbox.ts' +import Toolbox from '@/model/itemType/Toolbox.ts' /** * 测量工具箱,用于处理测量相关的操作 */ -export default class MeasureToolbox extends ItemTypeLineToolbox { +export default class MeasureToolbox extends ToolboxLine { group: THREE.Group @@ -71,7 +71,7 @@ export default class MeasureToolbox extends ItemTypeLineToolbox { this.tempLabel.name = MeasureToolbox.TMP_LABEL_NAME this.tempLabel.userData = { mode: this.mode, - type: ItemTypeBaseToolbox.TMP_TYPE + type: Toolbox.TMP_TYPE } this.viewport.scene.add(this.tempLabel) }