feat: implement chroma key operation and related keybinds, update workspace panel management
This commit is contained in:
13
view/App.tsx
13
view/App.tsx
@@ -13,7 +13,7 @@ import { ShortcutsDisplay } from "./ShortcutsDisplay";
|
||||
import { ToolOverlay } from "./ToolOverlay";
|
||||
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
||||
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 { downloadArtboardPng } from "@operations/export/downloadArtboard";
|
||||
import { useImageImport } from "./useImageImport";
|
||||
@@ -34,6 +34,7 @@ export function App({ app }: AppProps) {
|
||||
const transformTarget = transformSession?.target ?? selectedTransformTarget(document, selection);
|
||||
const activeArtboard = document.artboards.find((artboard) => artboard.id === selection.artboardId) ?? document.artboards[0];
|
||||
const generateOpen = workspace.panel === "generate";
|
||||
const chromaKeyOpen = workspace.panel === "chromaKey";
|
||||
const layersOpen = workspace.panel === "layers";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,6 +106,12 @@ export function App({ app }: AppProps) {
|
||||
|
||||
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 });
|
||||
if (toolConsumed) {
|
||||
event.preventDefault();
|
||||
@@ -179,6 +186,7 @@ export function App({ app }: AppProps) {
|
||||
<ToolOverlay
|
||||
activeTool={tools.activeTool}
|
||||
interactionMode={tools.interactionMode}
|
||||
panel={workspace.panel}
|
||||
dispatch={app.store.dispatch}
|
||||
/>
|
||||
</div>
|
||||
@@ -200,9 +208,10 @@ export function App({ app }: AppProps) {
|
||||
document={document}
|
||||
selection={selection}
|
||||
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}
|
||||
activeTool={tools.activeTool}
|
||||
operation={generateOpen ? "generate" : chromaKeyOpen ? "chromaKey" : undefined}
|
||||
brushSettings={tools.brush}
|
||||
generateSettings={tools.generate}
|
||||
generation={generation}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppStore } from "@editor/store";
|
||||
import type { ImageDocument } from "@core/document";
|
||||
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 { ChromaKeyControls } from "./bottom-controls/ChromaKeyControls";
|
||||
import { MagicWandControls } from "./bottom-controls/MagicWandControls";
|
||||
@@ -21,6 +21,7 @@ export type BottomControlsIslandProps = {
|
||||
visible: boolean;
|
||||
action: BottomControlsAction;
|
||||
activeTool: ToolId;
|
||||
operation?: OperationId;
|
||||
brushSettings: BrushSettings;
|
||||
generateSettings: GenerateSettings;
|
||||
generation: GenerationState;
|
||||
@@ -35,7 +36,7 @@ export type BottomControlsIslandProps = {
|
||||
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 x = Math.round(viewport.center.x);
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{activeTool === "generate" ? (
|
||||
{operation === "generate" ? (
|
||||
<GenerateActionControls settings={generateSettings} generation={generation} dispatch={dispatch} workflow={generationWorkflow} />
|
||||
) : (activeTool === "brush" || activeTool === "eraser") && brushHint ? (
|
||||
<BrushHint tool={activeTool} hint={brushHint} />
|
||||
) : activeTool === "brush" || activeTool === "eraser" ? (
|
||||
<BrushControls tool={activeTool} settings={brushSettings} editingMask={editingMask} maskViewMode={maskViewMode} dispatch={dispatch} />
|
||||
) : activeTool === "chromaKey" ? (
|
||||
) : operation === "chromaKey" ? (
|
||||
<ChromaKeyControls document={document} selection={selection} settings={chromaKeySettings} dispatch={dispatch} />
|
||||
) : activeTool === "magicWand" ? (
|
||||
<MagicWandControls settings={magicWandSettings} dispatch={dispatch} />
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Cursor, Eraser, Hand, PaintBrush, DropHalf, MagicWand, Sparkle } from "@phosphor-icons/react";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import type { InteractionMode, ToolId } from "@editor/tools";
|
||||
import { availableToolIds } from "@editor/tools";
|
||||
import type { InteractionMode, OperationId, ToolId } from "@editor/tools";
|
||||
import { availableOperationIds, availableToolIds } from "@editor/tools";
|
||||
import type { WorkspacePanel } from "@editor/state";
|
||||
import { labelForTool } from "./toolLabels";
|
||||
|
||||
export type ToolOverlayProps = {
|
||||
activeTool: ToolId;
|
||||
interactionMode: InteractionMode;
|
||||
panel: WorkspacePanel;
|
||||
dispatch: AppStore["dispatch"];
|
||||
};
|
||||
|
||||
export function ToolOverlay({ activeTool, interactionMode, dispatch }: ToolOverlayProps) {
|
||||
export function ToolOverlay({ activeTool, interactionMode, panel, dispatch }: ToolOverlayProps) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Tools"
|
||||
@@ -35,20 +37,34 @@ export function ToolOverlay({ activeTool, interactionMode, dispatch }: ToolOverl
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function iconForTool(tool: ToolId) {
|
||||
switch (tool) {
|
||||
case "generate":
|
||||
return Sparkle;
|
||||
case "brush":
|
||||
return PaintBrush;
|
||||
case "eraser":
|
||||
return Eraser;
|
||||
case "chromaKey":
|
||||
return DropHalf;
|
||||
case "magicWand":
|
||||
return MagicWand;
|
||||
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) {
|
||||
if (interactionMode.type === "temporary-pan") return tool === "pan";
|
||||
return activeTool === tool;
|
||||
|
||||
@@ -7,6 +7,6 @@ export function canvasCursorClass(interactionMode: InteractionMode, isPanning: b
|
||||
if (!canBrush) return "cursor-not-allowed";
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -62,10 +62,7 @@ export function createPaletteItems(options: {
|
||||
subtitle: tool === tools.activeTool ? "Current tool" : undefined,
|
||||
keywords: [tool],
|
||||
icon: toolIcon(tool),
|
||||
run: () => {
|
||||
if (tool === "generate") openGenerate();
|
||||
else dispatch(commandIds.toolSetActive, { tool });
|
||||
},
|
||||
run: () => dispatch(commandIds.toolSetActive, { tool }),
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -214,11 +211,18 @@ export function createPaletteItems(options: {
|
||||
id: "open-generate",
|
||||
section: "Generate",
|
||||
title: "Open generate panel",
|
||||
subtitle: tools.activeTool === "generate" ? "Current tool" : undefined,
|
||||
keywords: ["ai"],
|
||||
icon: <Sparkle size={20} />,
|
||||
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) => ({
|
||||
id: `generate-mode-${modeItem.mode}`,
|
||||
section: "Generate",
|
||||
@@ -342,14 +346,10 @@ function toolIcon(tool: ToolId) {
|
||||
switch (tool) {
|
||||
case "select":
|
||||
return <Cursor size={20} />;
|
||||
case "generate":
|
||||
return <Sparkle size={20} />;
|
||||
case "brush":
|
||||
return <PaintBrush size={20} />;
|
||||
case "eraser":
|
||||
return <Eraser size={20} />;
|
||||
case "chromaKey":
|
||||
return <DropHalf size={20} />;
|
||||
case "magicWand":
|
||||
return <MagicWand size={20} />;
|
||||
case "pan":
|
||||
|
||||
@@ -2,12 +2,8 @@ import type { ToolId } from "@editor/tools";
|
||||
|
||||
export function labelForTool(tool: ToolId): string {
|
||||
switch (tool) {
|
||||
case "generate":
|
||||
return "Generate";
|
||||
case "brush":
|
||||
return "Brush";
|
||||
case "chromaKey":
|
||||
return "Chroma key";
|
||||
case "magicWand":
|
||||
return "Magic wand";
|
||||
case "eraser":
|
||||
|
||||
Reference in New Issue
Block a user