feat(commands): add tool mode commands

This commit is contained in:
syntaxbullet
2026-07-03 10:12:28 +02:00
parent c23efeb397
commit 9e1050e5f3
5 changed files with 91 additions and 1 deletions

View File

@@ -8,6 +8,8 @@ export type { CommandRegistry } from "./registry";
export { createCommandRegistry } from "./registry";
export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection";
export type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
export { toolCommands, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand } from "./tool";
export type { ToolSetActivePayload } from "./tool";
export {
viewportCommands,
viewportPanCommand,

View File

@@ -1,5 +1,6 @@
import type { DocumentAddArtboardPayload } from "./document";
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
import type { ToolSetActivePayload } from "./tool";
import type {
ViewportPanPayload,
ViewportSetSizePayload,
@@ -12,6 +13,9 @@ export type CommandPayloads = {
"selection.set": SelectionSetPayload;
"selection.clear": void;
"selection.addLayer": SelectionAddLayerPayload;
"tool.setActive": ToolSetActivePayload;
"tool.enterTemporaryPan": void;
"tool.exitTemporaryPan": void;
"viewport.pan": ViewportPanPayload;
"viewport.setZoom": ViewportSetZoomPayload;
"viewport.zoomAroundPoint": ViewportZoomAroundPointPayload;

19
commands/tool.test.ts Normal file
View File

@@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test";
import { createInitialAppState } from "@editor/initial-state";
import { toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand } from "./tool";
describe("tool commands", () => {
test("sets active tool", () => {
const next = toolSetActiveCommand.execute({ state: createInitialAppState("Test") }, { tool: "pan" });
expect(next.editor.tools).toEqual({ activeTool: "pan", interactionMode: { type: "tool", tool: "pan" } });
});
test("enters and exits temporary pan", () => {
const initial = createInitialAppState("Test");
const panning = toolEnterTemporaryPanCommand.execute({ state: initial }, undefined);
const restored = toolExitTemporaryPanCommand.execute({ state: panning }, undefined);
expect(panning.editor.tools).toEqual({ activeTool: "select", interactionMode: { type: "temporary-pan", previousTool: "select" } });
expect(restored.editor.tools).toEqual(initial.editor.tools);
});
});

64
commands/tool.ts Normal file
View File

@@ -0,0 +1,64 @@
import type { ToolId } from "@editor/tools";
import type { Command } from "./command";
export type ToolSetActivePayload = {
tool: ToolId;
};
export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
id: "tool.setActive",
name: "Set active tool",
execute({ state }, payload) {
return {
...state,
editor: {
...state.editor,
tools: {
activeTool: payload.tool,
interactionMode: { type: "tool", tool: payload.tool },
},
},
};
},
};
export const toolEnterTemporaryPanCommand: Command = {
id: "tool.enterTemporaryPan",
name: "Enter temporary pan",
execute({ state }) {
if (state.editor.tools.interactionMode.type === "temporary-pan") return state;
return {
...state,
editor: {
...state.editor,
tools: {
...state.editor.tools,
interactionMode: { type: "temporary-pan", previousTool: state.editor.tools.activeTool },
},
},
};
},
};
export const toolExitTemporaryPanCommand: Command = {
id: "tool.exitTemporaryPan",
name: "Exit temporary pan",
execute({ state }) {
const mode = state.editor.tools.interactionMode;
if (mode.type !== "temporary-pan") return state;
return {
...state,
editor: {
...state.editor,
tools: {
activeTool: mode.previousTool,
interactionMode: { type: "tool", tool: mode.previousTool },
},
},
};
},
};
export const toolCommands = [toolSetActiveCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command<unknown>[];