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:
@@ -30,6 +30,7 @@ export type NormalizedMaskOptions = {
|
||||
feather?: number;
|
||||
blur?: number;
|
||||
despeckle?: number;
|
||||
limit?: Rect;
|
||||
};
|
||||
|
||||
export async function createSolidMaskSource(width: number, height: number, fill: MaskFill): Promise<string> {
|
||||
@@ -58,6 +59,36 @@ export async function applyMaskRasterOperation(source: string, width: number, he
|
||||
return maskValuesToDataUrl(next, mask.width, mask.height);
|
||||
}
|
||||
|
||||
export async function applyPolygonMask(source: string, width: number, height: number, points: readonly { x: number; y: number }[], mode: "replace" | "add" | "subtract"): Promise<string> {
|
||||
assertProcessingRasterSize(width, height, "Mask");
|
||||
if (points.length < 3) return source;
|
||||
const canvas = await loadImageCanvas(source, width, height);
|
||||
const context = require2dContext(canvas);
|
||||
if (mode === "replace") context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.save();
|
||||
context.globalCompositeOperation = mode === "subtract" ? "destination-out" : "source-over";
|
||||
context.fillStyle = "#ffffff";
|
||||
context.beginPath();
|
||||
context.moveTo(points[0]?.x ?? 0, points[0]?.y ?? 0);
|
||||
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index]?.x ?? 0, points[index]?.y ?? 0);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
export async function mergeMaskSources(existingSource: string, selectedSource: string, width: number, height: number, mode: "replace" | "add" | "subtract"): Promise<string> {
|
||||
assertProcessingRasterSize(width, height, "Mask");
|
||||
const [existing, selected] = await Promise.all([loadMaskValues(existingSource, width, height), loadMaskValues(selectedSource, width, height)]);
|
||||
const values = new Uint8ClampedArray(width * height);
|
||||
for (let pixel = 0; pixel < values.length; pixel += 1) {
|
||||
const current = existing.values[pixel] ?? 0;
|
||||
const choice = selected.values[pixel] ?? 0;
|
||||
values[pixel] = mode === "add" ? Math.max(current, choice) : mode === "subtract" ? Math.min(current, 255 - choice) : choice;
|
||||
}
|
||||
return maskValuesToDataUrl(values, width, height);
|
||||
}
|
||||
|
||||
export async function analyzeMaskSource(source: string, width: number, height: number): Promise<MaskAnalysis> {
|
||||
assertProcessingRasterSize(width, height, "Mask");
|
||||
const mask = await loadMaskValues(source, width, height);
|
||||
@@ -81,6 +112,7 @@ export async function createNormalizedMaskSource(source: string, width: number,
|
||||
assertProcessingRasterSize(width, height, "Mask");
|
||||
const mask = await loadMaskValues(source, width, height);
|
||||
let values = options.polarity === "hidden" ? invertMaskValues(mask.values) : new Uint8ClampedArray(mask.values);
|
||||
if (options.limit) values = limitMaskValues(values, mask.width, mask.height, options.limit);
|
||||
|
||||
const despeckle = Math.round(clampNumber(options.despeckle ?? 0, 0, 64));
|
||||
const expand = Math.round(clampNumber(options.expand ?? 0, -256, 256));
|
||||
@@ -97,6 +129,16 @@ export async function createNormalizedMaskSource(source: string, width: number,
|
||||
return { source: maskValuesToDataUrl(values, mask.width, mask.height), values, bounds: analysis.bounds };
|
||||
}
|
||||
|
||||
export function limitMaskValues(values: Uint8ClampedArray, width: number, height: number, limit: Rect): Uint8ClampedArray {
|
||||
const next = new Uint8ClampedArray(values.length);
|
||||
const x1 = Math.max(0, Math.floor(limit.x));
|
||||
const y1 = Math.max(0, Math.floor(limit.y));
|
||||
const x2 = Math.min(width, Math.ceil(limit.x + limit.w));
|
||||
const y2 = Math.min(height, Math.ceil(limit.y + limit.h));
|
||||
for (let y = y1; y < y2; y += 1) for (let x = x1; x < x2; x += 1) next[y * width + x] = values[y * width + x] ?? 0;
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function loadImageCanvas(source: string, width?: number, height?: number): Promise<HTMLCanvasElement> {
|
||||
const image = await loadImage(source);
|
||||
assertCanvasRasterSize(width ?? image.naturalWidth, height ?? image.naturalHeight);
|
||||
@@ -122,6 +164,33 @@ export function cropCanvas(sourceCanvas: HTMLCanvasElement, crop: Rect, outputWi
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
export function sampleDocumentCanvasInLayerSpace(
|
||||
documentCanvas: HTMLCanvasElement,
|
||||
artboardBounds: Rect,
|
||||
layer: { transform: { position: { x: number; y: number }; scale: { x: number; y: number }; rotation: number }; sourceRect?: Rect },
|
||||
intrinsicSize: { w: number; h: number },
|
||||
crop: Rect,
|
||||
outputWidth = crop.w,
|
||||
outputHeight = crop.h,
|
||||
): string {
|
||||
assertProcessingRasterSize(outputWidth, outputHeight, "Context crop");
|
||||
const canvas = createCanvas(outputWidth, outputHeight);
|
||||
const context = require2dContext(canvas);
|
||||
const source = layer.sourceRect ?? { x: 0, y: 0, ...intrinsicSize };
|
||||
const center = {
|
||||
x: layer.transform.position.x + (source.x + source.w / 2) * layer.transform.scale.x,
|
||||
y: layer.transform.position.y + (source.y + source.h / 2) * layer.transform.scale.y,
|
||||
};
|
||||
context.translate(-crop.x, -crop.y);
|
||||
context.scale(1 / Math.max(0.0001, layer.transform.scale.x), 1 / Math.max(0.0001, layer.transform.scale.y));
|
||||
context.translate(-layer.transform.position.x, -layer.transform.position.y);
|
||||
context.translate(center.x, center.y);
|
||||
context.rotate(-layer.transform.rotation);
|
||||
context.translate(-center.x, -center.y);
|
||||
context.drawImage(documentCanvas, artboardBounds.x, artboardBounds.y);
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
export function cropMaskValuesToDataUrl(values: Uint8ClampedArray, width: number, height: number, crop: Rect, outputWidth = crop.w, outputHeight = crop.h): string {
|
||||
assertProcessingRasterSize(outputWidth, outputHeight, "Mask crop");
|
||||
const canvas = createCanvas(outputWidth, outputHeight);
|
||||
|
||||
Reference in New Issue
Block a user