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:
@@ -1,7 +1,6 @@
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { Vec2D } from "@core/geometry";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { RasterLayer } from "@core/raster-layer";
|
||||
import type { EditorState } from "@editor/state";
|
||||
import type { RgbaColor, WebGlRendererContext } from "./types";
|
||||
|
||||
@@ -97,12 +96,12 @@ function resolveBrushPreview(document: ImageDocument, editor: EditorState, canva
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBrushTargetLayer(document: ImageDocument, editor: EditorState): RasterLayer | undefined {
|
||||
function resolveBrushTargetLayer(document: ImageDocument, editor: EditorState): Extract<Layer, { type: "image" | "raster" }> | undefined {
|
||||
const editingMask = Boolean(editor.maskEdit);
|
||||
const layerId = editor.maskEdit?.maskLayerId ?? editor.selection.layerIds[0];
|
||||
const layerId = editor.maskEdit?.kind === "inpaintRegion" ? editor.maskEdit.targetLayerId : editor.maskEdit?.maskLayerId ?? editor.selection.layerIds[0];
|
||||
if (!layerId) return undefined;
|
||||
|
||||
const layer = findRasterLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId);
|
||||
const layer = findPaintableLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId);
|
||||
if (!layer || layer.locked || (!editingMask && !layer.visible)) return undefined;
|
||||
return layer;
|
||||
}
|
||||
@@ -124,11 +123,11 @@ function previewVertices(center: Vec2D, radius: Vec2D) {
|
||||
return new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]);
|
||||
}
|
||||
|
||||
function findRasterLayer(layers: readonly Layer[], layerId: string): RasterLayer | undefined {
|
||||
function findPaintableLayer(layers: readonly Layer[], layerId: string): Extract<Layer, { type: "image" | "raster" }> | undefined {
|
||||
for (const layer of layers) {
|
||||
if (layer.id === layerId && layer.type === "raster") return layer;
|
||||
if (layer.id === layerId && (layer.type === "image" || layer.type === "raster")) return layer;
|
||||
if (layer.type === "group") {
|
||||
const child = findRasterLayer(layer.children, layerId);
|
||||
const child = findPaintableLayer(layer.children, layerId);
|
||||
if (child) return child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ export function maskVisualizationModeValue(mode: MaskVisualizationMode) {
|
||||
return 1;
|
||||
case "hiddenOverlay":
|
||||
return 2;
|
||||
case "activeOverlay":
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ function createMaskVisualizationProgram(gl: WebGL2RenderingContext): MaskVisuali
|
||||
return;
|
||||
}
|
||||
|
||||
float alpha = (1.0 - maskAlpha) * u_color.a;
|
||||
float alpha = (u_mode == 3 ? maskAlpha : (1.0 - maskAlpha)) * u_color.a;
|
||||
outColor = vec4(u_color.rgb * alpha, alpha);
|
||||
}`,
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Rect } from "@core/geometry";
|
||||
import { rotatedRectBounds, rotatedRectCorners } from "./rotated-rect";
|
||||
import { textureCoordinatesForCrop, textureCoordinatesForRect } from "./texture-coordinates";
|
||||
|
||||
export type MaskVisualizationMode = "blackWhite" | "alpha" | "hiddenOverlay";
|
||||
export type MaskVisualizationMode = "blackWhite" | "alpha" | "hiddenOverlay" | "activeOverlay";
|
||||
|
||||
export type ImageTextureRenderer = {
|
||||
syncAssets(assets: readonly Pick<Asset, "id">[]): void;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { rasterizedTextLayerRenderAsset } from "./text-asset";
|
||||
const imageLayerColor: RgbaColor = [0.38, 0.42, 0.5, 1];
|
||||
const imageLayerInsetColor: RgbaColor = [0.48, 0.54, 0.64, 1];
|
||||
const hiddenMaskOverlayColor: RgbaColor = [1, 0.08, 0.08, 0.45];
|
||||
const inpaintRegionOverlayColor: RgbaColor = [1, 0.12, 0.18, 0.48];
|
||||
const comparisonDividerColor: RgbaColor = [1, 1, 1, 0.9];
|
||||
const maskRevealPreviewOpacity = 0.28;
|
||||
|
||||
@@ -99,6 +100,24 @@ function renderLeafLayer(
|
||||
|
||||
const rect = documentRectToScreenRect(context.canvas, bounds, editor.viewport);
|
||||
const asset = assetWithBrushStrokePreview(documentIndex.assetById.get(layer.assetId), editor);
|
||||
const activeInpaintRegion = editor.maskEdit?.kind === "inpaintRegion" && editor.maskEdit.targetLayerId === layer.id
|
||||
? documentIndex.assetById.get(editor.maskEdit.maskAssetId)
|
||||
: undefined;
|
||||
if (asset && activeInpaintRegion) {
|
||||
const inpaintMaskAsset = assetWithBrushStrokePreview(activeInpaintRegion, editor);
|
||||
const inpaintMaskRect = documentRectToScreenRect(context.canvas, {
|
||||
x: layer.transform.position.x,
|
||||
y: layer.transform.position.y,
|
||||
w: inpaintMaskAsset.intrinsicSize.w * layer.transform.scale.x,
|
||||
h: inpaintMaskAsset.intrinsicSize.h * layer.transform.scale.y,
|
||||
}, editor.viewport);
|
||||
if (maskViewMode === "blackWhite" && imageTextureRenderer.renderMaskVisualization(inpaintMaskAsset, inpaintMaskRect, "blackWhite", undefined, effectiveClipRect)) return;
|
||||
if (maskViewMode === "alpha" && imageTextureRenderer.renderMaskVisualization(inpaintMaskAsset, inpaintMaskRect, "alpha", undefined, effectiveClipRect)) return;
|
||||
if (imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity, layer.transform.rotation, layer.sourceRect)) {
|
||||
imageTextureRenderer.renderMaskVisualization(inpaintMaskAsset, inpaintMaskRect, "activeOverlay", inpaintRegionOverlayColor, effectiveClipRect);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const layerMask = getLayerMask(layer);
|
||||
const maskLayer = !editingMaskLayer && layerMask?.enabled ? documentIndex.layerById.get(layerMask.maskLayerId) : undefined;
|
||||
const maskAsset = assetWithBrushStrokePreview(maskLayer && (maskLayer.type === "image" || maskLayer.type === "raster") ? documentIndex.assetById.get(maskLayer.assetId) : undefined, editor);
|
||||
|
||||
32
renderer/mask-shape-preview.ts
Normal file
32
renderer/mask-shape-preview.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { EditorState } from "@editor/state";
|
||||
import { clearScreenRect } from "./clear-rect";
|
||||
import type { WebGlRendererContext } from "./types";
|
||||
|
||||
const previewColor = [1, 0.18, 0.24, 0.95] as const;
|
||||
|
||||
export function renderMaskShapePreview(context: WebGlRendererContext, editor: EditorState) {
|
||||
const session = editor.maskShapeSession;
|
||||
const points = session?.shape === "rectangle" && session.points.length > 1
|
||||
? rectanglePoints(session.points[0]!, session.points[1]!)
|
||||
: session?.points;
|
||||
if (!points || points.length === 0) return;
|
||||
const screenPoints = points.map((point) => ({
|
||||
x: context.canvas.width / 2 + (point.x - editor.viewport.center.x) * editor.viewport.zoom,
|
||||
y: context.canvas.height / 2 + (point.y - editor.viewport.center.y) * editor.viewport.zoom,
|
||||
}));
|
||||
const closed = screenPoints.length > 2 ? [...screenPoints, screenPoints[0]!] : screenPoints;
|
||||
for (let index = 1; index < closed.length; index += 1) {
|
||||
const from = closed[index - 1]!;
|
||||
const to = closed[index]!;
|
||||
const distance = Math.max(1, Math.hypot(to.x - from.x, to.y - from.y));
|
||||
const steps = Math.max(1, Math.ceil(distance / 3));
|
||||
for (let step = 0; step <= steps; step += 1) {
|
||||
const amount = step / steps;
|
||||
clearScreenRect(context, { x: from.x + (to.x - from.x) * amount - 1.5, y: from.y + (to.y - from.y) * amount - 1.5, w: 3, h: 3 }, previewColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rectanglePoints(start: { x: number; y: number }, end: { x: number; y: number }) {
|
||||
return [start, { x: end.x, y: start.y }, end, { x: start.x, y: end.y }];
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { renderSelectionOverlay } from "./selection";
|
||||
import { renderTransformControls } from "./transform-controls";
|
||||
import type { WebGlRendererContext } from "./types";
|
||||
import { createAdjustmentPass } from "./adjustment-pass";
|
||||
import { renderMaskShapePreview } from "./mask-shape-preview";
|
||||
|
||||
export type RenderFrame = {
|
||||
document: ImageDocument;
|
||||
@@ -77,6 +78,7 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke
|
||||
renderTransformControls(rendererContext, frame.document, frame.editor);
|
||||
}
|
||||
brushPreviewRenderer?.render(frame.document, frame.editor);
|
||||
renderMaskShapePreview(rendererContext, frame.editor);
|
||||
|
||||
context.disable(context.SCISSOR_TEST);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user