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

@@ -1,7 +1,9 @@
import type { Asset } from "@core/asset";
import type { ImageDocument } from "@core/document";
import type { Rect } from "@core/geometry";
import type { Layer } from "@core/layer";
import { createDocumentReadIndex, forEachLayerBackToFront, resolveIndexedLayerBounds, type DocumentReadIndex } from "@editor/document-indexes";
import type { EditorState, MaskViewMode, ViewportState } from "@editor/state";
import type { EditorState, GenerationCandidate, MaskViewMode, ViewportState } from "@editor/state";
import { clearScreenRect } from "./clear-rect";
import type { ImageTextureRenderer } from "./image-textures";
import { documentRectToScreenRect } from "./screen-rect";
@@ -14,15 +16,21 @@ const maskRevealPreviewOpacity = 0.28;
export function renderLayers(context: WebGlRendererContext, document: ImageDocument, editor: EditorState, imageTextureRenderer: ImageTextureRenderer) {
const documentIndex = createDocumentReadIndex(document);
const generationCandidate = selectedGenerationCandidate(editor);
for (const artboard of document.artboards) {
if (!artboard.visible) continue;
const clipRect = documentRectToScreenRect(context.canvas, artboard.bounds, editor.viewport);
const maskLayerIds = documentIndex.maskLayerIdsByArtboardId.get(artboard.id) ?? emptyLayerIds;
forEachLayerBackToFront(artboard.layers, (layer) => renderLayer(context, documentIndex, editor, layer, imageTextureRenderer, clipRect, maskLayerIds));
if (generationCandidate?.placement.artboardId === artboard.id) renderGenerationCandidatePreview(context, editor, generationCandidate, imageTextureRenderer, clipRect);
}
}
export function generationCandidatePreviewAssets(editor: EditorState): Asset[] {
return editor.generation.candidates.map((candidate) => generationCandidateAsset(candidate));
}
function renderLayer(
context: WebGlRendererContext,
documentIndex: DocumentReadIndex,
@@ -102,6 +110,44 @@ function isIsolatedMaskView(mode: MaskViewMode) {
return mode === "blackWhite" || mode === "alpha" || mode === "overlay";
}
function renderGenerationCandidatePreview(
context: WebGlRendererContext,
editor: EditorState,
candidate: GenerationCandidate,
imageTextureRenderer: ImageTextureRenderer,
clipRect: ScreenRect,
) {
const rect = documentRectToScreenRect(context.canvas, generationCandidateBounds(candidate), editor.viewport);
imageTextureRenderer.render(generationCandidateAsset(candidate), rect, clipRect);
}
function selectedGenerationCandidate(editor: EditorState): GenerationCandidate | undefined {
return editor.generation.candidates.find((candidate) => candidate.id === editor.generation.selectedCandidateId) ?? editor.generation.candidates[0];
}
function generationCandidateAsset(candidate: GenerationCandidate): Asset {
return {
id: generationCandidateAssetId(candidate.id),
name: candidate.placement.layerName,
mimeType: candidate.mimeType,
source: candidate.source,
intrinsicSize: candidate.intrinsicSize,
};
}
function generationCandidateAssetId(candidateId: string) {
return `generation-candidate:${candidateId}`;
}
function generationCandidateBounds(candidate: GenerationCandidate): Rect {
return {
x: candidate.placement.transform.position.x,
y: candidate.placement.transform.position.y,
w: candidate.intrinsicSize.w * candidate.placement.transform.scale.x,
h: candidate.intrinsicSize.h * candidate.placement.transform.scale.y,
};
}
function assetWithBrushStrokePreview<TAsset extends ImageDocument["assets"][number] | undefined>(asset: TAsset, editor: EditorState): TAsset {
if (!asset || editor.brushStrokePreview?.assetId !== asset.id) return asset;
return { ...asset, source: editor.brushStrokePreview.source } as TAsset;