From 6dd148249a04f6d51707fbb860b57a4b54d3b7f1 Mon Sep 17 00:00:00 2001 From: yvan Date: Tue, 3 Jun 2025 00:09:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E7=B3=BB=E4=BF=AE=E8=A1=A5=20ensureEn?= =?UTF-8?q?tityRelationsConsistency.=20EntityManager=20=E5=9B=9E=E4=BC=A0?= =?UTF-8?q?=20writeBackEntities=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/ModelUtils.ts | 79 ++++++++++++++++++++ src/core/base/BaseInteraction.ts | 7 +- src/core/base/BaseRenderer.ts | 2 - src/core/engine/Viewport.ts | 20 +++--- src/core/manager/EntityManager.ts | 21 ++++-- src/core/manager/StateManager.ts | 8 +++ src/example/example1.js | 147 ++++++++++++++++++++++++++++++-------- src/runtime/System.ts | 3 +- src/utils/webutils.ts | 63 +++++----------- 9 files changed, 252 insertions(+), 98 deletions(-) diff --git a/src/core/ModelUtils.ts b/src/core/ModelUtils.ts index d799418..a28e093 100644 --- a/src/core/ModelUtils.ts +++ b/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() + + // 构建 ID -> Item 映射,便于快速查找 + for (const item of items) { + if (item.id) { + itemMap.set(item.id, item) + } + } + + // 初始化关系集合 + const centerMap = new Map>() // A <-> B + const inMap = new Map>() // A <- B (B.in.push(A)) + const outMap = new Map>() // 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 { diff --git a/src/core/base/BaseInteraction.ts b/src/core/base/BaseInteraction.ts index a00fb92..21e1010 100644 --- a/src/core/base/BaseInteraction.ts +++ b/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) } // 提交状态管理器 diff --git a/src/core/base/BaseRenderer.ts b/src/core/base/BaseRenderer.ts index ca89a9e..0cdf819 100644 --- a/src/core/base/BaseRenderer.ts +++ b/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) diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts index 5aade39..b0c03fb 100644 --- a/src/core/engine/Viewport.ts +++ b/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) { diff --git a/src/core/manager/EntityManager.ts b/src/core/manager/EntityManager.ts index 44d1ff9..298e3d1 100644 --- a/src/core/manager/EntityManager.ts +++ b/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 }) } } diff --git a/src/core/manager/StateManager.ts b/src/core/manager/StateManager.ts index aa7f08c..ed5d569 100644 --- a/src/core/manager/StateManager.ts +++ b/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) diff --git a/src/example/example1.js b/src/example/example1.js index 590b8ac..0bda2ee 100644 --- a/src/example/example1.js +++ b/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'] } } ] diff --git a/src/runtime/System.ts b/src/runtime/System.ts index 0e1e3db..e061b58 100644 --- a/src/runtime/System.ts +++ b/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 diff --git a/src/utils/webutils.ts b/src/utils/webutils.ts index df909e7..2a85757 100644 --- a/src/utils/webutils.ts +++ b/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'); } /**