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:
1 parent
cd9cf91ef0
commit
647f09b287
9 files changed
+285
-19
No files matched your search
+65
-2
@@ -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
|
||||
|
||||
Reference in new issue
Block a user