diff --git a/src/core/Constract.ts b/src/core/Constract.ts
index b97c525..34d4c0a 100644
--- a/src/core/Constract.ts
+++ b/src/core/Constract.ts
@@ -10,6 +10,9 @@ export default Object.freeze({
CursorModeLinkAdd: 'LinkAdd',
CursorModeLinkAdd2: 'LinkAdd2',
+ Mode2D: '2D',
+ Mode3D: '3D',
+
// 测量相关的光标模式
CursorModeMeasure: 'measure',
CursorModeWay: 'way',
@@ -17,4 +20,4 @@ export default Object.freeze({
// 选择模式
CursorModeSelectByRec: 'selectByRec'
-})
\ No newline at end of file
+})
diff --git a/src/core/ModelUtils.ts b/src/core/ModelUtils.ts
index 2fe0620..3abd3ec 100644
--- a/src/core/ModelUtils.ts
+++ b/src/core/ModelUtils.ts
@@ -100,6 +100,24 @@ export function getLineId(startId: string, endId: string, type: LinkType): strin
}
/**
+ * 获取某个 Object3D 先上查找最近的有效业务 Object3D
+ * @param object
+ */
+export function getClosestObject(object: THREE.Object3D) {
+ if (!object) {
+ return undefined
+ }
+ while (object) {
+ if (object.userData && object.userData.t && object.userData.entityId) {
+ // 找到第一个有效的业务 Object3D
+ return object
+ }
+ // 向上查找父级
+ object = object.parent
+ }
+}
+
+/**
* 解析线条 ID, 返回 线条类型, 起点 ID 和终点 ID
*/
export function parseLineId(lineId): [LinkType, string, string] {
@@ -434,4 +452,4 @@ function loadObject3DFromJson(items: ItemJson[]): THREE.Object3D[] {
}
return result
-}
\ No newline at end of file
+}
diff --git a/src/core/base/BaseRenderer.ts b/src/core/base/BaseRenderer.ts
index e65a077..1455622 100644
--- a/src/core/base/BaseRenderer.ts
+++ b/src/core/base/BaseRenderer.ts
@@ -79,7 +79,7 @@ export default abstract class BaseRenderer {
}
fillObjectUserDataFromItem(item: ItemJson, ...objects: THREE.Object3D[]) {
- _.forEach(objects, (object) => {
+ for (const object of objects) {
if (!object.name && item.name) {
object.name = item.name
}
@@ -90,12 +90,12 @@ export default abstract class BaseRenderer {
draggable: item.dt.protected !== true,
t: item.t
}
- })
+ }
}
fillObjectUserDataFromLine(start: ItemJson, end: ItemJson, type: LinkType, ...objects: THREE.Object3D[]) {
const id = getLineId(start.id, end.id, type)
- _.forEach(objects, (object) => {
+ for (const object of objects) {
if (!object.name) {
object.name = id
}
@@ -105,7 +105,7 @@ export default abstract class BaseRenderer {
entityId: getLineId(start.id, end.id, type),
t: start.t
}
- })
+ }
}
/**
@@ -116,9 +116,10 @@ export default abstract class BaseRenderer {
console.warn('No active viewport to append objects to.')
return
}
- // const dragObjects = objects.filter(obj => !!obj.userData.draggable)
- // this.tempViewport.dragControl.setDragObjects(dragObjects, 'push')
+
this.tempViewport.scene.add(...objects)
+ const dragObjects = objects.filter(obj => !!obj.userData.draggable)
+ this.tempViewport.dragControl.setDragObjects(dragObjects, 'push')
}
removeFromScene(...objects: THREE.Object3D[]) {
diff --git a/src/core/controls/DragControls.js b/src/core/controls/DragControls.js
index a4a7c74..7e777df 100644
--- a/src/core/controls/DragControls.js
+++ b/src/core/controls/DragControls.js
@@ -1,38 +1,45 @@
-import {
- EventDispatcher,
- Matrix4,
- Plane,
- Raycaster,
- Vector2,
- Vector3
-} from 'three'
-import { calcPositionUseSnap } from '@/core/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()
+import { EventDispatcher, Matrix4, Plane, Raycaster, Vector2, Vector3 } from 'three'
+import { calcPositionUseSnap, getClosestObject } from '@/core/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() // 用于将位置转换到局部空间
+
+/**
+ * DragControls 控制器类
+ * 提供基于鼠标或触摸的拖拽交互功能,并支持 hover 和 clickblank 等事件
+ */
class DragControls extends EventDispatcher {
+ /**
+ * 构造函数
+ * @param _objects 可拖拽的对象数组
+ * @param _camera 当前使用的相机
+ * @param _domElement 绑定事件的目标 DOM 元素(通常是 canvas)
+ */
constructor(_objects, _camera, _domElement) {
super()
- _domElement.style.touchAction = 'none' // disable touch scroll
-
- let _selected = null, _hovered = null
+ // 禁止触摸滚动行为
+ _domElement.style.touchAction = 'none'
- const _intersections = []
+ let _selected = null // 当前选中(正在拖动)的对象
+ let _hovered = null // 当前悬停的对象
- //
+ const _intersections = [] // 存储射线检测结果的数组
- let isMove = false
- let isMouseDownClicked = false
- const scope = this
+ let isMove = false // 标记是否发生了移动
+ let isMouseDownClicked = false // 标记是否按下了鼠标
+ const scope = this // 保存当前上下文
+ /**
+ * 激活事件监听器
+ */
function activate() {
_domElement.addEventListener('pointermove', onPointerMove)
_domElement.addEventListener('pointerdown', onPointerDown)
@@ -40,6 +47,9 @@ class DragControls extends EventDispatcher {
_domElement.addEventListener('pointerleave', onPointerCancel)
}
+ /**
+ * 去激活事件监听器
+ */
function deactivate() {
_domElement.removeEventListener('pointermove', onPointerMove)
_domElement.removeEventListener('pointerdown', onPointerDown)
@@ -49,22 +59,41 @@ class DragControls extends EventDispatcher {
_domElement.style.cursor = ''
}
+ /**
+ * 销毁控制器并释放资源
+ */
function dispose() {
deactivate()
}
+ /**
+ * 设置可拖拽的对象列表
+ * @param {Array} objects - 新的对象数组
+ */
function setObjects(objects) {
_objects = objects
}
+ /**
+ * 获取当前可拖拽的对象列表
+ * @returns {Array}
+ */
function getObjects() {
return _objects
}
+ /**
+ * 获取内部的 Raycaster 实例
+ * @returns {Raycaster}
+ */
function getRaycaster() {
return _raycaster
}
+ /**
+ * 鼠标/指针移动事件处理函数
+ * 处理悬停、拖拽等交互逻辑
+ */
function onPointerMove(event) {
if (!scope.enabled || !scope.enabledMove) return
@@ -76,6 +105,7 @@ class DragControls extends EventDispatcher {
updatePointer(event)
_raycaster.setFromCamera(_pointer, _camera)
+ // 如果有选中的对象,则更新其位置
if (_selected) {
if (_raycaster.ray.intersectPlane(_plane, _intersection)) {
const pos = _intersection.sub(_offset).applyMatrix4(_inverseMatrix)
@@ -86,7 +116,7 @@ class DragControls extends EventDispatcher {
return
}
- // hover support
+ // 鼠标/笔悬停检测
if (event.pointerType === 'mouse' || event.pointerType === 'pen') {
_intersections.length = 0
@@ -98,24 +128,21 @@ class DragControls extends EventDispatcher {
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) {
-
scope.dispatchEvent({ type: 'hoveroff', object: _hovered })
-
_domElement.style.cursor = 'auto'
_hovered = null
-
}
if (_hovered !== object) {
-
scope.dispatchEvent({ type: 'hoveron', object: object })
-
_domElement.style.cursor = 'pointer'
_hovered = object
-
}
} else {
@@ -132,6 +159,10 @@ class DragControls extends EventDispatcher {
}
+ /**
+ * 鼠标按下事件处理函数
+ * 检测是否点击了可拖拽对象,并准备开始拖拽
+ */
function onPointerDown(event) {
if (scope.enabled === false) return
@@ -145,41 +176,58 @@ class DragControls extends EventDispatcher {
_raycaster.intersectObjects(objects, true, _intersections)
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))
+ // 设置拖拽平面
+ _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))
+ _offset.copy(_intersection).sub(
+ _worldPosition.setFromMatrixPosition(_selected.matrixWorld)
+ )
}
- // setTimeout(() => {
- // _domElement.style.cursor = 'move'
- // }, 20)
isMouseDownClicked = true
}
+
+ // 触发 dragstart 事件
scope.dispatchEvent({ type: 'dragstart', object: _selected, e: event })
}
isMove = false
}
+ /**
+ * 鼠标释放或离开事件处理函数
+ * 结束拖拽操作或触发点击空白区域事件
+ */
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 })
}
+ // 恢复光标状态
_domElement.style.cursor = _hovered ? 'pointer' : 'auto'
isMouseDownClicked = false
}
+ /**
+ * 更新指针位置
+ * 将屏幕坐标转换为 NDC 设备坐标 (-1 ~ 1)
+ */
function updatePointer(event) {
const rect = _domElement.getBoundingClientRect()
@@ -187,13 +235,13 @@ class DragControls extends EventDispatcher {
_pointer.y = -(event.clientY - rect.top) / rect.height * 2 + 1
}
+ // 初始化:激活事件监听器
activate()
- // API
-
- this.enabled = true
- this.enabledMove = true
- this.transformGroup = false
+ // 暴露 API 方法和属性
+ this.enabled = true // 是否启用控制器
+ this.enabledMove = true // 是否允许移动操作
+ this.transformGroup = false // 是否以组形式变换多个对象
this.activate = activate
this.deactivate = deactivate
@@ -201,9 +249,7 @@ class DragControls extends EventDispatcher {
this.setObjects = setObjects
this.getObjects = getObjects
this.getRaycaster = getRaycaster
-
}
-
}
export { DragControls }
diff --git a/src/core/controls/EsDragControls.ts b/src/core/controls/EsDragControls.ts
index 6a0b66e..83f4935 100644
--- a/src/core/controls/EsDragControls.ts
+++ b/src/core/controls/EsDragControls.ts
@@ -71,7 +71,7 @@ export default class EsDragControls {
// 拖拽开始
dragControlsStart(e) {
// 右键拖拽不响应
- if (e.e.button === 2 || !e.object.visible) return
+ if (e.e.button === 2 || !e.object?.visible) return
const type = e.object.userData?.t
const entityId = e.object.userData?.entityId
@@ -127,25 +127,6 @@ export default class EsDragControls {
if (e.object.userData.onClick) {
e.object.userData.onClick(e)
}
- if (e.object.userData?.entityId) {
- const entityId = e.object.userData.entityId
- const item = this.viewport.entityManager.findItemById(entityId)
- const itemTypeName = e.object.userData.t
- if (item.dt.protected !== true) {
- this.viewport.state.selectedObject = markRaw(e.object)
- this.viewport.state.selectedItem = markRaw(item)
- this.viewport.state.selectedEntityId = entityId
- this.viewport.state.selectedObjectMeta = getMeta(itemTypeName)
-
- EventBus.dispatch('selectedObjectChanged', {
- viewport: this,
- selectedObject: this.viewport.state.selectedObject,
- selectedItem: this.viewport.state.selectedItem,
- selectedEntityId: this.viewport.state.selectedEntityId,
- selectedObjectMeta: this.viewport.state.selectedObjectMeta
- })
- }
- }
}
const ret = this.currentInteraction?.dragPointComplete(this.viewport, e)
@@ -182,4 +163,4 @@ export default class EsDragControls {
this.dragControls.removeEventListener('dragend', dragEndFn)
this.dragControls.dispose()
}
-}
\ No newline at end of file
+}
diff --git a/src/core/controls/SelectInspect.ts b/src/core/controls/SelectInspect.ts
index f2def9f..62c3eff 100644
--- a/src/core/controls/SelectInspect.ts
+++ b/src/core/controls/SelectInspect.ts
@@ -19,7 +19,12 @@ export default class SelectInspect implements IControls {
/**
* 线框材质,用于显示选中对象的包围盒
*/
- material: LineMaterial = new LineMaterial({ color: 0xffff00, linewidth: 2 })
+ yellowMaterial: LineMaterial = new LineMaterial({ color: 0xffff00, linewidth: 2 })
+
+ /**
+ * 线框材质,用于显示选中对象的包围盒
+ */
+ redMaterial: LineMaterial = new LineMaterial({ color: 0xff0000, linewidth: 2 })
/**
* 矩形材质,用于显示鼠标拖拽选择的矩形区域
@@ -76,6 +81,11 @@ export default class SelectInspect implements IControls {
this.updateSelectionBox(this.viewport.state.selectedObject)
})
+ EventBus.on('multiSelectedObjectsChanged', (data) => {
+ // 如果多选对象发生变化,清除当前选中对象的包围盒线框
+ this.updateMultiSelectionBoxes(data.multiSelectedObjects)
+ })
+
EventBus.on('selectedObjectPropertyChanged', (data) => {
this.updateSelectionBox(this.viewport.state.selectedObject)
})
@@ -90,6 +100,69 @@ export default class SelectInspect implements IControls {
})
}
+ redSelectionGroup = new THREE.Group()
+
+ private updateMultiSelectionBoxes(multiSelectedObjects: THREE.Object3D[]) {
+ // 为所有多选对象创建包围盒线框
+ this.clearRedSelectionBoxes()
+
+ if (!multiSelectedObjects || multiSelectedObjects.length === 0) {
+ return
+ }
+
+ for (const object of multiSelectedObjects) {
+ if (object.userData.entityId) {
+ this.createRedSelectionBox(object)
+ }
+ }
+ }
+
+ 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)
+ }
+
+ createRedSelectionBox(object: THREE.Object3D) {
+ // 如果对象没有 entityId,则不创建包围盒线框
+ if (!object.userData.entityId) {
+ return
+ }
+
+ // 如果选中的对象小于 0.5,要扩展包围盒
+ const RED_EXPAND_AMOUNT = 0.01 // 扩展包围盒的大小
+ // 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb)
+ object.updateWorldMatrix(false, true)
+
+ const box = new THREE.Box3().setFromObject(object)
+ box.expandByScalar(RED_EXPAND_AMOUNT)
+
+ 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)
+ const lineGeom = new LineGeometry()
+ // @ts-ignore
+ lineGeom.setPositions(edgesGeometry.attributes.position.array)
+
+ const selectionBox = new Line2(lineGeom, this.redMaterial)
+ selectionBox.computeLineDistances()
+ selectionBox.position.copy(center)
+
+ this.redSelectionGroup.add(selectionBox)
+ }
+
/**
* 更新选中对象的包围盒线框
*/
@@ -101,12 +174,13 @@ export default class SelectInspect implements IControls {
}
this.selectionId = selectedObject.userData?.entityId
- const expandAmount = 0.2 // 扩展包围盒的大小
+ // 如果选中的对象小于 0.5,要扩展包围盒
+ const YELLOW_EXPAND_AMOUNT = 0.03 // 扩展包围盒的大小
// 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb)
selectedObject.updateWorldMatrix(false, true)
const box = new THREE.Box3().setFromObject(selectedObject)
- box.expandByScalar(expandAmount)
+ box.expandByScalar(YELLOW_EXPAND_AMOUNT)
const size = new THREE.Vector3()
box.getSize(size)
@@ -117,16 +191,13 @@ export default class SelectInspect implements IControls {
// 创建包围盒几何体
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
+ // @ts-ignore
lineGeom.setPositions(edgesGeometry.attributes.position.array)
- const selectionBox = new Line2(lineGeom, this.material)
+ const selectionBox = new Line2(lineGeom, this.yellowMaterial)
selectionBox.computeLineDistances()
selectionBox.position.copy(center)
- selectionBox.name = 'selectionBox'
this.selectionBox = selectionBox
console.log('selectedItem', this.viewport.state.selectedItem)
@@ -162,7 +233,7 @@ export default class SelectInspect implements IControls {
if (this.recStartPos) {
// 创建矩形
this.rectangle = new THREE.Mesh(
- new THREE.PlaneGeometry(1, 1),
+ new THREE.PlaneGeometry(0.001, 0.001),
this.rectMaterial
)
this.rectangle.name = 'selectRectangle'
@@ -176,6 +247,7 @@ export default class SelectInspect implements IControls {
}
}
+
updateRectangle(position: THREE.Vector3) {
if (!this.rectangle || !this.recStartPos) return
// console.log('updateRectangle', this.recStartPos, position)
@@ -201,6 +273,8 @@ export default class SelectInspect implements IControls {
// 记录鼠标按下位置
this.recStartPos = this.viewport.getClosestIntersection(event)
this.createRectangle()
+ this.viewport.controls.enabled = false // 禁用控制器
+
} else {
// 为 click 事件添加处理逻辑
this.clickTime = Date.now()
@@ -220,11 +294,14 @@ export default class SelectInspect implements IControls {
disposeRect() {
if (this.rectangle !== null) {
+ // 查找在这个矩形内的所有有效业务对象,并将他们添加进 viewport.state.multiSelectedObjects
+ this.multipleSelectedObjects()
this.viewport.scene.remove(this.rectangle)
this.rectangle.geometry.dispose()
this.rectangle = null
}
this.recStartPos = null
+ this.viewport.controls.enabled = true // 启用控制器
}
onMouseUp(event: MouseEvent) {
@@ -235,6 +312,7 @@ export default class SelectInspect implements IControls {
// 如果是点击事件,触发选中逻辑
const objects: THREE.Object3D[] = this.viewport.entityManager.getObjectByCanvasMouse(event)
if (objects.length > 0) {
+ console.log('mouseClick', objects)
const object = objects[0]
const entityId = object.userData.entityId
const item = this.viewport.entityManager.findItemById(entityId)
@@ -244,7 +322,6 @@ export default class SelectInspect implements IControls {
this.viewport.state.selectedItem = markRaw(item)
this.viewport.state.selectedEntityId = entityId
this.viewport.state.selectedObjectMeta = getMeta(itemTypeName)
-
EventBus.dispatch('selectedObjectChanged', {
viewport: markRaw(this.viewport),
selectedObject: this.viewport.state.selectedObject,
@@ -253,7 +330,71 @@ export default class SelectInspect implements IControls {
selectedObjectMeta: this.viewport.state.selectedObjectMeta
})
}
+ } else {
+ // 如果没有选中任何对象,清除选中状态
+ this.viewport.state.selectedObject = null
+ this.viewport.state.selectedItem = null
+ this.viewport.state.selectedEntityId = null
+ this.viewport.state.selectedObjectMeta = null
+ EventBus.dispatch('selectedObjectChanged', {
+ viewport: markRaw(this.viewport),
+ selectedObject: null,
+ selectedItem: null,
+ selectedEntityId: null,
+ selectedObjectMeta: null
+ })
+ }
+ }
+ }
+
+ private multipleSelectedObjects() {
+ 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 objects = this.viewport.entityManager.getObjectsInBox(startX, startZ, endX, endZ)
+
+ // 清空之前的多选对象
+ this.viewport.state.multiSelectedObjects = []
+
+ // 遍历找到的对象,添加到多选对象中
+ const multiSelectedObjects = []
+ const multiSelectedItems = []
+ const multiSelectedEntityIds = []
+ const multiSelectedObjectMetas = []
+ for (const object of objects) {
+ 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)
+ multiSelectedObjectMetas.push(getMeta(object.userData.t))
+ }
}
}
+
+ // 触发多选对象更新事件
+ this.viewport.state.multiSelectedObjects = markRaw(objects)
+ this.viewport.state.multiSelectedItems = markRaw(multiSelectedItems)
+ this.viewport.state.multiSelectedEntityIds = multiSelectedEntityIds
+ this.viewport.state.multiSelectedObjectMetas = multiSelectedObjectMetas
+ EventBus.dispatch('multiSelectedObjectsChanged', {
+ viewport: markRaw(this.viewport),
+ multiSelectedObjects: this.viewport.state.multiSelectedObjects,
+ multiSelectedItems: this.viewport.state.multiSelectedItems,
+ multiSelectedEntityIds: this.viewport.state.multiSelectedEntityIds,
+ multiSelectedObjectMetas: this.viewport.state.multiSelectedObjectMetas
+ })
}
+
+
}
diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts
index f1f41b2..c87036f 100644
--- a/src/core/engine/Viewport.ts
+++ b/src/core/engine/Viewport.ts
@@ -20,6 +20,7 @@ import InteractionManager from '@/core/manager/InteractionManager'
import { calcPositionUseSnap } from '@/core/ModelUtils'
import StateManager from '@/core/manager/StateManager.ts'
import EventBus from '@/runtime/EventBus.ts'
+import Constract from '@/core/Constract.ts'
import type { IMeta } from '@/core/base/IMeta.ts'
/**
@@ -28,7 +29,7 @@ import type { IMeta } from '@/core/base/IMeta.ts'
*/
export default class Viewport {
viewerDom: HTMLElement
- camera: THREE.OrthographicCamera
+ camera: THREE.Camera // THREE.OrthographicCamera
renderer: THREE.WebGLRenderer
statsControls: Stats
controls: OrbitControls
@@ -65,7 +66,18 @@ export default class Viewport {
isReady: false,
isUpdating: false,
cursorMode: 'normal',
- selectedObject: null,
+
+ selectedObject: undefined,
+ selectedItem: undefined,
+ selectedEntityId: undefined,
+ selectedObjectMeta: undefined,
+
+ multiSelectedObjects: [],
+ multiSelectedItems: [],
+ multiSelectedEntityIds: [],
+ multiSelectedObjectMetas: [],
+
+ view3DMode: Constract.Mode2D,
camera: {
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 }
@@ -142,7 +154,14 @@ export default class Viewport {
this.renderer = renderer
// 创建正交摄像机
- this.initMode2DCamera()
+ // this.initMode2DCamera()
+ this.watchList.push(watch(() => this.state.view3DMode, (newVal) => {
+ if (newVal === Constract.Mode3D) {
+ this.initMode3DCamera()
+ } else {
+ this.initMode2DCamera()
+ }
+ }, { immediate: true }))
// 注册拖拽组件
this.dragControl = new EsDragControls(this)
@@ -165,6 +184,7 @@ export default class Viewport {
this.updateGridVisibility()
}))
+
// 监听窗口大小变化
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.viewerDom)
@@ -211,6 +231,44 @@ export default class Viewport {
}
/**
+ * 初始化3D相机
+ */
+ initMode3DCamera() {
+ if (this.camera) {
+ this.scene.remove(this.camera)
+ }
+
+ // ============================ 创建透视相机
+ const viewerDom = this.viewerDom
+ const cameraNew = new THREE.PerspectiveCamera(
+ 25,
+ viewerDom.clientWidth / viewerDom.clientHeight,
+ 1,
+ 2000
+ )
+ cameraNew.position.set(4, 2, -3)
+ cameraNew.lookAt(0, 0, 0)
+ this.camera = cameraNew
+ this.scene.add(this.camera)
+
+ // ============================ 创建控制器
+ const controls = new OrbitControls(
+ this.camera,
+ this.renderer?.domElement
+ )
+ controls.enableDamping = false
+ controls.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动
+ controls.minDistance = 2
+ controls.maxDistance = 1000
+ controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE }
+ // 下面这句话非常影响性能
+ // this.controls.addEventListener('change', ()=>{
+ // this.renderer.render(this.scene, this.camera);
+ // });
+ this.controls = controls
+ }
+
+ /**
* 初始化2D相机
*/
initMode2DCamera() {
@@ -253,7 +311,7 @@ export default class Viewport {
controlsNew.maxDistance = 1000
this.controls = controlsNew
- this.camera.updateProjectionMatrix()
+ cameraNew.updateProjectionMatrix()
this.syncCameraState()
}
@@ -290,26 +348,32 @@ export default class Viewport {
syncCameraState() {
if (this.camera) {
const camera = this.camera
+ if (this.camera instanceof THREE.PerspectiveCamera) {
+ this.state.camera.position.x = camera.position.x
+ this.state.camera.position.y = (camera as THREE.PerspectiveCamera).zoom // this.getEffectiveViewDistance()
+ this.state.camera.position.z = camera.position.z
- this.state.camera.position.x = camera.position.x
- this.state.camera.position.y = camera.zoom // this.getEffectiveViewDistance()
- this.state.camera.position.z = camera.position.z
+ } else {
+ this.state.camera.position.x = camera.position.x
+ this.state.camera.position.y = 5
+ this.state.camera.position.z = camera.position.z
+ }
}
}
- /**
- * 计算相机到目标的有效视距
- */
- getEffectiveViewDistance() {
- if (!this.camera) {
- return 10
- }
- const camera = this.camera
- const viewHeight = (camera.top - camera.bottom) / camera.zoom
- // 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考)
- const referenceFOV = 45 // 参考视场角
- return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2))
- }
+ // /**
+ // * 计算相机到目标的有效视距
+ // */
+ // getEffectiveViewDistance() {
+ // if (!this.camera || !(this.camera instanceof THREE.PerspectiveCamera)) {
+ // return 5
+ // }
+ // const camera = this.camera as THREE.PerspectiveCamera
+ // const viewHeight = (camera.top - camera.bottom) / camera.zoom
+ // // 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考)
+ // const referenceFOV = 45 // 参考视场角
+ // return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2))
+ // }
handleResize(entries: any) {
for (let entry of entries) {
@@ -358,6 +422,12 @@ export default class Viewport {
* 根据可视化范围更新网格的透明度
*/
updateGridVisibility() {
+ if (this.camera === undefined || !(this.camera instanceof THREE.PerspectiveCamera)) {
+ // 如果没有相机或相机不是透视相机,则不更新网格可见性
+ this.gridHelper.visible = true
+ this.gridHelper.material.opacity = 1
+ return
+ }
const cameraDistance = this.state.camera.position.y
const maxVisibleDistance = 4 // 网格完全不可见的最小距离
const fadeStartDistance = 9 // 开始淡出的最大距离
@@ -490,18 +560,22 @@ export interface ViewportState {
cursorMode: string // CursorMode,
/**
- * 选中的对象
+ * 黄选的对象
*/
selectedObject: THREE.Object3D | undefined
-
selectedItem: ItemJson | undefined
-
selectedEntityId: string | undefined
+ selectedObjectMeta: IMeta | undefined
/**
- * 选中的对象的元数据
+ * 红选的对象集
*/
- selectedObjectMeta: IMeta | undefined
+ multiSelectedObjects: THREE.Object3D[]
+ multiSelectedItems: ItemJson[]
+ multiSelectedEntityIds: string[]
+ multiSelectedObjectMetas: IMeta[]
+
+ view3DMode: string // Constract.Mode2D | Constract.Mode3D
/**
* 是否正在更新中
diff --git a/src/core/manager/EntityManager.ts b/src/core/manager/EntityManager.ts
index a09fc19..2fa5744 100644
--- a/src/core/manager/EntityManager.ts
+++ b/src/core/manager/EntityManager.ts
@@ -2,7 +2,7 @@ import * as THREE from 'three'
import type Viewport from '@/core/engine/Viewport'
import type BaseRenderer from '@/core/base/BaseRenderer'
import { getRenderer } from './ModuleManager'
-import { getLineId, parseLineId } from '@/core/ModelUtils'
+import { getClosestObject, getLineId, parseLineId } from '@/core/ModelUtils'
import { Vector2 } from 'three'
/**
@@ -541,11 +541,33 @@ export default class EntityManager {
if (!_intersections || _intersections.length === 0) {
return []
}
+
// 根据距离排序射线命中的对象集
return _.map(
_intersections.sort((a, b) => a.distance - b.distance),
- r => r.object
+ r => getClosestObject(r.object)
+ ).filter(obj => obj?.userData && obj.userData.selectable !== false)
+ }
+
+ /**
+ * 获取指定范围内的所有对象
+ */
+ getObjectsInBox(startX: number, startZ: number, endX: number, endZ: number) {
+ const box = new THREE.Box2(
+ new THREE.Vector2(startX, startZ),
+ new THREE.Vector2(endX, endZ)
)
+ const objectsInBox: THREE.Object3D[] = []
+
+ for (const [id, objects] of this.objects.entries()) {
+ for (const obj of objects) {
+ if (box.containsPoint(new Vector2(obj.position.x, obj.position.z))) {
+ objectsInBox.push(obj)
+ }
+ }
+ }
+
+ return objectsInBox
}
}
@@ -585,4 +607,4 @@ export class Relation {
else
throw new Error(`Unknown link type: ${type}`)
}
-}
\ No newline at end of file
+}
diff --git a/src/editor/Model2DEditor.vue b/src/editor/Model2DEditor.vue
index 364a4fd..136f18f 100644
--- a/src/editor/Model2DEditor.vue
+++ b/src/editor/Model2DEditor.vue
@@ -7,6 +7,15 @@