diff --git a/app/app.ts b/app/app.ts index d35b123..0f0e723 100644 --- a/app/app.ts +++ b/app/app.ts @@ -1,6 +1,7 @@ import { documentCommands } from "@commands/document"; import { createCommandRegistry } from "@commands/registry"; import { selectionCommands } from "@commands/selection"; +import { toolCommands } from "@commands/tool"; import { viewportCommands } from "@commands/viewport"; import { createInitialAppState } from "@editor/initial-state"; import { createAppStore } from "@editor/store"; @@ -8,7 +9,7 @@ import { createAppStore } from "@editor/store"; export type ImageStudioApp = ReturnType; export function createImageStudioApp(options?: { documentName?: string; createDefaultArtboard?: boolean }) { - const registry = createCommandRegistry([...viewportCommands, ...selectionCommands, ...documentCommands]); + const registry = createCommandRegistry([...viewportCommands, ...selectionCommands, ...documentCommands, ...toolCommands]); const store = createAppStore(createInitialAppState(options?.documentName), registry); if (options?.createDefaultArtboard !== false) { diff --git a/commands/index.ts b/commands/index.ts index c105f56..2f29ae9 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -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, diff --git a/commands/payloads.ts b/commands/payloads.ts index 4244b13..62cd7c9 100644 --- a/commands/payloads.ts +++ b/commands/payloads.ts @@ -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; diff --git a/commands/tool.test.ts b/commands/tool.test.ts new file mode 100644 index 0000000..efb13e4 --- /dev/null +++ b/commands/tool.test.ts @@ -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); + }); +}); diff --git a/commands/tool.ts b/commands/tool.ts new file mode 100644 index 0000000..b1a72ce --- /dev/null +++ b/commands/tool.ts @@ -0,0 +1,64 @@ +import type { ToolId } from "@editor/tools"; +import type { Command } from "./command"; + +export type ToolSetActivePayload = { + tool: ToolId; +}; + +export const toolSetActiveCommand: Command = { + 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[];