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.
This commit is contained in:
syntaxbullet
2026-07-11 16:41:22 +02:00
parent f4e13b80e7
commit ff762b8f17
78 changed files with 1632 additions and 301 deletions

View File

@@ -4,6 +4,7 @@ import type { Layer } from "@core/layer";
import { createDocumentReadIndex, forEachLayerBackToFront, resolveIndexedLayerBounds } from "./document-indexes";
const document: ImageDocument = {
inpaintRegions: [],
id: "d1",
name: "Indexed Document",
version: 1,

View File

@@ -31,6 +31,7 @@ export const initialEditorState: EditorState = {
maskEdit: undefined,
brushPreview: undefined,
brushStrokePreview: undefined,
maskShapeSession: undefined,
pointerSession: undefined,
};
@@ -42,6 +43,7 @@ export function createInitialAppState(name = "Untitled"): AppState {
version: 1,
artboards: [],
assets: [],
inpaintRegions: [],
},
editor: initialEditorState,
history: { past: [], future: [] },

View File

@@ -1,6 +1,6 @@
import type { ImageDocument } from "@core/document";
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, InpaintRegionId, LayerId } from "@core/id";
import type { GenerateArchitecture, GenerateMode, GenerateSettings, ToolState } from "./tools";
import type { TransformSession } from "./transform";
@@ -19,8 +19,11 @@ export type SelectionState = {
export type MaskViewMode = "composite" | "blackWhite" | "alpha" | "overlay";
export type MaskEditState = {
kind: "layerMask" | "inpaintRegion";
targetLayerId: LayerId;
maskLayerId: LayerId;
maskAssetId: AssetId;
maskLayerId?: LayerId;
inpaintRegionId?: InpaintRegionId;
viewMode?: MaskViewMode;
};
@@ -34,6 +37,12 @@ export type BrushStrokePreviewState = {
source: string;
};
export type MaskShapeSession = {
shape: "lasso" | "rectangle";
points: Vec2D[];
mode: "replace" | "add" | "subtract";
};
export type GenerationCandidate = {
id: GenerationCandidateId;
source: string;
@@ -42,10 +51,12 @@ export type GenerationCandidate = {
mode: GenerateSettings["mode"];
settings: GenerateSettings;
seed: number;
favorite?: boolean;
width: number;
height: number;
inputImage?: string;
maskImage?: string;
blendMaskImage?: string;
placement: {
artboardId: ArtboardId;
layerName: string;
@@ -53,11 +64,14 @@ export type GenerationCandidate = {
};
inpaint?: {
targetLayerId: LayerId;
maskLayerId: LayerId;
regionId: InpaintRegionId;
sourceAssetId: AssetId;
maskAssetId: AssetId;
inputImage: string;
maskImage: string;
editMaskImage: string;
blendMaskImage: string;
revision: { source: string; mask: string };
crop: {
assetBounds: Rect;
documentBounds: Rect;
@@ -81,7 +95,7 @@ export type GenerationCandidate = {
export type GenerationCompareMode = "result" | "before" | "split";
export type GenerationJobKind = "generate" | "regenerate" | "refine" | "replace";
export type GenerationJobKind = "generate" | "regenerate" | "refine" | "replace" | "mask";
export type GenerationJobStatus = "running" | "succeeded" | "failed" | "cancelled";
export type GenerationJob = {
@@ -92,6 +106,8 @@ export type GenerationJob = {
startedAt: number;
finishedAt?: number;
error?: string;
progress?: number;
detail?: string;
};
export type GenerationState = {
@@ -113,10 +129,15 @@ export type GenerationArchitectureOption = {
export type GenerationOptions = {
architectures?: GenerationArchitectureOption[];
models?: string[];
inpaintModels?: string[];
textEncoders?: string[];
vaes?: string[];
samplers?: string[];
schedulers?: string[];
controlModels?: string[];
structureControls?: Array<"canny" | "depth" | "pose">;
semanticSelection?: boolean;
sam3Models?: string[];
};
export type GenerationResourcesState = { status: "idle" | "loading" | "ready" | "failed"; options?: GenerationOptions; error?: string };
@@ -148,6 +169,7 @@ export type EditorState = {
maskEdit?: MaskEditState;
brushPreview?: BrushPreviewState;
brushStrokePreview?: BrushStrokePreviewState;
maskShapeSession?: MaskShapeSession;
pointerSession?: { type: "pan" };
};

View File

@@ -1,4 +1,4 @@
export const availableToolIds = ["select", "brush", "eraser", "magicWand", "pan"] as const;
export const availableToolIds = ["select", "brush", "eraser", "magicWand", "semanticSelect", "maskLasso", "maskRectangle", "pan"] as const;
export const availableOperationIds = ["generate", "chromaKey"] as const;
@@ -13,6 +13,10 @@ export type BrushSettings = {
color: string;
size: number;
hardness: number;
opacity: number;
flow: number;
smoothing: number;
pressureSize: boolean;
};
export type ChromaKeySettings = {
@@ -28,13 +32,15 @@ export type ChromaKeySettings = {
export type MagicWandMode = "replace" | "add" | "subtract";
export type GenerateMode = "text-to-image" | "image-to-image" | "inpaint" | "outpaint";
export type GenerateIntent = "create" | "replace" | "extend" | "variations";
export type GenerateIntent = "create" | "replace" | "remove" | "extend" | "variations";
export const generateArchitectures = ["sdxl", "z-image", "z-image-turbo", "anima"] as const;
export type GenerateArchitecture = (typeof generateArchitectures)[number];
export type GenerateModel = string;
export type InpaintMaskedContent = "neutral" | "original" | "originalColor" | "edges";
export type InpaintProfile = "remove" | "replace" | "repair" | "material" | "reshape" | "custom";
export type InpaintStructureControl = "none" | "canny" | "depth" | "pose";
export type GenerateSettings = {
architecture: GenerateArchitecture;
@@ -52,6 +58,9 @@ export type GenerateSettings = {
scheduler: string;
width: number;
height: number;
batchSize: number;
refinePass: boolean;
refineStrength: number;
outpaint: {
left: number;
top: number;
@@ -60,6 +69,7 @@ export type GenerateSettings = {
feathering: number;
};
inpaint: {
profile: InpaintProfile;
maskedAreaOnly: boolean;
cropPadding: number;
maskPolarity: "hidden" | "revealed";
@@ -69,6 +79,10 @@ export type GenerateSettings = {
maskFeather: number;
maskBlur: number;
maskDespeckle: number;
structureControl: InpaintStructureControl;
controlStrength: number;
controlModel: string;
colorMatch: boolean;
};
};
@@ -109,7 +123,7 @@ export type ToolState = {
export const initialToolState: ToolState = {
activeTool: "select",
interactionMode: { type: "tool", tool: "select" },
brush: { color: "#111827", size: 8, hardness: 100 },
brush: { color: "#111827", size: 8, hardness: 100, opacity: 100, flow: 100, smoothing: 20, pressureSize: true },
chromaKey: { color: "#00ff00", tolerance: 32, softness: 24, feather: 0, choke: 0, despeckle: 0, spill: 50 },
magicWand: { tolerance: 32, feather: 0, choke: 0, despeckle: 0, contiguous: true, mode: "replace" },
generate: {
@@ -128,11 +142,22 @@ export const initialToolState: ToolState = {
scheduler: "normal",
width: 1024,
height: 1024,
batchSize: 4,
refinePass: true,
refineStrength: 20,
outpaint: { left: 128, top: 128, right: 128, bottom: 128, feathering: 32 },
inpaint: { maskedAreaOnly: true, cropPadding: 96, maskPolarity: "hidden", maskedContent: "neutral", growMaskBy: 6, maskExpand: 0, maskFeather: 0, maskBlur: 0, maskDespeckle: 0 },
inpaint: { profile: "replace", maskedAreaOnly: true, cropPadding: 128, maskPolarity: "revealed", maskedContent: "neutral", growMaskBy: 6, maskExpand: 8, maskFeather: 3, maskBlur: 0, maskDespeckle: 0, structureControl: "none", controlStrength: 0.55, controlModel: "auto", colorMatch: true },
},
};
export const inpaintProfileDefaults: Record<Exclude<InpaintProfile, "custom">, Omit<Partial<GenerateSettings>, "inpaint"> & { inpaint: Partial<GenerateSettings["inpaint"]> }> = {
remove: { strength: 100, inpaint: { maskedContent: "neutral", cropPadding: 160, maskExpand: 12, maskFeather: 4, structureControl: "none" } },
replace: { strength: 85, inpaint: { maskedContent: "neutral", cropPadding: 128, maskExpand: 8, maskFeather: 3, structureControl: "none" } },
repair: { strength: 45, refinePass: true, refineStrength: 15, inpaint: { maskedContent: "original", cropPadding: 96, maskExpand: 4, maskFeather: 2, structureControl: "none" } },
material: { strength: 65, refinePass: true, refineStrength: 20, inpaint: { maskedContent: "edges", cropPadding: 128, maskExpand: 6, maskFeather: 3, structureControl: "canny", controlStrength: 0.55, colorMatch: false } },
reshape: { strength: 90, inpaint: { maskedContent: "neutral", cropPadding: 160, maskExpand: 10, maskFeather: 4, structureControl: "depth", controlStrength: 0.4 } },
};
export function isPanInteractionMode(interactionMode: InteractionMode): boolean {
return interactionMode.type === "temporary-pan" || (interactionMode.type === "tool" && interactionMode.tool === "pan");
}

View File

@@ -4,6 +4,7 @@ import { resolveTransformTargetBounds, selectedTransformTarget } from "./transfo
import { applyTransformTargetBounds } from "@commands/transform-document";
const document: ImageDocument = {
inpaintRegions: [],
id: "d1",
name: "Test",
version: 1,