This commit is contained in:
th committed 2026-04-19 20:31:53 +02:00
1 parent e4aa0dce86
commit cc6b11e61d
23 files changed
+2047 -524

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
import { computed, onBeforeUnmount, onMounted, ref, watch, type ComputedRef } from 'vue'
import { useSceneStore } from '@/stores/scene'
import type { Scene } from '@/types/scene'
export const HISTORY_MAX = 100
export const HISTORY_DEBOUNCE_MS = 400
type SceneStore = ReturnType<typeof useSceneStore>
export interface HistoryApi {
undo: () => void
redo: () => void
canUndo: ComputedRef<boolean>
canRedo: ComputedRef<boolean>
resetTo: (scene?: Scene) => void
commitNow: () => void
start: () => void
stop: () => void
}
export interface CreateHistoryOptions {
max?: number
debounceMs?: number
}
export function createHistory(store: SceneStore, opts: CreateHistoryOptions = {}): HistoryApi {
const max = opts.max ?? HISTORY_MAX
const debounceMs = opts.debounceMs ?? HISTORY_DEBOUNCE_MS
const stack = ref<string[]>([])
const index = ref(-1)
let suspend = false
let debounceTimer: ReturnType<typeof setTimeout> | null = null
let unsub: (() => void) | null = null
const snap = () => JSON.stringify(store.scene)
function pushNow() {
if (suspend) return
const next = snap()
if (stack.value[index.value] === next) return
stack.value = stack.value.slice(0, index.value + 1)
stack.value.push(next)
if (stack.value.length > max) {
const overflow = stack.value.length - max
stack.value.splice(0, overflow)
index.value -= overflow
}
index.value = stack.value.length - 1
}
function schedulePush() {
if (suspend) return
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debounceTimer = null
pushNow()
}, debounceMs)
}
function apply(json: string) {
suspend = true
store.scene = JSON.parse(json)
store.setPlayhead(store.playhead)
if (store.selectedLayerId && !store.scene.layers.some((l) => l.id === store.selectedLayerId)) {
store.selectLayer(null)
}
setTimeout(() => {
suspend = false
}, 0)
}
function flushPending() {
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
pushNow()
}
}
function undo() {
flushPending()
if (index.value <= 0) return
index.value -= 1
apply(stack.value[index.value])
}
function redo() {
if (index.value >= stack.value.length - 1) return
index.value += 1
apply(stack.value[index.value])
}
function resetTo(scene?: Scene) {
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
const json = scene ? JSON.stringify(scene) : snap()
stack.value = [json]
index.value = 0
}
const canUndo = computed(() => index.value > 0)
const canRedo = computed(() => index.value < stack.value.length - 1)
function start() {
stack.value = [snap()]
index.value = 0
unsub = watch(
() => store.scene,
() => schedulePush(),
{ deep: true, flush: 'post' },
)
}
function stop() {
if (unsub) unsub()
unsub = null
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
}
return {
undo,
redo,
canUndo,
canRedo,
resetTo,
commitNow: pushNow,
start,
stop,
}
}
export function useHistory(): HistoryApi {
const store = useSceneStore()
const api = createHistory(store)
onMounted(api.start)
onBeforeUnmount(api.stop)
return api
}