feat: add magic wand tool with settings and controls, including UI integration and functionality

This commit is contained in:
syntaxbullet
2026-07-04 11:49:35 +02:00
parent 6b6c2ebb80
commit af50a165da
19 changed files with 382 additions and 18 deletions

View File

@@ -26,6 +26,7 @@ export const commandIds = {
toolSetActive: "tool.setActive",
toolSetBrushSettings: "tool.setBrushSettings",
toolSetChromaKeySettings: "tool.setChromaKeySettings",
toolSetMagicWandSettings: "tool.setMagicWandSettings",
toolSetBrushPreview: "tool.setBrushPreview",
toolSetBrushStrokePreview: "tool.setBrushStrokePreview",
toolSetMaskViewMode: "tool.setMaskViewMode",

View File

@@ -54,10 +54,10 @@ export type { CommandRegistry } from "./registry";
export { createCommandRegistry } from "./registry";
export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection";
export type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
export { toolCommands, toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEditCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushPreviewCommand, toolSetBrushSettingsCommand, toolSetBrushStrokePreviewCommand, toolSetChromaKeySettingsCommand, toolSetMaskViewModeCommand } from "./tool";
export { toolCommands, toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEditCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushPreviewCommand, toolSetBrushSettingsCommand, toolSetBrushStrokePreviewCommand, toolSetChromaKeySettingsCommand, toolSetMagicWandSettingsCommand, toolSetMaskViewModeCommand } from "./tool";
export { transformBeginCommand, transformCommands, transformEndCommand, transformSetBoundsCommand, transformUpdateCommand } from "./transform";
export type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
export type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetMaskViewModePayload } from "./tool";
export type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
export {
viewportCommands,
viewportPanCommand,

View File

@@ -23,7 +23,7 @@ import type {
DocumentUngroupLayerPayload,
} from "./document";
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
import type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetMaskViewModePayload } from "./tool";
import type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
import type {
ViewportFitArtboardPayload,
@@ -61,6 +61,7 @@ export type CommandPayloads = {
[commandIds.toolSetActive]: ToolSetActivePayload;
[commandIds.toolSetBrushSettings]: ToolSetBrushSettingsPayload;
[commandIds.toolSetChromaKeySettings]: ToolSetChromaKeySettingsPayload;
[commandIds.toolSetMagicWandSettings]: ToolSetMagicWandSettingsPayload;
[commandIds.toolSetBrushPreview]: ToolSetBrushPreviewPayload;
[commandIds.toolSetBrushStrokePreview]: ToolSetBrushStrokePreviewPayload;
[commandIds.toolSetMaskViewMode]: ToolSetMaskViewModePayload;

View File

@@ -4,11 +4,12 @@ import { toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEdi
const defaultBrush = { color: "#111827", size: 8, hardness: 100 };
const defaultChromaKey = { color: "#00ff00", tolerance: 32, softness: 24, feather: 0, choke: 0, despeckle: 0, spill: 50 };
const defaultMagicWand = { tolerance: 32, feather: 0, choke: 0, despeckle: 0, contiguous: true, mode: "replace" as const };
describe("tool commands", () => {
test("sets active tool", () => {
const next = toolSetActiveCommand.execute({ state: createInitialAppState("Test") }, { tool: "brush" });
expect(next.editor.tools).toEqual({ activeTool: "brush", interactionMode: { type: "tool", tool: "brush" }, brush: defaultBrush, chromaKey: defaultChromaKey });
expect(next.editor.tools).toEqual({ activeTool: "brush", interactionMode: { type: "tool", tool: "brush" }, brush: defaultBrush, chromaKey: defaultChromaKey, magicWand: defaultMagicWand });
});
test("sets brush settings", () => {
@@ -63,7 +64,7 @@ describe("tool commands", () => {
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" }, brush: defaultBrush, chromaKey: defaultChromaKey });
expect(panning.editor.tools).toEqual({ activeTool: "select", interactionMode: { type: "temporary-pan", previousTool: "select" }, brush: defaultBrush, chromaKey: defaultChromaKey, magicWand: defaultMagicWand });
expect(restored.editor.tools).toEqual(initial.editor.tools);
});
});

View File

@@ -3,7 +3,7 @@ import type { Vec2D } from "@core/geometry";
import type { LayerId, ArtboardId, AssetId } from "@core/id";
import type { Layer } from "@core/layer";
import type { MaskViewMode } from "@editor/state";
import type { BrushSettings, ChromaKeySettings, ToolId } from "@editor/tools";
import type { BrushSettings, ChromaKeySettings, MagicWandSettings, ToolId } from "@editor/tools";
import type { Command } from "./command";
import { commandIds } from "./ids";
@@ -15,6 +15,8 @@ export type ToolSetBrushSettingsPayload = Partial<BrushSettings>;
export type ToolSetChromaKeySettingsPayload = Partial<ChromaKeySettings>;
export type ToolSetMagicWandSettingsPayload = Partial<MagicWandSettings>;
export type ToolSetBrushPreviewPayload = { position: Vec2D } | undefined;
export type ToolSetBrushStrokePreviewPayload =
@@ -100,6 +102,30 @@ export const toolSetChromaKeySettingsCommand: Command<ToolSetChromaKeySettingsPa
},
};
export const toolSetMagicWandSettingsCommand: Command<ToolSetMagicWandSettingsPayload> = {
id: commandIds.toolSetMagicWandSettings,
name: "Set magic wand settings",
execute({ state }, payload) {
return {
...state,
editor: {
...state.editor,
tools: {
...state.editor.tools,
magicWand: {
tolerance: clampNumber(payload.tolerance ?? state.editor.tools.magicWand.tolerance, 0, 255),
feather: clampNumber(payload.feather ?? state.editor.tools.magicWand.feather, 0, 20),
choke: clampNumber(payload.choke ?? state.editor.tools.magicWand.choke, -20, 20),
despeckle: clampNumber(payload.despeckle ?? state.editor.tools.magicWand.despeckle, 0, 20),
contiguous: payload.contiguous ?? state.editor.tools.magicWand.contiguous,
mode: payload.mode ?? state.editor.tools.magicWand.mode,
},
},
},
};
},
};
export const toolSetBrushPreviewCommand: Command<ToolSetBrushPreviewPayload> = {
id: commandIds.toolSetBrushPreview,
name: "Set brush preview",
@@ -250,6 +276,7 @@ export const toolCommands = [
toolSetActiveCommand,
toolSetBrushSettingsCommand,
toolSetChromaKeySettingsCommand,
toolSetMagicWandSettingsCommand,
toolSetBrushPreviewCommand,
toolSetBrushStrokePreviewCommand,
toolSetMaskViewModeCommand,