import { useEffect, useMemo, useState } from "react"; import { commandIds } from "@commands/ids"; import type { ImageDocument } from "@core/document"; import type { SelectionState } from "@editor/state"; import type { AppStore } from "@editor/store"; import type { GenerateSettings } from "@editor/tools"; import { runInpaintRegionOperation } from "@operations/masks/rasterActions"; import { createNormalizedMaskSource } from "@platform/browser/maskRaster"; type ProcessedMasks = { edit: string; noise: string; blend: string; coverage: number; bounds?: { x: number; y: number; w: number; h: number } }; export function InpaintRegionPanel({ document, selection, settings, dispatch }: { document: ImageDocument; selection: SelectionState; settings: GenerateSettings; dispatch: AppStore["dispatch"] }) { const targetLayerId = selection.layerIds.length === 1 ? selection.layerIds[0] : undefined; const region = document.inpaintRegions.find((candidate) => candidate.targetLayerId === targetLayerId && candidate.enabled); const asset = region ? document.assets.find((candidate) => candidate.id === region.maskAssetId) : undefined; const [processed, setProcessed] = useState(); const [busy, setBusy] = useState(false); const processingKey = useMemo(() => asset ? [asset.source, settings.inpaint.maskExpand, settings.inpaint.maskFeather, settings.inpaint.maskBlur, settings.inpaint.maskDespeckle].join(":") : "", [asset, settings.inpaint]); useEffect(() => { if (!asset) { setProcessed(undefined); return; } let cancelled = false; const width = Math.max(1, Math.round(asset.intrinsicSize.w)); const height = Math.max(1, Math.round(asset.intrinsicSize.h)); void Promise.all([ createNormalizedMaskSource(asset.source, width, height, { polarity: "revealed", despeckle: settings.inpaint.maskDespeckle }), createNormalizedMaskSource(asset.source, width, height, { polarity: "revealed", expand: settings.inpaint.maskExpand, blur: settings.inpaint.maskBlur, despeckle: settings.inpaint.maskDespeckle }), createNormalizedMaskSource(asset.source, width, height, { polarity: "revealed", feather: settings.inpaint.maskFeather, despeckle: settings.inpaint.maskDespeckle }), ]).then(([edit, noise, blend]) => { if (cancelled) return; const active = edit.values.reduce((sum, value) => sum + value / 255, 0); setProcessed({ edit: edit.source, noise: noise.source, blend: blend.source, coverage: active / edit.values.length, bounds: edit.bounds }); }).catch(() => !cancelled && setProcessed(undefined)); return () => { cancelled = true; }; }, [processingKey, asset, settings.inpaint.maskDespeckle, settings.inpaint.maskExpand, settings.inpaint.maskBlur, settings.inpaint.maskFeather]); if (!targetLayerId) return

Select one image or raster layer to create an AI edit region.

; if (!region || !asset) return

No AI edit region yet. Use Add region beside Generate, then paint where pixels should be replaced.

; const operation = async (type: "invert" | "clear" | "fill") => { setBusy(true); try { await runInpaintRegionOperation(region.id, asset, type === "invert" ? { type: "invert" } : { type: "fill", fill: type === "fill" ? "white" : "black" }, dispatch); } finally { setBusy(false); } }; return (
AI edit regionPainted pixels are replaced; unpainted pixels are protected.
{processed ? (
) : Preparing mask previews…}
{processed ? Replace {Math.round(processed.coverage * 100)}%{processed.bounds ? ` · ${Math.round(processed.bounds.w)}×${Math.round(processed.bounds.h)} px` : " · empty"} : } void operation("invert")} /> { dispatch(commandIds.toolEnterInpaintRegionEdit, { targetLayerId, regionId: region.id }); dispatch(commandIds.toolSetActive, { tool: "magicWand" }); }} /> { dispatch(commandIds.toolEnterInpaintRegionEdit, { targetLayerId, regionId: region.id }); dispatch(commandIds.toolSetActive, { tool: "semanticSelect" }); }} /> { dispatch(commandIds.toolEnterInpaintRegionEdit, { targetLayerId, regionId: region.id }); dispatch(commandIds.toolSetActive, { tool: "maskLasso" }); }} /> { dispatch(commandIds.toolEnterInpaintRegionEdit, { targetLayerId, regionId: region.id }); dispatch(commandIds.toolSetActive, { tool: "maskRectangle" }); }} /> void operation("clear")} /> void operation("fill")} /> dispatch(commandIds.documentRemoveInpaintRegion, { regionId: region.id })} />
); } function MaskPreview({ label, source }: { label: string; source: string }) { return
{`${label}{label}
; } function RegionButton({ label, disabled, danger, onClick }: { label: string; disabled?: boolean; danger?: boolean; onClick: () => void }) { return ; }