feat: add project management features including open/save functionality and recovery support

This commit is contained in:
syntaxbullet
2026-07-10 23:46:01 +02:00
parent 6d652c3264
commit 9697075d29
15 changed files with 613 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
export const commandIds = {
projectOpen: "project.open",
documentAddArtboard: "document.addArtboard",
documentSetArtboardBounds: "document.setArtboardBounds",
documentRemoveArtboard: "document.removeArtboard",

View File

@@ -1,4 +1,6 @@
export type { Command, CommandContext } from "./command";
export { projectCommands, projectOpenCommand } from "./project";
export type { ProjectOpenPayload } from "./project";
export {
documentAddArtboardCommand,
documentAddAssetCommand,

View File

@@ -46,6 +46,7 @@ import type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPrevie
import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
import type { WorkspaceSetPanelPayload } from "./workspace";
import type { EditorSetPointerSessionPayload } from "./editor";
import type { ProjectOpenPayload } from "./project";
import type {
ViewportFitArtboardPayload,
ViewportPanPayload,
@@ -55,6 +56,7 @@ import type {
} from "./viewport";
export type CommandPayloads = {
[commandIds.projectOpen]: ProjectOpenPayload;
[commandIds.documentAddArtboard]: DocumentAddArtboardPayload;
[commandIds.documentSetArtboardBounds]: DocumentSetArtboardBoundsPayload;
[commandIds.documentRemoveArtboard]: DocumentRemoveArtboardPayload;

31
commands/project.test.ts Normal file
View File

@@ -0,0 +1,31 @@
import { describe, expect, test } from "bun:test";
import { createInitialAppState } from "@editor/initial-state";
import { createAppStore } from "@editor/store";
import { createCommandRegistry } from "./registry";
import { documentAddArtboardCommand, documentRenameArtboardCommand } from "./document";
import { projectOpenCommand } from "./project";
import { commandIds } from "./ids";
describe("open project command", () => {
test("replaces the document and resets transient sessions and history", () => {
const app = createTestApp("Original");
const replacement = createTestApp("Opened").store.getState().document;
const originalArtboardId = app.store.getState().document.artboards[0]!.id;
app.store.dispatch(commandIds.documentRenameArtboard, { id: originalArtboardId, name: "Changed" });
expect(app.store.getState().history.past).toHaveLength(2);
app.store.dispatch(commandIds.projectOpen, { document: replacement });
const state = app.store.getState();
expect(state.document).toBe(replacement);
expect(state.editor.selection).toEqual({ artboardId: replacement.artboards[0]!.id, layerIds: [] });
expect(state.editor.generation.candidates).toEqual([]);
expect(state.history).toEqual({ past: [], future: [] });
});
});
function createTestApp(name: string) {
const store = createAppStore(createInitialAppState(name), createCommandRegistry([projectOpenCommand, documentAddArtboardCommand, documentRenameArtboardCommand]));
store.dispatch(commandIds.documentAddArtboard, { id: `${name}-artboard`, name: "Board", bounds: { x: 0, y: 0, w: 100, h: 100 } });
return { store };
}

37
commands/project.ts Normal file
View File

@@ -0,0 +1,37 @@
import type { ImageDocument } from "@core/document";
import { initialEditorState } from "@editor/initial-state";
import type { Command } from "./command";
import { commandIds } from "./ids";
export type ProjectOpenPayload = { document: ImageDocument };
export const projectOpenCommand: Command<ProjectOpenPayload> = {
id: commandIds.projectOpen,
name: "Open project",
history: { mode: "ignore" },
execute({ state }, payload) {
const firstArtboard = payload.document.artboards[0];
const viewport = state.editor.viewport;
const padding = 48;
const availableWidth = Math.max(0, viewport.size.w - padding * 2);
const availableHeight = Math.max(0, viewport.size.h - padding * 2);
const canFit = Boolean(firstArtboard && availableWidth > 0 && availableHeight > 0 && firstArtboard.bounds.w > 0 && firstArtboard.bounds.h > 0);
return {
document: payload.document,
editor: {
...initialEditorState,
viewport: {
...viewport,
center: firstArtboard
? { x: firstArtboard.bounds.x + firstArtboard.bounds.w / 2, y: firstArtboard.bounds.y + firstArtboard.bounds.h / 2 }
: viewport.center,
zoom: canFit && firstArtboard ? Math.max(0.01, Math.min(availableWidth / firstArtboard.bounds.w, availableHeight / firstArtboard.bounds.h)) : viewport.zoom,
},
selection: { artboardId: firstArtboard?.id, layerIds: [] },
},
history: { past: [], future: [] },
};
},
};
export const projectCommands = [projectOpenCommand] satisfies Command<unknown>[];