Browse Source

关系修补 ensureEntityRelationsConsistency. EntityManager 回传 writeBackEntities bug

master
修宁 7 months ago
parent
commit
6dd148249a
  1. 79
      src/core/ModelUtils.ts
  2. 7
      src/core/base/BaseInteraction.ts
  3. 2
      src/core/base/BaseRenderer.ts
  4. 20
      src/core/engine/Viewport.ts
  5. 21
      src/core/manager/EntityManager.ts
  6. 8
      src/core/manager/StateManager.ts
  7. 147
      src/example/example1.js
  8. 3
      src/runtime/System.ts
  9. 63
      src/utils/webutils.ts

79
src/core/ModelUtils.ts

@ -7,6 +7,85 @@ import { Vector2 } from 'three/src/math/Vector2'
import type Toolbox from '@/model/itemType/Toolbox.ts' import type Toolbox from '@/model/itemType/Toolbox.ts'
/** /**
*
* - 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 * 线 ID
*/ */
export function getLineId(startId: string, endId: string, type: LinkType): string { export function getLineId(startId: string, endId: string, type: LinkType): string {

7
src/core/base/BaseInteraction.ts

@ -8,7 +8,6 @@ import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
import { numberToString } from '@/utils/webutils.ts' import { numberToString } from '@/utils/webutils.ts'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
let pdFn, pmFn, puFn let pdFn, pmFn, puFn
/** /**
@ -288,10 +287,10 @@ export default abstract class BaseInteraction {
} as ItemJson } as ItemJson
// 关联2个点 // 关联2个点
const fromItem = this.viewport.entityManager.findItemById(this.linkStartPointId) const from = this.viewport.entityManager.findItemById(this.linkStartPointId)
if (this.linkStartPointId && fromItem) { if (this.linkStartPointId && from) {
itemJson.dt.center.push(this.linkStartPointId) itemJson.dt.center.push(this.linkStartPointId)
fromItem.dt.center.push(itemJson.id) from.dt.center.push(itemJson.id)
} }
// 提交状态管理器 // 提交状态管理器

2
src/core/base/BaseRenderer.ts

@ -229,7 +229,6 @@ export default abstract class BaseRenderer {
*/ */
updateLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) { updateLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) {
const lineId = getLineId(start.id, end.id, type) const lineId = getLineId(start.id, end.id, type)
console.log('updateline ', lineId)
const lines = this.tempViewport.entityManager.findLineObjectsById(lineId) const lines = this.tempViewport.entityManager.findLineObjectsById(lineId)
_.forEach(lines, (line: THREE.Object3D) => { _.forEach(lines, (line: THREE.Object3D) => {
@ -253,7 +252,6 @@ export default abstract class BaseRenderer {
*/ */
deleteLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) { deleteLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) {
const lineId = getLineId(start.id, end.id, type) const lineId = getLineId(start.id, end.id, type)
console.log('deleteline ', lineId)
const lines = this.tempViewport.entityManager.findLineObjectsById(lineId) const lines = this.tempViewport.entityManager.findLineObjectsById(lineId)
if (lines) { if (lines) {
this.removeFromScene(...lines) this.removeFromScene(...lines)

20
src/core/engine/Viewport.ts

@ -420,16 +420,6 @@ export default class Viewport {
this.renderer.domElement = null this.renderer.domElement = null
} }
if (this.controls) {
this.controls.dispose()
this.controls = null
}
if (this.dragControl) {
this.dragControl.dispose()
this.dragControl = null
}
if (this.interactionManager) { if (this.interactionManager) {
this.interactionManager.dispose() this.interactionManager.dispose()
this.interactionManager = null this.interactionManager = null
@ -444,6 +434,16 @@ export default class Viewport {
this.entityManager.dispose() this.entityManager.dispose()
this.entityManager = null this.entityManager = null
} }
if (this.controls) {
this.controls.dispose()
this.controls = null
}
if (this.dragControl) {
this.dragControl.dispose()
this.dragControl = null
}
} }
getIntersects(point: THREE.Vector2) { getIntersects(point: THREE.Vector2) {

21
src/core/manager/EntityManager.ts

@ -4,7 +4,6 @@ import type BaseRenderer from '@/core/base/BaseRenderer'
import { getRenderer } from './ModuleManager' import { getRenderer } from './ModuleManager'
import { getLineId, parseLineId } from '@/core/ModelUtils' import { getLineId, parseLineId } from '@/core/ModelUtils'
/** /**
* *
* (ItemJson), THREE.Object3D * (ItemJson), THREE.Object3D
@ -101,7 +100,6 @@ export default class EntityManager {
// 先判断坐标是否变化 // 先判断坐标是否变化
const coordinateChanged = originEntity?.tf && entity?.tf && !_.isEqual(originEntity.tf[0], entity.tf[0]) const coordinateChanged = originEntity?.tf && entity?.tf && !_.isEqual(originEntity.tf[0], entity.tf[0])
this.entities.set(entity.id, entity) this.entities.set(entity.id, entity)
// 更新关系网 // 更新关系网
@ -111,6 +109,7 @@ export default class EntityManager {
this.writeBackEntities.add(entity.id) this.writeBackEntities.add(entity.id)
// 点的坐标发生变化, 要通知所有关联线更新 // 点的坐标发生变化, 要通知所有关联线更新
const relation = this.relationIndex.get(entity.id) const relation = this.relationIndex.get(entity.id)
if (relation) { if (relation) {
for (const type of (['center', 'in', 'out'] as LinkType[])) { for (const type of (['center', 'in', 'out'] as LinkType[])) {
const relatedIds = relation[type] const relatedIds = relation[type]
@ -118,8 +117,11 @@ export default class EntityManager {
for (const relatedId of relatedIds) { for (const relatedId of relatedIds) {
const lineId = getLineId(entity.id, relatedId, type) const lineId = getLineId(entity.id, relatedId, type)
console.log(`[update] ${entity.id} -> ${relatedId} [${type}] => ${lineId}`)
this.lineDiffs.update.set(lineId, { startId: entity.id, endId: relatedId, type }) this.lineDiffs.update.set(lineId, { startId: entity.id, endId: relatedId, type })
this.writeBackEntities.add(relatedId)
// 如果是双向线(比如 center),也要反向加一次 // 如果是双向线(比如 center),也要反向加一次
if (type === 'center') { if (type === 'center') {
this.lineDiffs.update.set(lineId, { startId: relatedId, endId: entity.id, type }) this.lineDiffs.update.set(lineId, { startId: relatedId, endId: entity.id, type })
@ -146,7 +148,7 @@ export default class EntityManager {
if (!entity) return if (!entity) return
option.originEntity = _.cloneDeep(entity) option.originEntity = _.cloneDeep(entity)
this.writeBackEntities.add(entity.id) this.writeBackEntities.add(id)
// 先生成线差量,再清理关系 // 先生成线差量,再清理关系
this.generateLineDiffsForDelete(id) this.generateLineDiffsForDelete(id)
@ -162,6 +164,7 @@ export default class EntityManager {
const removeLine = (relatedId: string, type: LinkType) => { const removeLine = (relatedId: string, type: LinkType) => {
const lineId = getLineId(id, relatedId, type) const lineId = getLineId(id, relatedId, type)
this.writeBackEntities.add(relatedId)
this.lineDiffs.delete.set(lineId, { startId: id, endId: relatedId, type }) this.lineDiffs.delete.set(lineId, { startId: id, endId: relatedId, type })
} }
@ -314,7 +317,7 @@ export default class EntityManager {
if (!newIds.has(relatedId)) { if (!newIds.has(relatedId)) {
const rev = this.relationIndex.get(relatedId) const rev = this.relationIndex.get(relatedId)
rev.delete(relationType, id) rev.delete(relationType, id)
this.writeBackEntities.add(id) this.writeBackEntities.add(relatedId)
} }
} }
@ -327,7 +330,7 @@ export default class EntityManager {
this.relationIndex.set(relatedId, rev) this.relationIndex.set(relatedId, rev)
} }
rev.add(relationType, id) rev.add(relationType, id)
this.writeBackEntities.add(id) this.writeBackEntities.add(relatedId)
} }
} }
} }
@ -340,6 +343,10 @@ export default class EntityManager {
for (const relatedId of oldIds) { for (const relatedId of oldIds) {
if (!newIds.has(relatedId)) { if (!newIds.has(relatedId)) {
const lineId = getLineId(id, relatedId, lineType) const lineId = getLineId(id, relatedId, lineType)
// 如果这条线已经在 update 列表中,则跳过 delete
if (this.lineDiffs.update.has(lineId)) continue
console.log(`[delete] ${id} -> ${relatedId} [${lineType}] => ${lineId}`)
this.lineDiffs.delete.set(lineId, { startId: id, endId: relatedId, type: lineType }) this.lineDiffs.delete.set(lineId, { startId: id, endId: relatedId, type: lineType })
} }
} }
@ -348,6 +355,10 @@ export default class EntityManager {
for (const relatedId of newIds) { for (const relatedId of newIds) {
if (!oldIds.has(relatedId)) { if (!oldIds.has(relatedId)) {
const lineId = getLineId(id, relatedId, lineType) const lineId = getLineId(id, relatedId, lineType)
// 如果这条线已经在 update 列表中,则跳过 create
if (this.lineDiffs.update.has(lineId)) continue
this.lineDiffs.create.set(lineId, { startId: id, endId: relatedId, type: lineType }) this.lineDiffs.create.set(lineId, { startId: id, endId: relatedId, type: lineType })
} }
} }

8
src/core/manager/StateManager.ts

@ -4,6 +4,7 @@ import type EntityManager from './EntityManager'
import { markRaw, reactive, ref } from 'vue' import { markRaw, reactive, ref } from 'vue'
import type Viewport from '@/core/engine/Viewport.ts' import type Viewport from '@/core/engine/Viewport.ts'
import { getQueryParams, setQueryParam } from '@/utils/webutils.ts' import { getQueryParams, setQueryParam } from '@/utils/webutils.ts'
import { ensureEntityRelationsConsistency } from '@/core/ModelUtils.ts'
// 差异类型定义 // 差异类型定义
interface DataDiff { interface DataDiff {
@ -418,7 +419,14 @@ export default class StateManager {
} }
} }
/**
*
* @private
*/
private fullSync() { private fullSync() {
this.vdata.items = ensureEntityRelationsConsistency(this.vdata.items)
this.entityManager.beginEntityUpdate() this.entityManager.beginEntityUpdate()
this.vdata.items.forEach(item => { this.vdata.items.forEach(item => {
this.entityManager.createOrUpdateEntity(item) this.entityManager.createOrUpdateEntity(item)

147
src/example/example1.js

@ -33,47 +33,132 @@ export default {
{ {
catalogCode: 'f1', t: 'floor', // 楼层 catalogCode: 'f1', t: 'floor', // 楼层
items: [ items: [
// {
// id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid
// t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理
// tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系
// [-9.0, 0, -1.0], // 平移向量 position
// [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算
// [0.25, 0.1, 0.25] // 缩放向量 scale
// ],
// dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中
// label: '测量1', // 标签名称, 显示用
// color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
// center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid)
// in: [], // 物流入方向关联的对象(id)
// out: [] // 物流出方向关联的对象(id)
// }
// },
// {
// id: 'p2',
// t: 'measure',
// tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]],
// dt: {
// color: '#ff0000',
// label: '测量2',
// center: ['p1', 'p3', 'p4']
// }
// },
// {
// id: 'p3', t: 'measure',
// tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]],
// dt: {
// label: '测量3',
// center: ['p2']
// }
// },
// {
// id: 'p4',
// t: 'measure',
// tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]],
// dt: {
// label: '测量4',
// center: ['p2']
// }
// }
{ {
id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid id: 'P1',
t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 t: 'measure',
tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 v: true,
[-9.0, 0, -1.0], // 平移向量 position tf: [
[0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 [-4, 0.1, 4.75],
[0.25, 0.1, 0.25] // 缩放向量 scale [0, 0, 0],
[0.25, 0.1, 0.25]
], ],
dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 dt: {
label: '测量1', // 标签名称, 显示用 in: [],
color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 out: [],
center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) center: []
in: [], // 物流入方向关联的对象(id)
out: [] // 物流出方向关联的对象(id)
} }
}, }, {
{ id: 'P2',
id: 'p2',
t: 'measure', t: 'measure',
tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], v: true,
tf: [
[5, 0.1, 2.75],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: { dt: {
color: '#ff0000', in: [],
label: '测量2', out: [],
center: ['p1', 'p3', 'p4'] center: ['P1']
} }
}, }, {
{ id: 'P3',
id: 'p3', t: 'measure', t: 'measure',
tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], v: true,
tf: [
[5, 0.1, 5.75],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: { dt: {
label: '测量3', in: [],
center: ['p2'] out: [],
center: ['P2']
} }
}, }, {
{ id: 'P4',
id: 'p4', t: 'measure',
v: true,
tf: [
[-1.25, 0.1, 7.25],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: {
in: [],
out: [],
center: ['P3']
}
}, {
id: 'P5',
t: 'measure', t: 'measure',
tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], v: true,
tf: [
[-2, 0.1, 6],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: {
in: [],
out: [],
center: ['P4']
}
}, {
id: 'P6',
t: 'measure',
v: true,
tf: [
[-3.5, 0.1, 5.25],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: { dt: {
label: '测量4', in: [],
center: ['p2'] out: [],
center: ['P5']
} }
} }
] ]

3
src/runtime/System.ts

@ -6,7 +6,7 @@ import hotkeys from 'hotkeys-js'
import { defineComponent, h, markRaw, nextTick, reactive, toRaw, unref, type App, createApp, type Component } from 'vue' import { defineComponent, h, markRaw, nextTick, reactive, toRaw, unref, type App, createApp, type Component } from 'vue'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue' import { QuestionFilled } from '@element-plus/icons-vue'
import { decompressUUID, renderIcon, createShortUUID, setQueryParam, getQueryParams } from '@/utils/webutils.ts' import { renderIcon, createShortUUID, setQueryParam, getQueryParams } from '@/utils/webutils.ts'
import type { showDialogOption } from '@/SystemOption' import type { showDialogOption } from '@/SystemOption'
import ShowDialogWrap from '@/components/ShowDialogWrap.vue' import ShowDialogWrap from '@/components/ShowDialogWrap.vue'
import LoadingDialog from '@/components/LoadingDialog.vue' import LoadingDialog from '@/components/LoadingDialog.vue'
@ -39,7 +39,6 @@ export default class System {
rootElementList: { cmp: Component, props: any }[] = reactive([]) rootElementList: { cmp: Component, props: any }[] = reactive([])
createUUID = createShortUUID createUUID = createShortUUID
decompressUUID = decompressUUID
setQueryParam = setQueryParam setQueryParam = setQueryParam
getQueryParams = getQueryParams getQueryParams = getQueryParams

63
src/utils/webutils.ts

@ -5,6 +5,7 @@ import { ElIcon } from 'element-plus'
import * as FaIcon from '@vicons/fa' import * as FaIcon from '@vicons/fa'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import * as THREE from 'three' import * as THREE from 'three'
import Decimal from 'decimal.js'
export function getQueryParams() { export function getQueryParams() {
// const search = window.location.search || window.location.hash.split('?')[1] || '' // const search = window.location.search || window.location.hash.split('?')[1] || ''
@ -66,58 +67,32 @@ export function createShortUUID() {
* UUID * UUID
*/ */
export function compressUUID(uuid) { export function compressUUID(uuid) {
// 移除连字符并转换为 ArrayBuffer // 移除 UUID 中的连字符
const raw = uuid.replace(/-/g, '') const hex = uuid.replace(/-/g, '');
const buf = new Uint8Array(16)
for (let i = 0; i < 32; i += 2) { // 将 Hex 转换为十进制的大整数字符串
buf[i / 2] = parseInt(raw.substr(i, 2), 16) const decimalValue = new Decimal(`0x${hex}`);
}
// 将字节数组转换为 Base64 字符串
const base64 = btoa(String.fromCharCode.apply(null, buf))
// 去掉 Base64 中的填充字符 '=' 并替换 '/' 为 '_', '+' 为 '-' 以便 URL 安全
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
/** // 定义 Base62 字符集
* UUID const base62Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
*/
export function decompressUUID(shortUuid: string) {
// 补全 Base64 填充字符
let padded = shortUuid
padded = padded.replace(/-/g, '+').replace(/_/g, '/')
while (padded.length % 4 !== 0) {
padded += '='
}
// 解码 Base64 let result = '';
const binStr = atob(padded) let num = decimalValue;
const buf = new Uint8Array(binStr.length)
for (let i = 0; i < binStr.length; i++) { // 使用 decimal.js 进行 Base62 转换
buf[i] = binStr.charCodeAt(i) while (num.greaterThanOrEqualTo(62)) {
const remainder = num.mod(62);
result = base62Chars[remainder.toNumber()] + result;
num = num.dividedToIntegerBy(62);
} }
// 转换为标准 UUID 格式 if (num.toNumber() > 0) {
const hex = [] result = base62Chars[num.toNumber()] + result;
for (let i = 0; i < 16; i++) {
hex.push((buf[i] >> 4).toString(16))
hex.push((buf[i] & 0x0f).toString(16))
} }
const raw = hex.join('') // UUID 总共 16 字节,理论上最多是 128 bits,所以压缩后应该是 22 位 Base62 字符左右
return ( // 补足前导 0 保证长度一致(可选)
raw.substr(0, 8) + '-' + return result.padStart(22, '0');
raw.substr(8, 4) + '-' +
raw.substr(12, 4) + '-' +
raw.substr(16, 4) + '-' +
raw.substr(20, 12)
)
} }
/** /**

Loading…
Cancel
Save