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:
@@ -25,6 +25,7 @@ export type BrushSession = {
|
||||
previewInFlight?: boolean;
|
||||
previewFrame?: number;
|
||||
previewSource?: string;
|
||||
targetLayer: RasterLayer;
|
||||
};
|
||||
|
||||
export type BrushTargetEditorState = {
|
||||
@@ -34,11 +35,9 @@ export type BrushTargetEditorState = {
|
||||
};
|
||||
|
||||
export function beginBrushSession(document: ImageDocument, editor: BrushTargetEditorState, point: Vec2D): BrushSession | undefined {
|
||||
const layer = resolveBrushTargetLayer(document, editor);
|
||||
if (!layer || (editor.tools.activeTool !== "brush" && editor.tools.activeTool !== "eraser")) return undefined;
|
||||
|
||||
const asset = document.assets.find((candidate) => candidate.id === layer.assetId);
|
||||
if (!asset) return undefined;
|
||||
const target = resolveBrushTarget(document, editor);
|
||||
if (!target || (editor.tools.activeTool !== "brush" && editor.tools.activeTool !== "eraser")) return undefined;
|
||||
const { layer, asset } = target;
|
||||
|
||||
const surface = createBrushSurface(asset.intrinsicSize.w, asset.intrinsicSize.h, asset.source);
|
||||
if (!surface) return undefined;
|
||||
@@ -52,17 +51,18 @@ export function beginBrushSession(document: ImageDocument, editor: BrushTargetEd
|
||||
ready: surface.ready,
|
||||
previousPoint: point,
|
||||
mode: editor.tools.activeTool,
|
||||
targetLayer: layer,
|
||||
};
|
||||
return session;
|
||||
}
|
||||
|
||||
export function canPreviewBrush(document: ImageDocument, editor: BrushTargetEditorState): boolean {
|
||||
return Boolean(resolveBrushTargetLayer(document, editor));
|
||||
return Boolean(resolveBrushTarget(document, editor));
|
||||
}
|
||||
|
||||
export function brushUnavailableHint(document: ImageDocument, editor: BrushTargetEditorState): string | undefined {
|
||||
if (isPanInteractionMode(editor.tools.interactionMode) || (editor.tools.activeTool !== "brush" && editor.tools.activeTool !== "eraser")) return undefined;
|
||||
if (resolveBrushTargetLayer(document, editor)) return undefined;
|
||||
if (resolveBrushTarget(document, editor)) return undefined;
|
||||
|
||||
const layerId = editor.maskEdit?.maskLayerId ?? editor.selection.layerIds[0];
|
||||
if (!layerId) {
|
||||
@@ -79,14 +79,24 @@ export function brushUnavailableHint(document: ImageDocument, editor: BrushTarge
|
||||
return "Select a raster layer or layer mask to paint.";
|
||||
}
|
||||
|
||||
function resolveBrushTargetLayer(document: ImageDocument, editor: BrushTargetEditorState): RasterLayer | undefined {
|
||||
function resolveBrushTarget(document: ImageDocument, editor: BrushTargetEditorState): { layer: RasterLayer; asset: ImageDocument["assets"][number] } | undefined {
|
||||
if (isPanInteractionMode(editor.tools.interactionMode) || (editor.tools.activeTool !== "brush" && editor.tools.activeTool !== "eraser")) return undefined;
|
||||
if (editor.maskEdit?.kind === "inpaintRegion") {
|
||||
const target = findLayer(document.artboards.flatMap((artboard) => artboard.layers), editor.maskEdit.targetLayerId);
|
||||
const asset = document.assets.find((candidate) => candidate.id === editor.maskEdit?.maskAssetId);
|
||||
if (!target || (target.type !== "image" && target.type !== "raster") || target.locked || !asset) return undefined;
|
||||
return {
|
||||
layer: { ...target, type: "raster", assetId: asset.id },
|
||||
asset,
|
||||
};
|
||||
}
|
||||
const editingMask = Boolean(editor.maskEdit);
|
||||
const layerId = editor.maskEdit?.maskLayerId ?? editor.selection.layerIds[0];
|
||||
if (!layerId) return undefined;
|
||||
const layer = findRasterLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId);
|
||||
if (!layer || layer.locked || (!editingMask && !layer.visible)) return undefined;
|
||||
return layer;
|
||||
const asset = document.assets.find((candidate) => candidate.id === layer.assetId);
|
||||
return asset ? { layer, asset } : undefined;
|
||||
}
|
||||
|
||||
export function updateBrushSession(options: {
|
||||
@@ -96,13 +106,21 @@ export function updateBrushSession(options: {
|
||||
color: string;
|
||||
size: number;
|
||||
hardness: number;
|
||||
opacity: number;
|
||||
flow: number;
|
||||
smoothing: number;
|
||||
pressure: number;
|
||||
pressureSize: boolean;
|
||||
}): BrushSession {
|
||||
const state = options.store.getState();
|
||||
const layer = findRasterLayer(state.document.artboards.flatMap((artboard) => artboard.layers), options.session.layerId);
|
||||
if (!layer || layer.assetId !== options.session.assetId) return options.session;
|
||||
const asset = state.document.assets.find((candidate) => candidate.id === options.session.assetId);
|
||||
if (!asset) return options.session;
|
||||
const layer = options.session.targetLayer;
|
||||
|
||||
const from = options.session.previousPoint;
|
||||
const to = options.point;
|
||||
const smoothing = Math.max(0, Math.min(100, options.smoothing)) / 100;
|
||||
const follow = 1 - smoothing * 0.85;
|
||||
const to = { x: from.x + (options.point.x - from.x) * follow, y: from.y + (options.point.y - from.y) * follow };
|
||||
options.session.previousPoint = to;
|
||||
options.session.pending = (options.session.pending ?? Promise.resolve())
|
||||
.then(async () => {
|
||||
@@ -113,8 +131,10 @@ export function updateBrushSession(options: {
|
||||
from: documentPointToAssetPoint(from, layer, options.session.width, options.session.height),
|
||||
to: documentPointToAssetPoint(to, layer, options.session.width, options.session.height),
|
||||
color: state.editor.maskEdit ? "#ffffff" : options.color,
|
||||
size: options.size,
|
||||
size: options.size * (options.pressureSize ? Math.max(0.1, options.pressure) : 1),
|
||||
hardness: options.hardness,
|
||||
opacity: options.opacity,
|
||||
flow: options.flow,
|
||||
mode: options.session.mode,
|
||||
});
|
||||
|
||||
@@ -135,7 +155,9 @@ export async function commitBrushSession(options: { store: AppStore; session: Br
|
||||
if (source) {
|
||||
const state = options.store.getState();
|
||||
const maskEdit = state.editor.maskEdit;
|
||||
if (maskEdit?.maskLayerId === options.session.layerId) {
|
||||
if (maskEdit?.kind === "inpaintRegion" && maskEdit.inpaintRegionId && maskEdit.maskAssetId === options.session.assetId) {
|
||||
options.store.dispatch(commandIds.documentApplyInpaintRegionMaskOperation, { regionId: maskEdit.inpaintRegionId, source, mimeType: "image/png", operation: { type: "paint" } });
|
||||
} else if (maskEdit?.maskLayerId === options.session.layerId) {
|
||||
options.store.dispatch(commandIds.documentApplyLayerMaskOperation, { maskLayerId: maskEdit.maskLayerId, source, mimeType: "image/png", operation: { type: "paint" } });
|
||||
} else {
|
||||
options.store.dispatch(commandIds.documentUpdateAssetSource, { assetId: options.session.assetId, source });
|
||||
@@ -152,9 +174,22 @@ export function cancelBrushSession(options: { store: AppStore; session: BrushSes
|
||||
}
|
||||
|
||||
function documentPointToAssetPoint(point: Vec2D, layer: RasterLayer, width: number, height: number): Vec2D {
|
||||
const source = layer.sourceRect ?? { x: 0, y: 0, w: width, h: height };
|
||||
const destination = {
|
||||
x: layer.transform.position.x + source.x * layer.transform.scale.x,
|
||||
y: layer.transform.position.y + source.y * layer.transform.scale.y,
|
||||
w: source.w * layer.transform.scale.x,
|
||||
h: source.h * layer.transform.scale.y,
|
||||
};
|
||||
const center = { x: destination.x + destination.w / 2, y: destination.y + destination.h / 2 };
|
||||
const cos = Math.cos(-layer.transform.rotation);
|
||||
const sin = Math.sin(-layer.transform.rotation);
|
||||
const dx = point.x - center.x;
|
||||
const dy = point.y - center.y;
|
||||
const unrotated = { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos };
|
||||
return {
|
||||
x: ((point.x - layer.transform.position.x) / Math.max(0.0001, layer.transform.scale.x) / width) * width,
|
||||
y: ((point.y - layer.transform.position.y) / Math.max(0.0001, layer.transform.scale.y) / height) * height,
|
||||
x: source.x + (unrotated.x - destination.x) / Math.max(0.0001, layer.transform.scale.x),
|
||||
y: source.y + (unrotated.y - destination.y) / Math.max(0.0001, layer.transform.scale.y),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user