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.
535 lines
14 KiB
535 lines
14 KiB
import _ from 'lodash'
|
|
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'
|
|
|
|
// 差异类型定义
|
|
interface DataDiff {
|
|
added: ItemJson[]
|
|
removed: ItemJson[]
|
|
updated: { before: ItemJson; after: ItemJson }[]
|
|
}
|
|
|
|
// 历史记录项
|
|
interface HistoryStep {
|
|
diff: DataDiff
|
|
timestamp: number
|
|
}
|
|
|
|
/**
|
|
* 数据状态管理器
|
|
* 他能够对数据进行载入, 保存草稿, 载入草稿, 数据增删改查,撤销、重做等操作
|
|
* 最终他会将数据的变化同步到 EntityManager 中
|
|
* 主要功能包括:
|
|
* 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能
|
|
* 2. 管理撤销、重做功能
|
|
* 3. 交互控制组件通过如下步骤修改数据
|
|
* - 1. 调用 beginStateUpdate 开始修改数据
|
|
* - 2. 直接修改 vdata 数据
|
|
* - 3. 调用 endStateUpdate 完成数据修改
|
|
* 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 {
|
|
/**
|
|
* 唯一场景标识符, 用于做临时存储的 key
|
|
*/
|
|
readonly id: string
|
|
|
|
/**
|
|
* 实体对象, 用于同步当前场景的状态
|
|
*/
|
|
readonly entityManager: EntityManager
|
|
|
|
/**
|
|
* 是否发生了变化,通知外部是否需要保存数据
|
|
*/
|
|
readonly isChanged = ref(false)
|
|
|
|
/**
|
|
* 是否正在加载数据,通知外部是否需要等待加载完成
|
|
*/
|
|
readonly isLoading = ref(false)
|
|
|
|
readonly storeKey: string
|
|
|
|
/**
|
|
* 当前场景数据
|
|
*/
|
|
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: []
|
|
}
|
|
|
|
/**
|
|
* 数据快照(用于差异计算)
|
|
*/
|
|
private lastStateDict = new Map<string, ItemJson>()
|
|
|
|
/**
|
|
* @param id 唯一场景标识符, 用于做临时存储的 key
|
|
* @param viewport 视口对象, 用于获取、同步当前场景的状态
|
|
* @param maxHistorySteps 最大回撤步数 默认为 20
|
|
*/
|
|
constructor(id: string, viewport: Viewport, maxHistorySteps = 20) {
|
|
this.id = id
|
|
this.storeKey = `-tmp-yvan-lcc-${this.id}`
|
|
this.entityManager = viewport.entityManager
|
|
this.maxHistorySteps = maxHistorySteps
|
|
|
|
// this.historySteps = Array.from({ length: this.maxHistorySteps }, () => null)
|
|
this.historySteps = []
|
|
|
|
this.pendingChanges = false
|
|
this.isAutoSavingPaused = false
|
|
}
|
|
|
|
/**
|
|
* 开始用户操作(创建数据快照)
|
|
*/
|
|
beginStateUpdate(): 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 {
|
|
added: diff.removed,
|
|
removed: diff.added,
|
|
updated: diff.updated.map(u => ({ before: u.after, after: u.before }))
|
|
}
|
|
}
|
|
|
|
// 计算当前状态与快照的差异
|
|
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() 开始更新
|
|
* - 调用 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)
|
|
|
|
this.entityManager.beginEntityUpdate()
|
|
|
|
for (const item of diff.added) {
|
|
this.entityManager.createOrUpdateEntity(item)
|
|
}
|
|
|
|
for (const { after } of diff.updated) {
|
|
this.entityManager.createOrUpdateEntity(after)
|
|
}
|
|
|
|
// 最后处理删除
|
|
for (const item of diff.removed) {
|
|
this.entityManager.deleteEntity(item.id)
|
|
}
|
|
|
|
this.entityManager.endEntityUpdate()
|
|
|
|
// 从实体管理器中获取最新数据
|
|
const updatedItems = this.entityManager.getAllEntities()
|
|
this.vdata.items = Array.from(updatedItems.values())
|
|
}
|
|
|
|
/**
|
|
* 撤销
|
|
*/
|
|
undo() {
|
|
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('撤销完成')
|
|
this.startAutoSave()
|
|
}
|
|
|
|
/**
|
|
* 重做
|
|
*/
|
|
redo() {
|
|
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('重做完成')
|
|
this.startAutoSave()
|
|
}
|
|
|
|
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()
|
|
}, 5000)
|
|
}
|
|
|
|
/**
|
|
* 从外部加载数据
|
|
*/
|
|
async load(data: Partial<VData>) {
|
|
this.isLoading.value = true
|
|
this.historySteps = Array.from({ length: this.maxHistorySteps }, () => null)
|
|
this.historyIndex = -1
|
|
|
|
try {
|
|
// 停止自动保存,避免在加载过程中触发
|
|
this.stopAutoSave()
|
|
|
|
// 直接替换数组引用(避免响应式开销)
|
|
//@ts-ignore
|
|
this.vdata = {
|
|
id: this.id,
|
|
isChanged: false,
|
|
...data
|
|
}
|
|
|
|
// 同步到视口
|
|
this.fullSync()
|
|
this.isChanged.value = false
|
|
|
|
// 强制保存一次初始状态
|
|
await this.saveToLocalstore()
|
|
console.log('[StateManager] 加载完成,共', data.items.length, '个对象')
|
|
|
|
} finally {
|
|
this.isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 差异应用方法
|
|
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 fullSync() {
|
|
this.entityManager.beginEntityUpdate()
|
|
this.vdata.items.forEach(item => {
|
|
this.entityManager.createOrUpdateEntity(item)
|
|
})
|
|
this.entityManager.endEntityUpdate()
|
|
|
|
// 初始状态作为第一步
|
|
this.beginStateUpdate()
|
|
this.endStateUpdate({ autoSave: false })
|
|
this.pendingChanges = false
|
|
}
|
|
|
|
undoEnabled() {
|
|
return this.historyIndex >= 0
|
|
}
|
|
|
|
redoEnabled() {
|
|
const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps
|
|
return !!this.historySteps[nextIndex]
|
|
}
|
|
|
|
/**
|
|
* 保存数据到外部
|
|
*/
|
|
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() // 同步到视口
|
|
console.log('[StateManager] 从本地存储恢复', this.vdata.items.length, '个对象')
|
|
return true
|
|
}
|
|
|
|
return false
|
|
|
|
} catch (error) {
|
|
console.error('[StateManager] 从本地存储加载失败:', error)
|
|
return false
|
|
|
|
} finally {
|
|
this.isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除本地存储
|
|
*/
|
|
async removeLocalstore() {
|
|
try {
|
|
await localforage.removeItem(this.storeKey)
|
|
console.log('[StateManager] 本地存储已清除')
|
|
} catch (error) {
|
|
console.error('[StateManager] 清除本地存储失败:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 销毁资源
|
|
*/
|
|
destroy() {
|
|
// 清理引用
|
|
delete this.vdata
|
|
delete this.historySteps
|
|
}
|
|
|
|
/**
|
|
* 尝试从草稿中读取数据
|
|
*/
|
|
static async tryLoadCatalogFromLocalstore(): Promise<{
|
|
success: boolean,
|
|
catalogCode?: string,
|
|
catalog?: Catalog,
|
|
stateManagerId?: string
|
|
}> {
|
|
// 获取 url 中的 stateManagerId
|
|
// 从 localforage 中读取草稿数据
|
|
const stateManagerId = getQueryParams()?.get('store')
|
|
if (stateManagerId) {
|
|
const storeKey = `-tmp-yvan-lcc-${stateManagerId}`
|
|
const saved: VData = await localforage.getItem(storeKey)
|
|
if (saved && saved.catalog) {
|
|
const catalogCode = saved.catalogCode || ''
|
|
if (catalogCode) {
|
|
return {
|
|
success: true,
|
|
catalogCode,
|
|
catalog: saved.catalog,
|
|
stateManagerId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false
|
|
}
|
|
}
|
|
}
|
|
|
|
|