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

@@ -1,5 +1,4 @@
import type { ImageDocument } from "@core/document";
import { getLayerMask } from "@core/layer-mask-utils";
import { createDocumentReadIndex, resolveIndexedLayerBounds } from "@editor/document-indexes";
import type { SelectionState } from "@editor/state";
import type { GenerateSettings } from "@editor/tools";
@@ -44,21 +43,15 @@ export function checkGenerationPreconditions(
if (settings.mode !== "inpaint") return { ready: true };
const layerMask = getLayerMask(layerInfo.layer);
if (!layerMask?.enabled) return missing("Paint a mask over the area you want AI to replace.", "add-mask");
const maskLayer = index.layerById.get(layerMask.maskLayerId);
if (!maskLayer || (maskLayer.type !== "image" && maskLayer.type !== "raster")) return missing("The selected layer mask is missing.");
const maskAsset = index.assetById.get(maskLayer.assetId);
if (!maskAsset) return missing("The selected layer mask is missing its image data.");
const region = document.inpaintRegions.find((candidate) => candidate.targetLayerId === layerInfo.layer.id && candidate.enabled);
if (!region) return missing("Paint an AI edit region over the area you want to replace.", "add-mask");
const maskAsset = index.assetById.get(region.maskAssetId);
if (!maskAsset) return missing("The AI edit region is missing its mask data.");
if (Math.round(asset.intrinsicSize.w) !== Math.round(maskAsset.intrinsicSize.w) || Math.round(asset.intrinsicSize.h) !== Math.round(maskAsset.intrinsicSize.h)) {
return missing("The selected layer and mask image sizes must match.");
}
const layerBounds = resolveIndexedLayerBounds(index, layerInfo.layer);
const maskBounds = resolveIndexedLayerBounds(index, maskLayer);
if (!layerBounds || !maskBounds || !rectsAligned(layerBounds, maskBounds) || Math.abs(layerInfo.layer.transform.rotation - maskLayer.transform.rotation) > 0.001) {
return missing("Align the selected layer and its mask before inpainting.");
}
if (!resolveIndexedLayerBounds(index, layerInfo.layer)) return missing("The selected layer has invalid geometry.");
return { ready: true };
}
@@ -72,7 +65,3 @@ function modeSelectionMessage(mode: GenerateSettings["mode"]): string {
function missing(message: string, repair?: Extract<GenerationPrecondition, { ready: false }>["repair"]): GenerationPrecondition {
return { ready: false, message, ...(repair ? { repair } : {}) };
}
function rectsAligned(a: { x: number; y: number; w: number; h: number }, b: { x: number; y: number; w: number; h: number }): boolean {
return Math.abs(a.x - b.x) <= 0.5 && Math.abs(a.y - b.y) <= 0.5 && Math.abs(a.w - b.w) <= 0.5 && Math.abs(a.h - b.h) <= 0.5;
}