5 changed files with 541 additions and 4 deletions
@ -0,0 +1,399 @@ |
|||||
|
import _ from 'lodash' |
||||
|
import localforage from 'localforage' |
||||
|
import type Viewport from '@/designer/Viewport.ts' |
||||
|
import { markRaw, reactive, ref } from 'vue' |
||||
|
import type { VData, VDataItem } from '@/types/Types' |
||||
|
|
||||
|
/** |
||||
|
* THREE.js 场景状态管理器. |
||||
|
* 从数据结构还原 threejs 画布的一种结构 |
||||
|
* 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 |
||||
|
* 2. 管理撤销、重做功能 |
||||
|
* 3. 用户侧会通过如下步骤修改数据 |
||||
|
* - 1. 调用 beginUserWrite 开始修改数据 |
||||
|
* - 2. 直接修改 vdata 数据 |
||||
|
* - 3. 调用 endUserWrite 完成数据修改 |
||||
|
* 4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 viewport 进行同步 |
||||
|
* 5. syncDataState 方法会对比 vdata 与 viewport |
||||
|
* - 分别进行添加、删除、更新等操作 |
||||
|
* 6. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 |
||||
|
* 状态管理器,管理内部数据结构的读取、保存、临时保存、临时读取、撤销、重做等功能 |
||||
|
* 当数据结构发生变化时,只对发生变化的对象进行更新 |
||||
|
* |
||||
|
* // 初始化
|
||||
|
* const stateManager = new StateManager('scene-1', viewport) |
||||
|
* |
||||
|
* // 加载大数据
|
||||
|
* await stateManager.load(largeDataSet) // 5万+ items
|
||||
|
* |
||||
|
* // 修改数据
|
||||
|
* stateManager.beginUserWrite() |
||||
|
* |
||||
|
* // 直接修改状态(实际项目应通过封装方法)
|
||||
|
* stateManager.vdata.items.push(newItem) |
||||
|
* stateManager.vdata.items[0].name = 'updated' |
||||
|
* stateManager.vdata.items = stateManager.vdata.items.filter(i => i.id !== 'remove-id') |
||||
|
* |
||||
|
* stateManager.endUserWrite() // 自动计算差异并保存
|
||||
|
* |
||||
|
* // 撤销操作
|
||||
|
* stateManager.undo() |
||||
|
* |
||||
|
* // 保存到云端
|
||||
|
* const data = await stateManager.save() |
||||
|
* |
||||
|
*/ |
||||
|
export default class StateManager { |
||||
|
/** |
||||
|
* 是否发生了变化,通知外部是否需要保存数据 |
||||
|
*/ |
||||
|
isChanged = ref(false) |
||||
|
|
||||
|
/** |
||||
|
* 是否正在加载数据,通知外部是否需要等待加载完成 |
||||
|
*/ |
||||
|
isLoading = ref(false) |
||||
|
|
||||
|
/** |
||||
|
* 当前场景数据 |
||||
|
*/ |
||||
|
vdata: VData = { items: [], isChanged: false } |
||||
|
|
||||
|
/** |
||||
|
* 唯一场景标识符, 用于做临时存储的 key |
||||
|
*/ |
||||
|
id: string |
||||
|
|
||||
|
/** |
||||
|
* 视口对象, 用于获取、同步当前场景的状态 |
||||
|
*/ |
||||
|
viewport: Viewport |
||||
|
|
||||
|
/** |
||||
|
* 使用循环缓冲区存储历史记录 |
||||
|
*/ |
||||
|
private historySteps: HistoryStep[] = [] |
||||
|
private historyIndex = -1 |
||||
|
private readonly maxHistorySteps = 20 |
||||
|
private readonly historyBufferSize: number |
||||
|
|
||||
|
// 变化追踪器
|
||||
|
private changeTracker: DataDiff = { |
||||
|
added: [], |
||||
|
removed: [], |
||||
|
updated: [] |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 数据快照(用于差异计算) |
||||
|
*/ |
||||
|
private lastSnapshot: Map<string, VDataItem> = new Map() |
||||
|
|
||||
|
/** |
||||
|
* @param id 唯一场景标识符, 用于做临时存储的 key |
||||
|
* @param viewport 视口对象, 用于获取、同步当前场景的状态 |
||||
|
* @param bufferSize 历史记录缓冲区大小,默认为 50 |
||||
|
*/ |
||||
|
constructor(id: string, viewport: Viewport, bufferSize = 50) { |
||||
|
this.id = id |
||||
|
this.viewport = markRaw(viewport) |
||||
|
this.historyBufferSize = bufferSize |
||||
|
// 初始化固定大小的历史缓冲区
|
||||
|
this.historySteps = new Array(this.maxHistorySteps) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开始用户操作(创建数据快照) |
||||
|
*/ |
||||
|
beginUserWrite() { |
||||
|
// 创建当前状态快照(非深拷贝)
|
||||
|
this.lastSnapshot = new Map( |
||||
|
this.vdata.items.map(item => [item.id, item]) |
||||
|
) |
||||
|
this.changeTracker = { added: [], removed: [], updated: [] } |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 结束用户操作(计算差异并保存) |
||||
|
*/ |
||||
|
endUserWrite() { |
||||
|
this.calculateDiff() |
||||
|
this.saveStep() |
||||
|
this.syncDataState() |
||||
|
this.isChanged.value = true |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 计算当前状态与快照的差异 |
||||
|
*/ |
||||
|
private calculateDiff() { |
||||
|
const currentMap = new Map( |
||||
|
this.vdata.items.map(item => [item.id, item]) |
||||
|
) |
||||
|
|
||||
|
// 检测删除的项目
|
||||
|
for (const [id] of this.lastSnapshot) { |
||||
|
if (!currentMap.has(id)) { |
||||
|
this.changeTracker.removed.push(id) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检测新增和更新的项目
|
||||
|
for (const [id, currentItem] of currentMap) { |
||||
|
const lastItem = this.lastSnapshot.get(id) |
||||
|
|
||||
|
if (!lastItem) { |
||||
|
this.changeTracker.added.push(currentItem) |
||||
|
} else if (!_.isEqual(lastItem, currentItem)) { |
||||
|
this.changeTracker.updated.push(currentItem) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 清除空变更
|
||||
|
if (this.changeTracker.added.length === 0) delete this.changeTracker.added |
||||
|
if (this.changeTracker.removed.length === 0) delete this.changeTracker.removed |
||||
|
if (this.changeTracker.updated.length === 0) delete this.changeTracker.updated |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 保存差异到历史记录 |
||||
|
*/ |
||||
|
private saveStep() { |
||||
|
// 跳过空变更
|
||||
|
if ( |
||||
|
(!this.changeTracker.added || this.changeTracker.added.length === 0) && |
||||
|
(!this.changeTracker.removed || this.changeTracker.removed.length === 0) && |
||||
|
(!this.changeTracker.updated || this.changeTracker.updated.length === 0) |
||||
|
) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 使用循环缓冲区存储历史记录
|
||||
|
const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps |
||||
|
this.historySteps[nextIndex] = { |
||||
|
diff: _.cloneDeep(this.changeTracker), |
||||
|
timestamp: Date.now() |
||||
|
} |
||||
|
|
||||
|
this.historyIndex = nextIndex |
||||
|
this.saveToLocalstore() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 将当前数据 与 viewport 进行同步, 对比出不同的部分,分别进行更新 |
||||
|
* - 调用 viewport.beginSync() 开始更新场景 |
||||
|
* - 调用 viewport.syncOnRemove(id) 删除场景中不存在的对象 |
||||
|
* - 调用 viewport.syncOnAppend(vdataItem) 添加场景中新的对象 |
||||
|
* - 调用 viewport.syncOnUpdate(id) 更新场景中已存在的对象 |
||||
|
* - 调用 viewport.endSync() 结束更新场景 |
||||
|
*/ |
||||
|
syncDataState() { |
||||
|
// 没有变化时跳过同步
|
||||
|
if ( |
||||
|
(!this.changeTracker.added || this.changeTracker.added.length === 0) && |
||||
|
(!this.changeTracker.removed || this.changeTracker.removed.length === 0) && |
||||
|
(!this.changeTracker.updated || this.changeTracker.updated.length === 0) |
||||
|
) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.viewport.beginSync() |
||||
|
|
||||
|
// 处理删除
|
||||
|
if (this.changeTracker.removed) { |
||||
|
for (const id of this.changeTracker.removed) { |
||||
|
this.viewport.syncOnRemove(id) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理新增
|
||||
|
if (this.changeTracker.added) { |
||||
|
for (const item of this.changeTracker.added) { |
||||
|
this.viewport.syncOnAppend(item) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理更新
|
||||
|
if (this.changeTracker.updated) { |
||||
|
for (const item of this.changeTracker.updated) { |
||||
|
this.viewport.syncOnUpdate(item) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.viewport.endSync() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 从外部加载数据 |
||||
|
*/ |
||||
|
async load(items: VDataItem[]) { |
||||
|
this.isLoading.value = true |
||||
|
|
||||
|
try { |
||||
|
// 直接替换数组引用(避免响应式开销)
|
||||
|
this.vdata.items = items |
||||
|
|
||||
|
// 初始状态作为第一步
|
||||
|
this.beginUserWrite() |
||||
|
this.endUserWrite() |
||||
|
|
||||
|
this.isChanged.value = false |
||||
|
} finally { |
||||
|
this.isLoading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 保存数据到外部 |
||||
|
*/ |
||||
|
async save(): Promise<Object> { |
||||
|
return _.cloneDeep(this.vdata) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 撤销 |
||||
|
*/ |
||||
|
undo() { |
||||
|
if (!this.undoEnabled()) return |
||||
|
|
||||
|
const step = this.historySteps[this.historyIndex] |
||||
|
if (!step) return |
||||
|
|
||||
|
this.applyReverseDiff(step.diff) |
||||
|
this.historyIndex = (this.historyIndex - 1 + this.maxHistorySteps) % this.maxHistorySteps |
||||
|
this.isChanged.value = true |
||||
|
this.syncDataState() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重做 |
||||
|
*/ |
||||
|
redo() { |
||||
|
if (!this.redoEnabled()) return |
||||
|
|
||||
|
this.historyIndex = (this.historyIndex + 1) % this.maxHistorySteps |
||||
|
const step = this.historySteps[this.historyIndex] |
||||
|
if (!step) return |
||||
|
|
||||
|
this.applyDiff(step.diff) |
||||
|
this.isChanged.value = true |
||||
|
this.syncDataState() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 应用正向差异 |
||||
|
*/ |
||||
|
private applyDiff(diff: DataDiff) { |
||||
|
// 处理删除
|
||||
|
if (diff.removed) { |
||||
|
this.vdata.items = this.vdata.items.filter(item => !diff.removed.includes(item.id)) |
||||
|
} |
||||
|
|
||||
|
// 处理新增
|
||||
|
if (diff.added) { |
||||
|
this.vdata.items.push(...diff.added) |
||||
|
} |
||||
|
|
||||
|
// 处理更新
|
||||
|
if (diff.updated) { |
||||
|
const updateMap = new Map(diff.updated.map(item => [item.id, item])) |
||||
|
this.vdata.items = this.vdata.items.map(item => |
||||
|
updateMap.has(item.id) ? updateMap.get(item.id)! : item |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 应用反向差异(用于撤销) |
||||
|
*/ |
||||
|
private applyReverseDiff(diff: DataDiff) { |
||||
|
// 反向处理:删除 → 添加
|
||||
|
if (diff.removed) { |
||||
|
// 从历史快照恢复被删除的项目
|
||||
|
const restoredItems = diff.removed |
||||
|
.map(id => this.lastSnapshot.get(id)) |
||||
|
.filter(Boolean) as VDataItem[] |
||||
|
|
||||
|
this.vdata.items.push(...restoredItems) |
||||
|
} |
||||
|
|
||||
|
// 反向处理:添加 → 删除
|
||||
|
if (diff.added) { |
||||
|
const addedIds = new Set(diff.added.map(item => item.id)) |
||||
|
this.vdata.items = this.vdata.items.filter(item => !addedIds.has(item.id)) |
||||
|
} |
||||
|
|
||||
|
// 反向处理:更新 → 恢复旧值
|
||||
|
if (diff.updated) { |
||||
|
const restoreMap = new Map( |
||||
|
diff.updated |
||||
|
.map(item => [item.id, this.lastSnapshot.get(item.id)]) |
||||
|
.filter(([, item]) => !!item) as [string, VDataItem][] |
||||
|
) |
||||
|
|
||||
|
this.vdata.items = this.vdata.items.map(item => |
||||
|
restoreMap.has(item.id) ? restoreMap.get(item.id)! : item |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 保存到本地存储(防止数据丢失) |
||||
|
*/ |
||||
|
async saveToLocalstore() { |
||||
|
// 只保存变化部分和关键元数据
|
||||
|
const saveData = { |
||||
|
diff: this.changeTracker, |
||||
|
timestamp: Date.now(), |
||||
|
itemsCount: this.vdata.items.length |
||||
|
} |
||||
|
|
||||
|
await localforage.setItem(`scene-tmp-${this.id}`, saveData) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 从本地存储加载数据 |
||||
|
*/ |
||||
|
async loadFromLocalstore() { |
||||
|
const saved: any = await localforage.getItem(`scene-tmp-${this.id}`) |
||||
|
if (saved && saved.diff) { |
||||
|
this.applyDiff(saved.diff) |
||||
|
this.isChanged.value = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
undoEnabled() { |
||||
|
return this.historyIndex >= 0 && this.historySteps[this.historyIndex] !== undefined |
||||
|
} |
||||
|
|
||||
|
redoEnabled() { |
||||
|
const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps |
||||
|
return this.historySteps[nextIndex] !== undefined |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除本地存储 |
||||
|
*/ |
||||
|
async removeLocalstore() { |
||||
|
await localforage.removeItem(`scene-tmp-${this.id}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// 差异类型定义
|
||||
|
interface DataDiff { |
||||
|
added: VDataItem[] |
||||
|
removed: string[] |
||||
|
updated: VDataItem[] |
||||
|
} |
||||
|
|
||||
|
// 历史记录项
|
||||
|
interface HistoryStep { |
||||
|
diff: DataDiff |
||||
|
timestamp: number |
||||
|
} |
||||
Loading…
Reference in new issue