feat(commands): add app state dispatcher foundation
This commit is contained in:
29
commands/dispatcher.ts
Normal file
29
commands/dispatcher.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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
22
commands/registry.ts
Normal 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
138
commands/viewport.ts
Normal 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>[];
|
||||
Reference in New Issue
Block a user