Files
image-studio/view/ToolOverlay.tsx
syntaxbullet ff762b8f17 feat: add inpaint region functionality and related tools
- Enhanced cursor behavior for new tools: semantic select, mask lasso, and mask rectangle.
- Updated mask edit state to include mask asset ID and kind.
- Implemented inpaint region commands for adding, applying, and removing inpaint regions.
- Introduced new operations for lasso and semantic selection tools.
- Created UI components for candidate review and inpaint region management.
- Added tests for inpaint region commands to ensure functionality.
- Updated various components to support new inpaint features and improve user experience.
2026-07-11 16:41:22 +02:00

101 lines
3.6 KiB
TypeScript

import { Cursor, Eraser, Hand, PaintBrush, DropHalf, MagicWand, Sparkle, Polygon, Rectangle, Selection } from "@phosphor-icons/react";
import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store";
import type { InteractionMode, OperationId, ToolId } from "@editor/tools";
import { availableOperationIds, availableToolIds } from "@editor/tools";
import { isOperationWorkspacePanel, type WorkspacePanel } from "@editor/state";
import { labelForTool } from "./toolLabels";
export type ToolOverlayProps = {
activeTool: ToolId;
interactionMode: InteractionMode;
panel: WorkspacePanel;
inpaintMaskEditing: boolean;
dispatch: AppStore["dispatch"];
};
export function ToolOverlay({ activeTool, interactionMode, panel, inpaintMaskEditing, dispatch }: ToolOverlayProps) {
return (
<nav
aria-label="Tools"
className="app-surface pointer-events-auto flex w-11 flex-col items-center gap-1 rounded-xl p-1 text-white"
>
{availableToolIds.filter((tool) => inpaintMaskEditing || (tool !== "semanticSelect" && tool !== "maskLasso" && tool !== "maskRectangle")).map((tool) => {
const active = !isOperationWorkspacePanel(panel) && isToolHighlighted(tool, activeTool, interactionMode);
const Icon = iconForTool(tool);
return (
<button
key={tool}
type="button"
aria-label={labelForTool(tool)}
aria-pressed={active}
title={labelForTool(tool)}
className={buttonClass(active)}
onClick={() => dispatch(commandIds.toolSetActive, { tool })}
>
<Icon size={18} weight={active ? "fill" : "regular"} />
</button>
);
})}
<div className="my-0.5 h-px w-6 bg-white/10" 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={18} weight={active ? "fill" : "regular"} />
</button>
);
})}
</nav>
);
}
function iconForTool(tool: ToolId) {
switch (tool) {
case "brush":
return PaintBrush;
case "eraser":
return Eraser;
case "magicWand":
return MagicWand;
case "semanticSelect":
return Selection;
case "maskLasso":
return Polygon;
case "maskRectangle":
return Rectangle;
case "pan":
return Hand;
case "select":
return Cursor;
}
}
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;
}
function buttonClass(active: boolean) {
const base = "inline-flex size-9 items-center justify-center rounded-lg text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50";
return active ? `${base} bg-sky-300 text-slate-950` : `${base} text-white/55 hover:bg-white/[0.07] hover:text-white`;
}