You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
649 lines
18 KiB
649 lines
18 KiB
import _ from 'lodash'
|
|
import * as THREE from 'three'
|
|
import localforage from 'localforage'
|
|
import type EntityManager from './EntityManager'
|
|
import { markRaw, reactive, ref } from 'vue'
|
|
import type Viewport from '@/core/engine/Viewport.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 {
|
|
added: ItemJson[]
|
|
removed: ItemJson[]
|
|
updated: { before: ItemJson; after: ItemJson }[]
|
|
}
|
|
|
|
// 历史记录项
|
|
interface HistoryStep {
|
|
diff: DataDiff
|
|
timestamp: number
|
|
}
|
|
|
|
/**
|
|
* 数据状态管理器
|
|
* 他能够对数据进行载入, 保存草稿, 载入草稿, 数据增删改查,撤销、重做等操作
|
|
* 最终他会将数据的变化同步到 EntityManager 中
|
|
* 主要功能包括:
|
|
* 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能
|
|
* 2. 管理撤销、重做功能
|
|
* 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 个对象, 需要高效的管理数据状态
|
|
*/
|
|
export default class StateManager {
|
|
/**
|
|
* 唯一场景标识符, 用于做临时存储的 key
|
|
*/
|
|
readonly id: string
|
|
|
|
/**
|
|
* 实体对象, 用于同步当前场景的状态
|
|
*/
|
|
entityManager: EntityManager
|
|
|
|
viewport: Viewport
|
|
|
|
/**
|
|
* 是否发生了变化,通知外部是否需要保存数据
|
|
*/
|
|
readonly isChanged = ref(false)
|
|
|
|
/**
|
|
* 是否正在加载数据,通知外部是否需要等待加载完成
|
|
*/
|
|
readonly isLoading = ref(false)
|
|
|
|
readonly storeKey: string
|
|
|
|
/**
|
|
* 当前场景数据
|
|
*/
|
|
private ___vdata: VData
|
|
|
|
/**
|
|
* 使用循环缓冲区存储历史记录
|
|
*/
|
|
private historySteps: HistoryStep[] = []
|
|
private historyIndex = -1
|
|
private maxHistorySteps = 20
|
|
|
|
// 标记是否有未保存的更改
|
|
private pendingChanges = false
|
|
private isAutoSavingPaused = false
|
|
private autoSaveTimer: number | null = null
|
|
private isUpdating = false
|
|
|
|
// 变化追踪器
|
|
private readonly changeTracker: DataDiff = {
|
|
added: [],
|
|
removed: [],
|
|
updated: []
|
|
}
|
|
|
|
/**
|
|
* @param id 唯一场景标识符, 用于做临时存储的 key
|
|
* @param maxHistorySteps 最大回撤步数 默认为 20
|
|
*/
|
|
constructor(id: string, maxHistorySteps = 20) {
|
|
this.id = id
|
|
this.storeKey = `-tmp-yvan-lcc-${this.id}`
|
|
this.maxHistorySteps = maxHistorySteps
|
|
this.historySteps = []
|
|
this.pendingChanges = false
|
|
this.isAutoSavingPaused = false
|
|
}
|
|
|
|
init(viewport: Viewport) {
|
|
this.viewport = viewport
|
|
this.entityManager = viewport.entityManager
|
|
|
|
_.defer(() => {
|
|
this.fullSync(false)
|
|
})
|
|
}
|
|
|
|
// 差异反转方法
|
|
private invertDiff(diff: DataDiff): DataDiff {
|
|
return {
|
|
added: diff.removed,
|
|
removed: diff.added,
|
|
updated: diff.updated.map(u => ({ before: u.after, after: u.before }))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 将当前数据 与 entityManager 进行同步, 对比出不同的部分,分别进行更新
|
|
* - 调用 entityManager.beginEntityUpdate() 开始更新
|
|
* - 调用 entityManager.createOrUpdateEntity(vdataItem) 添加场景中新的实体
|
|
* - 调用 entityManager.deleteEntity(id) 删除场景中的实体
|
|
* - 调用 entityManager.endEntityUpdate() 结束更新场景
|
|
*/
|
|
syncDataState(diff: DataDiff): void {
|
|
// 没有变化时跳过同步
|
|
if (
|
|
diff.added.length === 0 &&
|
|
diff.removed.length === 0 &&
|
|
diff.updated.length === 0
|
|
) {
|
|
return
|
|
}
|
|
|
|
console.log('[StateManager] 同步数据状态,变更:', diff)
|
|
|
|
let needUpdateSelectBox = false
|
|
let needDeleteSelectBox = false
|
|
let needUpdateMulitSelectBox = false
|
|
this.entityManager.beginEntityUpdate()
|
|
|
|
for (const item of diff.added) {
|
|
this.entityManager.createOrUpdateEntity(item)
|
|
}
|
|
|
|
for (const { after } of diff.updated) {
|
|
this.entityManager.createOrUpdateEntity(after)
|
|
// 如果更新的对象 id 包含当前选中的
|
|
if (after.id === this.viewport.state.selectedEntityId) {
|
|
needUpdateSelectBox = true
|
|
}
|
|
if (_.includes(this.viewport.state.multiSelectedEntityIds, after.id)) {
|
|
needUpdateMulitSelectBox = true
|
|
}
|
|
}
|
|
|
|
// 最后处理删除
|
|
for (const item of diff.removed) {
|
|
this.entityManager.deleteEntity(item.id)
|
|
if (item.id === this.viewport.state.selectedEntityId) {
|
|
needDeleteSelectBox = true
|
|
}
|
|
if (_.includes(this.viewport.state.multiSelectedEntityIds, item.id)) {
|
|
needUpdateMulitSelectBox = true
|
|
}
|
|
}
|
|
|
|
this.entityManager.endEntityUpdate()
|
|
|
|
// 更新选中框
|
|
if (needDeleteSelectBox) {
|
|
this.viewport.selectManager.selectById(null)
|
|
} else if (needUpdateSelectBox) {
|
|
this.viewport.selectManager.selectById(this.viewport.state.selectedEntityId)
|
|
}
|
|
|
|
// 重新计算并更新多选框
|
|
if (needUpdateMulitSelectBox) {
|
|
const newMultiSelectedEntityIds = []
|
|
for (const entityId of this.viewport.state.multiSelectedEntityIds) {
|
|
if (this.entityManager.findItemById(entityId)) {
|
|
newMultiSelectedEntityIds.push(entityId)
|
|
}
|
|
}
|
|
this.viewport.selectManager.multiSelectByIds(newMultiSelectedEntityIds)
|
|
}
|
|
|
|
// 获取被改过的数据, 覆盖之前的数据
|
|
const writeBackMap = this.entityManager.cloneWriteBackEntities()
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 撤销
|
|
*/
|
|
undo() {
|
|
const viewport: Viewport = window['viewport']
|
|
if (!viewport) {
|
|
system.msg('没有找到当前视图')
|
|
return
|
|
}
|
|
if (!this.undoEnabled()) return
|
|
|
|
const step = this.historySteps[this.historyIndex]
|
|
if (!step) return
|
|
|
|
// 生成和应用反转差异
|
|
const reverseDiff = this.invertDiff(step.diff)
|
|
this.applyDiff(reverseDiff)
|
|
this.syncDataState(reverseDiff)
|
|
this.historyIndex--
|
|
this.isChanged.value = true
|
|
this.pendingChanges = true
|
|
|
|
system.msg('撤销完成')
|
|
// if (viewport.state.multiSelectedObjects.length > 0) {
|
|
// EventBus.dispatch('multiSelectedObjectsChanged', {
|
|
// multiSelectedObjects: viewport.state.multiSelectedObjects
|
|
// })
|
|
// }
|
|
// if (viewport.state.selectedObject) {
|
|
// EventBus.dispatch('selectedObjectPropertyChanged', {
|
|
// selectedObject: viewport.state.selectedObject
|
|
// })
|
|
// }
|
|
this.startAutoSave()
|
|
}
|
|
|
|
/**
|
|
* 重做
|
|
*/
|
|
redo() {
|
|
const viewport: Viewport = window['viewport']
|
|
if (!viewport) {
|
|
system.msg('没有找到当前视图')
|
|
return
|
|
}
|
|
if (!this.redoEnabled()) return
|
|
|
|
this.historyIndex++
|
|
const step = this.historySteps[this.historyIndex]
|
|
if (!step) return
|
|
|
|
this.applyDiff(step.diff)
|
|
this.syncDataState(step.diff)
|
|
|
|
this.isChanged.value = true
|
|
this.pendingChanges = true
|
|
|
|
system.msg('重做完成')
|
|
// if (viewport.state.multiSelectedObjects.length > 0) {
|
|
// EventBus.dispatch('multiSelectedObjectsChanged', {
|
|
// multiSelectedObjects: viewport.state.multiSelectedObjects
|
|
// })
|
|
// }
|
|
// if (viewport.state.selectedObject) {
|
|
// EventBus.dispatch('selectedObjectPropertyChanged', {
|
|
// selectedObject: viewport.state.selectedObject
|
|
// })
|
|
// }
|
|
this.startAutoSave()
|
|
}
|
|
|
|
// 差异应用方法
|
|
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)
|
|
|
|
// 更新
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 完整读取数据
|
|
* @private
|
|
*/
|
|
private fullSync(isAutoSave = true) {
|
|
// 修补关系数据, 确保 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.pendingChanges = false
|
|
|
|
if (isAutoSave) {
|
|
this.startAutoSave()
|
|
}
|
|
}
|
|
|
|
undoEnabled() {
|
|
return this.historyIndex >= 0
|
|
}
|
|
|
|
redoEnabled() {
|
|
const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps
|
|
return !!this.historySteps[nextIndex]
|
|
}
|
|
|
|
/**
|
|
* 从外部加载数据
|
|
*/
|
|
async load(data: VData) {
|
|
this.isLoading.value = true
|
|
this.historySteps = Array.from({ length: this.maxHistorySteps }, () => null)
|
|
this.historyIndex = -1
|
|
|
|
try {
|
|
// 停止自动保存,避免在加载过程中触发
|
|
this.stopAutoSave()
|
|
|
|
this.___vdata = data
|
|
|
|
// 同步到视口
|
|
// this.fullSync()
|
|
this.isChanged.value = false
|
|
|
|
console.log('[StateManager] 加载完成,共', data.items.length, '个对象')
|
|
|
|
// 强制保存一次初始状态
|
|
await this.saveToLocalstore()
|
|
|
|
} finally {
|
|
EventBus.dispatch('dataLoadComplete', {
|
|
stateManager: this
|
|
})
|
|
this.isLoading.value = false
|
|
}
|
|
}
|
|
|
|
|
|
startAutoSave() {
|
|
this.isAutoSavingPaused = false
|
|
|
|
if (this.autoSaveTimer !== null) {
|
|
window.clearTimeout(this.autoSaveTimer)
|
|
this.autoSaveTimer = null
|
|
}
|
|
|
|
|
|
// 设置新的定时器,在 5 秒后尝试保存
|
|
this.queueAutoSave()
|
|
}
|
|
|
|
stopAutoSave() {
|
|
if (this.autoSaveTimer !== null) {
|
|
window.clearTimeout(this.autoSaveTimer)
|
|
this.autoSaveTimer = null
|
|
}
|
|
this.isAutoSavingPaused = true
|
|
}
|
|
|
|
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<Object> {
|
|
return _.cloneDeep(this.___vdata)
|
|
}
|
|
|
|
/**
|
|
* 保存到本地存储 浏览器indexDb(防止数据丢失)
|
|
*/
|
|
async saveToLocalstore() {
|
|
await localforage.setItem(this.storeKey, this.___vdata)
|
|
console.log('[StateManager] 数据已保存草稿:' + this.storeKey + ', 共', this.___vdata.items.length, '个对象')
|
|
}
|
|
|
|
/**
|
|
* 从本地存储还原数据
|
|
*/
|
|
async loadFromLocalstore(): Promise<boolean> {
|
|
try {
|
|
this.isLoading.value = true
|
|
const saved: VData = await localforage.getItem(this.storeKey)
|
|
if (saved) {
|
|
this.___vdata = {
|
|
...saved
|
|
}
|
|
this.isChanged.value = saved.isChanged || false
|
|
|
|
// this.fullSync(false) // 同步到视口
|
|
console.log('[StateManager] 从本地存储 [' + this.storeKey + '] 恢复', this.___vdata.items.length, '个对象')
|
|
return true
|
|
}
|
|
|
|
return false
|
|
|
|
} catch (error) {
|
|
console.error('[StateManager] 从本地存储加载失败:', error)
|
|
return false
|
|
|
|
} finally {
|
|
this.isLoading.value = false
|
|
EventBus.dispatch('dataLoadComplete', {
|
|
stateManager: this
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除本地存储
|
|
*/
|
|
async removeLocalstore() {
|
|
try {
|
|
await localforage.removeItem(this.storeKey)
|
|
console.log('[StateManager] 本地存储已清除')
|
|
} catch (error) {
|
|
console.error('[StateManager] 清除本地存储失败:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 销毁资源
|
|
*/
|
|
dispose() {
|
|
// 清理引用
|
|
delete this.___vdata
|
|
delete this.historySteps
|
|
}
|
|
|
|
/**
|
|
* 尝试从草稿中读取数据
|
|
*/
|
|
static async tryLoadCatalogFromLocalstore(stateManagerId: string): Promise<VData | null> {
|
|
// 获取 url 中的 stateManagerId
|
|
// 从 localforage 中读取草稿数据
|
|
if (stateManagerId) {
|
|
const storeKey = `-tmp-yvan-lcc-${stateManagerId}`
|
|
const saved: VData = await localforage.getItem(storeKey)
|
|
if (saved && saved.catalog && saved.catalogCode && saved.worldData) {
|
|
return saved
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 根据 ID 查找实体
|
|
*/
|
|
findItemById(entityId: string): ItemJson | undefined {
|
|
return getFreezeDeep(_.find(this.___vdata.items, item => item.id === entityId))
|
|
}
|
|
|
|
/**
|
|
* 根据位置查找实体, 可带类型
|
|
*/
|
|
findItemByPosition(point: THREE.Vector3, itemTypeName?: string): ItemJson | undefined {
|
|
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 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
|
|
}
|
|
|