From e9a95face8678c380f81edbf54e377919c66494a Mon Sep 17 00:00:00 2001 From: yvan Date: Fri, 30 May 2025 01:05:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?SelectInspect=20=E9=80=89=E6=8B=A9=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/designer/Viewport.ts | 48 ++++---------- src/designer/model2DEditor/EsDragControls.ts | 4 ++ src/designer/model2DEditor/tools/ITool.ts | 4 +- .../model2DEditor/tools/MouseMoveInspect.ts | 3 + src/designer/model2DEditor/tools/SelectInspect.ts | 76 ++++++++++++++++++++++ src/model/itemType/ItemTypeLine.ts | 11 ++-- 6 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 src/designer/model2DEditor/tools/SelectInspect.ts diff --git a/src/designer/Viewport.ts b/src/designer/Viewport.ts index 5525ab9..05f1483 100644 --- a/src/designer/Viewport.ts +++ b/src/designer/Viewport.ts @@ -6,18 +6,16 @@ import Stats from 'three/examples/jsm/libs/stats.module' import type WorldModel from '@/model/WorldModel.ts' import $ from 'jquery' import { reactive, watch } from 'vue' -import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts' import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' 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 Toolbox from '@/model/itemType/Toolbox.ts' import { calcPositionUseSnap } from '@/model/ModelUtils.ts' -import textureUrl from '@/assets/images/conveyor/shapes/Belt1.png' -import { Line2 } from 'three/examples/jsm/lines/Line2.js' -import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' -import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' +import SelectInspect from '@/designer/model2DEditor/tools/SelectInspect.ts' +import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts' /** * 编辑器对象 @@ -41,7 +39,8 @@ export default class Viewport { toolStartObject: THREE.Object3D | null = null currentTool: Toolbox | null = null tools: ITool[] = [ - new MouseMoveInspect() + new MouseMoveInspect(), + new SelectInspect() ] toolbox: Record = {} @@ -63,6 +62,7 @@ export default class Viewport { currentFloor: '', isReady: false, cursorMode: 'normal', + selectedObject: null, camera: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } @@ -209,35 +209,8 @@ export default class Viewport { } this.updateGridVisibility() })) - this.watchList.push(watch(() => this.state.cursorMode, (newVal: CursorMode) => { - if (!this.state.isReady) { - return - } - if (this.currentTool) { - this.currentTool.stop() - this.currentTool = null - } - if (newVal === 'normal' || !newVal) { - this.dragControl.dragControls.enabled = true - return - } - - const currentTool = this.toolbox[newVal] - if (currentTool) { - // 选择标尺工具 - this.currentTool = currentTool - this.dragControl.dragControls.enabled = false - - } else { - system.showErrorDialog(`当前鼠标模式 ${newVal} 不支持`) - } - - if (this.currentTool) { - this.currentTool.start(this.toolStartObject) - this.toolStartObject = null - } - })) + // 监听窗口大小变化 if (this.resizeObserver) { this.resizeObserver.unobserve(this.viewerDom) } @@ -252,7 +225,7 @@ export default class Viewport { tool.init(this) } - + // 触发所有物品类型的 afterAddViewport 方法 _.forEach(getAllItemTypes(), (itemType: ItemTypeDefineOption) => { itemType.clazz.afterAddViewport(this) }) @@ -495,6 +468,11 @@ export interface ViewportState { cursorMode: CursorMode, /** + * 选中的对象 + */ + selectedObject: THREE.Object3D | null + + /** * 相机状态 */ camera: { diff --git a/src/designer/model2DEditor/EsDragControls.ts b/src/designer/model2DEditor/EsDragControls.ts index 24cfc12..1824455 100644 --- a/src/designer/model2DEditor/EsDragControls.ts +++ b/src/designer/model2DEditor/EsDragControls.ts @@ -4,6 +4,7 @@ import type Viewport from '@/designer/Viewport.ts' import Constract from '@/designer/Constract.ts' import { getItemTypeByName } from '@/runtime/DefineItemType.ts' import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' +import { markRaw } from 'vue' // dragControls 绑定函数 let dragStartFn, dragFn, dragEndFn, clickblankFn @@ -114,6 +115,9 @@ export default class EsDragControls { if (e.object.userData.onClick) { e.object.userData.onClick(e) } + if (e.object.userData.selectable) { + this.viewport.state.selectedObject = markRaw(e.object) + } } if (e.object.userData?.type) { diff --git a/src/designer/model2DEditor/tools/ITool.ts b/src/designer/model2DEditor/tools/ITool.ts index 0130e4c..20806e0 100644 --- a/src/designer/model2DEditor/tools/ITool.ts +++ b/src/designer/model2DEditor/tools/ITool.ts @@ -1,5 +1,7 @@ -export interface ITool { +export interface ITool { init(viewport: any): void destory(): void + + animate?: () => void; } \ No newline at end of file diff --git a/src/designer/model2DEditor/tools/MouseMoveInspect.ts b/src/designer/model2DEditor/tools/MouseMoveInspect.ts index bd03d9a..fc539be 100644 --- a/src/designer/model2DEditor/tools/MouseMoveInspect.ts +++ b/src/designer/model2DEditor/tools/MouseMoveInspect.ts @@ -70,4 +70,7 @@ export default class MouseMoveInspect implements ITool { } }, 1) + + animate(): void { + } } \ No newline at end of file diff --git a/src/designer/model2DEditor/tools/SelectInspect.ts b/src/designer/model2DEditor/tools/SelectInspect.ts new file mode 100644 index 0000000..c57264e --- /dev/null +++ b/src/designer/model2DEditor/tools/SelectInspect.ts @@ -0,0 +1,76 @@ +import * as THREE from 'three' +import type { ITool } from './ITool.ts' +import { watch } from 'vue' +import type Viewport from '@/designer/Viewport.ts' +import { Line2 } from 'three/examples/jsm/lines/Line2.js' +import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' +import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' + +/** + * 选择工具,用于在设计器中显示选中对象的包围盒 + */ +export default class SelectInspect implements ITool { + viewport: any + material: LineMaterial + selectionBox: Line2 + + constructor() { + // 颜色变成黄 + const color = 0xffff00 // 0xff0000 + const lineWidth = 2 + this.material = new LineMaterial({ color, linewidth: lineWidth }) + } + + init(viewport: Viewport) { + this.viewport = viewport + + this.viewport.watchList.push(watch(() => this.viewport.state.selectedObject, (selectedObject) => { + if (this.selectionBox) { + viewport.scene.remove(this.selectionBox) + this.selectionBox.geometry.dispose() + this.selectionBox = null + } + + const expandAmount = 0.2 // 扩展包围盒的大小 + if (selectedObject !== null) { + // 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb) + selectedObject.updateWorldMatrix(false, true) + + const box = new THREE.Box3().setFromObject(selectedObject) + box.expandByScalar(expandAmount) + + const size = new THREE.Vector3() + box.getSize(size) + + const center = new THREE.Vector3() + box.getCenter(center) + + // 创建包围盒几何体 + const helperGeometry = new THREE.BoxGeometry(size.x, size.y, size.z) + const edgesGeometry = new THREE.EdgesGeometry(helperGeometry) + + // 使用 LineGeometry 包装 edgesGeometry + const lineGeom = new LineGeometry() + //@ts-ignore + lineGeom.setPositions(edgesGeometry.attributes.position.array) + + const selectionBox = new Line2(lineGeom, this.material) + selectionBox.computeLineDistances() + selectionBox.position.copy(center) + selectionBox.name = 'selectionBox' + this.selectionBox = selectionBox + + this.viewport.scene.add(selectionBox) + } + })) + } + + destory() { + // 销毁选择工具 + if (this.selectionBox) { + this.viewport.scene.remove(this.selectionBox) + this.selectionBox.geometry.dispose() + this.selectionBox = null + } + } +} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeLine.ts b/src/model/itemType/ItemTypeLine.ts index 57cc6a5..bbdd634 100644 --- a/src/model/itemType/ItemTypeLine.ts +++ b/src/model/itemType/ItemTypeLine.ts @@ -105,10 +105,13 @@ export default abstract class ItemTypeLine extends ItemType { 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 + _.extend(point.userData, { + type: item.t, + actionType: item.a, + label: item.l, + color: item.c, + selectable: true + }) point.rotation.set( THREE.MathUtils.degToRad(item.tf[1][0]), From 07a095ee32a516e733d742c6351d7cc49bb3fe64 Mon Sep 17 00:00:00 2001 From: yvan Date: Fri, 30 May 2025 02:01:47 +0800 Subject: [PATCH 2/2] =?UTF-8?q?SelectInspect=20=E9=80=89=E6=8B=A9=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/designer/model2DEditor/tools/SelectInspect.ts | 143 ++++++++++++++++++++-- 1 file changed, 131 insertions(+), 12 deletions(-) diff --git a/src/designer/model2DEditor/tools/SelectInspect.ts b/src/designer/model2DEditor/tools/SelectInspect.ts index c57264e..36c93ec 100644 --- a/src/designer/model2DEditor/tools/SelectInspect.ts +++ b/src/designer/model2DEditor/tools/SelectInspect.ts @@ -6,30 +6,63 @@ import { Line2 } from 'three/examples/jsm/lines/Line2.js' import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' +let pdFn, pmFn, puFn + /** * 选择工具,用于在设计器中显示选中对象的包围盒 */ export default class SelectInspect implements ITool { - viewport: any - material: LineMaterial + viewport: Viewport + /** + * 线框材质,用于显示选中对象的包围盒 + */ + material: LineMaterial = new LineMaterial({ color: 0xffff00, linewidth: 2 }) + + /** + * 矩形材质,用于显示鼠标拖拽选择的矩形区域 + */ + rectMaterial: THREE.MeshBasicMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, + opacity: 0.5, + transparent: true + }) + + /** + * 当前选中对象的矩形选择框 + */ + rectangle: THREE.Mesh | null = null + + /** + * 当前选中对象的包围盒线框 + */ selectionBox: Line2 + /** + * 当前鼠标所在的画布, 对应 viewport.renderer.domElement + */ + canvas: HTMLCanvasElement + + /** + * 鼠标按下时记录的起始位置,用于绘制矩形选择框 + */ + recStartPos: THREE.Vector3 | null + constructor() { - // 颜色变成黄 - const color = 0xffff00 // 0xff0000 - const lineWidth = 2 - this.material = new LineMaterial({ color, linewidth: lineWidth }) } init(viewport: Viewport) { this.viewport = viewport + this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement + + pdFn = this.onMouseDown.bind(this) + this.canvas.addEventListener('pointerdown', pdFn) + pmFn = this.onMouseMove.bind(this) + this.canvas.addEventListener('pointermove', pmFn) + puFn = this.onMouseUp.bind(this) + this.canvas.addEventListener('pointerup', puFn) this.viewport.watchList.push(watch(() => this.viewport.state.selectedObject, (selectedObject) => { - if (this.selectionBox) { - viewport.scene.remove(this.selectionBox) - this.selectionBox.geometry.dispose() - this.selectionBox = null - } + this.disposeSelectionBox() const expandAmount = 0.2 // 扩展包围盒的大小 if (selectedObject !== null) { @@ -66,11 +99,97 @@ export default class SelectInspect implements ITool { } destory() { + + this.canvas.removeEventListener('pointerdown', pdFn) + pdFn = undefined + this.canvas.removeEventListener('pointermove', pmFn) + pmFn = undefined + this.canvas.removeEventListener('pointerup', puFn) + puFn = undefined + // 销毁选择工具 + this.disposeSelectionBox() + this.disposeRect() + } + + disposeSelectionBox() { if (this.selectionBox) { this.viewport.scene.remove(this.selectionBox) this.selectionBox.geometry.dispose() this.selectionBox = null } } -} \ No newline at end of file + + createRectangle() { + if (this.rectangle !== null) { + this.disposeRect() + } + if (this.recStartPos) { + // 创建矩形 + this.rectangle = new THREE.Mesh( + new THREE.PlaneGeometry(1, 1), + this.rectMaterial + ) + this.rectangle.name = 'selectRectangle' + this.rectangle.rotation.x = -Math.PI / 2 // 关键!让平面正对相机 + this.rectangle.position.set( + this.recStartPos.x, + this.recStartPos.y, + this.recStartPos.z + ) + this.viewport.scene.add(this.rectangle) + } + } + + updateRectangle(position: THREE.Vector3) { + if (!this.rectangle || !this.recStartPos) return + // console.log('updateRectangle', this.recStartPos, position) + + const width = position.x - this.recStartPos.x + const height = position.z - this.recStartPos.z + + const newWidth = Math.abs(width) + const newHeight = Math.abs(height) + + // 清理旧几何体 + this.rectangle.geometry.dispose() + this.rectangle.geometry = new THREE.PlaneGeometry(newWidth, newHeight) + this.rectangle.position.set( + this.recStartPos.x + width / 2, + this.recStartPos.y, + this.recStartPos.z + height / 2 + ) + } + + onMouseDown(event: MouseEvent) { + if (event.shiftKey) { + // 记录鼠标按下位置 + this.recStartPos = this.viewport.getClosestIntersection(event) + this.createRectangle() + } + } + + + onMouseMove(event: MouseEvent) { + if (!this.recStartPos) { + this.disposeRect() + } + // 更新矩形大小或重新生成矩形 + const position = this.viewport.getClosestIntersection(event) + if (!position) return + this.updateRectangle(position) + } + + disposeRect() { + if (this.rectangle !== null) { + this.viewport.scene.remove(this.rectangle) + this.rectangle.geometry.dispose() + this.rectangle = null + } + this.recStartPos = null + } + + onMouseUp(event: MouseEvent) { + this.disposeRect() + } +}