diff --git a/package.json b/package.json index c900836..0570df9 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@rolldown/pluginutils": "1.0.0-beta.8-commit.56abf23", "@tsconfig/node22": "^22.0.1", "@types/jquery": "^3.3.31", - "@types/lodash-es": "^4.17.7", + "@types/lodash": "^4.17.7", "@types/node": "^22.14.0", "@types/three": "^0.176.0", "@vicons/antd": "^0.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59fabd5..2e27d1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,9 +87,9 @@ importers: '@types/jquery': specifier: ^3.3.31 version: 3.5.32 - '@types/lodash-es': + '@types/lodash': specifier: ^4.17.7 - version: 4.17.12 + version: 4.17.17 '@types/node': specifier: ^22.14.0 version: 22.15.21 diff --git a/src/designer/Viewport.ts b/src/designer/Viewport.ts index 38cff37..f58d2bd 100644 --- a/src/designer/Viewport.ts +++ b/src/designer/Viewport.ts @@ -1,6 +1,5 @@ import _ from 'lodash' import * as THREE from 'three' -import { GridHelper, OrthographicCamera, Raycaster, Scene, Vector3, WebGLRenderer } from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import EsDragControls from './model2DEditor/EsDragControls' import Stats from 'three/examples/jsm/libs/stats.module' @@ -14,6 +13,7 @@ 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' /** * 编辑器对象 @@ -21,15 +21,15 @@ import type Toolbox from '@/model/itemType/Toolbox.ts' */ export default class Viewport { viewerDom: HTMLElement - scene: Scene - camera: OrthographicCamera - renderer: WebGLRenderer - axesHelper: GridHelper - gridHelper: GridHelper + scene: THREE.Scene + camera: THREE.OrthographicCamera + renderer: THREE.WebGLRenderer + axesHelper: THREE.GridHelper + gridHelper: THREE.GridHelper statsControls: Stats controls: OrbitControls worldModel: WorldModel - raycaster: Raycaster + raycaster: THREE.Raycaster dragControl: EsDragControls animationFrameId: any = null @@ -140,20 +140,30 @@ export default class Viewport { this.initMode2DCamera() // 辅助线 - const axesHelper = new THREE.GridHelper(1000, 2) - axesHelper.material.color.setHex(0x000000) + const gridOption = this.worldModel.gridOption + const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions) + axesHelper.material.color.setHex(gridOption.axesColor) axesHelper.material.linewidth = 2 + axesHelper.material.opacity = gridOption.gridOpacity + axesHelper.material.transparent = true + if (!gridOption.axesEnabled) { + axesHelper.visible = false + } + // @ts-ignore axesHelper.material.vertexColors = false this.axesHelper = axesHelper this.scene.add(this.axesHelper) - const gridHelper = new THREE.GridHelper(1000, 1000) - gridHelper.material.color.setHex(0x999999) - gridHelper.material.opacity = 0.8 + const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions) + gridHelper.material.color.setHex(gridOption.gridColor) + gridHelper.material.opacity = gridOption.gridOpacity gridHelper.material.transparent = true // @ts-ignore gridHelper.material.vertexColors = false + if (!gridOption.gridEnabled) { + gridHelper.visible = false + } this.gridHelper = gridHelper this.scene.add(this.gridHelper) @@ -224,7 +234,7 @@ export default class Viewport { this.resizeObserver.observe(this.viewerDom) // 初始化射线投射器 - this.raycaster = new Raycaster() + this.raycaster = new THREE.Raycaster() // 初始化所有常驻工具 for (const tool of this.tools) { @@ -356,6 +366,8 @@ export default class Viewport { } this.renderer.setSize(width, height) + this.css2DRenderer.setSize(width, height) + this.css3DRenderer.setSize(width, height) break } } @@ -425,15 +437,6 @@ export default class Viewport { } } - /** - * 获取鼠标在设计图上的坐标 - */ - getGridHelpAtPosition(param: { x: number; z: number }) { - const pickPosition = new THREE.Vector2(param.x, param.z) - this.raycaster.setFromCamera(pickPosition, this.camera) - return this.raycaster.intersectObject(this.gridHelper, false) - } - getIntersects(point: THREE.Vector2) { const mouse = new THREE.Vector2() mouse.set((point.x * 2) - 1, -(point.y * 2) + 1) @@ -443,7 +446,8 @@ export default class Viewport { } /** - * 获取按下对应三维位置 相对于 canvas 元素 (renderer.domElement) 元素 + * 获取鼠标所在的 x,y,z 位置。 + * 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的 */ getClosestIntersection(e: MouseEvent) { const _point = new THREE.Vector2() @@ -452,7 +456,9 @@ export default class Viewport { const intersects = this.getIntersects(_point) if (intersects && intersects.length > 2) { - return new Vector3(intersects[0].point.x, 0.1, intersects[1].point.z) + const point = new THREE.Vector3(intersects[0].point.x, 0.1, intersects[1].point.z) + + return calcPositionUseSnap(e, point) } return null } diff --git a/src/designer/model2DEditor/DragControls.js b/src/designer/model2DEditor/DragControls.js index 4aa6886..f8afa3d 100644 --- a/src/designer/model2DEditor/DragControls.js +++ b/src/designer/model2DEditor/DragControls.js @@ -1,211 +1,214 @@ import { - EventDispatcher, - Matrix4, - Plane, - Raycaster, - Vector2, - Vector3 -} from 'three'; - -const _plane = new Plane(); -const _raycaster = new Raycaster(); - -const _pointer = new Vector2(); -const _offset = new Vector3(); -const _intersection = new Vector3(); -const _worldPosition = new Vector3(); -const _inverseMatrix = new Matrix4(); + EventDispatcher, + Matrix4, + Plane, + Raycaster, + Vector2, + Vector3 +} from 'three' +import { calcPositionUseSnap } from '@/model/ModelUtils.js' + +const _plane = new Plane() +const _raycaster = new Raycaster() + +const _pointer = new Vector2() +const _offset = new Vector3() +const _intersection = new Vector3() +const _worldPosition = new Vector3() +const _inverseMatrix = new Matrix4() class DragControls extends EventDispatcher { - constructor( _objects, _camera, _domElement ) { - super(); + constructor(_objects, _camera, _domElement) { + super() - _domElement.style.touchAction = 'none'; // disable touch scroll + _domElement.style.touchAction = 'none' // disable touch scroll - let _selected = null, _hovered = null; + let _selected = null, _hovered = null - const _intersections = []; + const _intersections = [] - // + // - let isMove = false; + let isMove = false - const scope = this; + const scope = this - function activate() { - _domElement.addEventListener( 'pointermove', onPointerMove ); - _domElement.addEventListener( 'pointerdown', onPointerDown ); - _domElement.addEventListener( 'pointerup', onPointerCancel ); - _domElement.addEventListener( 'pointerleave', onPointerCancel ); - } + function activate() { + _domElement.addEventListener('pointermove', onPointerMove) + _domElement.addEventListener('pointerdown', onPointerDown) + _domElement.addEventListener('pointerup', onPointerCancel) + _domElement.addEventListener('pointerleave', onPointerCancel) + } - function deactivate() { - _domElement.removeEventListener( 'pointermove', onPointerMove ); - _domElement.removeEventListener( 'pointerdown', onPointerDown ); - _domElement.removeEventListener( 'pointerup', onPointerCancel ); - _domElement.removeEventListener( 'pointerleave', onPointerCancel ); + function deactivate() { + _domElement.removeEventListener('pointermove', onPointerMove) + _domElement.removeEventListener('pointerdown', onPointerDown) + _domElement.removeEventListener('pointerup', onPointerCancel) + _domElement.removeEventListener('pointerleave', onPointerCancel) - _domElement.style.cursor = ''; - } + _domElement.style.cursor = '' + } - function dispose() { - deactivate(); - } + function dispose() { + deactivate() + } - function setObjects( objects ) { - _objects = objects; - } + function setObjects(objects) { + _objects = objects + } - function getObjects() { - return _objects; - } + function getObjects() { + return _objects + } - function getRaycaster() { - return _raycaster; - } + function getRaycaster() { + return _raycaster + } - function onPointerMove( event ) { + function onPointerMove(event) { - if ( !scope.enabled || !scope.enabledMove) return; + if (!scope.enabled || !scope.enabledMove) return - isMove = true; + isMove = true - updatePointer( event ); + updatePointer(event) - _raycaster.setFromCamera( _pointer, _camera ); + _raycaster.setFromCamera(_pointer, _camera) - if ( _selected ) { + if (_selected) { - if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { + if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) ); + const pos = _intersection.sub(_offset).applyMatrix4(_inverseMatrix) + const newIntersection = calcPositionUseSnap(event, pos) + _selected.position.copy(newIntersection) - } + } - scope.dispatchEvent( { type: 'drag', object: _selected } ); + scope.dispatchEvent({ type: 'drag', object: _selected }) - return; + return - } + } - // hover support + // hover support - if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) { + if (event.pointerType === 'mouse' || event.pointerType === 'pen') { - _intersections.length = 0; + _intersections.length = 0 - _raycaster.setFromCamera( _pointer, _camera ); - _raycaster.intersectObjects( _objects, true, _intersections ); + _raycaster.setFromCamera(_pointer, _camera) + _raycaster.intersectObjects(_objects, true, _intersections) - if ( _intersections.length > 0 ) { + if (_intersections.length > 0) { - const object = _intersections[ 0 ].object; + const object = _intersections[0].object - _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) ); + _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(object.matrixWorld)) - if ( _hovered !== object && _hovered !== null ) { + if (_hovered !== object && _hovered !== null) { - scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); + scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) - _domElement.style.cursor = 'auto'; - _hovered = null; + _domElement.style.cursor = 'auto' + _hovered = null - } + } - if ( _hovered !== object ) { + if (_hovered !== object) { - scope.dispatchEvent( { type: 'hoveron', object: object } ); + scope.dispatchEvent({ type: 'hoveron', object: object }) - _domElement.style.cursor = 'pointer'; - _hovered = object; + _domElement.style.cursor = 'pointer' + _hovered = object - } + } - } else { + } else { - if ( _hovered !== null ) { + if (_hovered !== null) { - scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); + scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) - _domElement.style.cursor = 'auto'; - _hovered = null; + _domElement.style.cursor = 'auto' + _hovered = null - } + } - } + } - } + } - } + } - function onPointerDown( event ) { - if (scope.enabled === false) return; + function onPointerDown(event) { + if (scope.enabled === false) return - updatePointer(event); + updatePointer(event) - _intersections.length = 0; + _intersections.length = 0 - _raycaster.setFromCamera( _pointer, _camera ); - let objects = _objects; + _raycaster.setFromCamera(_pointer, _camera) + let objects = _objects - _raycaster.intersectObjects( objects, true, _intersections ); + _raycaster.intersectObjects(objects, true, _intersections) - if (_intersections.length > 0) { - _selected = (scope.transformGroup === true) ? _objects[ 0 ] : _intersections[0].object; + if (_intersections.length > 0) { + _selected = (scope.transformGroup === true) ? _objects[0] : _intersections[0].object - if(scope.enabledMove) { - _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld)); - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _inverseMatrix.copy(_selected.parent.matrixWorld).invert(); - _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld)); - } + if (scope.enabledMove) { + _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld)) + if (_raycaster.ray.intersectPlane(_plane, _intersection)) { + _inverseMatrix.copy(_selected.parent.matrixWorld).invert() + _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld)) + } - _domElement.style.cursor = 'move'; - } - scope.dispatchEvent( { type: 'dragstart', object: _selected,e:event } ); - } + _domElement.style.cursor = 'move' + } + scope.dispatchEvent({ type: 'dragstart', object: _selected, e: event }) + } - isMove = false; - } + isMove = false + } - function onPointerCancel(event) { - if ( scope.enabled === false ) return; + function onPointerCancel(event) { + if (scope.enabled === false) return - if ( _selected ) { - scope.dispatchEvent( { type: 'dragend', object: _selected,e:event } ); - _selected = null; - }else if(!isMove){ - // 添加点击空白处的事件 - scope.dispatchEvent( { type: 'clickblank',e:event } ); - } + if (_selected) { + scope.dispatchEvent({ type: 'dragend', object: _selected, e: event }) + _selected = null + } else if (!isMove) { + // 添加点击空白处的事件 + scope.dispatchEvent({ type: 'clickblank', e: event }) + } - _domElement.style.cursor = _hovered ? 'pointer' : 'auto'; - } + _domElement.style.cursor = _hovered ? 'pointer' : 'auto' + } - function updatePointer( event ) { - const rect = _domElement.getBoundingClientRect(); + function updatePointer(event) { + const rect = _domElement.getBoundingClientRect() - _pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1; - _pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1; - } + _pointer.x = (event.clientX - rect.left) / rect.width * 2 - 1 + _pointer.y = -(event.clientY - rect.top) / rect.height * 2 + 1 + } - activate(); + activate() - // API + // API - this.enabled = true; - this.enabledMove = true; - this.transformGroup = false; + this.enabled = true + this.enabledMove = true + this.transformGroup = false - this.activate = activate; - this.deactivate = deactivate; - this.dispose = dispose; - this.setObjects = setObjects; - this.getObjects = getObjects; - this.getRaycaster = getRaycaster; + this.activate = activate + this.deactivate = deactivate + this.dispose = dispose + this.setObjects = setObjects + this.getObjects = getObjects + this.getRaycaster = getRaycaster - } + } } -export { DragControls }; +export { DragControls } diff --git a/src/designer/model2DEditor/tools/CursorTool.ts b/src/designer/model2DEditor/tools/CursorTool.ts deleted file mode 100644 index a7952de..0000000 --- a/src/designer/model2DEditor/tools/CursorTool.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface ICursorTool { - - /** - * 初始化工具 - */ - init(viewport: any): void - - /** - * 开始运行工具, 进入 cursor 状态 - */ - start(): void - - /** - * 停止运行工具, 鼠标点击右键, 或者 ESC 退出 cursor 状态 - */ - stop(): void - - /** - * 销毁工具, 释放资源 - */ - destory():void -} \ No newline at end of file diff --git a/src/designer/model2DEditor/tools/MeasureTool.ts b/src/designer/model2DEditor/tools/MeasureTool.ts deleted file mode 100644 index 901db58..0000000 --- a/src/designer/model2DEditor/tools/MeasureTool.ts +++ /dev/null @@ -1,321 +0,0 @@ -import * as THREE from 'three' -import Viewport from '@/designer/Viewport.ts' -import { getUnitString, numberToString } from '@/utils/webutils' -import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' -import Constract from '@/designer/Constract.ts' -import type { ICursorTool } from '@/designer/model2DEditor/tools/CursorTool.ts' -import type Measure from '@/model/itemType/measure/Measure.ts' -import { getItemTypeByName } from '@/runtime/DefineItemType.ts' -import { LINE_NAME } from '@/model/itemType/measure/Measure.ts' -import type { ItemJson } from '@/model/itemType/ItemTypeDefine.ts' - -let pdFn, pmFn, puFn, kdFn - -const TEMP_POINT_NAME = '_measure_temp_point' -const TMP_LABEL_NAME = '_measure_temp_label' -const TMP_TYPE = '_TMP' - -/** - * 用于在 threejs 中创建一系列的 object_for_measure 点,并标记他的距离 - */ -export default class MeasureTool implements ICursorTool { - /** - * 测量工具所在的视图窗口,从这里可以取到 所有 Three.js 相关的对象. - * 比如: - * - viewport.scene 场景 - * - viewport.renderer 渲染器 - * - viewport.controls 控制器 - * - viewport.camera 摄像机 - * - viewport.raycaster 射线投射器 - * - viewport.dragControl 拖拽控制器 - * - viewport.measure 测量工具 - */ - viewport: Viewport - - /** - * 当前测绘内容组, 所有测量点、线、标签都在这个组中. 但不包括临时点、线 - */ - measure: Measure - - /** - * 是否完成测量 - */ - isCompleted = false - /** - * 是否鼠标移动事件 - */ - mouseMoved = false - - /** - * 当前鼠标所在的画布, 对应 viewport.renderer.domElement - */ - canvas: HTMLCanvasElement - - /** - * 用于存储临时点 - */ - tempPointMarker?: THREE.Mesh - - /** - * 临时线条 - */ - tempLine?: THREE.Line - - /** - * 临时标签对象, 用于在鼠标移动时显示距离 - */ - tempLabel?: CSS2DObject - - /** - * 保存上次点击时间,以便检测双击事件 - * @protected - */ - lastClickTime: number = 0 - - /** - * 测量起始点 - */ - startPoint?: THREE.Object3D = undefined - - /** - * 上次鼠标移动位置 - */ - lastMovePosition: THREE.Vector3 | undefined = undefined - - mode: CursorMode - - /** - * 测量工具初始化 - */ - init(viewport: Viewport) { - this.viewport = viewport - this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement - - this.measure = getItemTypeByName('measure')!.clazz - } - - /** - * 测量工具开始, 监听鼠标事件, 变量初始化等 - */ - 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('进入测量模式') - } - - /** - * 停止测量模式, 清除所有临时点、线、标签. 停止所有鼠标事件监听 - */ - 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.tempLine && this.viewport.scene.remove(this.tempLine) - this.tempLabel && this.viewport.scene.remove(this.tempLabel) - this.tempPointMarker = undefined - this.tempLine = undefined - this.tempLabel = undefined - } - - /** - * 销毁测量工具, 当视图窗口被销毁时调用 - */ - destory() { - } - - /** - * 鼠标按下事件 - */ - mousedown() { - this.mouseMoved = false - } - - /** - * 鼠标移动,创建对应的临时点与线 - */ - mousemove(e: MouseEvent) { - 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) - } - - // 移动时绘制临时线 - if (this.startPoint) { - // 获取最后一个点 - if (!this.tempLine) { - this.tempLine = this.measure.createLineBasic() - this.viewport.scene.add(this.tempLine) - } - - const p0 = this.startPoint.position - const line = this.tempLine - const geom = line.geometry - geom.setFromPoints([p0, point]) - - const dist = p0.distanceTo(point) - const label = `${numberToString(dist)} ${getUnitString(this.mode)}` - const position = new THREE.Vector3().addVectors(p0, point).multiplyScalar(0.5) - this.addOrUpdateTempLabel(label, position) - } - - // 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) - } - } - } - - private onMouseClicked(e: MouseEvent) { - 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 itemJson = { - t: this.measure.name, - a: 'ln', - tf: [ - [point.x, point.y, point.z], - [this.measure.defaultRotation.x, this.measure.defaultRotation.y, this.measure.defaultRotation.z], - [this.measure.defaultScale.x, this.measure.defaultScale.y, this.measure.defaultScale.z] - ], - dt: { - link: [] as string[] - } - } as ItemJson - const marker = this.measure.createPoint(point, itemJson) - this.measure.group.add(marker) - - // 把点加入拖拽控制器 - this.viewport.dragControl.setDragObjects([marker], 'push') - - if (this.startPoint) { - // 如果起始点存在,则将新点添加到起始点的链接中 - this.startPoint.userData.link.push(marker.uuid) - this.measure.createLine(this.viewport.scene, this.startPoint, marker) - } - - // 更新起始点为新添加的点 - this.startPoint = marker - - // 删除临时线 - this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) - this.tempPointMarker = undefined - - this.tempLabel && this.viewport.scene.remove(this.tempLabel) - this.tempLabel = undefined - - this.tempLine && this.viewport.scene.remove(this.tempLabel) - this.tempLine = undefined - } - - /** - * 创建点标记 - */ - createTempPointMarker(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 = TEMP_POINT_NAME - obj.userData = { - mode: this.mode, - type: TMP_TYPE - } - return obj - } - - - /** - * 添加或更新临时标签和位置 - */ - addOrUpdateTempLabel(label: string, position: THREE.Vector3) { - if (!this.tempLabel) { - this.tempLabel = this.measure.createLabel(label) - this.tempLabel.name = TMP_LABEL_NAME - this.tempLabel.userData = { - mode: this.mode, - type: TMP_TYPE - } - this.viewport.scene.add(this.tempLabel) - } - this.tempLabel.position.set(position.x, position.y, position.z) - this.tempLabel.element.innerHTML = label - } - - -} \ No newline at end of file diff --git a/src/model/ModelUtils.ts b/src/model/ModelUtils.ts index 55c1177..683f16f 100644 --- a/src/model/ModelUtils.ts +++ b/src/model/ModelUtils.ts @@ -1,8 +1,29 @@ import * as THREE from 'three' -import type { ItemJson, ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' +import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' +import type { ItemJson } from '@/model/WorldModelType.ts' import { getAllItemTypes, getItemTypeByName } from '@/runtime/DefineItemType.ts' /** + * 考虑吸附的情况下计算鼠标事件位置 + */ +export function calcPositionUseSnap(e: MouseEvent, point: THREE.Vector3) { + // 按下 ctrl 键,不启用吸附,其他情况启用吸附 + const gridOption = worldModel.gridOption + if (!e.ctrlKey && !e.metaKey) { + if (gridOption.snapEnabled && gridOption.snapDistance > 0) { + // 启用吸附, 针对 point 的 x 和 z 坐标进行吸附, 吸附距离为 gridOption.snapDistance + const snapDistance = gridOption.snapDistance + const newPoint = new THREE.Vector3(point.x, point.y, point.z) + newPoint.x = Math.round(newPoint.x / snapDistance) * snapDistance + newPoint.z = Math.round(newPoint.z / snapDistance) * snapDistance + return newPoint + } + } + + return point +} + +/** * 在给定的场景中查找具有指定 uuid 的 Object3D 对象 */ export function findObject3DById(scene: THREE.Object3D, uuid: string): THREE.Object3D | undefined { diff --git a/src/model/WorldModel.ts b/src/model/WorldModel.ts index 7f2a530..11829a8 100644 --- a/src/model/WorldModel.ts +++ b/src/model/WorldModel.ts @@ -7,6 +7,7 @@ import type Viewport from '@/designer/Viewport.ts' import { loadSceneFromJson } from '@/model/ModelUtils.ts' import MeasureMeta from './itemType/measure/MeasureMeta' +import type { IGridHelper } from '@/model/WorldModelType.ts' /** * 世界模型 @@ -28,6 +29,24 @@ export default class WorldModel { sceneMap = new Map() viewPorts: Viewport[] = [] + get gridOption(): IGridHelper { + const data = _.get(this.data, 'Tool.gridHelper') + return _.defaultsDeep(data, { + axesEnabled: true, + axesSize: 1000, + axesDivisions: 4, + axesColor: 0x000000, + axesOpacity: 1, + + gridEnabled: true, // 启用网格 + gridSize: 1000, // 网格大小, 单位米 + gridDivisions: 1000, // 网格分割数 + gridColor: 0x999999, // 网格颜色, 十六进制颜色值 + gridOpacity: 0.8, // 网格透明度 + snapEnabled: true, // 启用吸附 + snapDistance: 0.25 // 吸附距离, 单位米 + }) + } constructor() { } diff --git a/src/model/WorldModelType.ts b/src/model/WorldModelType.ts new file mode 100644 index 0000000..77cf2a8 --- /dev/null +++ b/src/model/WorldModelType.ts @@ -0,0 +1,160 @@ +import type { ActionType } from '@/model/itemType/ItemTypeDefine.ts' + +export interface IGridHelper { + /** + * 启用坐标轴 + */ + axesEnabled: boolean; + /** + * 坐标轴大小, 单位米 + */ + axesSize: number; + /** + * 坐标轴分割数 + */ + axesDivisions: number; + /** + * 坐标轴颜色, 十六进制颜色值 + */ + axesColor: number; + /** + * 坐标轴透明度 + */ + axesOpacity: number; + + /** + * 启用网格 + */ + gridEnabled: boolean; + /** + * 网格大小, 单位米 + */ + gridSize: number; + /** + * 网格分割数 + */ + gridDivisions: number; + /** + * 网格颜色, 十六进制颜色值 + */ + gridColor: number; + /** + * 网格透明度 + */ + gridOpacity: number; + + /** + * 启用吸附 + */ + snapEnabled: boolean; + /** + * 吸附距离, 单位米 + */ + snapDistance: number; +} + + +/** + * 定义物体类型的元数据 + * 举例: + * { + * id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid + * t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 + * a: 'ln', // 交互类型, ln表示线点操作, pt 表示点操作 + * l: '测量1', // 标签名称, 显示用 + * c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 + * tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + * [-9.0, 0, -1.0], // 平移向量 position + * [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + * [0.25, 0.1, 0.25] // 缩放向量 scale + * ], + * dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + * link: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) + * center: [], // 物流关联对象(uuid) + * in: [], // 物流入方向关联的对象(uuid) + * out: [] // 物流出方向关联的对象(uuid) + * } + * } + */ +export interface ItemJson { + /** + * 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一 + */ + name?: string + + /** + * 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一 + */ + id?: string + + /** + * 物体类型, 对应 defineItemType.name + */ + t: string + + /** + * 交互类型 + */ + a: ActionType + + /** + * 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中 + */ + l?: string + + /** + * 颜色, 最后初始化到 three.js 的 userData.color 中 + */ + c?: string + + /** + * 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + */ + tf: [ + /** + * 平移向量 position, 三维坐标 + */ + [number, number, number], + /** + * 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + */ + [number, number, number], + /** + * 缩放向量 scale, 三维缩放比例 + */ + [number, number, number], + ] + + /** + * 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + */ + dt: { + /** + * 测量线段关联的点对象(uuid), 仅在 a='ln' 时有效 + */ + link?: string[] + + /** + * 物流关联对象(uuid) + */ + center?: string[] + /** + * 物流入方向关联的对象(uuid) + */ + in?: string[] + /** + * 物流出方向关联的对象(uuid) + */ + out?: string[] + + /** + * 其他自定义数据, 可以存储任何数据 + */ + [key: string]: any + }, + + /** + * 子元素, 用于分组等, 可以为空数组 + */ + items: ItemJson[] +} \ No newline at end of file diff --git a/src/model/example1.js b/src/model/example1.js index 9f10289..9273172 100644 --- a/src/model/example1.js +++ b/src/model/example1.js @@ -11,7 +11,23 @@ export default { { name: 'OnReset', fn: '' }, { name: 'OnStart', fn: '' }, { name: 'OnStop', fn: '' } - ] + ], + gridHelper: { + axesEnabled: true, + axesSize: 1000, + axesDivisions: 4, + axesColor: 0x000000, + axesOpacity: 1, + + gridEnabled: true, + gridSize: 1000, + gridDivisions: 1000, + gridColor: 0x999999, + gridOpacity: 0.8, + + snapEnabled: true, + snapDistance: 0.25 + } }, items: [ { diff --git a/src/model/itemType/ItemTypeDefine.ts b/src/model/itemType/ItemTypeDefine.ts index fcfa569..a2f2125 100644 --- a/src/model/itemType/ItemTypeDefine.ts +++ b/src/model/itemType/ItemTypeDefine.ts @@ -24,108 +24,3 @@ export interface ItemTypeDefineOption { actionType: ActionType clazz: ItemType } - -/** - * 定义物体类型的元数据 - * 举例: - * { - * id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid - * t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 - * a: 'ln', // 交互类型, ln表示线点操作, pt 表示点操作 - * l: '测量1', // 标签名称, 显示用 - * c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 - * tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 - * [-9.0, 0, -1.0], // 平移向量 position - * [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 - * [0.25, 0.1, 0.25] // 缩放向量 scale - * ], - * dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 - * link: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) - * center: [], // 物流关联对象(uuid) - * in: [], // 物流入方向关联的对象(uuid) - * out: [] // 物流出方向关联的对象(uuid) - * } - * } - */ -export interface ItemJson { - /** - * 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一 - */ - name?: string - - /** - * 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一 - */ - id?: string - - /** - * 物体类型, 对应 defineItemType.name - */ - t: string - - /** - * 交互类型 - */ - a: ActionType - - /** - * 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中 - */ - l?: string - - /** - * 颜色, 最后初始化到 three.js 的 userData.color 中 - */ - c?: string - - /** - * 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 - */ - tf: [ - /** - * 平移向量 position, 三维坐标 - */ - [number, number, number], - /** - * 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 - */ - [number, number, number], - /** - * 缩放向量 scale, 三维缩放比例 - */ - [number, number, number], - ] - - /** - * 用户数据, 可自定义, 一般用在 three.js 的 userData 中 - */ - dt: { - /** - * 测量线段关联的点对象(uuid), 仅在 a='ln' 时有效 - */ - link?: string[] - - /** - * 物流关联对象(uuid) - */ - center?: string[] - /** - * 物流入方向关联的对象(uuid) - */ - in?: string[] - /** - * 物流出方向关联的对象(uuid) - */ - out?: string[] - - /** - * 其他自定义数据, 可以存储任何数据 - */ - [key: string]: any - }, - - /** - * 子元素, 用于分组等, 可以为空数组 - */ - items: ItemJson[] -} \ No newline at end of file diff --git a/src/model/itemType/Toolbox.ts b/src/model/itemType/Toolbox.ts index 89e0c0f..00b88a2 100644 --- a/src/model/itemType/Toolbox.ts +++ b/src/model/itemType/Toolbox.ts @@ -1,7 +1,7 @@ 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' +import type { ItemJson } from '@/model/WorldModelType.ts' let pdFn, pmFn, puFn diff --git a/tsconfig.app.json b/tsconfig.app.json index d001f13..b531cee 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -39,6 +39,7 @@ ] }, "types": [ + "node_modules/@types/lodash-es", ] }, "extends": "@vue/tsconfig/tsconfig.dom.json",