7 changed files with 892 additions and 4 deletions
Binary file not shown.
|
After Width: | Height: | Size: 706 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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 { |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue