diff --git a/src/assets/Models/Pallet.glb b/src/assets/Models/Pallet.glb new file mode 100644 index 0000000..1ea32e4 Binary files /dev/null and b/src/assets/Models/Pallet.glb differ diff --git a/src/assets/Models/Pallet.jpg b/src/assets/Models/Pallet.jpg new file mode 100644 index 0000000..829dd2f Binary files /dev/null and b/src/assets/Models/Pallet.jpg differ diff --git a/src/assets/Models/Platistic1.jpg b/src/assets/Models/Platistic1.jpg new file mode 100644 index 0000000..682e4fb Binary files /dev/null and b/src/assets/Models/Platistic1.jpg differ diff --git a/src/core/manager/DragManager.ts b/src/core/manager/DragManager.ts new file mode 100644 index 0000000..6c6a770 --- /dev/null +++ b/src/core/manager/DragManager.ts @@ -0,0 +1,319 @@ +import * as THREE from 'three' +import type Viewport from '@/core/engine/Viewport.ts' +import { getClosestObject } from '@/core/ModelUtils.ts' +import EventBus from '@/runtime/EventBus.ts' +import type { Object3DLike } from '@/types/ModelTypes.ts' +import InstancePointManager, { PointManageWrap } from '@/core/manager/InstancePointManager.ts' +import { LineManageWrap } from '@/core/manager/LineSegmentManager.ts' + +/** + * ThreeJS 拖拽管理器(仅限 X/Z 平面) + */ +export default class DragControl { + private viewport: Viewport + private _is_enabled: boolean = true + private domElement: HTMLElement + private isPointerDown: boolean = false + private dragStartMouse: THREE.Vector2 = new THREE.Vector2() + private checkStateInterval: number | null = null + private dragShadowsGroup: THREE.Group | null = null + private dragDelayTimeout: number | null = null + + + private static readonly SHADOW_MATERIAL = new THREE.MeshBasicMaterial({ + color: 0x222222, + transparent: true, + opacity: 0.5, + depthWrite: false, + side: THREE.DoubleSide + }) + + public isDragging: boolean = false + + init(viewport: Viewport): void { + this.viewport = viewport + const domElement = this.viewport.renderer.domElement + this.domElement = domElement + + domElement.addEventListener('pointerdown', this.onPointerDown) + domElement.addEventListener('pointermove', this.onPointerMove) + domElement.addEventListener('pointerup', this.onPointerUp) + domElement.addEventListener('pointerleave', this.onPointerLeave) + + domElement.style.cursor = 'auto' + } + + + /** + * 卸载资源 + */ + dispose(): void { + if (this.domElement) { + this.domElement.removeEventListener('pointermove', this.onPointerMove) + this.domElement.removeEventListener('pointerdown', this.onPointerDown) + this.domElement.removeEventListener('pointerup', this.onPointerUp) + this.domElement.removeEventListener('pointerleave', this.onPointerLeave) + } + + this.cleanupDrag() + this.viewport = null + } + + /** + * pointerdown 事件处理 + */ + private onPointerDown = (event: PointerEvent): void => { + if (!this.enabled) return + + const mouse = this.getMousePosition(event.clientX, event.clientY) + const intersected = this.getIntersectedDraggableObject(mouse) + + if (!intersected) return + + this.viewport.controls.enabled = false + + // 设置定时器:0.1秒后才抓取拖拽 + this.dragDelayTimeout = window.setTimeout(() => { + this.isPointerDown = true + this.dragStartMouse.set(intersected.position.x, intersected.position.z) + + let selectedObjects = [intersected] + const multiSelected = this.viewport.state.multiSelectedObjects + if (multiSelected.length > 0 && multiSelected.includes(intersected)) { + selectedObjects = multiSelected + } + + this.createShadows(selectedObjects) + this.domElement.style.cursor = 'grabbing' + this.checkStateInterval = setInterval(() => { + if (isNaN(CurrentMouseInfo.x) || isNaN(CurrentMouseInfo.z) || !this.isPointerDown) { + this.cancelDrag() + } + }, 100) // 每100毫秒检查一次状态, 鼠标移出要主动清理 + + }, 100) // 0.1秒延迟抓取 + + } + + /** + * pointermove 事件处理 + */ + private onPointerMove = (event: PointerEvent): void => { + if (!this.enabled || !this.domElement) return + + if (!isNaN(this.dragStartMouse.x) && !isNaN(this.dragStartMouse.y) && this.dragShadowsGroup) { + this.isDragging = true + this.domElement.style.cursor = 'grabbing' + this.updateShadows(new THREE.Vector2(CurrentMouseInfo.x, CurrentMouseInfo.z)) + + } else { + // 射线方法修改 ========================== + const mouse = this.getMousePosition(event.clientX, event.clientY) + const intersected = this.getIntersectedDraggableObject(mouse) + // ===================================== + // const ids = this.viewport.itemFindManager.getItemsByPosition(CurrentMouseInfo.x, CurrentMouseInfo.z) + this.domElement.style.cursor = intersected ? 'grab' : 'auto' + } + } + + /** + * pointerup 事件处理 + */ + private onPointerUp = (event: PointerEvent): void => { + if (this.isDragging) { + const startPos = this.dragStartMouse.clone() + const targetPos = new THREE.Vector2(CurrentMouseInfo.x, CurrentMouseInfo.z) + + if (startPos && targetPos && !_.isNaN(startPos.x) && !_.isNaN(startPos.y)) { + this.dragComplete(startPos, targetPos) + } + } + + this.cleanupDrag() + } + + /** + * 清理拖拽状态 + */ + private cleanupDrag(): void { + if (this.domElement) { + this.domElement.style.cursor = 'auto' + } + + this.isDragging = false + this.isPointerDown = false + this.dragStartMouse.set(NaN, NaN) + + this.removeShadows() + + if (this.viewport) { + this.viewport.controls.enabled = true + } + + if (this.dragDelayTimeout !== null) { + clearTimeout(this.dragDelayTimeout) + this.dragDelayTimeout = null + } + + if (this.checkStateInterval) { + clearInterval(this.checkStateInterval) + this.checkStateInterval = null + } + } + + /** + * 获取当前鼠标坐标(归一化设备坐标) + */ + private getMousePosition(clientX: number, clientY: number): THREE.Vector2 { + const rect = this.domElement.getBoundingClientRect() + return new THREE.Vector2( + ((clientX - rect.left) / rect.width) * 2 - 1, + ((clientY - rect.top) / rect.height) * -2 + 1 + ) + } + + /** + * 射线检测,返回第一个可拖拽对象 + */ + private getIntersectedDraggableObject(mouse: THREE.Vector2): Object3DLike | undefined { + const raycaster = new THREE.Raycaster() + raycaster.setFromCamera(mouse, this.viewport.camera) + + const draggableObjects = this.viewport.entityManager._draggableObjects || [] + const intersects = raycaster.intersectObjects(draggableObjects, true) + + if (intersects.length > 0) { + return getClosestObject(this.viewport, intersects[0].object, intersects[0].instanceId) + } + } + + + /** + * 创建拖拽阴影 + */ + private createShadows(objects: Object3DLike[]): void { + this.removeShadows() + + this.dragShadowsGroup = new THREE.Group() + for (const obj of objects) { + if (_.isNil(_.get(obj, 'userData.entityId'))) { + console.error(`Object ${obj.name} missing entityId`) + continue + } + + // 克隆对象的几何体和材质 + // const shadowBox = obj.clone() + let box: THREE.Box3 + if (obj instanceof THREE.Object3D) { + box = new THREE.Box3().setFromObject(obj) + } else if (obj instanceof PointManageWrap) { + box = obj.createBox3() + } + const size = new THREE.Vector3() + box.getSize(size) + const geometry = new THREE.PlaneGeometry(size.x, size.z) + const shadowBox = new THREE.Mesh(geometry, DragControl.SHADOW_MATERIAL) + shadowBox.position.copy(obj.position) + shadowBox.rotation.x = -Math.PI / 2 + + shadowBox.userData = { + isShadow: true, + entityId: obj.userData.entityId, + originPosition: obj.position.clone() // 保存原始位置 + } + this.dragShadowsGroup.add(shadowBox) + } + this.viewport.scene.add(this.dragShadowsGroup) + } + + /** + * 移除阴影 + */ + private removeShadows(): void { + if (this.dragShadowsGroup) { + console.log('removeShadows') + this.viewport.scene.remove(this.dragShadowsGroup) + this.dragShadowsGroup = null + } + } + + /** + * 更新阴影位置(仅 X/Z 平面) + */ + private updateShadows(newPosition: THREE.Vector2): void { + if (!this.dragShadowsGroup) return + + // 计算新位置与拖拽开始位置的偏移量 + const offsetX = newPosition.x - this.dragStartMouse.x + const offsetZ = newPosition.y - this.dragStartMouse.y + + for (let i = 0; i < this.dragShadowsGroup.children.length; i++) { + const shadow = this.dragShadowsGroup.children[i] as THREE.Mesh + if (!shadow.userData.originPosition) { + console.error(`Shadow ${shadow.name} does not have originPosition`) + continue + } + + const newPosX = shadow.userData.originPosition.x + offsetX + const newPosZ = shadow.userData.originPosition.z + offsetZ + shadow.position.set(newPosX, shadow.userData.originPosition.y, newPosZ) // 锁定 Y 轴高度 + // console.log(`Updating shadow ${shadow.name} position with offset:`, offsetX, offsetZ) + } + } + + private dragComplete = (startPos: THREE.Vector2, targetPos: THREE.Vector2): void => { + // console.log(`Drag completed from ${startPos.toArray()} to ${targetPos.toArray()}`) + + this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + for (const object of this.dragShadowsGroup.children) { + const entityId = object.userData.entityId + if (entityId) { + const entity = getEntity(entityId) + // 更新实体位置 + entity.tf[0][0] = object.position.x + entity.tf[0][2] = object.position.z + putEntity(entity) + + } else { + system.showErrorDialog('not found entity') + } + } + }) + + // EventBus.dispatch('multiSelectedObjectsChanged', { + // multiSelectedObjects: this.viewport.state.multiSelectedObjects + // }) + // EventBus.dispatch('selectedObjectPropertyChanged', {}) + + this.cleanupDrag() + } + + /** + * pointerleave 事件处理 + */ + private onPointerLeave = (_event: PointerEvent): void => { + this.cancelDrag() + } + + /** + * 取消当前拖拽状态 + */ + cancelDrag(): void { + this.cleanupDrag() + } + + + /** + * 设置启用/禁用 + */ + public set enabled(value: boolean) { + this._is_enabled = value + if (!value) { + this.cleanupDrag() + } + } + + public get enabled(): boolean { + return this._is_enabled + } +} diff --git a/src/core/manager/MouseMoveManager.ts b/src/core/manager/MouseMoveManager.ts new file mode 100644 index 0000000..8117805 --- /dev/null +++ b/src/core/manager/MouseMoveManager.ts @@ -0,0 +1,83 @@ +import type Viewport from '@/core/engine/Viewport' +import * as THREE from 'three' + +let pmFn, otFn, lvFn + +/** + * 鼠标移动时,将鼠标位置的坐标转换为设计图上的坐标,并设置到 designer.mousePos 属性中 + */ +export default class MouseMoveManager { + viewport: Viewport + canvas: HTMLCanvasElement + + constructor() { + } + + init(viewport: Viewport) { + this.viewport = viewport + this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement + + pmFn = this.mouseMove.bind(this) + otFn = this.mouseLv.bind(this) + lvFn = this.mouseLv.bind(this) + this.canvas.addEventListener('pointermove', pmFn) + this.canvas.addEventListener('pointerout', otFn) + this.canvas.addEventListener('mouseleave', lvFn) + } + + dispose() { + this.canvas.removeEventListener('pointermove', pmFn) + pmFn = undefined + this.canvas.removeEventListener('pointerout', otFn) + otFn = undefined + this.canvas.removeEventListener('mouseleave', lvFn) + lvFn = undefined + } + + mouseLv(event: MouseEvent) { + this.viewport.state.mouse.x = NaN + this.viewport.state.mouse.z = NaN + window['CurrentMouseInfo'] = { + x: NaN, + z: NaN, + isShiftKey: false, + isCtrlKey: false, + isAltKey: false, + isMetaKey: false + } + } + + mouseMove = _.throttle(function(this: MouseMoveManager, event: MouseEvent) { + + const pointv = new THREE.Vector2() + pointv.x = event.offsetX / this.viewport.renderer.domElement.offsetWidth + pointv.y = event.offsetY / this.viewport.renderer.domElement.offsetHeight + + const mouse = new THREE.Vector2() + mouse.set((pointv.x * 2) - 1, -(pointv.y * 2) + 1) + + // 当前鼠标所在的点 + const point = this.viewport.getClosestIntersection(event) + if (!point) { + return + } + + this.viewport.state.mouse.x = point.x + this.viewport.state.mouse.z = point.z + + window['CurrentMouseInfo'] = { + viewport: this.viewport, + x: point.x, + z: point.z, + isShiftKey: event.shiftKey, + isCtrlKey: event.ctrlKey, + isAltKey: event.altKey, + isMetaKey: event.metaKey, + mouse: mouse + } + + }, 1) + + animate(): void { + } +} diff --git a/src/core/manager/SelectManager.ts b/src/core/manager/SelectManager.ts new file mode 100644 index 0000000..147bc47 --- /dev/null +++ b/src/core/manager/SelectManager.ts @@ -0,0 +1,436 @@ +import * as THREE from 'three' +import type Viewport from '@/core/engine/Viewport' +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 EventBus from '@/runtime/EventBus' +import { markRaw } from 'vue' +import { getSetter } from '@/core/manager/ModuleManager.ts' +import Constract from '@/core/Constract.ts' +import type { Object3DLike } from '@/types/ModelTypes.ts' +import { PointManageWrap } from '@/core/manager/InstancePointManager.ts' +import { getAABBox, getOBBox } from '@/core/ModelUtils.ts' + +/** + * 选择工具,用于在设计器中显示选中对象的包围盒 + */ +export default class SelectManager { + viewport: Viewport + /** + * 线框材质,用于显示选中对象的包围盒 + */ + yellowMaterial: LineMaterial = new LineMaterial({ color: 0xffff00, linewidth: 3 }) + + /** + * 线框材质,用于显示选中对象的包围盒 + */ + redMaterial: LineMaterial = new LineMaterial({ color: 0xff0000, linewidth: 3 }) + + /** + * 矩形材质,用于显示鼠标拖拽选择的矩形区域 + */ + rectMaterial: THREE.MeshBasicMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, + opacity: 0.3, + transparent: true + }) + + /** + * 当前选中对象的矩形选择框 + */ + rectangle: THREE.Mesh | null = null + + /** + * 当前选中对象的包围盒线框 + */ + selectionBox: Line2 + + /** + * 当前鼠标所在的画布, 对应 viewport.renderer.domElement + */ + canvas: HTMLCanvasElement + + /** + * 鼠标按下时记录的起始位置,用于绘制矩形选择框 + */ + recStartPos: THREE.Vector3 | null + + clickTime: number | null = null + + constructor() { + } + + init(viewport: Viewport) { + this.viewport = viewport + this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement + + // 监听 shift 按住之后的矩形 + this.canvas.addEventListener('pointerdown', this.onMouseDown) + this.canvas.addEventListener('pointermove', this.onMouseMove) + this.canvas.addEventListener('pointerup', this.onMouseUp) + + EventBus.on('selectedObjectChanged', this.updateSelectionBox) + EventBus.on('multiSelectedObjectsChanged', this.updateMultiSelectionBoxes) + EventBus.on('selectedObjectPropertyChanged', this.updateSelectionBox) + // EventBus.on('multiselectedObjectChanged', this.updateMultiSelectionBoxes) + } + + redSelectionGroup = new THREE.Group() + + updateMultiSelectionBoxes = () => { + const multiSelectedObjects = this.viewport.state.multiSelectedObjects + // 为所有多选对象创建包围盒线框 + this.clearRedSelectionBoxes() + + if (!multiSelectedObjects || multiSelectedObjects.length === 0) { + return + } + + for (const object of multiSelectedObjects) { + if (object.userData.entityId) { + this.createRedSelectionBox(object) + } + } + } + + // 取消红选 + cancelMultiSelect() { + this.viewport.state.multiSelectedObjects = [] + this.viewport.state.multiSelectedItems = [] + this.viewport.state.multiSelectedEntityIds = [] + EventBus.dispatch('multiSelectedObjectsChanged', { + viewport: markRaw(this.viewport), + multiSelectedObjects: [], + multiSelectedItems: [], + multiSelectedEntityIds: [] + }) + } + + // 取消黄选 + cancelSelect() { + this.viewport.state.selectedObject = null + this.viewport.state.selectedItem = null + this.viewport.state.selectedEntityId = null + this.viewport.state.selectedObjectSetter = null + EventBus.dispatch('selectedObjectChanged', { + viewport: markRaw(this.viewport), + selectedObject: null, + selectedItem: null, + selectedEntityId: null, + selectedObjectSetter: null + }) + } + + // 根据 entityId 选择对象 + selectById(entityId: string | null) { + if (!entityId) { + this.viewport.state.selectedObject = null + this.viewport.state.selectedItem = null + this.viewport.state.selectedEntityId = null + this.viewport.state.selectedObjectSetter = null + EventBus.dispatch('selectedObjectChanged', { + viewport: markRaw(this.viewport), + selectedObject: null, + selectedItem: null, + selectedEntityId: null, + selectedObjectSetter: null + }) + return + } + + const item = this.viewport.entityManager.findItemById(entityId) + const object = this.viewport.entityManager.findObjectById(entityId) + const itemTypeName = item.t + if (item.dt.protected !== true) { + this.viewport.state.selectedObject = markRaw(object) + this.viewport.state.selectedItem = markRaw(item) + this.viewport.state.selectedEntityId = entityId + this.viewport.state.selectedObjectSetter = getSetter(itemTypeName) + EventBus.dispatch('selectedObjectChanged', { + viewport: markRaw(this.viewport), + selectedObject: this.viewport.state.selectedObject, + selectedItem: this.viewport.state.selectedItem, + selectedEntityId: this.viewport.state.selectedEntityId, + selectedObjectSetter: this.viewport.state.selectedObjectSetter + }) + } + } + + // 多选对象 + multiSelectByIds(entityIds: string[]) { + // 遍历找到的对象,添加到多选对象中 + const multiSelectedObjects = [] + const multiSelectedItems = [] + const multiSelectedEntityIds = [] + for (const entityId of entityIds) { + const object = this.viewport.entityManager.findObjectById(entityId) + if (object.userData.entityId && object.userData.t) { + const item = this.viewport.entityManager.findItemById(object.userData.entityId) + if (item && item.dt.protected !== true) { + multiSelectedObjects.push(object) + multiSelectedItems.push(item) + multiSelectedEntityIds.push(object.userData.entityId) + } + } + } + + this.viewport.state.multiSelectedObjects = markRaw(multiSelectedObjects) + this.viewport.state.multiSelectedItems = markRaw(multiSelectedItems) + this.viewport.state.multiSelectedEntityIds = multiSelectedEntityIds + EventBus.dispatch('multiSelectedObjectsChanged', { + viewport: markRaw(this.viewport), + multiSelectedObjects: this.viewport.state.multiSelectedObjects, + multiSelectedItems: this.viewport.state.multiSelectedItems, + multiSelectedEntityIds: this.viewport.state.multiSelectedEntityIds + }) + } + + // 清除之前的红色包围盒线框 + private clearRedSelectionBoxes() { + if (this.redSelectionGroup.children.length > 0) { + for (const child of this.redSelectionGroup.children) { + this.redSelectionGroup.remove(child) + } + } + this.viewport.scene.remove(this.redSelectionGroup) + this.redSelectionGroup = new THREE.Group() + this.viewport.scene.add(this.redSelectionGroup) + } + + // 创建红选包围盒 + private createRedSelectionBox(object: Object3DLike) { + // 如果对象没有 entityId,则不创建包围盒线框 + if (!object.userData.entityId) { + return + } + let box: THREE.Box3 + if (object instanceof PointManageWrap) { + box = object.createBox3() + + } else if (object instanceof THREE.Object3D) { + box = new THREE.Box3().setFromObject(object) + } + + const min = box.min + const max = box.max + + const corners = [ + new THREE.Vector3(min.x - Constract.RED_EXPAND_AMOUNT, max.y + Constract.RED_EXPAND_AMOUNT, min.z - Constract.RED_EXPAND_AMOUNT), + new THREE.Vector3(max.x + Constract.RED_EXPAND_AMOUNT, max.y + Constract.RED_EXPAND_AMOUNT, min.z - Constract.RED_EXPAND_AMOUNT), + new THREE.Vector3(max.x + Constract.RED_EXPAND_AMOUNT, max.y + Constract.RED_EXPAND_AMOUNT, max.z + Constract.RED_EXPAND_AMOUNT), + new THREE.Vector3(min.x - Constract.RED_EXPAND_AMOUNT, max.y + Constract.RED_EXPAND_AMOUNT, max.z + Constract.RED_EXPAND_AMOUNT) + ] + + // 构建矩形边框(4 条边) + const positions = [] + for (let i = 0; i < 4; i++) { + const p1 = corners[i] + const p2 = corners[(i + 1) % 4] + positions.push(p1.x, p1.y, p1.z) + positions.push(p2.x, p2.y, p2.z) + } + + // 创建几何体 + const lineGeom = new LineGeometry() + const vertices = new Float32Array(positions) + lineGeom.setPositions(vertices) + + const selectionBox = new Line2(lineGeom, this.redMaterial) + selectionBox.computeLineDistances() + + this.redSelectionGroup.add(selectionBox) + } + + // 更新选中对象的包围盒线框 + private updateSelectionBox = () => { + this.clearSelectionBox() + + const item = this.viewport.state.selectedItem + if (!item) { + return + } + + // 构造 positions 数组 + const edgePositions = getOBBox(item) // getOBBox(item) // getAABBox(item) + const positions = [] + for (let i = 0; i < edgePositions.length; i += 2) { + const p1 = edgePositions[i] + const p2 = edgePositions[i + 1] + positions.push(p1.x, p1.y, p1.z) + positions.push(p2.x, p2.y, p2.z) + } + + const lineGeom = new LineGeometry().setPositions(new Float32Array(positions)) + const selectionBox = new Line2(lineGeom, this.yellowMaterial) + selectionBox.computeLineDistances() + selectionBox.scale.set(1, 1, 1) + this.selectionBox = selectionBox + + console.log('selectedItem', this.viewport.state.selectedItem) + + this.viewport.scene.add(selectionBox) + } + + dispose() { + this.canvas.removeEventListener('pointerdown', this.onMouseDown) + this.canvas.removeEventListener('pointermove', this.onMouseMove) + this.canvas.removeEventListener('pointerup', this.onMouseUp) + + // 销毁选择工具 + this.clearSelectionBox() + this.disposeRect() + this.clearRedSelectionBoxes() + } + + // 清除当前选中对象的包围盒线框 + private clearSelectionBox = () => { + if (this.selectionBox) { + this.viewport.scene.remove(this.selectionBox) + this.selectionBox.geometry.dispose() + this.selectionBox = null + } + } + + private createRectangle = () => { + if (this.rectangle !== null) { + this.disposeRect() + } + if (this.recStartPos) { + // 创建矩形 + this.rectangle = new THREE.Mesh( + new THREE.PlaneGeometry(0.001, 0.001), + 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) + } + } + + private 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 + ) + } + + private onMouseDown = (event: MouseEvent) => { + if (event.shiftKey) { + // 记录鼠标按下位置 + this.recStartPos = this.viewport.getClosestIntersection(event) + this.createRectangle() + this.viewport.controls.enabled = false // 禁用控制器 + + } else { + // 为 click 事件添加处理逻辑 + this.clickTime = Date.now() + } + } + + + private onMouseMove = (event: MouseEvent) => { + if (!this.recStartPos) { + this.disposeRect() + } + // 更新矩形大小或重新生成矩形 + const position = this.viewport.getClosestIntersection(event) + if (!position) return + this.updateRectangle(position) + } + + private onMouseUp = (event: MouseEvent) => { + this.disposeRect() + const clickTime = this.clickTime + this.clickTime = null + if (Date.now() - clickTime < 200) { + // 如果是点击事件,触发选中逻辑 + const objects: Object3DLike[] = this.viewport.entityManager.getObjectByCanvasMouse(event) + if (objects.length > 0 && objects[0]?.userData?.entityId && objects[0]?.userData?.createType !== 'line') { + console.log('mouseClick', objects) + const object = objects[0] + this.selectById(object.userData.entityId) + + } else { + // 如果没有选中任何对象,清除选中状态 + this.selectById(null) + } + } + } + + private disposeRect = () => { + if (this.rectangle !== null) { + // 查找在这个矩形内的所有有效业务对象,并将他们添加进 viewport.state.multiSelectedObjects + this.calcRectangleObjectToState() + this.viewport.scene.remove(this.rectangle) + this.rectangle.geometry.dispose() + this.rectangle = null + } + this.recStartPos = null + this.viewport.controls.enabled = true // 启用控制器 + } + + private calcRectangleObjectToState = () => { + if (!this.rectangle || !this.recStartPos) return + + // 获取矩形的包围盒 + const box = new THREE.Box3().setFromObject(this.rectangle) + + // 获取盒子的 startX, startZ, endX, endZ + const startX = box.min.x + const startZ = box.min.z + const endX = box.max.x + const endZ = box.max.z + + // 查找所有在矩形内的对象 + const ids = this.viewport.itemFindManager.getItemsByRect(startX, startZ, endX, endZ) + + // 清空之前的多选对象 + this.viewport.state.multiSelectedObjects = [] + + // 遍历找到的对象,添加到多选对象中 + const multiSelectedObjects = [] + const multiSelectedItems = [] + const multiSelectedEntityIds = [] + for (const id of ids) { + const object = this.viewport.entityManager.findObjectById(id) + if (object.userData.entityId && object.userData.t) { + const item = this.viewport.entityManager.findItemById(object.userData.entityId) + if (item && item.dt.protected !== true) { + multiSelectedObjects.push(object) + multiSelectedItems.push(item) + multiSelectedEntityIds.push(id) + } + } + } + + // 触发多选对象更新事件 + this.viewport.state.multiSelectedObjects = markRaw(multiSelectedObjects) + this.viewport.state.multiSelectedItems = markRaw(multiSelectedItems) + this.viewport.state.multiSelectedEntityIds = multiSelectedEntityIds + EventBus.dispatch('multiSelectedObjectsChanged', { + viewport: markRaw(this.viewport), + multiSelectedObjects: this.viewport.state.multiSelectedObjects, + multiSelectedItems: this.viewport.state.multiSelectedItems, + multiSelectedEntityIds: this.viewport.state.multiSelectedEntityIds + }) + } + +} diff --git a/src/modules/tote/ToteRenderer.ts b/src/modules/tote/ToteRenderer.ts index 9bdd40f..58c7682 100644 --- a/src/modules/tote/ToteRenderer.ts +++ b/src/modules/tote/ToteRenderer.ts @@ -5,7 +5,7 @@ import InstancePointManager from '@/core/manager/InstancePointManager.ts' import type { Object3DLike } from '@/types/ModelTypes.ts' import MODULE_3DS_File from '@/assets/Models/Tote.3ds?url' import MODULE_3DS_TEX from '@/assets/Models/ToteTex.png?url' -import { load3DModule, loadByUrl, loadTexture } from '@/core/ModelUtils.ts' +import { load3DModule, loadByUrl, loadTexture, processModel } from '@/core/ModelUtils.ts' /** * 货架货位渲染器 @@ -26,6 +26,50 @@ export default class PalletRenderer extends BaseRenderer { toteGeometry: THREE.BufferGeometry toteMaterial: THREE.Material + // + // processModel(mesh: THREE.Mesh) { + // const geometry = mesh.geometry.clone() as THREE.BufferGeometry + // geometry.rotateX(-Math.PI / 2) + // + // // 获取原始包围盒 + // //@ts-ignore + // const box = new THREE.Box3().setFromBufferAttribute(geometry.attributes.position) + // const size = new THREE.Vector3() + // box.getSize(size) + // + // // 计算缩放因子:让整个包围盒变成 1x1x1 的立方体 + // const scaleX = 1 / size.x + // const scaleY = 1 / size.y + // const scaleZ = 1 / size.z + // + // // 缩放几何体到 1x1x1 立方体(保持原点不变) + // geometry.scale(scaleX, scaleY, scaleZ) + // + // // 更新包围盒 + // //@ts-ignore + // const scaledBox = new THREE.Box3().setFromBufferAttribute(geometry.attributes.position) + // const center = new THREE.Vector3() + // scaledBox.getCenter(center) + // + // // 平移:让中心在 (0, height/2, 0),底部贴地 + // const translate = new THREE.Matrix4().makeTranslation( + // -center.x, + // -scaledBox.min.y, // 保证底部贴地 + // -center.z + // ) + // geometry.applyMatrix4(translate) + // + // // 可选:验证最终包围盒 + // //@ts-ignore + // const finalBox = new THREE.Box3().setFromBufferAttribute(geometry.attributes.position) + // const finalSize = new THREE.Vector3() + // finalBox.getSize(finalSize) + // console.log('最终包围盒大小:', finalSize) // 应该是 (1, 1, 1) + // console.log('包围盒底部 Y 值:', finalBox.min.y) // 应该是 0 + // + // return geometry + // } + init() { return Promise.all([ super.init(), @@ -34,9 +78,15 @@ export default class PalletRenderer extends BaseRenderer { ]).then(([_, { data: queue3dsFile }, queueTexture]) => { const mesh = load3DModule(queue3dsFile, '.3ds').children[0] as THREE.Mesh - this.toteGeometry = mesh.geometry.rotateX(-Math.PI / 2) - this.toteGeometry.scale(1, 1, 1) - this.toteGeometry.center() + mesh.geometry.rotateX(-Math.PI / 2) + this.toteGeometry = processModel(mesh) + + queueTexture.flipY = false + queueTexture.wrapS = THREE.RepeatWrapping + queueTexture.wrapT = THREE.RepeatWrapping + queueTexture.repeat.set(1, 1) + + // this.toteMaterial = new THREE.MeshPhongMaterial({ color: 0x2b5d94 }) this.toteMaterial = mesh.material as THREE.Material //@ts-ignore this.toteMaterial.map = queueTexture