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.
483 lines
14 KiB
483 lines
14 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'
|
|
import type { Object3DLike } from '@/types/ModelTypes.ts'
|
|
|
|
export function setUserDataForItem(item: ItemJson, object: Object3DLike) {
|
|
if (!object.name && item.name) {
|
|
object.name = item.name
|
|
}
|
|
object.userData = {
|
|
...object.userData,
|
|
t: item.t,
|
|
createType: 'point',
|
|
entityId: item.id,
|
|
draggable: item.dt.protected !== true,
|
|
selectable: item.dt.selectable !== false
|
|
}
|
|
}
|
|
|
|
export function setUserDataForLine(start: ItemJson, end: ItemJson, type: LinkType, object: Object3DLike) {
|
|
const id = getLineId(start.id, end.id, type)
|
|
|
|
if (!object.name) {
|
|
object.name = id
|
|
}
|
|
object.userData = {
|
|
...object.userData,
|
|
createType: 'line',
|
|
entityId: id,
|
|
startId: start.id,
|
|
endId: end.id,
|
|
draggable: false,
|
|
selectable: false,
|
|
t: start.t
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 确保所有实体之间的关系满足一致性:
|
|
* - 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
|
|
}
|
|
|
|
let deleteCount = 0
|
|
viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => {
|
|
for (const entityId of multiSelectedEntityIds) {
|
|
deleteEntity(entityId) && deleteCount++
|
|
}
|
|
})
|
|
if (deleteCount === 0) {
|
|
system.msg('没有找到要删除的实体', 'error')
|
|
} else {
|
|
system.msg('删除了 ' + deleteCount + ' 个实体')
|
|
}
|
|
|
|
viewport.selectInspect.cancelMultiSelect()
|
|
return
|
|
}
|
|
|
|
viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => {
|
|
// 删除实体
|
|
if (deleteEntity(entityId)) {
|
|
system.msg('删除实体 [' + entityId + '] 成功')
|
|
viewport.selectInspect.cancelSelect()
|
|
}
|
|
})
|
|
}
|
|
|
|
export function escByKeyboard(e: Event) {
|
|
// 按下 ESC 键,取消当前操作
|
|
const viewport: Viewport = window['viewport']
|
|
if (!viewport) {
|
|
system.msg('没有找到当前视图')
|
|
return
|
|
}
|
|
|
|
if (viewport.interactionManager.currentTool) {
|
|
// 1.退出当前交互
|
|
viewport.interactionManager.exitInteraction()
|
|
system.msg('退出新建模式')
|
|
|
|
} else if (viewport.dragControl.isDragging) {
|
|
// 2.取消拖拽
|
|
viewport.dragControl.cancelDrag()
|
|
system.msg('取消拖拽')
|
|
|
|
} else if (viewport.state.multiSelectedEntityIds?.length > 0) {
|
|
// 3.取消多选
|
|
viewport.selectInspect.cancelMultiSelect()
|
|
system.msg('取消多选(红选)')
|
|
|
|
} else if (viewport.state.selectedEntityId) {
|
|
// 4.取消单选
|
|
viewport.selectInspect.cancelSelect()
|
|
system.msg('取消单选(黄选)')
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 群体移动
|
|
viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => {
|
|
for (const id of multiSelectedEntityIds) {
|
|
const item = getEntity(id)
|
|
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
|
|
}
|
|
putEntity(item)
|
|
}
|
|
})
|
|
// EventBus.dispatch('multiSelectedObjectsChanged', {
|
|
// multiSelectedObjects: viewport.state.multiSelectedObjects
|
|
// })
|
|
// EventBus.dispatch('selectedObjectPropertyChanged', {})
|
|
return
|
|
}
|
|
|
|
viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => {
|
|
const item = getEntity(entityId)
|
|
if (!item) {
|
|
system.msg('没有找到选中的实体', 'error')
|
|
return
|
|
}
|
|
|
|
// 根据方向移动
|
|
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
|
|
}
|
|
putEntity(item)
|
|
})
|
|
// EventBus.dispatch('multiSelectedObjectsChanged', {
|
|
// 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
|
|
|
|
// 如果不在线上,查找1米内的有效点 Object3D, 如果有,则以这个点为起点, 延伸同类型的点,并让他们相连
|
|
const items = viewport.stateManager.findStateItemsByDistance(new Vector2(x, z), 1)
|
|
if (items[0]) {
|
|
// 找到一个有效点,执行复制操作
|
|
viewport.interactionManager.startInteraction(items[0].t, { startPoint: items[0].id })
|
|
|
|
} else {
|
|
system.msg('鼠标所在位置,没有可复制的对象', 'error')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 考虑吸附的情况下计算鼠标事件位置
|
|
*/
|
|
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()
|
|
}
|
|
|