From ca9e2656142214313972f7c00e7ea99a1a7e6b10 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 3 Jul 2026 09:51:26 +0200 Subject: [PATCH] feat(commands): add selection commands --- app/app.ts | 3 +- commands/index.ts | 2 ++ commands/payloads.ts | 4 +++ commands/selection.test.ts | 25 +++++++++++++++ commands/selection.ts | 63 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 commands/selection.test.ts create mode 100644 commands/selection.ts diff --git a/app/app.ts b/app/app.ts index 9d65b59..187b077 100644 --- a/app/app.ts +++ b/app/app.ts @@ -1,4 +1,5 @@ import { createCommandRegistry } from "@commands/registry"; +import { selectionCommands } from "@commands/selection"; import { viewportCommands } from "@commands/viewport"; import { createInitialAppState } from "@editor/initial-state"; import { createAppStore } from "@editor/store"; @@ -6,7 +7,7 @@ import { createAppStore } from "@editor/store"; export type ImageStudioApp = ReturnType; export function createImageStudioApp(options?: { documentName?: string }) { - const registry = createCommandRegistry([...viewportCommands]); + const registry = createCommandRegistry([...viewportCommands, ...selectionCommands]); const store = createAppStore(createInitialAppState(options?.documentName), registry); return { diff --git a/commands/index.ts b/commands/index.ts index 8c7463c..b3853e5 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -4,6 +4,8 @@ export type { CommandId, CommandPayloads } from "./payloads"; export { createCommandDispatcher } from "./dispatcher"; export type { CommandRegistry } from "./registry"; export { createCommandRegistry } from "./registry"; +export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection"; +export type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection"; export { viewportCommands, viewportPanCommand, diff --git a/commands/payloads.ts b/commands/payloads.ts index b301717..abefd3c 100644 --- a/commands/payloads.ts +++ b/commands/payloads.ts @@ -1,3 +1,4 @@ +import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection"; import type { ViewportPanPayload, ViewportSetSizePayload, @@ -6,6 +7,9 @@ import type { } from "./viewport"; export type CommandPayloads = { + "selection.set": SelectionSetPayload; + "selection.clear": void; + "selection.addLayer": SelectionAddLayerPayload; "viewport.pan": ViewportPanPayload; "viewport.setZoom": ViewportSetZoomPayload; "viewport.zoomAroundPoint": ViewportZoomAroundPointPayload; diff --git a/commands/selection.test.ts b/commands/selection.test.ts new file mode 100644 index 0000000..5399609 --- /dev/null +++ b/commands/selection.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { createInitialAppState } from "@editor/initial-state"; +import { selectionAddLayerCommand, selectionClearCommand, selectionSetCommand } from "./selection"; + +describe("selection commands", () => { + test("sets selection", () => { + const next = selectionSetCommand.execute({ state: createInitialAppState("Test") }, { artboardId: "a1", layerIds: ["l1"] }); + expect(next.editor.selection).toEqual({ artboardId: "a1", layerIds: ["l1"] }); + }); + + test("adds unique layer selection", () => { + const selected = selectionSetCommand.execute({ state: createInitialAppState("Test") }, { layerIds: ["l1"] }); + const next = selectionAddLayerCommand.execute({ state: selected }, { layerId: "l2" }); + const same = selectionAddLayerCommand.execute({ state: next }, { layerId: "l2" }); + + expect(next.editor.selection.layerIds).toEqual(["l1", "l2"]); + expect(same).toBe(next); + }); + + test("clears selection", () => { + const selected = selectionSetCommand.execute({ state: createInitialAppState("Test") }, { artboardId: "a1", layerIds: ["l1"] }); + const next = selectionClearCommand.execute({ state: selected }, undefined); + expect(next.editor.selection).toEqual({ layerIds: [] }); + }); +}); diff --git a/commands/selection.ts b/commands/selection.ts new file mode 100644 index 0000000..4bc30e6 --- /dev/null +++ b/commands/selection.ts @@ -0,0 +1,63 @@ +import type { ArtboardId, LayerId } from "@core/id"; +import type { Command } from "./command"; + +export type SelectionSetPayload = { + artboardId?: ArtboardId; + layerIds: LayerId[]; +}; + +export type SelectionAddLayerPayload = { + layerId: LayerId; +}; + +export const selectionSetCommand: Command = { + id: "selection.set", + name: "Set selection", + execute({ state }, payload) { + return { + ...state, + editor: { + ...state.editor, + selection: { + artboardId: payload.artboardId, + layerIds: [...payload.layerIds], + }, + }, + }; + }, +}; + +export const selectionClearCommand: Command = { + id: "selection.clear", + name: "Clear selection", + execute({ state }) { + return { + ...state, + editor: { + ...state.editor, + selection: { layerIds: [] }, + }, + }; + }, +}; + +export const selectionAddLayerCommand: Command = { + id: "selection.addLayer", + name: "Add layer to selection", + execute({ state }, payload) { + if (state.editor.selection.layerIds.includes(payload.layerId)) return state; + + return { + ...state, + editor: { + ...state.editor, + selection: { + ...state.editor.selection, + layerIds: [...state.editor.selection.layerIds, payload.layerId], + }, + }, + }; + }, +}; + +export const selectionCommands = [selectionSetCommand, selectionClearCommand, selectionAddLayerCommand] satisfies Command[];