Files
image-studio/commands/inpaint-region.ts
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
4.5 KiB
TypeScript

import type { Asset } from "@core/asset";
import type { InpaintRegion } from "@core/inpaint-region";
import type { InpaintRegionId } from "@core/id";
import type { Command } from "./command";
import { commandIds } from "./ids";
import type { LayerMaskOperation } from "./document";
import { findLayerLocation } from "./document-tree";
export type DocumentAddInpaintRegionPayload = {
region: InpaintRegion;
maskAsset: Asset;
};
export type DocumentApplyInpaintRegionMaskOperationPayload = {
regionId: InpaintRegionId;
source: string;
mimeType?: string;
operation: LayerMaskOperation;
};
export type DocumentRemoveInpaintRegionPayload = {
regionId: InpaintRegionId;
};
export const documentAddInpaintRegionCommand: Command<DocumentAddInpaintRegionPayload> = {
id: commandIds.documentAddInpaintRegion,
name: "Add inpaint region",
execute({ state }, payload) {
const target = findLayerLocation(state.document, payload.region.targetLayerId)?.layer;
if (!target || (target.type !== "image" && target.type !== "raster")) return state;
const targetAsset = state.document.assets.find((asset) => asset.id === target.assetId);
if (!targetAsset || state.document.inpaintRegions.some((region) => region.id === payload.region.id)) return state;
if (state.document.assets.some((asset) => asset.id === payload.maskAsset.id)) return state;
if (payload.region.maskAssetId !== payload.maskAsset.id || !payload.region.name.trim()) return state;
if (Math.round(payload.maskAsset.intrinsicSize.w) !== Math.round(targetAsset.intrinsicSize.w) || Math.round(payload.maskAsset.intrinsicSize.h) !== Math.round(targetAsset.intrinsicSize.h)) return state;
return {
...state,
document: {
...state.document,
assets: [...state.document.assets, payload.maskAsset],
inpaintRegions: [...state.document.inpaintRegions, { ...payload.region, name: payload.region.name.trim() }],
},
};
},
};
export const documentApplyInpaintRegionMaskOperationCommand: Command<DocumentApplyInpaintRegionMaskOperationPayload> = {
id: commandIds.documentApplyInpaintRegionMaskOperation,
name: "Apply inpaint region mask operation",
execute({ state }, payload) {
const region = state.document.inpaintRegions.find((candidate) => candidate.id === payload.regionId);
if (!region || !payload.source.trim()) return state;
return {
...state,
document: {
...state.document,
assets: state.document.assets.map((asset) => asset.id === region.maskAssetId ? { ...asset, source: payload.source, mimeType: payload.mimeType ?? asset.mimeType } : asset),
},
editor: {
...state.editor,
brushStrokePreview: state.editor.brushStrokePreview?.assetId === region.maskAssetId ? undefined : state.editor.brushStrokePreview,
},
};
},
};
export const documentRemoveInpaintRegionCommand: Command<DocumentRemoveInpaintRegionPayload> = {
id: commandIds.documentRemoveInpaintRegion,
name: "Remove inpaint region",
execute({ state }, payload) {
const region = state.document.inpaintRegions.find((candidate) => candidate.id === payload.regionId);
if (!region) return state;
const inpaintRegions = state.document.inpaintRegions.filter((candidate) => candidate.id !== payload.regionId);
const maskStillUsed = inpaintRegions.some((candidate) => candidate.maskAssetId === region.maskAssetId);
const exitingEdit = state.editor.maskEdit?.inpaintRegionId === region.id;
const contextualTool = state.editor.tools.activeTool === "semanticSelect" || state.editor.tools.activeTool === "maskLasso" || state.editor.tools.activeTool === "maskRectangle";
return {
...state,
document: {
...state.document,
inpaintRegions,
assets: maskStillUsed ? state.document.assets : state.document.assets.filter((asset) => asset.id !== region.maskAssetId),
},
editor: {
...state.editor,
maskEdit: exitingEdit ? undefined : state.editor.maskEdit,
brushStrokePreview: state.editor.brushStrokePreview?.assetId === region.maskAssetId ? undefined : state.editor.brushStrokePreview,
maskShapeSession: exitingEdit ? undefined : state.editor.maskShapeSession,
tools: exitingEdit && contextualTool ? { ...state.editor.tools, activeTool: "select", interactionMode: { type: "tool", tool: "select" } } : state.editor.tools,
},
};
},
};
export const inpaintRegionCommands = [
documentAddInpaintRegionCommand,
documentApplyInpaintRegionMaskOperationCommand,
documentRemoveInpaintRegionCommand,
] satisfies Command<unknown>[];