You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

589 lines
18 KiB

import * as THREE from 'three'
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts'
import { getAllItemTypes, getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts'
import type Viewport from '@/core/engine/Viewport'
import { Vector2 } from 'three/src/math/Vector2'
import EventBus from '@/runtime/EventBus.ts'
import Decimal from 'decimal.js'
/**
* 确保所有实体之间的关系满足一致性:
* - center 是双向的
* - in/out 是相互对应的
* - 不允许自己指向自己
*/
export function ensureEntityRelationsConsistency(items: ItemJson[]) {
const itemMap = new Map<string, ItemJson>()
// 构建 ID -> Item 映射,便于快速查找
for (const item of items) {
if (item.id) {
itemMap.set(item.id, item)
}
}
// 初始化关系集合
const centerMap = new Map<string, Set<string>>() // A <-> B
const inMap = new Map<string, Set<string>>() // A <- B (B.in.push(A))
const outMap = new Map<string, Set<string>>() // A -> B (B.out.push(A))
// 初始化所有节点的关系集
for (const item of items) {
const id = item.id
if (!id) continue
centerMap.set(id, new Set(item.dt?.center || []))
inMap.set(id, new Set(item.dt?.in || []))
outMap.set(id, new Set(item.dt?.out || []))
}
// Step 1: 补全 center 双向关系
for (const [source, targets] of centerMap.entries()) {
for (const target of targets) {
if (!centerMap.get(target)?.has(source)) {
centerMap.get(target)?.add(source)
}
}
}
// Step 2: 补全 in/out 对应关系
for (const [source, targets] of outMap.entries()) {
for (const target of targets) {
if (!inMap.get(target)?.has(source)) {
inMap.get(target)?.add(source)
}
}
}
for (const [source, targets] of inMap.entries()) {
for (const target of targets) {
if (!outMap.get(target)?.has(source)) {
outMap.get(target)?.add(source)
}
}
}
// Step 3: 清理自环引用(center / in / out 都不能包含自己)
for (const id of itemMap.keys()) {
centerMap.get(id)?.delete(id)
inMap.get(id)?.delete(id)
outMap.get(id)?.delete(id)
}
// Step 4: 将补全后的关系写回原数据
for (const item of items) {
const id = item.id
if (!id) continue
item.dt = item.dt || {}
item.dt.center = Array.from(centerMap.get(id) || [])
item.dt.in = Array.from(inMap.get(id) || [])
item.dt.out = Array.from(outMap.get(id) || [])
}
return items
}
/**
* 获取线条的唯一 ID
*/
export function getLineId(startId: string, endId: string, type: LinkType): string {
if (type === 'center' && startId > endId) {
// 无序线, start / end 大的在前
return `${type}$${endId}$${startId}`
}
// 其他的线是有序线
// 线条必须加上 type, 因为 center 与 in/out 是可以并存的
return `${type}$${startId}$${endId}`
}
/**
* 获取某个 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] {
const parts = lineId.split('$')
if (parts.length !== 3) {
throw new Error(`Invalid lineId format: ${lineId}`)
}
const type = parts[0] as LinkType
const startId = parts[1]
const endId = parts[2]
return [type, startId, endId]
}
export function deletePointByKeyboard() {
const viewport: Viewport = window['viewport']
if (!viewport) {
system.msg('没有找到当前视图')
return
}
const entityId = viewport.state.selectedEntityId
if (!entityId) {
const multiSelectedEntityIds = viewport.state.multiSelectedEntityIds
if (!multiSelectedEntityIds && multiSelectedEntityIds.length === 0) {
system.msg('请选中要删除的实体', 'error')
return
}
const stateManager = viewport.stateManager
stateManager.beginStateUpdate()
const deleteItems = _.remove(stateManager.vdata.items, (item) => multiSelectedEntityIds.includes(item.id))
stateManager.endStateUpdate()
if (deleteItems.length === 0) {
system.msg('没有找到要删除的实体', 'error')
} else {
system.msg('删除了 ' + deleteItems.length + ' 个实体')
}
for (const deleteEntityId of multiSelectedEntityIds) {
EventBus.dispatch('entityDeleted', {
deleteEntityId: deleteEntityId
})
viewport.selectInspect.clearRedSelectionBoxes()
}
return
}
const stateManager = viewport.stateManager
viewport.stateManager.beginStateUpdate()
_.remove(stateManager.vdata.items, (item) => item.id === entityId)
viewport.stateManager.endStateUpdate()
if (viewport.state.selectedEntityId === entityId) {
viewport.state.selectedObject = undefined
viewport.state.selectedItem = undefined
viewport.state.selectedEntityId = undefined
viewport.state.selectedObjectSetter = undefined
}
EventBus.dispatch('entityDeleted', {
deleteEntityId: entityId
})
system.msg('删除 [' + entityId + ']')
viewport.selectInspect.clearSelectionBox()
}
export function escByKeyboard() {
// 按下 ESC 键,取消当前操作
const viewport: Viewport = window['viewport']
if (!viewport) {
system.msg('没有找到当前视图')
return
}
if (viewport.interactionManager.currentTool) {
// 1.退出当前交互
viewport.interactionManager.exitInteraction()
} else if (viewport.dragControl.isDragging) {
// 2.取消拖拽
viewport.dragControl.cancelDrag()
} else if (viewport.state.multiSelectedEntityIds?.length > 0) {
// 3.取消多选
viewport.selectInspect.cancelMultiSelect()
} else if (viewport.state.selectedEntityId) {
// 4.取消单选
viewport.selectInspect.cancelSelect()
}
system.msg('操作已取消')
}
/**
* 查找指定点附近指定距离内的所有 stateManager 下的 item 对象
* 如果要修改数据做提交, 执行此方法之前必须执行 beginStateUpdate
* @param viewport 视窗
* @param point 点位 x,z 坐标
* @param distance 距离
*/
export function findStateItemsByDistance(viewport: Viewport, point: Vector2, distance: number): ItemJson[] {
const result: ItemJson[] = []
for (const item of viewport.stateManager.vdata.items) {
// 安全校验 tf 结构
if (!item.tf || !Array.isArray(item.tf) || item.tf[0].length < 3) {
continue
}
const [x, , z] = item.tf[0]
const itemPoint = new Vector2(x, z)
const dist = itemPoint.distanceTo(point)
if (dist <= distance) {
result.push(item)
}
}
// 按距离升序排序(近距离优先)
return result.sort((a, b) => {
const aPos = new Vector2(a.tf[0][0], a.tf[0][2])
const bPos = new Vector2(b.tf[0][0], b.tf[0][2])
return aPos.distanceTo(point) - bPos.distanceTo(point)
})
}
export function moveSelectedItem(direct: '↑' | '↓' | '←' | '→') {
// 获取当前是否按住了 Shift
const viewport: Viewport = window['viewport']
if (!viewport) {
system.msg('没有找到当前视图')
return
}
let delta = 0.25
if (CurrentMouseInfo.isShiftKey || CurrentMouseInfo.isMetaKey) {
// 按住 Shift 键时,移动距离只有0.1
delta = 0.1
}
const entityId = viewport.state.selectedEntityId
if (!entityId) {
const multiSelectedEntityIds = viewport.state.multiSelectedEntityIds
if (!multiSelectedEntityIds && multiSelectedEntityIds.length === 0) {
system.msg('请选中要调整坐标的实体', 'error')
return
}
// 群体移动
const stateManager = viewport.stateManager
stateManager.beginStateUpdate()
for (const item of stateManager.vdata.items) {
if (multiSelectedEntityIds.includes(item.id)) {
// 根据方向移动
switch (direct) {
case '↑':
console.log('向上移动', item.tf[0][2], '-=', delta)
item.tf[0][2] -= delta // 向上移动
break
case '↓':
item.tf[0][2] += delta // 向下移动
break
case '←':
item.tf[0][0] -= delta // 向左移动
break
case '→':
item.tf[0][0] += delta // 向右移动
break
}
}
}
stateManager.endStateUpdate()
EventBus.dispatch('multiselectedObjectChanged', {
multiSelectedObjects: viewport.state.multiSelectedObjects
})
EventBus.dispatch('selectedObjectPropertyChanged', {})
return
}
const stateManager = viewport.stateManager
viewport.stateManager.beginStateUpdate()
for (const item of stateManager.vdata.items) {
if (item.id === entityId) {
// 根据方向移动
switch (direct) {
case '↑':
item.tf[0][2] -= delta // 向上移动
break
case '↓':
item.tf[0][2] += delta // 向下移动
break
case '←':
item.tf[0][0] -= delta // 向左移动
break
case '→':
item.tf[0][0] += delta // 向右移动
break
}
}
}
viewport.stateManager.endStateUpdate()
EventBus.dispatch('multiselectedObjectChanged', {
multiSelectedObjects: viewport.state.multiSelectedObjects
})
EventBus.dispatch('selectedObjectPropertyChanged', {})
}
export function quickCopyByMouse() {
// 获取鼠标位置,查看鼠标是否在某个 viewport 的画布上,并取得该 viewport
if (!CurrentMouseInfo?.viewport ||
isNaN(CurrentMouseInfo.x) || isNaN(CurrentMouseInfo.z) ||
isNaN(CurrentMouseInfo.x) || isNaN(CurrentMouseInfo.z)) {
system.msg('无法获取鼠标位置')
return
}
const x = CurrentMouseInfo.x
const z = CurrentMouseInfo.z
const viewport: Viewport = CurrentMouseInfo.viewport
// const point: THREE.Vector2 = currentMouseInfo.mouse
//
// const ray = new THREE.Raycaster()
// ray.setFromCamera(point, viewport.camera)
// const intersections = ray.intersectObjects(viewport.dragControl._dragObjects, true)
//
// if (intersections.length === 0) {
// system.msg('没有找到可复制的对象')
// return
// }
// console.log('intersections:', intersections)
// 如果不在线上,查找1米内的有效点 Object3D, 如果有,则以这个点为起点, 延伸同类型的点,并让他们相连
const items = findStateItemsByDistance(viewport, new Vector2(x, z), 1)
if (items[0]) {
// 找到一个有效点,执行复制操作
viewport.interactionManager.startInteraction(items[0].t, { startPoint: items[0].id })
return
}
// const r = findObject3DByCondition(viewport.scene, object => {
// // 判断 object 是否是有效的 Object3D, 并且是当前 viewport 的对象
// if (object instanceof THREE.Object3D && object.visible &&
// object.userData.type && viewport.toolbox[object.userData.type]) {
//
// const toolbox: Toolbox = viewport.toolbox[object.userData.type]
//
// // 检查是否在 0.2 米内
// const distance = object.position.distanceTo(new THREE.Vector3(x, 0, z))
// if (distance < 0.2) {
// // 找到一个有效点,执行复制操作
// viewport.toolStartObject = object
// viewport.state.cursorMode = object.userData.type
// // toolbox.start(object)
// system.msg('连线成功')
// return true
// }
// }
// return false
// })
if (!items || items.length === 0) {
system.msg('鼠标所在位置,没有可复制的对象', 'error')
return
}
}
//
// /**
// * 查找射线周围指定半径内的对象
// */
// export function findObjectsInRadius(viewport: Viewport,
// point: THREE.Vector2,
// radius: number,
// lines: { object: THREE.Object3D, distance: number }[],
// points: { object: THREE.Object3D, distance: number }[]
// ): void {
// const ray = new THREE.Raycaster()
// ray.setFromCamera(point, viewport.camera)
//
// viewport.dragControl._dragObjects.forEach(obj => {
// if (obj instanceof THREE.Points) {
// // 处理点云:遍历每个点
// const distance = distanceToRay(ray, point)
// if (distance <= radius) {
// points.push({ object: obj, distance })
// }
//
// } else if (obj instanceof THREE.Line) {
// // 处理线段:计算线段到射线的最近距离
// const distance = getLineDistanceToRay(ray, obj)
// if (distance <= radius) {
// lines.push({ object: obj, distance })
// }
// }
// })
// }
//
// /**
// * 计算点到射线的最短距离
// */
// function distanceToRay(ray: THREE.Raycaster, point: THREE.Vector2) {
// const closestPoint = new THREE.Vector3()
// ray.closestPointToPoint(point, closestPoint)
// return point.distanceTo(closestPoint)
// }
//
// /**
// * 计算线段到射线的最短距离
// */
// function getLineDistanceToRay(ray: THREE.Raycaster, line: THREE.Line) {
// const lineStart = new THREE.Vector3()
// const lineEnd = new THREE.Vector3()
// line.geometry.attributes.position.getXYZ(0, lineStart)
// line.geometry.attributes.position.getXYZ(1, lineEnd)
// line.localToWorld(lineStart)
// line.localToWorld(lineEnd)
//
// const lineSegment = new THREE.Line3(lineStart, lineEnd)
// const closestOnRay = new THREE.Vector3()
// const closestOnLine = new THREE.Vector3()
// THREE.Line3.prototype.closestPointsRayLine ??= function(ray, line, closestOnRay, closestOnLine) {
// // 实现射线与线段最近点计算(需自定义或使用数学库)
// }
//
// lineSegment.closestPointsRayLine(ray, true, closestOnRay, closestOnLine)
// return closestOnRay.distanceTo(closestOnLine)
// }
/**
* 考虑吸附的情况下计算鼠标事件位置
*/
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
}
export function getAllControlPoints(): THREE.Object3D[] {
const allPoints: THREE.Object3D[] = []
getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => {
if (itemType.clazz && itemType.clazz.pointArray) {
// 将每个 ItemType 的点添加到结果数组中
allPoints.push(...itemType.clazz.pointArray)
}
})
return allPoints
}
/**
* 在给定的场景中查找具有指定 uuid 的 Object3D 对象
*/
export function findObject3DById(scene: THREE.Object3D, uuid: string): THREE.Object3D | undefined {
const rets = findObject3DByCondition(scene, object => object.uuid === uuid)
if (rets.length > 0) {
return rets[0]
}
return undefined
}
/**
* 在给定场景中查找满足特定条件的 Object3D 对象集合
*/
export function findObject3DByCondition(scene: THREE.Object3D, condition: (object: THREE.Object3D) => boolean): THREE.Object3D[] {
const foundObjects: THREE.Object3D[] = []
// 定义一个内部递归函数来遍历每个节点及其子节点
function traverse(obj: THREE.Object3D) {
if (condition(obj)) {
foundObjects.push(obj)
}
// 遍历当前对象的所有子对象
for (let i = 0; i < obj.children.length; i++) {
traverse(obj.children[i])
}
}
// 开始从场景根节点进行遍历
traverse(scene)
return foundObjects
}
export function loadSceneFromJson(viewport: Viewport, scene: THREE.Scene, items: ItemJson[]) {
console.time('loadSceneFromJson')
const object3ds: THREE.Object3D[] = []
// beforeLoad 通知所有加载的对象, 模型加载开始
getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => {
const ret = itemType.clazz.beforeLoad()
Array.isArray(ret) && object3ds.push(...ret)
})
const loads = loadObject3DFromJson(items)
Array.isArray(loads) && object3ds.push(...loads)
// afterLoadComplete 通知所有加载的对象, 模型加载完成
getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => {
const ret = itemType.clazz.afterLoadComplete(object3ds)
Array.isArray(ret) && object3ds.push(...ret)
})
scene.add(...object3ds)
// afterAddScene 通知所有加载的对象, 模型加载完成
getAllItemTypes().forEach(itemType => {
itemType.clazz.afterAddScene(viewport, scene, object3ds)
})
console.log('loadSceneFromJson:', items.length, 'items,', object3ds.length, 'objects')
console.timeEnd('loadSceneFromJson')
}
function loadObject3DFromJson(items: ItemJson[]): THREE.Object3D[] {
const result: THREE.Object3D[] = []
for (const item of items) {
if (!item || !item.t) {
console.error('unkown item:', item)
continue
}
const object3D: THREE.Object3D | undefined = getItemTypeByName(item.t)?.clazz.loadFromJson(item)
if (object3D === undefined) {
continue
}
if (_.isArray(item.items)) {
// 如果有子元素,递归处理
const children = loadObject3DFromJson(item.items)
children.forEach(child => object3D.add(child))
}
result.push(object3D)
}
return result
}
/**
* 十进制求和
* @param collection
* @param iteratee
*/
export function decimalSumBy<T>(collection: ArrayLike<T> | null | undefined, iteratee?: ((value: T) => number)): number {
let sum = new Decimal(0)
_.forEach(collection, (t) => {
if (typeof iteratee === 'function') {
sum = sum.add(new Decimal(iteratee(t)))
} else {
sum = sum.add(new Decimal(t))
}
})
return sum.toNumber()
}