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
@@ -1733,6 +1733,125 @@ export function registerAllTests(): void {
|
||||
})
|
||||
})
|
||||
|
||||
suite('editor state — save / load roundtrip', () => {
|
||||
// The store's editor block carries canvas zoom/pan, overlay
|
||||
// toggles and timeline display settings. It rides along with the
|
||||
// project file via `projectForSave` and comes back via
|
||||
// `loadProject`, so reopening restores the full workbench.
|
||||
|
||||
test('projectForSave embeds the current editor state', () => {
|
||||
const s = makeFreshStore()
|
||||
s.editor.canvasZoom = 2.5
|
||||
s.editor.canvasViewX = 40
|
||||
s.editor.canvasViewY = -80
|
||||
s.editor.cropObjects = true
|
||||
s.editor.maskPreview = true
|
||||
s.editor.centerCrosshair = true
|
||||
s.editor.timelineSnap = false
|
||||
s.editor.timelineDisplayMode = 'frames'
|
||||
const payload = s.projectForSave
|
||||
expect(payload.editor).toBeTruthy()
|
||||
expect(payload.editor!.canvasZoom).toBe(2.5)
|
||||
expect(payload.editor!.canvasViewX).toBe(40)
|
||||
expect(payload.editor!.canvasViewY).toBe(-80)
|
||||
expect(payload.editor!.cropObjects).toBe(true)
|
||||
expect(payload.editor!.maskPreview).toBe(true)
|
||||
expect(payload.editor!.centerCrosshair).toBe(true)
|
||||
expect(payload.editor!.timelineSnap).toBe(false)
|
||||
expect(payload.editor!.timelineDisplayMode).toBe('frames')
|
||||
})
|
||||
|
||||
test('loadProject restores editor state from the payload', () => {
|
||||
const s = makeFreshStore()
|
||||
s.editor.canvasZoom = 3
|
||||
const payload = JSON.parse(JSON.stringify(s.projectForSave))
|
||||
// Mutate live state so the load is observable.
|
||||
s.editor.canvasZoom = 1
|
||||
s.editor.cropObjects = false
|
||||
// Pre-bake a specific editor block into the payload.
|
||||
payload.editor = {
|
||||
canvasZoom: 1.75,
|
||||
canvasViewX: 12,
|
||||
canvasViewY: 34,
|
||||
cropObjects: true,
|
||||
maskPreview: false,
|
||||
stagePreview: true,
|
||||
centerCrosshair: true,
|
||||
timelineSnap: false,
|
||||
timelineShowLocked: true,
|
||||
timelineDisplayMode: 'frames',
|
||||
timelinePxPerSec: 80,
|
||||
timelineAutoFit: false,
|
||||
}
|
||||
s.loadProject(payload)
|
||||
expect(s.editor.canvasZoom).toBe(1.75)
|
||||
expect(s.editor.canvasViewX).toBe(12)
|
||||
expect(s.editor.canvasViewY).toBe(34)
|
||||
expect(s.editor.cropObjects).toBe(true)
|
||||
expect(s.editor.stagePreview).toBe(true)
|
||||
expect(s.editor.centerCrosshair).toBe(true)
|
||||
expect(s.editor.timelineSnap).toBe(false)
|
||||
expect(s.editor.timelineShowLocked).toBe(true)
|
||||
expect(s.editor.timelineDisplayMode).toBe('frames')
|
||||
expect(s.editor.timelinePxPerSec).toBe(80)
|
||||
expect(s.editor.timelineAutoFit).toBe(false)
|
||||
})
|
||||
|
||||
test('loadProject without an editor block falls back to defaults', () => {
|
||||
const s = makeFreshStore()
|
||||
// Pretend the user tweaked everything before loading an older
|
||||
// file that has no editor metadata.
|
||||
s.editor.canvasZoom = 4
|
||||
s.editor.cropObjects = true
|
||||
s.editor.timelineSnap = false
|
||||
const payload = JSON.parse(JSON.stringify(s.projectForSave))
|
||||
delete payload.editor
|
||||
s.loadProject(payload)
|
||||
// Back to defaults regardless of what was set before the load.
|
||||
expect(s.editor.canvasZoom).toBe(1)
|
||||
expect(s.editor.cropObjects).toBe(false)
|
||||
expect(s.editor.timelineSnap).toBe(true)
|
||||
})
|
||||
|
||||
test('applying a partial editor patch leaves untouched fields at default', () => {
|
||||
const s = makeFreshStore()
|
||||
s.applyEditorState({ canvasZoom: 2, cropObjects: true })
|
||||
expect(s.editor.canvasZoom).toBe(2)
|
||||
expect(s.editor.cropObjects).toBe(true)
|
||||
// Not in the patch → should be back to the default value (the
|
||||
// action rebuilds from scratch so stale flags don't leak).
|
||||
expect(s.editor.maskPreview).toBe(false)
|
||||
expect(s.editor.timelineSnap).toBe(true)
|
||||
})
|
||||
|
||||
test('undo/redo snapshots do not include editor state', async () => {
|
||||
// History only snapshots `store.project`; UI toggles must not
|
||||
// show up in the undo stack or a ctrl-Z would revert a pan,
|
||||
// zoom or snap-toggle — explicitly unwanted.
|
||||
const s = makeFreshStore()
|
||||
const history = createHistory(s)
|
||||
history.start()
|
||||
try {
|
||||
s.addAsset({ kind: 'rect', name: 'Before', width: 10, height: 10, fill: '#fff' })
|
||||
history.commitNow()
|
||||
// Now tweak editor state, then make a content change and
|
||||
// commit. Undo should revert the asset but leave the zoom
|
||||
// the user set in between intact.
|
||||
s.editor.canvasZoom = 3
|
||||
s.addAsset({ kind: 'rect', name: 'After', width: 10, height: 10, fill: '#000' })
|
||||
history.commitNow()
|
||||
history.undo()
|
||||
// Asset 'After' is gone, asset 'Before' is back — but the
|
||||
// canvas zoom stays at 3 because editor isn't snapshotted.
|
||||
expect(s.scene.assets.some((a) => a.name === 'After')).toBe(false)
|
||||
expect(s.scene.assets.some((a) => a.name === 'Before')).toBe(true)
|
||||
expect(s.editor.canvasZoom).toBe(3)
|
||||
} finally {
|
||||
history.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('scene store — project & scenes', () => {
|
||||
test('merged `scene` getter mirrors the active page\'s arrays by reference', () => {
|
||||
// Reads through store.scene must share the SAME array references
|
||||
|
||||
Reference in new issue
Block a user