feat: implement inpainting functionality with mask handling

- Added `createMaskedPixelReplacementSource` function to handle pixel replacement using inpainting.
- Introduced `buildInpaintBundle` to prepare inpainting data including mask generation and validation.
- Created utility functions for mask operations such as `applyMaskedContentModeToRgba`, `expandRectWithinBounds`, and others for mask manipulation.
- Developed tests for inpainting preparation and mask raster utilities to ensure functionality and correctness.
- Implemented mask raster operations including inversion, feathering, blurring, and more.
This commit is contained in:
syntaxbullet
2026-07-05 09:35:16 +02:00
parent 6e5d58a638
commit f5c610dac5
35 changed files with 2285 additions and 242 deletions

View File

@@ -12,6 +12,10 @@ export const initialEditorState: EditorState = {
layerIds: [],
},
tools: initialToolState,
generation: {
candidates: [],
selectedCandidateId: undefined,
},
transformSession: undefined,
maskEdit: undefined,
brushPreview: undefined,

View File

@@ -1,7 +1,7 @@
import type { ImageDocument } from "@core/document";
import type { Angle, Size, Vec2D } from "@core/geometry";
import type { Angle, Rect, Size, Transform, Vec2D } from "@core/geometry";
import type { ArtboardId, AssetId, LayerId } from "@core/id";
import type { ToolState } from "./tools";
import type { GenerateSettings, ToolState } from "./tools";
import type { TransformSession } from "./transform";
export type ViewportState = {
@@ -34,10 +34,61 @@ export type BrushStrokePreviewState = {
source: string;
};
export type GenerationCandidate = {
id: string;
source: string;
mimeType: string;
intrinsicSize: Size;
mode: GenerateSettings["mode"];
settings: GenerateSettings;
seed: number;
width: number;
height: number;
inputImage?: string;
maskImage?: string;
placement: {
artboardId: ArtboardId;
layerName: string;
transform: Transform;
};
inpaint?: {
targetLayerId: LayerId;
maskLayerId: LayerId;
sourceAssetId: AssetId;
maskAssetId: AssetId;
inputImage: string;
maskImage: string;
crop: {
assetBounds: Rect;
documentBounds: Rect;
padding: number;
maskedAreaOnly: boolean;
};
mask: {
polarity: GenerateSettings["inpaint"]["maskPolarity"];
activeBounds: Rect;
};
backend: {
growMaskBy: number;
maskedContent: GenerateSettings["inpaint"]["maskedContent"];
maskBlur: number;
maskFeather: number;
maskExpand: number;
cropPadding: number;
};
};
};
export type GenerationState = {
candidates: GenerationCandidate[];
selectedCandidateId?: string;
};
export type EditorState = {
viewport: ViewportState;
selection: SelectionState;
tools: ToolState;
generation: GenerationState;
transformSession?: TransformSession;
maskEdit?: MaskEditState;
brushPreview?: BrushPreviewState;

View File

@@ -27,6 +27,7 @@ export type MagicWandMode = "replace" | "add" | "subtract";
export type GenerateMode = "text-to-image" | "image-to-image" | "inpaint" | "outpaint";
export type GenerateModel = string;
export type InpaintMaskedContent = "neutral" | "original" | "originalColor" | "edges";
export type GenerateSettings = {
mode: GenerateMode;
@@ -48,6 +49,17 @@ export type GenerateSettings = {
bottom: number;
feathering: number;
};
inpaint: {
maskedAreaOnly: boolean;
cropPadding: number;
maskPolarity: "hidden" | "revealed";
maskedContent: InpaintMaskedContent;
growMaskBy: number;
maskExpand: number;
maskFeather: number;
maskBlur: number;
maskDespeckle: number;
};
};
export type MagicWandSettings = {
@@ -74,7 +86,22 @@ export const initialToolState: ToolState = {
brush: { color: "#111827", size: 8, hardness: 100 },
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: { mode: "text-to-image", model: "auto", prompt: "", negativePrompt: "", strength: 75, steps: 30, cfg: 7, seed: -1, sampler: "euler", scheduler: "normal", width: 1024, height: 1024, outpaint: { left: 128, top: 128, right: 128, bottom: 128, feathering: 32 } },
generate: {
mode: "text-to-image",
model: "auto",
prompt: "",
negativePrompt: "",
strength: 75,
steps: 30,
cfg: 7,
seed: -1,
sampler: "euler",
scheduler: "normal",
width: 1024,
height: 1024,
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 },
},
};
export function isPanInteractionMode(interactionMode: InteractionMode): boolean {