feat(commands): add app state dispatcher foundation

This commit is contained in:
syntaxbullet
2026-07-03 09:28:06 +02:00
parent 697d12725d
commit a8dbd26474
11 changed files with 331 additions and 3 deletions

29
commands/dispatcher.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { AppState } from "@editor/state";
import type { CommandContext } from "./command";
import type { CommandRegistry } from "./registry";
export type Dispatch = <TPayload>(commandId: string, payload: TPayload) => AppState;
export type CommandDispatcher = {
dispatch: Dispatch;
};
export function createCommandDispatcher(options: {
registry: CommandRegistry;
getState: () => AppState;
setState: (state: AppState) => void;
}): CommandDispatcher {
return {
dispatch(commandId, payload) {
const command = options.registry.get(commandId);
if (!command) {
throw new Error(`Unknown command: ${commandId}`);
}
const context: CommandContext = { state: options.getState() };
const nextState = command.execute(context, payload);
options.setState(nextState);
return nextState;
},
};
}

View File

@@ -1 +1,19 @@
export type { Command, CommandContext } from "./command";
export type { CommandDispatcher, Dispatch } from "./dispatcher";
export { createCommandDispatcher } from "./dispatcher";
export type { CommandRegistry } from "./registry";
export { createCommandRegistry } from "./registry";
export {
viewportCommands,
viewportPanCommand,
viewportResetCommand,
viewportSetSizeCommand,
viewportSetZoomCommand,
viewportZoomAroundPointCommand,
} from "./viewport";
export type {
ViewportPanPayload,
ViewportSetSizePayload,
ViewportSetZoomPayload,
ViewportZoomAroundPointPayload,
} from "./viewport";

22
commands/registry.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { Command } from "./command";
export type CommandRegistry = {
get(id: string): Command<unknown> | undefined;
list(): Command<unknown>[];
};
export function createCommandRegistry(commands: Command<unknown>[]): CommandRegistry {
const byId = new Map<string, Command<unknown>>();
for (const command of commands) {
if (byId.has(command.id)) {
throw new Error(`Duplicate command id: ${command.id}`);
}
byId.set(command.id, command);
}
return {
get: (id) => byId.get(id),
list: () => [...byId.values()],
};
}

138
commands/viewport.ts Normal file
View File

@@ -0,0 +1,138 @@
import type { Vec2D } from "@core/geometry";
import type { Command } from "./command";
export type ViewportPanPayload = {
delta: Vec2D;
};
export type ViewportSetZoomPayload = {
zoom: number;
};
export type ViewportZoomAroundPointPayload = {
zoom: number;
point: Vec2D;
};
export type ViewportSetSizePayload = {
w: number;
h: number;
};
export const viewportPanCommand: Command<ViewportPanPayload> = {
id: "viewport.pan",
name: "Pan viewport",
execute({ state }, payload) {
return {
...state,
editor: {
...state.editor,
viewport: {
...state.editor.viewport,
center: {
x: state.editor.viewport.center.x + payload.delta.x,
y: state.editor.viewport.center.y + payload.delta.y,
},
},
},
};
},
};
export const viewportSetZoomCommand: Command<ViewportSetZoomPayload> = {
id: "viewport.setZoom",
name: "Set viewport zoom",
execute({ state }, payload) {
const zoom = Math.max(0.01, payload.zoom);
return {
...state,
editor: {
...state.editor,
viewport: {
...state.editor.viewport,
zoom,
},
},
};
},
};
export const viewportZoomAroundPointCommand: Command<ViewportZoomAroundPointPayload> = {
id: "viewport.zoomAroundPoint",
name: "Zoom viewport around point",
execute({ state }, payload) {
const viewport = state.editor.viewport;
const zoom = Math.max(0.01, payload.zoom);
const offset = {
x: payload.point.x - viewport.size.w / 2,
y: payload.point.y - viewport.size.h / 2,
};
const documentPoint = {
x: viewport.center.x + offset.x / viewport.zoom,
y: viewport.center.y + offset.y / viewport.zoom,
};
return {
...state,
editor: {
...state.editor,
viewport: {
...viewport,
zoom,
center: {
x: documentPoint.x - offset.x / zoom,
y: documentPoint.y - offset.y / zoom,
},
},
},
};
},
};
export const viewportSetSizeCommand: Command<ViewportSetSizePayload> = {
id: "viewport.setSize",
name: "Set viewport size",
execute({ state }, payload) {
return {
...state,
editor: {
...state.editor,
viewport: {
...state.editor.viewport,
size: {
w: Math.max(0, payload.w),
h: Math.max(0, payload.h),
},
},
},
};
},
};
export const viewportResetCommand: Command = {
id: "viewport.reset",
name: "Reset viewport",
execute({ state }) {
return {
...state,
editor: {
...state.editor,
viewport: {
center: { x: 0, y: 0 },
zoom: 1,
rotation: 0,
size: state.editor.viewport.size,
},
},
};
},
};
export const viewportCommands = [
viewportPanCommand,
viewportSetZoomCommand,
viewportZoomAroundPointCommand,
viewportSetSizeCommand,
viewportResetCommand,
] satisfies Command<unknown>[];