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..36c93ec --- /dev/null +++ b/src/designer/model2DEditor/tools/SelectInspect.ts @@ -0,0 +1,195 @@ +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' + +let pdFn, pmFn, puFn + +/** + * 选择工具,用于在设计器中显示选中对象的包围盒 + */ +export default class SelectInspect implements ITool { + 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() { + } + + 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) => { + this.disposeSelectionBox() + + 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() { + + 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 + } + } + + 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() + } +} 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]),