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'
/**
*
* - 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 {

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 { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
let pdFn, pmFn, puFn
/**
@ -288,10 +287,10 @@ export default abstract class BaseInteraction {
} as ItemJson
// 关联2个点
const fromItem = this.viewport.entityManager.findItemById(this.linkStartPointId)
if (this.linkStartPointId && fromItem) {
const from = this.viewport.entityManager.findItemById(this.linkStartPointId)
if (this.linkStartPointId && from) {
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) {
const lineId = getLineId(start.id, end.id, type)
console.log('updateline ', lineId)
const lines = this.tempViewport.entityManager.findLineObjectsById(lineId)
_.forEach(lines, (line: THREE.Object3D) => {
@ -253,7 +252,6 @@ export default abstract class BaseRenderer {
*/
deleteLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) {
const lineId = getLineId(start.id, end.id, type)
console.log('deleteline ', lineId)
const lines = this.tempViewport.entityManager.findLineObjectsById(lineId)
if (lines) {
this.removeFromScene(...lines)

20
src/core/engine/Viewport.ts

@ -420,16 +420,6 @@ export default class Viewport {
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) {
this.interactionManager.dispose()
this.interactionManager = null
@ -444,6 +434,16 @@ export default class Viewport {
this.entityManager.dispose()
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) {

21
src/core/manager/EntityManager.ts

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

8
src/core/manager/StateManager.ts

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

147
src/example/example1.js

@ -33,47 +33,132 @@ export default {
{
catalogCode: 'f1', t: 'floor', // 楼层
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
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
id: 'P1',
t: 'measure',
v: true,
tf: [
[-4, 0.1, 4.75],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中
label: '测量1', // 标签名称, 显示用
color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid)
in: [], // 物流入方向关联的对象(id)
out: [] // 物流出方向关联的对象(id)
dt: {
in: [],
out: [],
center: []
}
},
{
id: 'p2',
}, {
id: 'P2',
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: {
color: '#ff0000',
label: '测量2',
center: ['p1', 'p3', 'p4']
in: [],
out: [],
center: ['P1']
}
},
{
id: 'p3', t: 'measure',
tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]],
}, {
id: 'P3',
t: 'measure',
v: true,
tf: [
[5, 0.1, 5.75],
[0, 0, 0],
[0.25, 0.1, 0.25]
],
dt: {
label: '测量3',
center: ['p2']
in: [],
out: [],
center: ['P2']
}
},
{
id: 'p4',
}, {
id: 'P4',
t: 'measure',
tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]],
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',
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: {
label: '测量4',
center: ['p2']
in: [],
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 { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
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 ShowDialogWrap from '@/components/ShowDialogWrap.vue'
import LoadingDialog from '@/components/LoadingDialog.vue'
@ -39,7 +39,6 @@ export default class System {
rootElementList: { cmp: Component, props: any }[] = reactive([])
createUUID = createShortUUID
decompressUUID = decompressUUID
setQueryParam = setQueryParam
getQueryParams = getQueryParams

63
src/utils/webutils.ts

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

Loading…
Cancel
Save