feat(tools): add brush controls
This commit is contained in:
@@ -22,6 +22,7 @@ export const commandIds = {
|
||||
selectionClear: "selection.clear",
|
||||
selectionAddLayer: "selection.addLayer",
|
||||
toolSetActive: "tool.setActive",
|
||||
toolSetBrushSettings: "tool.setBrushSettings",
|
||||
toolEnterTemporaryPan: "tool.enterTemporaryPan",
|
||||
toolExitTemporaryPan: "tool.exitTemporaryPan",
|
||||
transformBegin: "transform.begin",
|
||||
|
||||
@@ -50,10 +50,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, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand } from "./tool";
|
||||
export { toolCommands, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushSettingsCommand } from "./tool";
|
||||
export { transformBeginCommand, transformCommands, transformEndCommand, transformSetBoundsCommand, transformUpdateCommand } from "./transform";
|
||||
export type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
||||
export type { ToolSetActivePayload } from "./tool";
|
||||
export type { ToolSetActivePayload, ToolSetBrushSettingsPayload } from "./tool";
|
||||
export {
|
||||
viewportCommands,
|
||||
viewportPanCommand,
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
DocumentUngroupLayerPayload,
|
||||
} from "./document";
|
||||
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
||||
import type { ToolSetActivePayload } from "./tool";
|
||||
import type { ToolSetActivePayload, ToolSetBrushSettingsPayload } from "./tool";
|
||||
import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
||||
import type {
|
||||
ViewportFitArtboardPayload,
|
||||
@@ -55,6 +55,7 @@ export type CommandPayloads = {
|
||||
[commandIds.selectionClear]: void;
|
||||
[commandIds.selectionAddLayer]: SelectionAddLayerPayload;
|
||||
[commandIds.toolSetActive]: ToolSetActivePayload;
|
||||
[commandIds.toolSetBrushSettings]: ToolSetBrushSettingsPayload;
|
||||
[commandIds.toolEnterTemporaryPan]: void;
|
||||
[commandIds.toolExitTemporaryPan]: void;
|
||||
[commandIds.transformBegin]: TransformBeginPayload;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createInitialAppState } from "@editor/initial-state";
|
||||
import { toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand } from "./tool";
|
||||
import { toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushSettingsCommand } from "./tool";
|
||||
|
||||
describe("tool commands", () => {
|
||||
test("sets active tool", () => {
|
||||
const next = toolSetActiveCommand.execute({ state: createInitialAppState("Test") }, { tool: "crop" });
|
||||
expect(next.editor.tools).toEqual({ activeTool: "crop", interactionMode: { type: "tool", tool: "crop" } });
|
||||
expect(next.editor.tools).toEqual({ activeTool: "crop", interactionMode: { type: "tool", tool: "crop" }, brush: { color: "#111827", size: 8, hardness: 100 } });
|
||||
});
|
||||
|
||||
test("sets brush settings", () => {
|
||||
const next = toolSetBrushSettingsCommand.execute({ state: createInitialAppState("Test") }, { color: "#ff0000", size: 24, hardness: 50 });
|
||||
|
||||
expect(next.editor.tools.brush).toEqual({ color: "#ff0000", size: 24, hardness: 50 });
|
||||
});
|
||||
|
||||
test("enters and exits temporary pan", () => {
|
||||
@@ -13,7 +19,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" } });
|
||||
expect(panning.editor.tools).toEqual({ activeTool: "select", interactionMode: { type: "temporary-pan", previousTool: "select" }, brush: { color: "#111827", size: 8, hardness: 100 } });
|
||||
expect(restored.editor.tools).toEqual(initial.editor.tools);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ToolId } from "@editor/tools";
|
||||
import type { BrushSettings, ToolId } from "@editor/tools";
|
||||
import type { Command } from "./command";
|
||||
import { commandIds } from "./ids";
|
||||
|
||||
@@ -6,6 +6,8 @@ export type ToolSetActivePayload = {
|
||||
tool: ToolId;
|
||||
};
|
||||
|
||||
export type ToolSetBrushSettingsPayload = Partial<BrushSettings>;
|
||||
|
||||
export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
|
||||
id: commandIds.toolSetActive,
|
||||
name: "Set active tool",
|
||||
@@ -15,6 +17,7 @@ export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
|
||||
editor: {
|
||||
...state.editor,
|
||||
tools: {
|
||||
...state.editor.tools,
|
||||
activeTool: payload.tool,
|
||||
interactionMode: { type: "tool", tool: payload.tool },
|
||||
},
|
||||
@@ -23,6 +26,27 @@ export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const toolSetBrushSettingsCommand: Command<ToolSetBrushSettingsPayload> = {
|
||||
id: commandIds.toolSetBrushSettings,
|
||||
name: "Set brush settings",
|
||||
execute({ state }, payload) {
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
tools: {
|
||||
...state.editor.tools,
|
||||
brush: {
|
||||
color: payload.color ?? state.editor.tools.brush.color,
|
||||
size: clampNumber(payload.size ?? state.editor.tools.brush.size, 1, 200),
|
||||
hardness: clampNumber(payload.hardness ?? state.editor.tools.brush.hardness, 0, 100),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const toolEnterTemporaryPanCommand: Command = {
|
||||
id: commandIds.toolEnterTemporaryPan,
|
||||
name: "Enter temporary pan",
|
||||
@@ -54,6 +78,7 @@ export const toolExitTemporaryPanCommand: Command = {
|
||||
editor: {
|
||||
...state.editor,
|
||||
tools: {
|
||||
...state.editor.tools,
|
||||
activeTool: mode.previousTool,
|
||||
interactionMode: { type: "tool", tool: mode.previousTool },
|
||||
},
|
||||
@@ -62,4 +87,9 @@ export const toolExitTemporaryPanCommand: Command = {
|
||||
},
|
||||
};
|
||||
|
||||
export const toolCommands = [toolSetActiveCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command<unknown>[];
|
||||
export const toolCommands = [toolSetActiveCommand, toolSetBrushSettingsCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command<unknown>[];
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user