feat: implement chroma key operation and related keybinds, update workspace panel management

This commit is contained in:
syntaxbullet
2026-07-11 00:09:41 +02:00
parent 51a54dbdb2
commit 556fd685a2
21 changed files with 134 additions and 50 deletions

View File

@@ -13,6 +13,14 @@ describe("tool commands", () => {
expect(next.editor.tools).toEqual({ activeTool: "brush", interactionMode: { type: "tool", tool: "brush" }, brush: defaultBrush, generate: defaultGenerate, chromaKey: defaultChromaKey, magicWand: defaultMagicWand }); expect(next.editor.tools).toEqual({ activeTool: "brush", interactionMode: { type: "tool", tool: "brush" }, brush: defaultBrush, generate: defaultGenerate, chromaKey: defaultChromaKey, magicWand: defaultMagicWand });
}); });
test("selecting a persistent tool closes an open operation", () => {
const state = createInitialAppState("Test");
state.editor.workspace = { panel: "chromaKey" };
const next = toolSetActiveCommand.execute({ state }, { tool: "brush" });
expect(next.editor.tools.activeTool).toBe("brush");
expect(next.editor.workspace.panel).toBe("none");
});
test("sets brush settings", () => { test("sets brush settings", () => {
const next = toolSetBrushSettingsCommand.execute({ state: createInitialAppState("Test") }, { color: "#ff0000", size: 24, hardness: 50 }); const next = toolSetBrushSettingsCommand.execute({ state: createInitialAppState("Test") }, { color: "#ff0000", size: 24, hardness: 50 });

View File

@@ -44,8 +44,6 @@ export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
id: commandIds.toolSetActive, id: commandIds.toolSetActive,
name: "Set active tool", name: "Set active tool",
execute({ state }, payload) { execute({ state }, payload) {
const previousNonGenerateTool = payload.tool === "generate" ? state.editor.workspace.previousNonGenerateTool : payload.tool;
const panel = payload.tool === "generate" ? "generate" : state.editor.workspace.panel === "generate" ? "none" : state.editor.workspace.panel;
return { return {
...state, ...state,
editor: { editor: {
@@ -57,7 +55,9 @@ export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
}, },
brushPreview: undefined, brushPreview: undefined,
brushStrokePreview: undefined, brushStrokePreview: undefined,
workspace: { panel, previousNonGenerateTool }, workspace: state.editor.workspace.panel === "generate" || state.editor.workspace.panel === "chromaKey"
? { panel: "none" }
: state.editor.workspace,
}, },
}; };
}, },

View File

@@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test";
import { createInitialAppState } from "@editor/initial-state";
import { workspaceSetPanelCommand } from "./workspace";
describe("workspace commands", () => {
test.each(["generate", "chromaKey"] as const)("opens the %s operation without changing the persistent tool", (panel) => {
const state = createInitialAppState("Test");
const next = workspaceSetPanelCommand.execute({ state }, { panel });
expect(next.editor.workspace.panel).toBe(panel);
expect(next.editor.tools.activeTool).toBe("select");
expect(next.editor.tools.interactionMode).toEqual({ type: "tool", tool: "select" });
});
});

View File

@@ -10,23 +10,17 @@ export const workspaceSetPanelCommand: Command<WorkspaceSetPanelPayload> = {
history: { mode: "ignore" }, history: { mode: "ignore" },
execute({ state }, payload) { execute({ state }, payload) {
if (!workspacePanels.has(payload.panel)) return state; if (!workspacePanels.has(payload.panel)) return state;
const currentTool = state.editor.tools.activeTool; if (state.editor.workspace.panel === payload.panel) return state;
const previousNonGenerateTool = currentTool === "generate" ? state.editor.workspace.previousNonGenerateTool : currentTool;
const nextTool = payload.panel === "generate" ? "generate" : currentTool === "generate" ? previousNonGenerateTool : currentTool;
if (state.editor.workspace.panel === payload.panel && currentTool === nextTool) return state;
return { return {
...state, ...state,
editor: { editor: {
...state.editor, ...state.editor,
workspace: { panel: payload.panel, previousNonGenerateTool }, workspace: { panel: payload.panel },
tools: nextTool === currentTool ? state.editor.tools : { ...state.editor.tools, activeTool: nextTool, interactionMode: { type: "tool", tool: nextTool } },
brushPreview: nextTool === currentTool ? state.editor.brushPreview : undefined,
brushStrokePreview: nextTool === currentTool ? state.editor.brushStrokePreview : undefined,
}, },
}; };
}, },
}; };
const workspacePanels = new Set<WorkspacePanel>(["none", "generate", "layers"]); const workspacePanels = new Set<WorkspacePanel>(["none", "generate", "chromaKey", "layers"]);
export const workspaceCommands = [workspaceSetPanelCommand] satisfies Command<unknown>[]; export const workspaceCommands = [workspaceSetPanelCommand] satisfies Command<unknown>[];

View File

@@ -1,5 +1,5 @@
export type { AppState, EditorState, SelectionState, ViewportState } from "./state"; export type { AppState, EditorState, SelectionState, ViewportState } from "./state";
export type { BrushSettings, ChromaKeySettings, MagicWandSettings, InteractionMode, ToolId, ToolState } from "./tools"; export type { BrushSettings, ChromaKeySettings, MagicWandSettings, InteractionMode, OperationId, ToolId, ToolState } from "./tools";
export { initialToolState } from "./tools"; export { initialToolState } from "./tools";
export { createInitialAppState, initialEditorState } from "./initial-state"; export { createInitialAppState, initialEditorState } from "./initial-state";
export type { AppStore, StateListener } from "./store"; export type { AppStore, StateListener } from "./store";

View File

@@ -26,7 +26,6 @@ export const initialEditorState: EditorState = {
}, },
workspace: { workspace: {
panel: "none", panel: "none",
previousNonGenerateTool: "select",
}, },
transformSession: undefined, transformSession: undefined,
maskEdit: undefined, maskEdit: undefined,

View File

@@ -1,7 +1,7 @@
import type { ImageDocument } from "@core/document"; import type { ImageDocument } from "@core/document";
import type { Angle, Rect, Size, Transform, Vec2D } from "@core/geometry"; import type { Angle, Rect, Size, Transform, Vec2D } from "@core/geometry";
import type { ArtboardId, AssetId, GenerationCandidateId, GenerationJobId, LayerId } from "@core/id"; import type { ArtboardId, AssetId, GenerationCandidateId, GenerationJobId, LayerId } from "@core/id";
import type { GenerateArchitecture, GenerateMode, GenerateSettings, ToolId, ToolState } from "./tools"; import type { GenerateArchitecture, GenerateMode, GenerateSettings, ToolState } from "./tools";
import type { TransformSession } from "./transform"; import type { TransformSession } from "./transform";
export type ViewportState = { export type ViewportState = {
@@ -127,11 +127,10 @@ export type CommandPaletteState = {
selectedIndex: number; selectedIndex: number;
}; };
export type WorkspacePanel = "none" | "generate" | "layers"; export type WorkspacePanel = "none" | "generate" | "chromaKey" | "layers";
export type WorkspaceState = { export type WorkspaceState = {
panel: WorkspacePanel; panel: WorkspacePanel;
previousNonGenerateTool: ToolId;
}; };
export type EditorState = { export type EditorState = {

View File

@@ -1,6 +1,9 @@
export const availableToolIds = ["select", "generate", "brush", "eraser", "chromaKey", "magicWand", "pan"] as const; export const availableToolIds = ["select", "brush", "eraser", "magicWand", "pan"] as const;
export const availableOperationIds = ["generate", "chromaKey"] as const;
export type ToolId = (typeof availableToolIds)[number]; export type ToolId = (typeof availableToolIds)[number];
export type OperationId = (typeof availableOperationIds)[number];
export type InteractionMode = export type InteractionMode =
| { type: "tool"; tool: ToolId } | { type: "tool"; tool: ToolId }

View File

@@ -17,7 +17,7 @@ describe("command palette input", () => {
expect(dispatched).toEqual([{ commandId: commandIds.commandPaletteOpen, payload: undefined }]); expect(dispatched).toEqual([{ commandId: commandIds.commandPaletteOpen, payload: undefined }]);
}); });
test("ignores unmodified K so the chroma key tool keeps its shortcut", () => { test("ignores unmodified K so the chroma key operation keeps its shortcut", () => {
const consumed = handleCommandPaletteKey({ const consumed = handleCommandPaletteKey({
event: { key: "k", code: "KeyK", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false }, event: { key: "k", code: "KeyK", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false },
dispatch: () => undefined as never, dispatch: () => undefined as never,

View File

@@ -8,6 +8,7 @@ export { handleKeybind, keybindFromEvent } from "./keyboard";
export { handleCommandPaletteKey } from "./command-palette"; export { handleCommandPaletteKey } from "./command-palette";
export { handleHistoryKey } from "./history"; export { handleHistoryKey } from "./history";
export { handleToolKey } from "./tool-keybinds"; export { handleToolKey } from "./tool-keybinds";
export { handleOperationKey } from "./operation-keybinds";
export { findGroup, findLayerInfoInDocument, handleDeleteSelectionKey, resolveLayerDrop } from "./layers-panel"; export { findGroup, findLayerInfoInDocument, handleDeleteSelectionKey, resolveLayerDrop } from "./layers-panel";
export type { LayerDropTarget, LayerInfo } from "./layers-panel"; export type { LayerDropTarget, LayerInfo } from "./layers-panel";
export { handleArtboardSelection } from "./selection"; export { handleArtboardSelection } from "./selection";

View File

@@ -0,0 +1,22 @@
import { describe, expect, test } from "bun:test";
import { commandIds } from "@commands/ids";
import type { Dispatch } from "@commands/dispatcher";
import { handleOperationKey } from "./operation-keybinds";
describe("operation keybinds", () => {
test.each([
["g", "generate"],
["k", "chromaKey"],
] as const)("opens the %s operation without selecting a tool", (key, panel) => {
const dispatched: unknown[] = [];
const consumed = handleOperationKey({
event: { key, code: `Key${key.toUpperCase()}`, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false },
dispatch: ((commandId: unknown, payload: unknown) => {
dispatched.push({ commandId, payload });
return undefined;
}) as unknown as Dispatch,
});
expect(consumed).toBe(true);
expect(dispatched).toEqual([{ commandId: commandIds.workspaceSetPanel, payload: { panel } }]);
});
});

View File

@@ -0,0 +1,16 @@
import { commandIds } from "@commands/ids";
import type { Dispatch } from "@commands/dispatcher";
import type { KeybindEvent } from "./keyboard";
const operationKeybinds = {
g: "generate",
k: "chromaKey",
} as const;
export function handleOperationKey(options: { event: KeybindEvent; dispatch: Dispatch }): boolean {
if (options.event.altKey || options.event.ctrlKey || options.event.metaKey) return false;
const panel = operationKeybinds[options.event.key.toLowerCase() as keyof typeof operationKeybinds];
if (!panel) return false;
options.dispatch(commandIds.workspaceSetPanel, { panel });
return true;
}

View File

@@ -3,10 +3,8 @@ import type { Dispatch } from "@commands/dispatcher";
import type { KeybindEvent } from "./keyboard"; import type { KeybindEvent } from "./keyboard";
const toolKeybinds = { const toolKeybinds = {
g: "generate",
b: "brush", b: "brush",
e: "eraser", e: "eraser",
k: "chromaKey",
w: "magicWand", w: "magicWand",
p: "pan", p: "pan",
s: "select", s: "select",

View File

@@ -16,7 +16,7 @@ import type { PointerInputEvent } from "./pointer";
type TransformHandle = "body" | "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w"; type TransformHandle = "body" | "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w";
type InputToolId = "select" | "generate" | "brush" | "eraser" | "chromaKey" | "magicWand" | "pan"; type InputToolId = "select" | "brush" | "eraser" | "magicWand" | "pan";
type InputInteractionMode = type InputInteractionMode =
| { type: "tool"; tool: InputToolId } | { type: "tool"; tool: InputToolId }

View File

@@ -28,6 +28,7 @@ export async function applyChromaKeyMask(target: NonNullable<ReturnType<typeof r
dispatch(commandIds.toolSetBrushStrokePreview, undefined); dispatch(commandIds.toolSetBrushStrokePreview, undefined);
if (target.maskAsset && target.maskLayer && target.maskLayer.type !== "group") { if (target.maskAsset && target.maskLayer && target.maskLayer.type !== "group") {
dispatch(commandIds.documentApplyLayerMaskOperation, { maskLayerId: target.maskLayer.id, source, mimeType: "image/png", operation: { type: "chromaKey" } }); dispatch(commandIds.documentApplyLayerMaskOperation, { maskLayerId: target.maskLayer.id, source, mimeType: "image/png", operation: { type: "chromaKey" } });
dispatch(commandIds.workspaceSetPanel, { panel: "none" });
return; return;
} }
const assetId = crypto.randomUUID(); const assetId = crypto.randomUUID();
@@ -40,7 +41,7 @@ export async function applyChromaKeyMask(target: NonNullable<ReturnType<typeof r
maskLayer: { id: maskLayerId, type: "raster", name: `${target.layer.name} Chroma Mask`, visible: true, locked: false, opacity: 1, assetId, transform: { position: { x: target.bounds.x, y: target.bounds.y }, scale: { x: target.bounds.w / width, y: target.bounds.h / height }, rotation: target.layer.transform.rotation } }, maskLayer: { id: maskLayerId, type: "raster", name: `${target.layer.name} Chroma Mask`, visible: true, locked: false, opacity: 1, assetId, transform: { position: { x: target.bounds.x, y: target.bounds.y }, scale: { x: target.bounds.w / width, y: target.bounds.h / height }, rotation: target.layer.transform.rotation } },
}); });
dispatch(commandIds.toolExitMaskEdit, undefined); dispatch(commandIds.toolExitMaskEdit, undefined);
dispatch(commandIds.toolSetActive, { tool: "chromaKey" }); dispatch(commandIds.workspaceSetPanel, { panel: "none" });
} }
function findLayer(layers: readonly Layer[], layerId: string): Layer | undefined { function findLayer(layers: readonly Layer[], layerId: string): Layer | undefined {

View File

@@ -13,7 +13,7 @@ import { ShortcutsDisplay } from "./ShortcutsDisplay";
import { ToolOverlay } from "./ToolOverlay"; import { ToolOverlay } from "./ToolOverlay";
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets"; import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
import type { AppState } from "@editor/state"; import type { AppState } from "@editor/state";
import { handleCommandPaletteKey, handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index"; import { handleCommandPaletteKey, handleDeleteSelectionKey, handleHistoryKey, handleOperationKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
import { shallowEqual, useAppState } from "./useAppState"; import { shallowEqual, useAppState } from "./useAppState";
import { downloadArtboardPng } from "@operations/export/downloadArtboard"; import { downloadArtboardPng } from "@operations/export/downloadArtboard";
import { useImageImport } from "./useImageImport"; import { useImageImport } from "./useImageImport";
@@ -34,6 +34,7 @@ export function App({ app }: AppProps) {
const transformTarget = transformSession?.target ?? selectedTransformTarget(document, selection); const transformTarget = transformSession?.target ?? selectedTransformTarget(document, selection);
const activeArtboard = document.artboards.find((artboard) => artboard.id === selection.artboardId) ?? document.artboards[0]; const activeArtboard = document.artboards.find((artboard) => artboard.id === selection.artboardId) ?? document.artboards[0];
const generateOpen = workspace.panel === "generate"; const generateOpen = workspace.panel === "generate";
const chromaKeyOpen = workspace.panel === "chromaKey";
const layersOpen = workspace.panel === "layers"; const layersOpen = workspace.panel === "layers";
useEffect(() => { useEffect(() => {
@@ -105,6 +106,12 @@ export function App({ app }: AppProps) {
const key = event.key.toLowerCase(); const key = event.key.toLowerCase();
const operationConsumed = handleOperationKey({ event: keybindEvent, dispatch: app.store.dispatch });
if (operationConsumed) {
event.preventDefault();
return;
}
const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch }); const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch });
if (toolConsumed) { if (toolConsumed) {
event.preventDefault(); event.preventDefault();
@@ -179,6 +186,7 @@ export function App({ app }: AppProps) {
<ToolOverlay <ToolOverlay
activeTool={tools.activeTool} activeTool={tools.activeTool}
interactionMode={tools.interactionMode} interactionMode={tools.interactionMode}
panel={workspace.panel}
dispatch={app.store.dispatch} dispatch={app.store.dispatch}
/> />
</div> </div>
@@ -200,9 +208,10 @@ export function App({ app }: AppProps) {
document={document} document={document}
selection={selection} selection={selection}
viewport={viewport} viewport={viewport}
visible={tools.activeTool === "generate" || tools.activeTool === "brush" || tools.activeTool === "eraser" || tools.activeTool === "chromaKey" || tools.activeTool === "magicWand" || Boolean(transformBounds) || viewportActivityIsland.visible} visible={generateOpen || chromaKeyOpen || tools.activeTool === "brush" || tools.activeTool === "eraser" || tools.activeTool === "magicWand" || Boolean(transformBounds) || viewportActivityIsland.visible}
action={viewportActivityIsland.action} action={viewportActivityIsland.action}
activeTool={tools.activeTool} activeTool={tools.activeTool}
operation={generateOpen ? "generate" : chromaKeyOpen ? "chromaKey" : undefined}
brushSettings={tools.brush} brushSettings={tools.brush}
generateSettings={tools.generate} generateSettings={tools.generate}
generation={generation} generation={generation}

View File

@@ -1,7 +1,7 @@
import type { AppStore } from "@editor/store"; import type { AppStore } from "@editor/store";
import type { ImageDocument } from "@core/document"; import type { ImageDocument } from "@core/document";
import type { GenerationState, MaskViewMode, SelectionState, ViewportState } from "@editor/state"; import type { GenerationState, MaskViewMode, SelectionState, ViewportState } from "@editor/state";
import type { BrushSettings, ChromaKeySettings, GenerateSettings, MagicWandSettings, ToolId } from "@editor/tools"; import type { BrushSettings, ChromaKeySettings, GenerateSettings, MagicWandSettings, OperationId, ToolId } from "@editor/tools";
import { BrushControls } from "./bottom-controls/BrushControls"; import { BrushControls } from "./bottom-controls/BrushControls";
import { ChromaKeyControls } from "./bottom-controls/ChromaKeyControls"; import { ChromaKeyControls } from "./bottom-controls/ChromaKeyControls";
import { MagicWandControls } from "./bottom-controls/MagicWandControls"; import { MagicWandControls } from "./bottom-controls/MagicWandControls";
@@ -21,6 +21,7 @@ export type BottomControlsIslandProps = {
visible: boolean; visible: boolean;
action: BottomControlsAction; action: BottomControlsAction;
activeTool: ToolId; activeTool: ToolId;
operation?: OperationId;
brushSettings: BrushSettings; brushSettings: BrushSettings;
generateSettings: GenerateSettings; generateSettings: GenerateSettings;
generation: GenerationState; generation: GenerationState;
@@ -35,7 +36,7 @@ export type BottomControlsIslandProps = {
generationWorkflow: GenerationWorkflow; generationWorkflow: GenerationWorkflow;
}; };
export function BottomControlsIsland({ document, selection, viewport, visible, action, activeTool, brushSettings, generateSettings, generation, chromaKeySettings, magicWandSettings, editingMask = false, maskViewMode = "composite", transformBounds, transformTarget, brushHint, dispatch, generationWorkflow }: BottomControlsIslandProps) { export function BottomControlsIsland({ document, selection, viewport, visible, action, activeTool, operation, brushSettings, generateSettings, generation, chromaKeySettings, magicWandSettings, editingMask = false, maskViewMode = "composite", transformBounds, transformTarget, brushHint, dispatch, generationWorkflow }: BottomControlsIslandProps) {
const zoomPercent = Math.round(viewport.zoom * 100); const zoomPercent = Math.round(viewport.zoom * 100);
const x = Math.round(viewport.center.x); const x = Math.round(viewport.center.x);
const y = Math.round(viewport.center.y); const y = Math.round(viewport.center.y);
@@ -47,13 +48,13 @@ export function BottomControlsIsland({ document, selection, viewport, visible, a
visible ? "pointer-events-auto translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0" visible ? "pointer-events-auto translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0"
}`} }`}
> >
{activeTool === "generate" ? ( {operation === "generate" ? (
<GenerateActionControls settings={generateSettings} generation={generation} dispatch={dispatch} workflow={generationWorkflow} /> <GenerateActionControls settings={generateSettings} generation={generation} dispatch={dispatch} workflow={generationWorkflow} />
) : (activeTool === "brush" || activeTool === "eraser") && brushHint ? ( ) : (activeTool === "brush" || activeTool === "eraser") && brushHint ? (
<BrushHint tool={activeTool} hint={brushHint} /> <BrushHint tool={activeTool} hint={brushHint} />
) : activeTool === "brush" || activeTool === "eraser" ? ( ) : activeTool === "brush" || activeTool === "eraser" ? (
<BrushControls tool={activeTool} settings={brushSettings} editingMask={editingMask} maskViewMode={maskViewMode} dispatch={dispatch} /> <BrushControls tool={activeTool} settings={brushSettings} editingMask={editingMask} maskViewMode={maskViewMode} dispatch={dispatch} />
) : activeTool === "chromaKey" ? ( ) : operation === "chromaKey" ? (
<ChromaKeyControls document={document} selection={selection} settings={chromaKeySettings} dispatch={dispatch} /> <ChromaKeyControls document={document} selection={selection} settings={chromaKeySettings} dispatch={dispatch} />
) : activeTool === "magicWand" ? ( ) : activeTool === "magicWand" ? (
<MagicWandControls settings={magicWandSettings} dispatch={dispatch} /> <MagicWandControls settings={magicWandSettings} dispatch={dispatch} />

View File

@@ -1,17 +1,19 @@
import { Cursor, Eraser, Hand, PaintBrush, DropHalf, MagicWand, Sparkle } from "@phosphor-icons/react"; import { Cursor, Eraser, Hand, PaintBrush, DropHalf, MagicWand, Sparkle } from "@phosphor-icons/react";
import { commandIds } from "@commands/ids"; import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store"; import type { AppStore } from "@editor/store";
import type { InteractionMode, ToolId } from "@editor/tools"; import type { InteractionMode, OperationId, ToolId } from "@editor/tools";
import { availableToolIds } from "@editor/tools"; import { availableOperationIds, availableToolIds } from "@editor/tools";
import type { WorkspacePanel } from "@editor/state";
import { labelForTool } from "./toolLabels"; import { labelForTool } from "./toolLabels";
export type ToolOverlayProps = { export type ToolOverlayProps = {
activeTool: ToolId; activeTool: ToolId;
interactionMode: InteractionMode; interactionMode: InteractionMode;
panel: WorkspacePanel;
dispatch: AppStore["dispatch"]; dispatch: AppStore["dispatch"];
}; };
export function ToolOverlay({ activeTool, interactionMode, dispatch }: ToolOverlayProps) { export function ToolOverlay({ activeTool, interactionMode, panel, dispatch }: ToolOverlayProps) {
return ( return (
<nav <nav
aria-label="Tools" aria-label="Tools"
@@ -35,20 +37,34 @@ export function ToolOverlay({ activeTool, interactionMode, dispatch }: ToolOverl
</button> </button>
); );
})} })}
<div className="h-px w-8 bg-white/15" aria-hidden="true" />
{availableOperationIds.map((operation) => {
const active = panel === operation;
const Icon = iconForOperation(operation);
return (
<button
key={operation}
type="button"
aria-label={labelForOperation(operation)}
aria-pressed={active}
title={labelForOperation(operation)}
className={buttonClass(active)}
onClick={() => dispatch(commandIds.workspaceSetPanel, { panel: active ? "none" : operation })}
>
<Icon size={24} weight={active ? "fill" : "regular"} />
</button>
);
})}
</nav> </nav>
); );
} }
function iconForTool(tool: ToolId) { function iconForTool(tool: ToolId) {
switch (tool) { switch (tool) {
case "generate":
return Sparkle;
case "brush": case "brush":
return PaintBrush; return PaintBrush;
case "eraser": case "eraser":
return Eraser; return Eraser;
case "chromaKey":
return DropHalf;
case "magicWand": case "magicWand":
return MagicWand; return MagicWand;
case "pan": case "pan":
@@ -58,6 +74,14 @@ function iconForTool(tool: ToolId) {
} }
} }
function iconForOperation(operation: OperationId) {
return operation === "generate" ? Sparkle : DropHalf;
}
function labelForOperation(operation: OperationId) {
return operation === "generate" ? "Generate" : "Chroma key";
}
function isToolHighlighted(tool: ToolId, activeTool: ToolId, interactionMode: InteractionMode) { function isToolHighlighted(tool: ToolId, activeTool: ToolId, interactionMode: InteractionMode) {
if (interactionMode.type === "temporary-pan") return tool === "pan"; if (interactionMode.type === "temporary-pan") return tool === "pan";
return activeTool === tool; return activeTool === tool;

View File

@@ -7,6 +7,6 @@ export function canvasCursorClass(interactionMode: InteractionMode, isPanning: b
if (!canBrush) return "cursor-not-allowed"; if (!canBrush) return "cursor-not-allowed";
return hasBrushPreview ? "cursor-none" : "cursor-crosshair"; return hasBrushPreview ? "cursor-none" : "cursor-crosshair";
} }
if (interactionMode.type === "tool" && (interactionMode.tool === "chromaKey" || interactionMode.tool === "magicWand")) return "cursor-crosshair"; if (interactionMode.type === "tool" && interactionMode.tool === "magicWand") return "cursor-crosshair";
return "cursor-default"; return "cursor-default";
} }

View File

@@ -62,10 +62,7 @@ export function createPaletteItems(options: {
subtitle: tool === tools.activeTool ? "Current tool" : undefined, subtitle: tool === tools.activeTool ? "Current tool" : undefined,
keywords: [tool], keywords: [tool],
icon: toolIcon(tool), icon: toolIcon(tool),
run: () => { run: () => dispatch(commandIds.toolSetActive, { tool }),
if (tool === "generate") openGenerate();
else dispatch(commandIds.toolSetActive, { tool });
},
})), })),
); );
@@ -214,11 +211,18 @@ export function createPaletteItems(options: {
id: "open-generate", id: "open-generate",
section: "Generate", section: "Generate",
title: "Open generate panel", title: "Open generate panel",
subtitle: tools.activeTool === "generate" ? "Current tool" : undefined,
keywords: ["ai"], keywords: ["ai"],
icon: <Sparkle size={20} />, icon: <Sparkle size={20} />,
run: openGenerate, run: openGenerate,
}, },
{
id: "open-chroma-key",
section: "Masks",
title: "Open chroma key operation",
keywords: ["key", "mask", "remove background"],
icon: <DropHalf size={20} />,
run: () => dispatch(commandIds.workspaceSetPanel, { panel: "chromaKey" }),
},
...generateModeItems.map((modeItem) => ({ ...generateModeItems.map((modeItem) => ({
id: `generate-mode-${modeItem.mode}`, id: `generate-mode-${modeItem.mode}`,
section: "Generate", section: "Generate",
@@ -342,14 +346,10 @@ function toolIcon(tool: ToolId) {
switch (tool) { switch (tool) {
case "select": case "select":
return <Cursor size={20} />; return <Cursor size={20} />;
case "generate":
return <Sparkle size={20} />;
case "brush": case "brush":
return <PaintBrush size={20} />; return <PaintBrush size={20} />;
case "eraser": case "eraser":
return <Eraser size={20} />; return <Eraser size={20} />;
case "chromaKey":
return <DropHalf size={20} />;
case "magicWand": case "magicWand":
return <MagicWand size={20} />; return <MagicWand size={20} />;
case "pan": case "pan":

View File

@@ -2,12 +2,8 @@ import type { ToolId } from "@editor/tools";
export function labelForTool(tool: ToolId): string { export function labelForTool(tool: ToolId): string {
switch (tool) { switch (tool) {
case "generate":
return "Generate";
case "brush": case "brush":
return "Brush"; return "Brush";
case "chromaKey":
return "Chroma key";
case "magicWand": case "magicWand":
return "Magic wand"; return "Magic wand";
case "eraser": case "eraser":