1.0.2: persist editor-session UI state with the project

Canvas zoom + pan, crop / mask-preview / stage-preview / centre-
crosshair overlays, and timeline snap / show-locked / displayMode
/ px-per-sec / auto-fit now ride along in the saved project under
a new optional `editor` block. Reopening a project restores the
workbench exactly how the user left it — zoom level, pan offset,
HUD toggles, timeline density — instead of snapping back to
defaults.

Editor state lives on the store as a sibling of `this.project`
rather than inside it, so `JSON.stringify(store.project)` (the
undo/redo snapshot) still only captures content. The new
`projectForSave` getter stitches the two on the way to disk;
`loadProject` reads them back apart on the way in. Saves through
App.vue, the rename flow, and IDB recents all go through
`projectForSave`.

CanvasStage skips its first auto-fit when a non-default
canvas zoom/pan was restored so the user's exact view survives
mount. Frame-based export options pick up the same source of
truth for a consistent starting point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-24 19:47:44 +02:00
1 parent cd9cf91ef0
commit 647f09b287
9 files changed
+285 -19

No files matched your search

+65 -2
View File
@@ -14,6 +14,7 @@ import type {
ClipInstance,
ClipTrack,
Easing,
EditorState,
HitShape,
Keyframe,
Layer,
@@ -39,6 +40,23 @@ import { useButtonRuntime } from '@/composables/useButtonRuntime'
const uid = () => Math.random().toString(36).slice(2, 10)
function defaultEditorState(): Required<EditorState> {
return {
canvasZoom: 1,
canvasViewX: 0,
canvasViewY: 0,
cropObjects: false,
maskPreview: false,
stagePreview: false,
centerCrosshair: false,
timelineSnap: true,
timelineShowLocked: false,
timelineDisplayMode: 'seconds',
timelinePxPerSec: 0,
timelineAutoFit: true,
}
}
type AssetInit = Asset extends infer A ? (A extends Asset ? Omit<A, 'id'> : never) : never
/** A source URL is considered SVG if it's a `data:image/svg+xml` payload.
@@ -234,6 +252,11 @@ interface State {
drawingMode: 'freeform' | null
drawingAssetId: string | null
drawingLayerId: string | null
/** Canvas zoom, pan, overlay toggles + timeline display settings.
* Persisted with the project so reopening restores the workbench
* exactly how the user left it. Deliberately NOT tracked by the
* undo history — ctrl-Z should never undo a pan or a snap-toggle. */
editor: Required<EditorState>
}
function allKeyframes(layer: Layer): Keyframe[] {
@@ -541,6 +564,7 @@ export const useSceneStore = defineStore('scene', {
drawingMode: null,
drawingAssetId: null,
drawingLayerId: null,
editor: defaultEditorState(),
}),
getters: {
/** The active page from `project.scenes`. Use this when you need to
@@ -549,6 +573,14 @@ export const useSceneStore = defineStore('scene', {
const p = state.project
return p.scenes.find((s) => s.id === p.activeSceneId) ?? p.scenes[0]
},
/** Project data PLUS the current editor-session UI state — what
* callers should serialize when saving. Keeping the editor
* block out of `this.project` directly avoids polluting the
* undo history; this getter stitches them together only at
* save time. */
projectForSave(state): Project {
return { ...state.project, editor: { ...state.editor } }
},
/** Merged Scene-shaped view: shared project fields combined with the
* active page's arrays. Read-only by convention — array references
* are live (so `store.scene.layers.push(x)` still hits the active
@@ -2367,8 +2399,12 @@ export const useSceneStore = defineStore('scene', {
layer.separated = false
}
},
/** Replace the entire project. Resets editor context (selection,
* playback, clip / button-edit drill-down). */
/** Replace the entire project. Resets transient editor context
* (selection, playback, clip / button-edit drill-down) and
* rehydrates the persisted editor UI state (zoom, pan, overlay
* toggles, timeline display) from `project.editor`. Missing
* editor block falls back to defaults so older files open the
* same as before. */
loadProject(project: Project) {
this.project = normalizeProject(project)
this.selectedLayerId = null
@@ -2383,6 +2419,33 @@ export const useSceneStore = defineStore('scene', {
this.drawingMode = null
this.drawingAssetId = null
this.drawingLayerId = null
this.applyEditorState(project.editor)
},
/** Merge a saved editor UI snapshot into the live state. Missing
* fields fall back to defaults so partial saves (e.g. older
* files with no editor block) still get a clean starting
* point. Called by `loadProject` and usable standalone by
* anything that wants to reset the workbench to defaults. */
applyEditorState(patch: EditorState | undefined) {
const defaults = defaultEditorState()
if (!patch) {
this.editor = defaults
return
}
this.editor = {
canvasZoom: patch.canvasZoom ?? defaults.canvasZoom,
canvasViewX: patch.canvasViewX ?? defaults.canvasViewX,
canvasViewY: patch.canvasViewY ?? defaults.canvasViewY,
cropObjects: patch.cropObjects ?? defaults.cropObjects,
maskPreview: patch.maskPreview ?? defaults.maskPreview,
stagePreview: patch.stagePreview ?? defaults.stagePreview,
centerCrosshair: patch.centerCrosshair ?? defaults.centerCrosshair,
timelineSnap: patch.timelineSnap ?? defaults.timelineSnap,
timelineShowLocked: patch.timelineShowLocked ?? defaults.timelineShowLocked,
timelineDisplayMode: patch.timelineDisplayMode ?? defaults.timelineDisplayMode,
timelinePxPerSec: patch.timelinePxPerSec ?? defaults.timelinePxPerSec,
timelineAutoFit: patch.timelineAutoFit ?? defaults.timelineAutoFit,
}
},
/** Backward-compat shim: legacy callers (older project files, recents
* stored as Scene, tests) still pass a Scene. Wrap into a one-page