diff --git a/doc/物流模型总体介绍.md b/doc/物流模型总体介绍.md index 60ed3b6..8040e3c 100644 --- a/doc/物流模型总体介绍.md +++ b/doc/物流模型总体介绍.md @@ -193,10 +193,15 @@ export default class WorldModel { 主要功能包括: 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 2. 管理撤销、重做功能 -3. 交互控制组件通过如下步骤修改数据 - - 1. 调用 beginStateUpdate 开始修改数据 - - 2. 直接修改 vdata 数据 - - 3. 调用 endStateUpdate 完成数据修改 +3. 交互控制组件通过 update() 方法修改数据 + this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + const entity = getEntity(id) // 获取实体 + entity.abc = 123 + putEntity(entity) // 提交修改 + + deleteEntity(id) // 删除实体 + addEntity(newEntity) // 添加新实体 + }) 4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 EntityManager 进行同步 5. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 @@ -258,16 +263,6 @@ export default class StateManager { constructor(id: string, viewport: Viewport, maxHistorySteps = 20) /** - * 开始用户操作(创建数据快照) - */ - beginStateUpdate() - - /** - * 结束用户操作(计算差异并保存) - */ - endStateUpdate(): void - - /** * 将当前数据 与 entityManager 进行同步, 对比出不同的部分,分别进行更新 * - 调用 entityManager.beginEntityUpdate() 开始更新 * - 调用 entityManager.createOrUpdateEntity(vdataItem) 添加场景中新的实体 @@ -294,7 +289,12 @@ export default class StateManager { /** * 重做 */ - redo() + redo() + + /** + * 使用 updater 函数更新状态, 并同步转换为实体和渲染 + */ + update(updaterFn: (updater: StateUpdater) => void): void /** * 保存到本地存储 浏览器indexDb(防止数据丢失) @@ -311,6 +311,18 @@ export default class StateManager { */ async removeLocalstore() } + + +// 状态更新器接口 +interface StateUpdater { + getEntity(id: string): ItemJson + + putEntity(entity: ItemJson): void // 修改已有实体 + + deleteEntity(id: string): boolean // 删除实体 + + addEntity(entity: ItemJson): void // 添加新实体 +} ``` @@ -868,4 +880,4 @@ StateManager -> EntityManager -> xxx.renderer -> InstancePool interaction 完成临时辅助对象的创建, 辅助线 / 临时单元 是不会被持久化的 -点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线. \ No newline at end of file +点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线. diff --git a/src/core/ModelUtils.ts b/src/core/ModelUtils.ts index 7befdd4..02578d9 100644 --- a/src/core/ModelUtils.ts +++ b/src/core/ModelUtils.ts @@ -137,50 +137,47 @@ export function deletePointByKeyboard() { return } + // 删除当前选中的实体 const entityId = viewport.state.selectedEntityId if (!entityId) { + // 删除多选实体 const multiSelectedEntityIds = viewport.state.multiSelectedEntityIds if (!multiSelectedEntityIds && multiSelectedEntityIds.length === 0) { system.msg('请选中要删除的实体', 'error') return } - const stateManager = viewport.stateManager - stateManager.beginStateUpdate() - const deleteItems = _.remove(stateManager.vdata.items, (item) => multiSelectedEntityIds.includes(item.id)) - stateManager.endStateUpdate() - if (deleteItems.length === 0) { + 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('删除了 ' + deleteItems.length + ' 个实体') + system.msg('删除了 ' + deleteCount + ' 个实体') } + for (const deleteEntityId of multiSelectedEntityIds) { EventBus.dispatch('entityDeleted', { deleteEntityId: deleteEntityId }) - viewport.selectInspect.clearRedSelectionBoxes() } + viewport.selectInspect.cancelMultiSelect() return } - const stateManager = viewport.stateManager - viewport.stateManager.beginStateUpdate() - _.remove(stateManager.vdata.items, (item) => item.id === entityId) - viewport.stateManager.endStateUpdate() - - if (viewport.state.selectedEntityId === entityId) { - viewport.state.selectedObject = undefined - viewport.state.selectedItem = undefined - viewport.state.selectedEntityId = undefined - viewport.state.selectedObjectSetter = undefined - } - - EventBus.dispatch('entityDeleted', { - deleteEntityId: entityId + viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => { + // 删除实体 + if (deleteEntity(entityId)) { + system.msg('删除实体 [' + entityId + '] 成功') + EventBus.dispatch('entityDeleted', { + deleteEntityId: entityId + }) + viewport.selectInspect.cancelSelect() + } }) - - system.msg('删除 [' + entityId + ']') - viewport.selectInspect.clearSelectionBox() } export function escByKeyboard() { @@ -210,38 +207,6 @@ export function escByKeyboard() { system.msg('操作已取消') } -/** - * 查找指定点附近指定距离内的所有 stateManager 下的 item 对象 - * 如果要修改数据做提交, 执行此方法之前必须执行 beginStateUpdate - * @param viewport 视窗 - * @param point 点位 x,z 坐标 - * @param distance 距离 - */ -export function findStateItemsByDistance(viewport: Viewport, point: Vector2, distance: number): ItemJson[] { - const result: ItemJson[] = [] - - for (const item of viewport.stateManager.vdata.items) { - // 安全校验 tf 结构 - if (!item.tf || !Array.isArray(item.tf) || item.tf[0].length < 3) { - continue - } - - const [x, , z] = item.tf[0] - const itemPoint = new Vector2(x, z) - const dist = itemPoint.distanceTo(point) - - if (dist <= distance) { - result.push(item) - } - } - - // 按距离升序排序(近距离优先) - return result.sort((a, b) => { - const aPos = new Vector2(a.tf[0][0], a.tf[0][2]) - const bPos = new Vector2(b.tf[0][0], b.tf[0][2]) - return aPos.distanceTo(point) - bPos.distanceTo(point) - }) -} export function moveSelectedItem(direct: '↑' | '↓' | '←' | '→') { // 获取当前是否按住了 Shift @@ -266,14 +231,11 @@ export function moveSelectedItem(direct: '↑' | '↓' | '←' | '→') { } // 群体移动 - const stateManager = viewport.stateManager - stateManager.beginStateUpdate() - for (const item of stateManager.vdata.items) { - if (multiSelectedEntityIds.includes(item.id)) { - // 根据方向移动 + viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => { + for (const id of multiSelectedEntityIds) { + const item = getEntity(id) switch (direct) { case '↑': - console.log('向上移动', item.tf[0][2], '-=', delta) item.tf[0][2] -= delta // 向上移动 break case '↓': @@ -286,9 +248,9 @@ export function moveSelectedItem(direct: '↑' | '↓' | '←' | '→') { item.tf[0][0] += delta // 向右移动 break } + putEntity(item) } - } - stateManager.endStateUpdate() + }) EventBus.dispatch('multiselectedObjectChanged', { multiSelectedObjects: viewport.state.multiSelectedObjects }) @@ -296,28 +258,30 @@ export function moveSelectedItem(direct: '↑' | '↓' | '←' | '→') { return } - const stateManager = viewport.stateManager - viewport.stateManager.beginStateUpdate() - for (const item of stateManager.vdata.items) { - if (item.id === entityId) { - // 根据方向移动 - 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 - } + viewport.stateManager.update(({ getEntity, putEntity, deleteEntity }) => { + const item = getEntity(entityId) + if (!item) { + system.msg('没有找到选中的实体', 'error') + return } - } - viewport.stateManager.endStateUpdate() + + // 根据方向移动 + 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('multiselectedObjectChanged', { multiSelectedObjects: viewport.state.multiSelectedObjects }) @@ -336,115 +300,18 @@ export function quickCopyByMouse() { const x = CurrentMouseInfo.x const z = CurrentMouseInfo.z const viewport: Viewport = CurrentMouseInfo.viewport - // const point: THREE.Vector2 = currentMouseInfo.mouse - // - // const ray = new THREE.Raycaster() - // ray.setFromCamera(point, viewport.camera) - // const intersections = ray.intersectObjects(viewport.dragControl._dragObjects, true) - // - // if (intersections.length === 0) { - // system.msg('没有找到可复制的对象') - // return - // } - // console.log('intersections:', intersections) // 如果不在线上,查找1米内的有效点 Object3D, 如果有,则以这个点为起点, 延伸同类型的点,并让他们相连 - const items = findStateItemsByDistance(viewport, new Vector2(x, z), 1) + const items = viewport.stateManager.findStateItemsByDistance(new Vector2(x, z), 1) if (items[0]) { // 找到一个有效点,执行复制操作 viewport.interactionManager.startInteraction(items[0].t, { startPoint: items[0].id }) - return - } - // const r = findObject3DByCondition(viewport.scene, object => { - // // 判断 object 是否是有效的 Object3D, 并且是当前 viewport 的对象 - // if (object instanceof THREE.Object3D && object.visible && - // object.userData.type && viewport.toolbox[object.userData.type]) { - // - // const toolbox: Toolbox = viewport.toolbox[object.userData.type] - // - // // 检查是否在 0.2 米内 - // const distance = object.position.distanceTo(new THREE.Vector3(x, 0, z)) - // if (distance < 0.2) { - // // 找到一个有效点,执行复制操作 - // viewport.toolStartObject = object - // viewport.state.cursorMode = object.userData.type - // // toolbox.start(object) - // system.msg('连线成功') - // return true - // } - // } - // return false - // }) - - if (!items || items.length === 0) { + } else { system.msg('鼠标所在位置,没有可复制的对象', 'error') - return } } -// -// /** -// * 查找射线周围指定半径内的对象 -// */ -// export function findObjectsInRadius(viewport: Viewport, -// point: THREE.Vector2, -// radius: number, -// lines: { object: THREE.Object3D, distance: number }[], -// points: { object: THREE.Object3D, distance: number }[] -// ): void { -// const ray = new THREE.Raycaster() -// ray.setFromCamera(point, viewport.camera) -// -// viewport.dragControl._dragObjects.forEach(obj => { -// if (obj instanceof THREE.Points) { -// // 处理点云:遍历每个点 -// const distance = distanceToRay(ray, point) -// if (distance <= radius) { -// points.push({ object: obj, distance }) -// } -// -// } else if (obj instanceof THREE.Line) { -// // 处理线段:计算线段到射线的最近距离 -// const distance = getLineDistanceToRay(ray, obj) -// if (distance <= radius) { -// lines.push({ object: obj, distance }) -// } -// } -// }) -// } -// -// /** -// * 计算点到射线的最短距离 -// */ -// function distanceToRay(ray: THREE.Raycaster, point: THREE.Vector2) { -// const closestPoint = new THREE.Vector3() -// ray.closestPointToPoint(point, closestPoint) -// return point.distanceTo(closestPoint) -// } -// -// /** -// * 计算线段到射线的最短距离 -// */ -// function getLineDistanceToRay(ray: THREE.Raycaster, line: THREE.Line) { -// const lineStart = new THREE.Vector3() -// const lineEnd = new THREE.Vector3() -// line.geometry.attributes.position.getXYZ(0, lineStart) -// line.geometry.attributes.position.getXYZ(1, lineEnd) -// line.localToWorld(lineStart) -// line.localToWorld(lineEnd) -// -// const lineSegment = new THREE.Line3(lineStart, lineEnd) -// const closestOnRay = new THREE.Vector3() -// const closestOnLine = new THREE.Vector3() -// THREE.Line3.prototype.closestPointsRayLine ??= function(ray, line, closestOnRay, closestOnLine) { -// // 实现射线与线段最近点计算(需自定义或使用数学库) -// } -// -// lineSegment.closestPointsRayLine(ray, true, closestOnRay, closestOnLine) -// return closestOnRay.distanceTo(closestOnLine) -// } - /** * 考虑吸附的情况下计算鼠标事件位置 */ @@ -513,62 +380,62 @@ export function findObject3DByCondition(scene: THREE.Object3D, condition: (objec 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 -} +// 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 +// } /** * 十进制求和 diff --git a/src/core/base/BaseInteraction.ts b/src/core/base/BaseInteraction.ts index 9255dfc..ffa703c 100644 --- a/src/core/base/BaseInteraction.ts +++ b/src/core/base/BaseInteraction.ts @@ -42,10 +42,6 @@ export default abstract class BaseInteraction { linkStartPointId: string linkStartPointObject: THREE.Object3D - dragOption: DragOption | undefined - dragOriginPosition: THREE.Vector3 | undefined - dragItem: ItemJson | undefined - templineMaterial = new LineMaterial({ color: 0xE63C17, // 主颜色 linewidth: 2, // 实际可用的线宽 @@ -90,56 +86,6 @@ export default abstract class BaseInteraction { } /** - * 拖拽点开始 - */ - dragPointStart(viewport: Viewport, dragOption: DragOption) { - this.viewport = viewport - this.dragOption = dragOption - this.dragOriginPosition = dragOption.object.position.clone() - - // 找到 itemJson - const itemJson = _.find(this.viewport.stateManager.vdata.items, (item) => item.id === dragOption.entityId) - if (!itemJson) { - system.showErrorDialog('Not found for entityId:' + dragOption.entityId) - return false - } - - this.dragItem = itemJson - return true - } - - /** - * 拖拽点移动 - */ - dragPointMove(viewport: Viewport, e: MouseEvent) { - if (this.viewport !== viewport) return - } - - /** - * 拖拽点完成 - */ - dragPointComplete(viewport: Viewport, e: MouseEvent) { - if (this.viewport !== viewport) return - - // 获取当前鼠标所在位置 - if (!CurrentMouseInfo || isNaN(CurrentMouseInfo.x) || isNaN(CurrentMouseInfo.z) || !this.dragItem?.tf?.[0]) { - return - } - - // 提交状态管理器 - const stateManager = this.viewport.stateManager - stateManager.beginStateUpdate({ createFromInteraction: true }) - this.dragItem.tf[0][0] = CurrentMouseInfo.x - this.dragItem.tf[0][2] = CurrentMouseInfo.z - stateManager.endStateUpdate() - - this.viewport = undefined - this.dragOption = undefined - this.dragItem = undefined - return true - } - - /** * 开始交互 */ start(viewport: Viewport, option: InteractionOption = {}) { @@ -318,12 +264,11 @@ export default abstract class BaseInteraction { } // 则添加一个新的点 - const stateManager = this.viewport.stateManager - stateManager.beginStateUpdate({ createFromInteraction: true }) - catchPoint = {} as ItemJson - catchPoint = this.createPointOfItem(catchPoint, point) - stateManager.vdata.items.push(catchPoint) - stateManager.endStateUpdate() + this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + catchPoint = {} as ItemJson + catchPoint = this.createPointOfItem(catchPoint, point) + addEntity(catchPoint) + }) return } @@ -343,19 +288,17 @@ export default abstract class BaseInteraction { system.msg('Cannot link to itself.') return } - // 关联2个点 - if (this.linkStartPointId && from) { + // 关联2个点 + this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + catchPoint = getEntity(catchPoint.id) catchPoint.dt.center.push(this.linkStartPointId) - from.dt.center.push(catchPoint.id) - } + putEntity(catchPoint) - // 提交状态管理器 - const stateManager = this.viewport.stateManager - stateManager.beginStateUpdate({ createFromInteraction: true }) - catchPoint.dt.center.push(this.linkStartPointId) - from.dt.center.push(catchPoint.id) - stateManager.endStateUpdate() + from = getEntity(from.id) + from.dt.center.push(catchPoint.id) + putEntity(from) + }) } else { // 添加正式点 @@ -363,18 +306,19 @@ export default abstract class BaseInteraction { catchPoint = this.createPointOfItem(catchPoint, point) // 提交状态管理器 - const stateManager = this.viewport.stateManager - stateManager.beginStateUpdate({ createFromInteraction: true }) - // 关联2个点 - stateManager.vdata.items.push(catchPoint) - if (from) { - catchPoint.dt.center.push(this.linkStartPointId) - from.dt.center.push(catchPoint.id) - - } else { - stateManager.vdata.items.push(catchPoint) - } - stateManager.endStateUpdate() + this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + if (from) { + catchPoint.dt.center.push(from.id) + addEntity(catchPoint) + + from = getEntity(from.id) + from.dt.center.push(catchPoint.id) + putEntity(from) + + } else { + addEntity(catchPoint) + } + }) } // 把点加入拖拽控制器 @@ -442,4 +386,4 @@ export default abstract class BaseInteraction { export interface DragOption { object: THREE.Object3D entityId: string -} \ No newline at end of file +} diff --git a/src/core/controls/DragControl.ts b/src/core/controls/DragControl.ts index f6dbe0a..f3ac737 100644 --- a/src/core/controls/DragControl.ts +++ b/src/core/controls/DragControl.ts @@ -255,24 +255,23 @@ export default class DragControl implements IControls { private dragComplete = (startPos: THREE.Vector2, targetPos: THREE.Vector2): void => { // console.log(`Drag completed from ${startPos.toArray()} to ${targetPos.toArray()}`) - this.viewport.stateManager.beginStateUpdate() - for (const object of this.dragShadowsGroup.children) { - const entityId = object.userData.entityId - if (entityId) { - const entity = this.viewport.stateManager.findItemById(entityId) - if (entity) { + + this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + for (const object of this.dragShadowsGroup.children) { + const entityId = object.userData.entityId + if (entityId) { + const entity = getEntity(entityId) // 更新实体位置 entity.tf[0][0] = object.position.x entity.tf[0][2] = object.position.z + putEntity(entity) } else { - system.showErrorDialog('not found entityId:' + entityId) + system.showErrorDialog('not found entity') } - } else { - system.showErrorDialog('not found entity') } - } - this.viewport.stateManager.endStateUpdate() + }) + EventBus.dispatch('multiselectedObjectChanged', { multiSelectedObjects: this.viewport.state.multiSelectedObjects }) diff --git a/src/core/manager/StateManager.ts b/src/core/manager/StateManager.ts index a1c13d9..3f856db 100644 --- a/src/core/manager/StateManager.ts +++ b/src/core/manager/StateManager.ts @@ -4,9 +4,10 @@ import localforage from 'localforage' 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 { getFreezeDeep, getQueryParams, setQueryParam } from '@/utils/webutils.ts' import { ensureEntityRelationsConsistency } from '@/core/ModelUtils.ts' import EventBus from '@/runtime/EventBus.ts' +import { Vector2 } from 'three/src/math/Vector2' // 差异类型定义 interface DataDiff { @@ -28,38 +29,20 @@ interface HistoryStep { * 主要功能包括: * 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 * 2. 管理撤销、重做功能 - * 3. 交互控制组件通过如下步骤修改数据 - * - 1. 调用 beginStateUpdate 开始修改数据 - * - 2. 直接修改 vdata 数据 - * - 3. 调用 endStateUpdate 完成数据修改 + * 3. 交互控制组件通过 update() 方法修改数据 + * this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => { + * const entity = getEntity(id) // 获取实体 + * entity.abc = 123 + * putEntity(entity) // 提交修改 + * + * deleteEntity(id) // 删除实体 + * addEntity(newEntity) // 添加新实体 + * }) * 4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 EntityManager 进行同步 * 5. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 * * 主要难点: * - 单张地图数据量可能超过 10000 个对象, 需要高效的管理数据状态 - * - * // 用例1, 修改 - * stateManager.beginStateUpdate();stateManager.vdata.items[1].tf[0] = [-10, 0, 4];stateManager.endStateUpdate(); - * stateManager.undo() - * stateManager.redo() - * - * // 用例2 添加 - * stateManager.beginStateUpdate();stateManager.vdata.items[3].dt.center.push('p5');stateManager.vdata.items.push({ id: 'p5', t: 'measure', tf: [[-6.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], dt: { center: ['p4'] } });stateManager.endStateUpdate(); - * stateManager.undo(); - * stateManager.redo(); - * - * // 用例3 删除 - * stateManager.beginStateUpdate(); stateManager.vdata.items.splice(3, 1); stateManager.endStateUpdate(); - * stateManager.undo() - * stateManager.redo() - * - * // 用例4 多连线 - * stateManager.beginStateUpdate();stateManager.vdata.items[0].dt.center.push('p3');stateManager.vdata.items[2].dt.center.push('p1');stateManager.endStateUpdate(); - * stateManager.undo() - * - * // 用例5 删除线 - * stateManager.beginStateUpdate();stateManager.vdata.items[0].dt.center=[];stateManager.vdata.items[1].dt.center.splice(0,1);stateManager.endStateUpdate(); - * stateManager.undo() */ export default class StateManager { /** @@ -87,7 +70,7 @@ export default class StateManager { /** * 当前场景数据 */ - vdata: VData + private ___vdata: VData /** * 使用循环缓冲区存储历史记录 @@ -110,11 +93,6 @@ export default class StateManager { } /** - * 数据快照(用于差异计算) - */ - private lastStateDict = new Map() - - /** * @param id 唯一场景标识符, 用于做临时存储的 key * @param viewport 视口对象, 用于获取、同步当前场景的状态 * @param maxHistorySteps 最大回撤步数 默认为 20 @@ -125,40 +103,12 @@ export default class StateManager { this.entityManager = viewport.entityManager this.maxHistorySteps = maxHistorySteps - // this.historySteps = Array.from({ length: this.maxHistorySteps }, () => null) this.historySteps = [] this.pendingChanges = false this.isAutoSavingPaused = false } - /** - * 开始用户操作(创建数据快照) - */ - beginStateUpdate(option: StateUpdateOption = {}): void { - this.lastStateDict = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) - this.changeTracker.added.length = 0 - this.changeTracker.removed.length = 0 - this.changeTracker.updated.length = 0 - this.isUpdating = true - } - - /** - * 结束用户操作(计算差异并保存) - */ - endStateUpdate(option = { autoSave: true }): void { - this.calculateDiff() - this.saveStep() - this.syncDataState(this.changeTracker) - this.isChanged.value = true - this.pendingChanges = true // 标记有需要保存的更改 - this.isUpdating = false - - if (option.autoSave) { - this.startAutoSave() // 触发自动保存 - } - } - // 差异反转方法 private invertDiff(diff: DataDiff): DataDiff { return { @@ -168,67 +118,6 @@ export default class StateManager { } } - // 计算当前状态与快照的差异 - private calculateDiff() { - const currentMap = new Map(this.vdata.items.map(item => [item.id, item])) - const added: ItemJson[] = [] - const removed: ItemJson[] = [] - const updated: { before: ItemJson; after: ItemJson }[] = [] - - // 检查删除 & 更新 - for (const [id, item] of this.lastStateDict.entries()) { - if (!currentMap.has(id)) { - removed.push(item) - - } else if (!_.isEqual(item, currentMap.get(id))) { - updated.push({ - before: item, - after: currentMap.get(id) - }) - } - } - - // 检查新增 - for (const [id, item] of currentMap.entries()) { - if (!this.lastStateDict.has(id)) { - added.push(item) - } - } - - this.changeTracker.added = added - this.changeTracker.removed = removed - this.changeTracker.updated = updated - } - - - /** - * 保存差异到历史记录 - */ - private saveStep() { - const { added, removed, updated } = this.changeTracker - if (added.length === 0 && removed.length === 0 && updated.length === 0) return - - // 深拷贝差异对象 - const clonedDiff = { - added: _.cloneDeep(added), - removed: _.cloneDeep(removed), - updated: _.cloneDeep(updated) - } - - const step: HistoryStep = { - diff: clonedDiff, - timestamp: Date.now() - } - - if (this.historySteps.length >= this.maxHistorySteps) { - this.historySteps.shift() - } - this.historySteps.push(step) - this.historyIndex = this.historySteps.length - 1 - this.pendingChanges = true - } - - /** * 将当前数据 与 entityManager 进行同步, 对比出不同的部分,分别进行更新 * - 调用 entityManager.beginEntityUpdate() 开始更新 @@ -267,10 +156,10 @@ export default class StateManager { // 获取被改过的数据, 覆盖之前的数据 const writeBackMap = this.entityManager.cloneWriteBackEntities() - for (let i = 0; i < this.vdata.items.length; i++) { - const item = this.vdata.items[i] + for (let i = 0; i < this.___vdata.items.length; i++) { + const item = this.___vdata.items[i] if (writeBackMap.has(item.id)) { - this.vdata.items[i] = writeBackMap.get(item.id) + this.___vdata.items[i] = writeBackMap.get(item.id) } } } @@ -346,50 +235,60 @@ export default class StateManager { this.startAutoSave() } - startAutoSave() { - this.isAutoSavingPaused = false - - if (this.autoSaveTimer !== null) { - window.clearTimeout(this.autoSaveTimer) - this.autoSaveTimer = null - } + // 差异应用方法 + private applyDiff(diff: DataDiff) { + const { added, removed, updated } = diff + // 删除 + const removedIds = new Set(removed.map(item => item.id)) + _.remove(this.___vdata.items, item => removedIds.has(item.id)) - // 设置新的定时器,在 5 秒后尝试保存 - this.queueAutoSave() - } + // 新增 + const addedIds = new Set(added.map(i => i.id)) + _.remove(this.___vdata.items, item => addedIds.has(item.id)) + this.___vdata.items.push(...added) - stopAutoSave() { - if (this.autoSaveTimer !== null) { - window.clearTimeout(this.autoSaveTimer) - this.autoSaveTimer = null + // 更新 + const updateMap = new Map(updated.map(u => [u.after.id, u.after])) + for (const [key, after] of updateMap) { + const idx = _.findIndex(this.___vdata.items, (item => item.id === key)) + if (idx >= 0) { + Object.assign(this.___vdata.items[idx], after) + } else { + this.___vdata.items.push(after) + } } - this.isAutoSavingPaused = true } - private queueAutoSave() { - if (this.autoSaveTimer !== null || this.isAutoSavingPaused) return - if (!this.pendingChanges) return + /** + * 完整读取数据 + * @private + */ + private fullSync(isAutoSave = true) { + // 修补关系数据, 确保 center / in / out 关系满足一致性 + this.___vdata.items = ensureEntityRelationsConsistency(this.___vdata.items) - this.autoSaveTimer = window.setTimeout(async () => { - this.autoSaveTimer = null + this.entityManager.beginEntityUpdate() + this.___vdata.items.forEach(item => { + this.entityManager.createOrUpdateEntity(item) + }) + this.entityManager.endEntityUpdate() - if (this.isUpdating) { - const checkInterval = setInterval(() => { - if (!this.isUpdating) { - clearInterval(checkInterval) - if (!this.isAutoSavingPaused) { - this.queueAutoSave() - } - } else { - console.log('wait for entityManager to finish updating...') - } - }, 200) - return - } + // 初始状态作为第一步 + this.pendingChanges = false - await this.saveToLocalstore() - }, 2500) + if (isAutoSave) { + this.startAutoSave() + } + } + + undoEnabled() { + return this.historyIndex >= 0 + } + + redoEnabled() { + const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps + return !!this.historySteps[nextIndex] } /** @@ -406,7 +305,7 @@ export default class StateManager { // 直接替换数组引用(避免响应式开销) //@ts-ignore - this.vdata = { + this.___vdata = { id: this.id, isChanged: false, ...data @@ -426,73 +325,66 @@ export default class StateManager { } } - // 差异应用方法 - private applyDiff(diff: DataDiff) { - const { added, removed, updated } = diff - - // 删除 - const removedIds = new Set(removed.map(item => item.id)) - _.remove(this.vdata.items, item => removedIds.has(item.id)) - // 新增 - const addedIds = new Set(added.map(i => i.id)) - _.remove(this.vdata.items, item => addedIds.has(item.id)) - this.vdata.items.push(...added) + startAutoSave() { + this.isAutoSavingPaused = false - // 更新 - const updateMap = new Map(updated.map(u => [u.after.id, u.after])) - for (const [key, after] of updateMap) { - const idx = _.findIndex(this.vdata.items, (item => item.id === key)) - if (idx >= 0) { - Object.assign(this.vdata.items[idx], after) - } else { - this.vdata.items.push(after) - } + if (this.autoSaveTimer !== null) { + window.clearTimeout(this.autoSaveTimer) + this.autoSaveTimer = null } - } - - /** - * 完整读取数据 - * @private - */ - private fullSync() { - // 修补关系数据, 确保 center / in / out 关系满足一致性 - this.vdata.items = ensureEntityRelationsConsistency(this.vdata.items) - this.entityManager.beginEntityUpdate() - this.vdata.items.forEach(item => { - this.entityManager.createOrUpdateEntity(item) - }) - this.entityManager.endEntityUpdate() - // 初始状态作为第一步 - this.beginStateUpdate() - this.endStateUpdate({ autoSave: false }) - this.pendingChanges = false + // 设置新的定时器,在 5 秒后尝试保存 + this.queueAutoSave() } - undoEnabled() { - return this.historyIndex >= 0 + stopAutoSave() { + if (this.autoSaveTimer !== null) { + window.clearTimeout(this.autoSaveTimer) + this.autoSaveTimer = null + } + this.isAutoSavingPaused = true } - redoEnabled() { - const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps - return !!this.historySteps[nextIndex] + private queueAutoSave() { + if (this.autoSaveTimer !== null || this.isAutoSavingPaused) return + if (!this.pendingChanges) return + + this.autoSaveTimer = window.setTimeout(async () => { + this.autoSaveTimer = null + + if (this.isUpdating) { + const checkInterval = setInterval(() => { + if (!this.isUpdating) { + clearInterval(checkInterval) + if (!this.isAutoSavingPaused) { + this.queueAutoSave() + } + } else { + console.log('wait for entityManager to finish updating...') + } + }, 200) + return + } + + await this.saveToLocalstore() + }, 2000) } /** * 保存数据到外部 */ async save(): Promise { - return _.cloneDeep(this.vdata) + return _.cloneDeep(this.___vdata) } /** * 保存到本地存储 浏览器indexDb(防止数据丢失) */ async saveToLocalstore() { - await localforage.setItem(this.storeKey, this.vdata) - console.log('[StateManager] 数据已保存草稿:' + this.storeKey + ', 共', this.vdata.items.length, '个对象') + await localforage.setItem(this.storeKey, this.___vdata) + console.log('[StateManager] 数据已保存草稿:' + this.storeKey + ', 共', this.___vdata.items.length, '个对象') } /** @@ -503,13 +395,13 @@ export default class StateManager { this.isLoading.value = true const saved: VData = await localforage.getItem(this.storeKey) if (saved) { - this.vdata = { + this.___vdata = { ...saved } this.isChanged.value = saved.isChanged || false - this.fullSync() // 同步到视口 - console.log('[StateManager] 从本地存储恢复', this.vdata.items.length, '个对象') + this.fullSync(false) // 同步到视口 + console.log('[StateManager] 从本地存储恢复', this.___vdata.items.length, '个对象') return true } @@ -541,7 +433,7 @@ export default class StateManager { */ dispose() { // 清理引用 - delete this.vdata + delete this.___vdata delete this.historySteps } @@ -582,23 +474,151 @@ export default class StateManager { * 根据 ID 查找实体 */ findItemById(linkStartPointId: string): ItemJson | undefined { - return _.find(this.vdata.items, item => item.id === linkStartPointId) + return getFreezeDeep(_.find(this.___vdata.items, item => item.id === linkStartPointId)) } /** * 根据位置查找实体, 可带类型 */ findItemByPosition(point: THREE.Vector3, itemTypeName?: string): ItemJson | undefined { - for (const item of this.vdata.items) { + for (const item of this.___vdata.items) { if (item.tf?.[0]?.[0] === point.x && item.tf?.[0]?.[2] === point.z) { if (!itemTypeName || item.t === itemTypeName) { - return item + return getFreezeDeep(item) } } } } + + /** + * 使用 updater 函数更新状态, 并同步转换为实体和渲染 + */ + update(updaterFn: (updater: StateUpdater) => void): void { + const changeTracker: DataDiff = { + added: [], + removed: [], + updated: [] + } + this.isUpdating = true + + const entityMap = new Map(this.___vdata.items.map(item => [item.id, item])) + + // 构建 Updater 实例 + const updater: StateUpdater = { + getEntity: (id: string) => { + const entity = entityMap.get(id) + if (!entity) throw new Error(`Entity with id ${id} not found`) + return _.cloneDeep(entity) + }, + putEntity: (entity: ItemJson) => { + const id = entity.id + const original = entityMap.get(id) + if (!original) throw new Error(`Entity with id ${id} not found for update`) + if (!_.isEqual(original, entity)) { + changeTracker.updated.push({ + before: _.cloneDeep(original), + after: _.cloneDeep(entity) + }) + entityMap.set(id, _.cloneDeep(entity)) + } + }, + deleteEntity: (id: string): boolean => { + const original = entityMap.get(id) + if (!original) return + changeTracker.removed.push(_.cloneDeep(original)) + return entityMap.delete(id) + }, + addEntity: (entity: ItemJson) => { + const id = entity.id + if (entityMap.has(id)) throw new Error(`Entity with id ${id} already exists`) + changeTracker.added.push(_.cloneDeep(entity)) + entityMap.set(id, _.cloneDeep(entity)) + } + } + + // 停止自动保存,避免在更新过程中触发 + this.stopAutoSave() + + // 执行用户传入的修改函数 + updaterFn(updater) + + // 更新 vdata.items + this.___vdata.items = Array.from(entityMap.values()) + + // 保存差分到 history 和 sync 到 entityManager + this.saveStepWithDiff(changeTracker) + this.syncDataState(changeTracker) + this.isChanged.value = true + this.pendingChanges = true + this.isUpdating = false + this.startAutoSave() + } + + private saveStepWithDiff(diff: DataDiff): void { + if (diff.added.length === 0 && diff.removed.length === 0 && diff.updated.length === 0) { + return + } + + const step: HistoryStep = { + diff: { + added: _.cloneDeep(diff.added), + removed: _.cloneDeep(diff.removed), + updated: _.cloneDeep(diff.updated) + }, + timestamp: Date.now() + } + + if (this.historySteps.length >= this.maxHistorySteps) { + this.historySteps.shift() + } + this.historySteps.push(step) + this.historyIndex = this.historySteps.length - 1 + } + + + /** + * 查找指定点附近指定距离内的所有 stateManager 下的 item 对象 + * @param point 点位 x,z 坐标 + * @param distance 距离 + */ + findStateItemsByDistance(point: Vector2, distance: number): ItemJson[] { + const result: ItemJson[] = [] + + for (const item of this.___vdata.items) { + // 安全校验 tf 结构 + if (!item.tf || !Array.isArray(item.tf) || item.tf[0].length < 3) { + continue + } + + const [x, , z] = item.tf[0] + const itemPoint = new Vector2(x, z) + const dist = itemPoint.distanceTo(point) + + if (dist <= distance) { + result.push(getFreezeDeep(item)) + } + } + + // 按距离升序排序(近距离优先) + return result.sort((a, b) => { + const aPos = new Vector2(a.tf[0][0], a.tf[0][2]) + const bPos = new Vector2(b.tf[0][0], b.tf[0][2]) + return aPos.distanceTo(point) - bPos.distanceTo(point) + }) + } +} + +export interface StateUpdater { + getEntity(id: string): ItemJson + + putEntity(entity: ItemJson): void // 修改已有实体 + + deleteEntity(id: string): boolean // 删除实体 + + addEntity(entity: ItemJson): void // 添加新实体 } export interface StateUpdateOption { + autoSave?: boolean createFromInteraction?: boolean } diff --git a/src/editor/widgets/property/PropertyPanel.vue b/src/editor/widgets/property/PropertyPanel.vue index 2b9b401..0391ab3 100644 --- a/src/editor/widgets/property/PropertyPanel.vue +++ b/src/editor/widgets/property/PropertyPanel.vue @@ -1,138 +1,139 @@ diff --git a/src/editor/widgets/property/PropertyView.vue b/src/editor/widgets/property/PropertyView.vue index afe09cd..ec0553b 100644 --- a/src/editor/widgets/property/PropertyView.vue +++ b/src/editor/widgets/property/PropertyView.vue @@ -1,166 +1,158 @@