From 0e0b7bf0da365cf7b0b6e5c87d815484b7327ec5 Mon Sep 17 00:00:00 2001 From: yvan Date: Sun, 1 Jun 2025 19:03:06 +0800 Subject: [PATCH] =?UTF-8?q?Model2DEditor=20=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/ModelUtils.ts | 27 +++++++++--- src/core/engine/SceneHelp.ts | 2 +- src/core/engine/Viewport.ts | 20 +++++++-- src/core/manager/EntityManager.ts | 88 ++++++++++++++++++++++++++++++++++----- src/core/manager/StateManager.ts | 25 ++++++----- src/core/manager/WorldModel.ts | 35 ++++++++++++---- src/editor/Model2DEditor.vue | 49 +++++++++------------- src/example/example1.js | 84 ++++++++++++++++++------------------- src/runtime/EventBus.ts | 2 +- src/types/Types.d.ts | 17 +++++++- 10 files changed, 236 insertions(+), 113 deletions(-) diff --git a/src/core/ModelUtils.ts b/src/core/ModelUtils.ts index b8a7650..d799418 100644 --- a/src/core/ModelUtils.ts +++ b/src/core/ModelUtils.ts @@ -6,16 +6,31 @@ import { computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh' import { Vector2 } from 'three/src/math/Vector2' import type Toolbox from '@/model/itemType/Toolbox.ts' +/** + * 获取线条的唯一 ID + */ export function getLineId(startId: string, endId: string, type: LinkType): string { - if (type === 'center') { + if (type === 'center' && startId > endId) { // 无序线, start / end 大的在前 - if (startId > endId) { - return `${type}_${endId}_${startId}` - } + return `${type}$${endId}$${startId}` } // 其他的线是有序线 - // 线条必须加上 type, 因为 center 与 in/out 是可以并存的, 他们类型不一样 - return `${type}_${startId}_${endId}` + // 线条必须加上 type, 因为 center 与 in/out 是可以并存的 + return `${type}$${startId}$${endId}` +} + +/** + * 解析线条 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() { diff --git a/src/core/engine/SceneHelp.ts b/src/core/engine/SceneHelp.ts index e5e90fd..03c60ef 100644 --- a/src/core/engine/SceneHelp.ts +++ b/src/core/engine/SceneHelp.ts @@ -74,7 +74,7 @@ export default class SceneHelp { // async loadFloorEntities(floorId: string): Promise { // const items = await this.worldModel.loadFloor(floorId) // items.forEach((item) => { - // this.entityManager.createEntity(item) + // this.entityManager.createOrUpdateEntity(item) // }) // } diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts index 0a82ef4..1750f4d 100644 --- a/src/core/engine/Viewport.ts +++ b/src/core/engine/Viewport.ts @@ -4,7 +4,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import Stats from 'three/examples/jsm/libs/stats.module' import type WorldModel from '../manager/WorldModel' import $ from 'jquery' -import { reactive, watch } from 'vue' +import { reactive, toRaw, watch } from 'vue' import type IControls from '../controls/IControls' import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' @@ -19,6 +19,7 @@ import EntityManager from '../manager/EntityManager' import InteractionManager from '@/core/manager/InteractionManager' import { calcPositionUseSnap } from '@/core/ModelUtils' import StateManager from '@/core/manager/StateManager.ts' +import EventBus from '@/runtime/EventBus.ts' /** * 视窗对象 @@ -82,7 +83,7 @@ export default class Viewport { /** * 初始化 THREE 渲染器 */ - initThree(option: InitThreeOption) { + async initThree(option: InitThreeOption) { console.log('viewport on catelogCode: ' + this.scene.catalogCode) const viewerDom = this.viewerDom @@ -185,7 +186,20 @@ export default class Viewport { itemType.clazz.afterAddViewport(this) }) - this.state.isReady = true + try { + const vdata = await this.worldModel.getCatalogData(this.scene.catalogCode) + if (!vdata) { + return + } + if (!vdata.catalog) { + vdata.catalog = toRaw(this.worldModel.state.catalog) + } + await this.stateManager.load(vdata) + EventBus.dispatch('dataLoadComplete', {}) + + } finally { + this.state.isReady = true + } } /** diff --git a/src/core/manager/EntityManager.ts b/src/core/manager/EntityManager.ts index ec9246b..38ff585 100644 --- a/src/core/manager/EntityManager.ts +++ b/src/core/manager/EntityManager.ts @@ -2,7 +2,7 @@ import * as THREE from 'three' import type Viewport from '@/core/engine/Viewport' import type BaseRenderer from '@/core/base/BaseRenderer' import { getRenderer } from './ModuleManager' -import { getLineId } from '@/core/ModelUtils' +import { getLineId, parseLineId } from '@/core/ModelUtils' /** * 实体管理器 @@ -21,20 +21,21 @@ export default class EntityManager { // 关系索引 readonly relationIndex = new Map() - // 所有 THREEJS "点"对象 + // 所有 THREEJS "点"对象, 检索值是"点实体"的 id, 值是 THREE.Object3D 数组 readonly objects = new Map() - // 所有 THREEJS "线"对象 + // 所有 THREEJS "线"对象, 检索值是"线实体"的 id, 取值方式是 {type}${startId}${endId}, 值是 THREE.Object3D 数组 readonly lines = new Map() // 差量渲染器 readonly diffRenderer = new Map() // 线差量记录 - lineDiffs = { + readonly lineDiffs = { create: new Map(), update: new Map(), delete: new Map() } + isUpdating = false init(viewport: Viewport) { this.viewport = viewport @@ -44,6 +45,7 @@ export default class EntityManager { * 批量更新开始 */ beginUpdate(): void { + this.isUpdating = true this.viewport.beginUpdate() this.diffRenderer.clear() this.lineDiffs.create.clear() @@ -134,6 +136,7 @@ export default class EntityManager { renderer.endUpdate() } this.viewport.endUpdate() + this.isUpdating = false } /** @@ -210,6 +213,71 @@ export default class EntityManager { } /** + * 重命名一个点 + * 注意, 不能在更新时刻改名. 所有的关系节点都应该改名 + */ + renamePoint(newId: string, originId: string) { + if (this.isUpdating) { + throw new Error('Cannot rename point during update') + } + const entity = this.entities.get(originId) + if (!entity) { + throw new Error(`Entity with id ${originId} does not exist`) + } + if (this.entities.has(newId)) { + throw new Error(`Entity with id ${newId} already exists`) + } + entity.id = newId + this.entities.set(newId, entity) + this.entities.delete(originId) + this.objects.set(newId, this.objects.get(originId) || []) + this.objects.delete(originId) + + // 更新关系索引 + const relations = this.relationIndex.get(originId) + if (relations) { + this.relationIndex.delete(originId) + + // 更新所有关系中的 id + relations.center.forEach((relatedId) => { + const rev = this.relationIndex.get(relatedId) + if (rev && rev.delete('center', originId)) { + rev.add('center', newId) + } + }) + relations.input.forEach((relatedId) => { + const rev = this.relationIndex.get(relatedId) + if (rev && rev.delete('out', originId)) { + rev.add('out', newId) + } + }) + relations.output.forEach((relatedId) => { + const rev = this.relationIndex.get(relatedId) + if (rev && rev.delete('in', originId)) { + rev.add('in', newId) + } + }) + + this.relationIndex.set(newId, relations) + } + + // 更新所有线段数据 + for (const [lineId, lineObjects] of this.lines.entries()) { + const [type, startId, endId] = parseLineId(lineId) + if (startId === originId) { + const newLineId = getLineId(newId, endId, type) + this.lines.set(newLineId, lineObjects) + this.lines.delete(lineId) + + } else if (endId === originId) { + const newLineId = getLineId(startId, newId, type) + this.lines.set(newLineId, lineObjects) + this.lines.delete(lineId) + } + } + } + + /** * 删除关系关系网, 计算出差值, 可以临时放在 diffRenderer 中, 等待 commitUpdate 时统一处理 */ private removeRelations(id: string): void { @@ -291,22 +359,22 @@ export class Relation { add(type: LinkType, id: string) { if (type === 'in') - this.input.add(id) + return this.input.add(id) else if (type === 'out') - this.output.add(id) + return this.output.add(id) else if (type === 'center') - this.center.add(id) + return this.center.add(id) else throw new Error(`Unknown link type: ${type}`) } delete(type: LinkType, id: string) { if (type === 'in') - this.input.delete(id) + return this.input.delete(id) else if (type === 'out') - this.output.delete(id) + return this.output.delete(id) else if (type === 'center') - this.center.delete(id) + return this.center.delete(id) else throw new Error(`Unknown link type: ${type}`) } diff --git a/src/core/manager/StateManager.ts b/src/core/manager/StateManager.ts index 24c0bde..b247bff 100644 --- a/src/core/manager/StateManager.ts +++ b/src/core/manager/StateManager.ts @@ -63,6 +63,8 @@ export default class StateManager { */ readonly isLoading = ref(false) + readonly storeKey: string + /** * 当前场景数据 */ @@ -101,6 +103,7 @@ export default class StateManager { */ constructor(id: string, viewport: Viewport, bufferSize = 50) { this.id = id + this.storeKey = `-tmp-yvan-lcc-${this.id}` this.entityManager = viewport.entityManager this.historyBufferSize = bufferSize @@ -222,14 +225,14 @@ export default class StateManager { // 处理新增 if (this.changeTracker.added) { for (const item of this.changeTracker.added) { - this.entityManager.createEntity(item) + this.entityManager.createOrUpdateEntity(item) } } // 处理更新 if (this.changeTracker.updated) { for (const item of this.changeTracker.updated) { - this.entityManager.updateEntity(item) + this.entityManager.createOrUpdateEntity(item) } } @@ -240,7 +243,7 @@ export default class StateManager { /** * 从外部加载数据 */ - async load(data: VData) { + async load(data: Partial) { this.isLoading.value = true this.historySteps = new Array(this.maxHistorySteps).fill(null) this.historyIndex = -1 @@ -250,11 +253,11 @@ export default class StateManager { this.stopAutoSave() // 直接替换数组引用(避免响应式开销) + //@ts-ignore this.vdata = { id: this.id, - items: data.items, isChanged: false, - catalog: data.catalog + ...data } this.fullSync() // 同步到视口 @@ -404,7 +407,7 @@ export default class StateManager { // itemsCount: this.vdata.items.length // } // - // await localforage.setItem(`scene-tmp-${this.id}`, saveData) + // await localforage.setItem(this.storeKey, saveData) // } // // /** @@ -413,7 +416,7 @@ export default class StateManager { // async loadFromLocalstore() { // try { // this.isLoading.value = true - // const saved: any = await localforage.getItem(`scene-tmp-${this.id}`) + // const saved: any = await localforage.getItem(this.storeKey) // if (saved && saved.diff) { // this.applyDiff(saved.diff) // this.isChanged.value = true @@ -433,7 +436,7 @@ export default class StateManager { * 保存到本地存储 浏览器indexDb(防止数据丢失) */ async saveToLocalstore() { - await localforage.setItem(`scene-tmp-${this.id}`, this.vdata) + await localforage.setItem(this.storeKey, this.vdata) } /** @@ -442,7 +445,7 @@ export default class StateManager { async loadFromLocalstore() { try { this.isLoading.value = true - const saved: VData = await localforage.getItem(`scene-tmp-${this.id}`) + const saved: VData = await localforage.getItem(this.storeKey) if (saved) { this.vdata.items = saved.items || [] this.isChanged.value = saved.isChanged || false @@ -461,7 +464,7 @@ export default class StateManager { private fullSync() { this.entityManager.beginUpdate() this.vdata.items.forEach(item => { - this.entityManager.createEntity(item) + this.entityManager.createOrUpdateEntity(item) }) this.entityManager.commitUpdate() } @@ -480,7 +483,7 @@ export default class StateManager { */ async removeLocalstore() { try { - await localforage.removeItem(`scene-tmp-${this.id}`) + await localforage.removeItem(this.storeKey) console.log('[StateManager] 本地存储已清除') } catch (error) { console.error('[StateManager] 清除本地存储失败:', error) diff --git a/src/core/manager/WorldModel.ts b/src/core/manager/WorldModel.ts index e2d0510..961411f 100644 --- a/src/core/manager/WorldModel.ts +++ b/src/core/manager/WorldModel.ts @@ -50,9 +50,7 @@ export default class WorldModel { init() { // 观察 this.state.catalogCode 的变化, 如果变化就调用 catalogCodeChange 方法 - watch(() => this.state.catalogCode, (newValue, oldValue) => { - worldModel.loadFloor(newValue) - }) + watch(() => this.state.catalogCode, this.onCatalogCodeChanged.bind(this)) return Promise.all([ import('@/modules/measure') @@ -72,11 +70,10 @@ export default class WorldModel { } /** - * 加载指定目录的楼层数据, 并返回世界模型+楼层的唯一id + * 当楼层发生改变时, 将事件派发出去 */ - loadFloor(catalogCode: string) { + onCatalogCodeChanged(catalogCode: string) { if (!catalogCode) { - this.state.catalogCode = '' this.state.stateManagerId = '' EventBus.dispatch('catalogChanged', { catalogCode: this.state.catalogCode, @@ -88,10 +85,10 @@ export default class WorldModel { const floor = _.find(this.data.items, r => r.catalogCode === catalogCode && r.t === 'floor') if (!floor) { system.msg('楼层不存在: ' + catalogCode) + this.state.catalogCode = '' return } - this.state.catalogCode = catalogCode this.state.stateManagerId = this.data.project_uuid + '_' + catalogCode EventBus.dispatch('catalogChanged', { catalogCode: this.state.catalogCode, @@ -99,6 +96,30 @@ export default class WorldModel { }) } + async getCatalogData(catalogCode: string): Promise> { + if (!this.data || !this.data.items) { + return Promise.reject('楼层数据未加载, catalogCode=' + catalogCode) + } + + const floor = _.find(this.data.items, r => r.catalogCode === this.state.catalogCode && r.t === 'floor') + if (floor) { + if (!floor.items) { + floor.items = [] + } + + const vdata: Partial = { + items: _.cloneDeep(floor.items) as ItemJson[], + isChanged: false, + server: '', + projectId: '', + catalogCode: catalogCode, + } + + return Promise.resolve(vdata) + } + return Promise.reject('楼层不存在, catalogCode=' + catalogCode) + } + // loadFloorToScene(viewport: Viewport, scene: THREE.Scene, levelCode: string) { // let floor = _.find(this.data.items, r => r.name === levelCode && r.t === 'floor') // if (!floor) { diff --git a/src/editor/Model2DEditor.vue b/src/editor/Model2DEditor.vue index 89b4287..5ffa920 100644 --- a/src/editor/Model2DEditor.vue +++ b/src/editor/Model2DEditor.vue @@ -120,20 +120,6 @@ export default defineComponent({ return parseFloat(num).toFixed(2) }, initByFloor() { - // 将当前 url 后缀加上 ?store=${stateManager.id} - // if (stateManager) { - // window.history.replaceState({}, '', window.location.href + '?store=' + stateManager.id) - // } - // const stateManager = new StateManager(id, 50) - // worldModel.stateManager = stateManager - // worldModel.stateManager = stateManager - // system.showLoading() - // stateManager.load(floor.items) - // .finally(() => { - // system.clearLoading() - // }) - // return stateManager - this.destroyScene() delete window['editor'] @@ -149,7 +135,7 @@ export default defineComponent({ if (!worldModel.state.catalogCode || !worldModel.state.isOpened || !id) { // 放弃加载 - setQueryParam('store', id) + setQueryParam('store', '') return } @@ -163,23 +149,28 @@ export default defineComponent({ this.scene = markRaw(sceneHelp) this.viewport = markRaw(viewport) - viewport.initThree({ stateManagerId: id }) - setQueryParam('store', id) - // window.history.replaceState({}, '', window.location.href + '?store=' + id) + system.showLoading('正在加载视口...') + viewport.initThree({ stateManagerId: id }).then(() => { + setQueryParam('store', id) + // window.history.replaceState({}, '', window.location.href + '?store=' + id) + + window['viewport'] = viewport + window['THREE'] = THREE + window['scene'] = sceneHelp.scene + window['renderer'] = viewport.renderer + window['camera'] = viewport.camera + window['renderer'] = viewport.renderer + window['controls'] = viewport.controls - window['viewport'] = viewport - window['THREE'] = THREE - window['scene'] = sceneHelp.scene - window['renderer'] = viewport.renderer - window['camera'] = viewport.camera - window['renderer'] = viewport.renderer - window['controls'] = viewport.controls + viewerDom.focus() - viewerDom.focus() + // 通知父组件视口已准备好 + this.$emit('viewportChanged', viewport) + this.isReady = true - // 通知父组件视口已准备好 - this.$emit('viewportChanged', viewport) - this.isReady = true + }).finally(() => { + system.clearLoading() + }) }) } }, diff --git a/src/example/example1.js b/src/example/example1.js index f0e954f..3e183a7 100644 --- a/src/example/example1.js +++ b/src/example/example1.js @@ -35,50 +35,46 @@ export default { catalogCode: 'f1', t: 'floor', // 楼层 items: [ { - name: 'measure-group', t: 'measure', a: 'gp', // 类型, itemType.name == 'measure' 的组件处理. a:'gp' 代表分组, 渲染时他会是 Three.Group - items: [ - { - id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid - t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 - a: 'ln', // 交互类型, ln表示线点操作, pt 表示点操作 - l: '测量1', // 标签名称, 显示用 - c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 - 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 中 - center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) - in: [], // 物流入方向关联的对象(uuid) - out: [] // 物流出方向关联的对象(uuid) - } - }, - { - id: 'p2', - t: 'measure', a: 'ln', l: '测量2', c: '#ff0000', - tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], - dt: { - center: ['p3', 'p4'] - } - }, - { - id: 'p3', - t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', - tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], - dt: { - center: [] - } - }, - { - id: 'p4', - t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', - tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], - dt: { - center: [] - } - } - ] + 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', a: 'ln', l: '测量3', c: '#ff0000', + tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + center: ['p2'] + } + }, + { + id: 'p4', + t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', + tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + center: ['p2'] + } } ] } diff --git a/src/runtime/EventBus.ts b/src/runtime/EventBus.ts index eac74dc..51c822a 100644 --- a/src/runtime/EventBus.ts +++ b/src/runtime/EventBus.ts @@ -2,7 +2,7 @@ import mitt from 'mitt' const instance = mitt() -export type DispatchNames = 'objectChanged' | 'catalogChanged' +export type DispatchNames = 'objectChanged' | 'catalogChanged' | 'dataLoadComplete' export default { dispatch(name: DispatchNames, data?: any) { diff --git a/src/types/Types.d.ts b/src/types/Types.d.ts index a7959ec..065c37a 100644 --- a/src/types/Types.d.ts +++ b/src/types/Types.d.ts @@ -28,9 +28,24 @@ interface VData { id: string /** - * 地图目录 + * 所有地图目录 */ catalog: Catalog + + /** + * 服务器地址 + */ + server?: string + + /** + * 项目ID + */ + projectId?: string + + /** + * 当前楼层代码 + */ + catalogCode: string } interface CatalogItem {