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