auto-git:

[add] .prettierrc.json
 [add] eslint.config.js
 [add] index.html
 [add] package.json
 [add] playwright.config.ts
 [add] src/app/App.tsx
 [add] src/app/app.css
 [add] src/app/editor-store.ts
 [add] src/app/use-editor-store.ts
 [add] src/assets/.gitkeep
 [add] src/commands/command-history.ts
 [add] src/commands/command.ts
 [add] src/commands/set-scene-name-command.ts
 [add] src/core/ids.ts
 [add] src/core/selection.ts
 [add] src/core/tool-mode.ts
 [add] src/core/vector.ts
 [add] src/document/migrate-scene-document.ts
 [add] src/document/scene-document.ts
 [add] src/entities/.gitkeep
 [add] src/geometry/.gitkeep
 [add] src/main.tsx
 [add] src/materials/.gitkeep
 [add] src/runtime-three/.gitkeep
 [add] src/serialization/local-draft-storage.ts
 [add] src/serialization/scene-document-json.ts
 [add] src/shared-ui/Panel.tsx
 [add] src/viewport-three/ViewportCanvas.tsx
 [add] src/viewport-three/viewport-host.ts
 [add] src/vite-env.d.ts
 [add] tests/domain/create-empty-scene-document.test.ts
 [add] tests/domain/editor-store.test.ts
 [add] tests/e2e/app-smoke.e2e.ts
 [add] tests/serialization/scene-document-json.test.ts
 [add] tests/setup/vitest.setup.ts
 [add] tsconfig.json
 [add] vite.config.ts
 [add] vitest.config.ts
This commit is contained in:
2026-03-31 01:29:35 +02:00
parent c90282ea45
commit 3af579c6bb
38 changed files with 1662 additions and 0 deletions

178
src/app/editor-store.ts Normal file
View File

@@ -0,0 +1,178 @@
import { CommandHistory } from "../commands/command-history";
import type { CommandContext, EditorCommand } from "../commands/command";
import type { EditorSelection } from "../core/selection";
import type { ToolMode } from "../core/tool-mode";
import { createEmptySceneDocument, type SceneDocument } from "../document/scene-document";
import {
DEFAULT_SCENE_DRAFT_STORAGE_KEY,
loadSceneDocumentDraft,
type KeyValueStorage,
saveSceneDocumentDraft
} from "../serialization/local-draft-storage";
import { parseSceneDocumentJson, serializeSceneDocument } from "../serialization/scene-document-json";
export interface EditorStoreState {
document: SceneDocument;
selection: EditorSelection;
toolMode: ToolMode;
canUndo: boolean;
canRedo: boolean;
lastCommandLabel: string | null;
}
interface EditorStoreOptions {
initialDocument?: SceneDocument;
storage?: KeyValueStorage | null;
storageKey?: string;
}
type EditorStoreListener = () => void;
export class EditorStore {
private document: SceneDocument;
private selection: EditorSelection = { kind: "none" };
private toolMode: ToolMode = "select";
private readonly history = new CommandHistory();
private readonly listeners = new Set<EditorStoreListener>();
private readonly storage: KeyValueStorage | null;
private readonly storageKey: string;
private lastCommandLabel: string | null = null;
private readonly commandContext: CommandContext = {
getDocument: () => this.document,
setDocument: (document) => {
this.document = document;
},
getSelection: () => this.selection,
setSelection: (selection) => {
this.selection = selection;
},
getToolMode: () => this.toolMode,
setToolMode: (toolMode) => {
this.toolMode = toolMode;
}
};
constructor(options: EditorStoreOptions = {}) {
this.document = options.initialDocument ?? createEmptySceneDocument();
this.storage = options.storage ?? null;
this.storageKey = options.storageKey ?? DEFAULT_SCENE_DRAFT_STORAGE_KEY;
}
subscribe = (listener: EditorStoreListener) => {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
};
getState = (): EditorStoreState => ({
document: this.document,
selection: this.selection,
toolMode: this.toolMode,
canUndo: this.history.canUndo(),
canRedo: this.history.canRedo(),
lastCommandLabel: this.lastCommandLabel
});
setToolMode(toolMode: ToolMode) {
if (this.toolMode === toolMode) {
return;
}
this.toolMode = toolMode;
this.emit();
}
setSelection(selection: EditorSelection) {
this.selection = selection;
this.emit();
}
executeCommand(command: EditorCommand) {
this.history.execute(command, this.commandContext);
this.lastCommandLabel = command.label;
this.emit();
}
undo(): boolean {
const command = this.history.undo(this.commandContext);
if (command === null) {
return false;
}
this.lastCommandLabel = `Undid ${command.label}`;
this.emit();
return true;
}
redo(): boolean {
const command = this.history.redo(this.commandContext);
if (command === null) {
return false;
}
this.lastCommandLabel = `Redid ${command.label}`;
this.emit();
return true;
}
replaceDocument(document: SceneDocument, resetHistory = true) {
this.document = document;
this.selection = { kind: "none" };
if (resetHistory) {
this.history.clear();
this.lastCommandLabel = null;
}
this.emit();
}
saveDraft(): boolean {
if (this.storage === null) {
return false;
}
saveSceneDocumentDraft(this.storage, this.document, this.storageKey);
return true;
}
loadDraft(): boolean {
if (this.storage === null) {
return false;
}
const document = loadSceneDocumentDraft(this.storage, this.storageKey);
if (document === null) {
return false;
}
this.replaceDocument(document);
return true;
}
exportDocumentJson(): string {
return serializeSceneDocument(this.document);
}
importDocumentJson(source: string): SceneDocument {
const document = parseSceneDocumentJson(source);
this.replaceDocument(document);
return document;
}
private emit() {
for (const listener of this.listeners) {
listener();
}
}
}
export function createEditorStore(options?: EditorStoreOptions): EditorStore {
return new EditorStore(options);
}