feat: unify AI edit workflow
This commit is contained in:
@@ -27,6 +27,7 @@ export const commandIds = {
|
|||||||
selectionAddLayer: "selection.addLayer",
|
selectionAddLayer: "selection.addLayer",
|
||||||
toolSetActive: "tool.setActive",
|
toolSetActive: "tool.setActive",
|
||||||
toolSetGenerateSettings: "tool.setGenerateSettings",
|
toolSetGenerateSettings: "tool.setGenerateSettings",
|
||||||
|
toolChooseGenerateIntent: "tool.chooseGenerateIntent",
|
||||||
toolSetBrushSettings: "tool.setBrushSettings",
|
toolSetBrushSettings: "tool.setBrushSettings",
|
||||||
toolSetChromaKeySettings: "tool.setChromaKeySettings",
|
toolSetChromaKeySettings: "tool.setChromaKeySettings",
|
||||||
toolSetMagicWandSettings: "tool.setMagicWandSettings",
|
toolSetMagicWandSettings: "tool.setMagicWandSettings",
|
||||||
|
|||||||
@@ -89,10 +89,10 @@ export type { CommandRegistry } from "./registry";
|
|||||||
export { createCommandRegistry } from "./registry";
|
export { createCommandRegistry } from "./registry";
|
||||||
export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection";
|
export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection";
|
||||||
export type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
export type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
||||||
export { toolCommands, toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEditCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushPreviewCommand, toolSetBrushSettingsCommand, toolSetBrushStrokePreviewCommand, toolSetChromaKeySettingsCommand, toolSetGenerateSettingsCommand, toolSetMagicWandSettingsCommand, toolSetMaskViewModeCommand } from "./tool";
|
export { toolChooseGenerateIntentCommand, toolCommands, toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEditCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushPreviewCommand, toolSetBrushSettingsCommand, toolSetBrushStrokePreviewCommand, toolSetChromaKeySettingsCommand, toolSetGenerateSettingsCommand, toolSetMagicWandSettingsCommand, toolSetMaskViewModeCommand } from "./tool";
|
||||||
export { transformBeginCommand, transformCommands, transformEndCommand, transformSetBoundsCommand, transformUpdateCommand } from "./transform";
|
export { transformBeginCommand, transformCommands, transformEndCommand, transformSetBoundsCommand, transformUpdateCommand } from "./transform";
|
||||||
export type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
export type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
||||||
export type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetGenerateSettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
|
export type { ToolChooseGenerateIntentPayload, ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetGenerateSettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
|
||||||
export {
|
export {
|
||||||
viewportCommands,
|
viewportCommands,
|
||||||
viewportPanCommand,
|
viewportPanCommand,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import type {
|
|||||||
CommandPaletteSetSelectedIndexPayload,
|
CommandPaletteSetSelectedIndexPayload,
|
||||||
} from "./palette";
|
} from "./palette";
|
||||||
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
||||||
import type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetGenerateSettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
|
import type { ToolChooseGenerateIntentPayload, ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetGenerateSettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
|
||||||
import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
||||||
import type { WorkspaceSetPanelPayload } from "./workspace";
|
import type { WorkspaceSetPanelPayload } from "./workspace";
|
||||||
import type { EditorSetPointerSessionPayload } from "./editor";
|
import type { EditorSetPointerSessionPayload } from "./editor";
|
||||||
@@ -86,6 +86,7 @@ export type CommandPayloads = {
|
|||||||
[commandIds.selectionAddLayer]: SelectionAddLayerPayload;
|
[commandIds.selectionAddLayer]: SelectionAddLayerPayload;
|
||||||
[commandIds.toolSetActive]: ToolSetActivePayload;
|
[commandIds.toolSetActive]: ToolSetActivePayload;
|
||||||
[commandIds.toolSetGenerateSettings]: ToolSetGenerateSettingsPayload;
|
[commandIds.toolSetGenerateSettings]: ToolSetGenerateSettingsPayload;
|
||||||
|
[commandIds.toolChooseGenerateIntent]: ToolChooseGenerateIntentPayload;
|
||||||
[commandIds.toolSetBrushSettings]: ToolSetBrushSettingsPayload;
|
[commandIds.toolSetBrushSettings]: ToolSetBrushSettingsPayload;
|
||||||
[commandIds.toolSetChromaKeySettings]: ToolSetChromaKeySettingsPayload;
|
[commandIds.toolSetChromaKeySettings]: ToolSetChromaKeySettingsPayload;
|
||||||
[commandIds.toolSetMagicWandSettings]: ToolSetMagicWandSettingsPayload;
|
[commandIds.toolSetMagicWandSettings]: ToolSetMagicWandSettingsPayload;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createInitialAppState } from "@editor/initial-state";
|
import { createInitialAppState } from "@editor/initial-state";
|
||||||
import { toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEditCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushPreviewCommand, toolSetBrushSettingsCommand, toolSetBrushStrokePreviewCommand, toolSetChromaKeySettingsCommand, toolSetGenerateSettingsCommand, toolSetMaskViewModeCommand } from "./tool";
|
import { toolChooseGenerateIntentCommand, toolEnterMaskEditCommand, toolEnterTemporaryPanCommand, toolExitMaskEditCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushPreviewCommand, toolSetBrushSettingsCommand, toolSetBrushStrokePreviewCommand, toolSetChromaKeySettingsCommand, toolSetGenerateSettingsCommand, toolSetMaskViewModeCommand } from "./tool";
|
||||||
|
|
||||||
const defaultBrush = { color: "#111827", size: 8, hardness: 100 };
|
const defaultBrush = { color: "#111827", size: 8, hardness: 100 };
|
||||||
const defaultChromaKey = { color: "#00ff00", tolerance: 32, softness: 24, feather: 0, choke: 0, despeckle: 0, spill: 50 };
|
const defaultChromaKey = { color: "#00ff00", tolerance: 32, softness: 24, feather: 0, choke: 0, despeckle: 0, spill: 50 };
|
||||||
@@ -60,6 +60,15 @@ describe("tool commands", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("maps user intents to supported generation modes and a compatible architecture", () => {
|
||||||
|
const zImage = toolSetGenerateSettingsCommand.execute({ state: createInitialAppState("Test") }, { architecture: "z-image" });
|
||||||
|
const replace = toolChooseGenerateIntentCommand.execute({ state: zImage }, { intent: "replace" });
|
||||||
|
const variations = toolChooseGenerateIntentCommand.execute({ state: replace }, { intent: "variations" });
|
||||||
|
|
||||||
|
expect(replace.editor.tools.generate).toMatchObject({ architecture: "sdxl", mode: "inpaint" });
|
||||||
|
expect(variations.editor.tools.generate).toMatchObject({ architecture: "sdxl", mode: "image-to-image" });
|
||||||
|
});
|
||||||
|
|
||||||
test("sets and clears brush preview", () => {
|
test("sets and clears brush preview", () => {
|
||||||
const showing = toolSetBrushPreviewCommand.execute({ state: createInitialAppState("Test") }, { position: { x: 10, y: 20 } });
|
const showing = toolSetBrushPreviewCommand.execute({ state: createInitialAppState("Test") }, { position: { x: 10, y: 20 } });
|
||||||
const cleared = toolSetBrushPreviewCommand.execute({ state: showing }, undefined);
|
const cleared = toolSetBrushPreviewCommand.execute({ state: showing }, undefined);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { Layer } from "@core/layer";
|
|||||||
import { getLayerMask } from "@core/layer-mask-utils";
|
import { getLayerMask } from "@core/layer-mask-utils";
|
||||||
import type { MaskViewMode } from "@editor/state";
|
import type { MaskViewMode } from "@editor/state";
|
||||||
import { generateArchitectureDefaults } from "@editor/tools";
|
import { generateArchitectureDefaults } from "@editor/tools";
|
||||||
import type { BrushSettings, ChromaKeySettings, GenerateSettings, MagicWandSettings, ToolId } from "@editor/tools";
|
import type { BrushSettings, ChromaKeySettings, GenerateIntent, GenerateSettings, MagicWandSettings, ToolId } from "@editor/tools";
|
||||||
import type { Command } from "./command";
|
import type { Command } from "./command";
|
||||||
import { commandIds } from "./ids";
|
import { commandIds } from "./ids";
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export type ToolSetActivePayload = {
|
|||||||
export type ToolSetBrushSettingsPayload = Partial<BrushSettings>;
|
export type ToolSetBrushSettingsPayload = Partial<BrushSettings>;
|
||||||
|
|
||||||
export type ToolSetGenerateSettingsPayload = Partial<GenerateSettings>;
|
export type ToolSetGenerateSettingsPayload = Partial<GenerateSettings>;
|
||||||
|
export type ToolChooseGenerateIntentPayload = { intent: GenerateIntent };
|
||||||
|
|
||||||
export type ToolSetChromaKeySettingsPayload = Partial<ChromaKeySettings>;
|
export type ToolSetChromaKeySettingsPayload = Partial<ChromaKeySettings>;
|
||||||
|
|
||||||
@@ -120,6 +121,25 @@ export const toolSetGenerateSettingsCommand: Command<ToolSetGenerateSettingsPayl
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modeByGenerateIntent: Record<GenerateIntent, GenerateSettings["mode"]> = {
|
||||||
|
create: "text-to-image",
|
||||||
|
replace: "inpaint",
|
||||||
|
extend: "outpaint",
|
||||||
|
variations: "image-to-image",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toolChooseGenerateIntentCommand: Command<ToolChooseGenerateIntentPayload> = {
|
||||||
|
id: commandIds.toolChooseGenerateIntent,
|
||||||
|
name: "Choose AI edit intent",
|
||||||
|
execute({ state }, payload) {
|
||||||
|
const mode = modeByGenerateIntent[payload.intent];
|
||||||
|
const architecture = generateArchitectureDefaults[state.editor.tools.generate.architecture].supportedModes.includes(mode)
|
||||||
|
? state.editor.tools.generate.architecture
|
||||||
|
: "sdxl";
|
||||||
|
return toolSetGenerateSettingsCommand.execute({ state }, { architecture, mode });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const toolSetBrushSettingsCommand: Command<ToolSetBrushSettingsPayload> = {
|
export const toolSetBrushSettingsCommand: Command<ToolSetBrushSettingsPayload> = {
|
||||||
id: commandIds.toolSetBrushSettings,
|
id: commandIds.toolSetBrushSettings,
|
||||||
name: "Set brush settings",
|
name: "Set brush settings",
|
||||||
@@ -339,6 +359,7 @@ export const toolExitTemporaryPanCommand: Command = {
|
|||||||
export const toolCommands = [
|
export const toolCommands = [
|
||||||
toolSetActiveCommand,
|
toolSetActiveCommand,
|
||||||
toolSetGenerateSettingsCommand,
|
toolSetGenerateSettingsCommand,
|
||||||
|
toolChooseGenerateIntentCommand,
|
||||||
toolSetBrushSettingsCommand,
|
toolSetBrushSettingsCommand,
|
||||||
toolSetChromaKeySettingsCommand,
|
toolSetChromaKeySettingsCommand,
|
||||||
toolSetMagicWandSettingsCommand,
|
toolSetMagicWandSettingsCommand,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type ChromaKeySettings = {
|
|||||||
export type MagicWandMode = "replace" | "add" | "subtract";
|
export type MagicWandMode = "replace" | "add" | "subtract";
|
||||||
|
|
||||||
export type GenerateMode = "text-to-image" | "image-to-image" | "inpaint" | "outpaint";
|
export type GenerateMode = "text-to-image" | "image-to-image" | "inpaint" | "outpaint";
|
||||||
|
export type GenerateIntent = "create" | "replace" | "extend" | "variations";
|
||||||
|
|
||||||
export const generateArchitectures = ["sdxl", "z-image", "z-image-turbo", "anima"] as const;
|
export const generateArchitectures = ["sdxl", "z-image", "z-image-turbo", "anima"] as const;
|
||||||
export type GenerateArchitecture = (typeof generateArchitectures)[number];
|
export type GenerateArchitecture = (typeof generateArchitectures)[number];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("generation preconditions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("requires an enabled mask for inpaint", () => {
|
test("requires an enabled mask for inpaint", () => {
|
||||||
expect(checkGenerationPreconditions(document(), selected(), settings("inpaint"))).toEqual({ ready: false, message: "Add and enable a layer mask on the selected image before inpainting." });
|
expect(checkGenerationPreconditions(document(), selected(), settings("inpaint"))).toEqual({ ready: false, message: "Paint a mask over the area you want AI to replace.", repair: "add-mask" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("allows inpaint when the selected source has an aligned enabled mask", () => {
|
test("allows inpaint when the selected source has an aligned enabled mask", () => {
|
||||||
@@ -24,7 +24,7 @@ describe("generation preconditions", () => {
|
|||||||
|
|
||||||
test("explains prompt and outpaint-padding requirements", () => {
|
test("explains prompt and outpaint-padding requirements", () => {
|
||||||
expect(checkGenerationPreconditions(document(), selected(), { ...settings("image-to-image"), prompt: " " })).toEqual({ ready: false, message: "Enter a prompt to generate an image." });
|
expect(checkGenerationPreconditions(document(), selected(), { ...settings("image-to-image"), prompt: " " })).toEqual({ ready: false, message: "Enter a prompt to generate an image." });
|
||||||
expect(checkGenerationPreconditions(document(), selected(), { ...settings("outpaint"), outpaint: { left: 0, top: 0, right: 0, bottom: 0, feathering: 0 } })).toEqual({ ready: false, message: "Add outpaint padding on at least one side." });
|
expect(checkGenerationPreconditions(document(), selected(), { ...settings("outpaint"), outpaint: { left: 0, top: 0, right: 0, bottom: 0, feathering: 0 } })).toEqual({ ready: false, message: "Add outpaint padding on at least one side.", repair: "set-outpaint-padding" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { GenerateSettings } from "@editor/tools";
|
|||||||
|
|
||||||
export type GenerationPrecondition =
|
export type GenerationPrecondition =
|
||||||
| { ready: true }
|
| { ready: true }
|
||||||
| { ready: false; message: string };
|
| { ready: false; message: string; repair?: "add-mask" | "set-outpaint-padding" };
|
||||||
|
|
||||||
export function checkGenerationPreconditions(
|
export function checkGenerationPreconditions(
|
||||||
document: ImageDocument,
|
document: ImageDocument,
|
||||||
@@ -38,14 +38,14 @@ export function checkGenerationPreconditions(
|
|||||||
if (settings.mode === "outpaint") {
|
if (settings.mode === "outpaint") {
|
||||||
const padding = settings.outpaint;
|
const padding = settings.outpaint;
|
||||||
if (padding.left + padding.top + padding.right + padding.bottom <= 0) {
|
if (padding.left + padding.top + padding.right + padding.bottom <= 0) {
|
||||||
return missing("Add outpaint padding on at least one side.");
|
return missing("Add outpaint padding on at least one side.", "set-outpaint-padding");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.mode !== "inpaint") return { ready: true };
|
if (settings.mode !== "inpaint") return { ready: true };
|
||||||
|
|
||||||
const layerMask = getLayerMask(layerInfo.layer);
|
const layerMask = getLayerMask(layerInfo.layer);
|
||||||
if (!layerMask?.enabled) return missing("Add and enable a layer mask on the selected image before inpainting.");
|
if (!layerMask?.enabled) return missing("Paint a mask over the area you want AI to replace.", "add-mask");
|
||||||
const maskLayer = index.layerById.get(layerMask.maskLayerId);
|
const maskLayer = index.layerById.get(layerMask.maskLayerId);
|
||||||
if (!maskLayer || maskLayer.type === "group") return missing("The selected layer mask is missing.");
|
if (!maskLayer || maskLayer.type === "group") return missing("The selected layer mask is missing.");
|
||||||
const maskAsset = index.assetById.get(maskLayer.assetId);
|
const maskAsset = index.assetById.get(maskLayer.assetId);
|
||||||
@@ -69,8 +69,8 @@ function modeSelectionMessage(mode: GenerateSettings["mode"]): string {
|
|||||||
return "Select exactly one image or raster layer to outpaint.";
|
return "Select exactly one image or raster layer to outpaint.";
|
||||||
}
|
}
|
||||||
|
|
||||||
function missing(message: string): GenerationPrecondition {
|
function missing(message: string, repair?: Extract<GenerationPrecondition, { ready: false }>["repair"]): GenerationPrecondition {
|
||||||
return { ready: false, message };
|
return { ready: false, message, ...(repair ? { repair } : {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function rectsAligned(a: { x: number; y: number; w: number; h: number }, b: { x: number; y: number; w: number; h: number }): boolean {
|
function rectsAligned(a: { x: number; y: number; w: number; h: number }, b: { x: number; y: number; w: number; h: number }): boolean {
|
||||||
|
|||||||
@@ -40,6 +40,22 @@ describe("generation workflow", () => {
|
|||||||
expect(app.store.getState().editor.generation.candidates).toHaveLength(0);
|
expect(app.store.getState().editor.generation.candidates).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("repairs replace prerequisites by adding an aligned editable mask through commands", async () => {
|
||||||
|
const app = createTestApp();
|
||||||
|
const state = app.store.getState();
|
||||||
|
state.document.assets.push({ id: "source-asset", name: "Source", mimeType: "image/png", source: "source", intrinsicSize: { w: 80, h: 60 } });
|
||||||
|
state.document.artboards[0]!.layers.push({ id: "source", type: "raster", name: "Source", visible: true, locked: false, opacity: 1, assetId: "source-asset", transform: { position: { x: 4, y: 8 }, scale: { x: 2, y: 2 }, rotation: 0 } });
|
||||||
|
state.editor.selection = { artboardId: "artboard", layerIds: ["source"] };
|
||||||
|
const ids = ["mask-asset", "mask-layer"];
|
||||||
|
const workflow = createGenerationWorkflow(app.store, dependencies({ createId: () => ids.shift() ?? "unused" }));
|
||||||
|
|
||||||
|
await workflow.prepareInpaintMask();
|
||||||
|
|
||||||
|
const next = app.store.getState();
|
||||||
|
expect(next.document.artboards[0]?.layers[0]).toMatchObject({ id: "mask-layer", transform: { position: { x: 4, y: 8 }, scale: { x: 2, y: 2 }, rotation: 0 } });
|
||||||
|
expect(next.editor.maskEdit).toEqual({ targetLayerId: "source", maskLayerId: "mask-layer" });
|
||||||
|
});
|
||||||
|
|
||||||
test("cancels the active operation and records a cancelled job", async () => {
|
test("cancels the active operation and records a cancelled job", async () => {
|
||||||
const app = createTestApp();
|
const app = createTestApp();
|
||||||
let receivedSignal: AbortSignal | undefined;
|
let receivedSignal: AbortSignal | undefined;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { commandIds } from "@commands/ids";
|
import { commandIds } from "@commands/ids";
|
||||||
|
import type { Layer } from "@core/layer";
|
||||||
import type { GenerationCandidate, GenerationJobKind } from "@editor/state";
|
import type { GenerationCandidate, GenerationJobKind } from "@editor/state";
|
||||||
import type { AppStore } from "@editor/store";
|
import type { AppStore } from "@editor/store";
|
||||||
import type { GenerateSettings } from "@editor/tools";
|
import type { GenerateSettings } from "@editor/tools";
|
||||||
@@ -50,6 +51,34 @@ export function createGenerationWorkflow(store: AppStore, dependencies: Generati
|
|||||||
|
|
||||||
loadResources: () => dependencies.loadGenerationResources(store),
|
loadResources: () => dependencies.loadGenerationResources(store),
|
||||||
|
|
||||||
|
prepareInpaintMask: async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const layerId = state.editor.selection.layerIds.length === 1 ? state.editor.selection.layerIds[0] : undefined;
|
||||||
|
if (!layerId) return;
|
||||||
|
const artboard = state.document.artboards.find((candidate) => candidate.id === state.editor.selection.artboardId);
|
||||||
|
const layer = artboard ? findLayer(artboard.layers, layerId) : undefined;
|
||||||
|
if (!layer || layer.type === "group") return;
|
||||||
|
const asset = state.document.assets.find((candidate) => candidate.id === layer.assetId);
|
||||||
|
if (!asset) return;
|
||||||
|
const source = await dependencies.createRefinementMask(asset.intrinsicSize.w, asset.intrinsicSize.h);
|
||||||
|
const maskAssetId = dependencies.createId();
|
||||||
|
const maskLayerId = dependencies.createId();
|
||||||
|
store.dispatch(commandIds.documentAddLayerMask, {
|
||||||
|
layerId,
|
||||||
|
asset: { id: maskAssetId, name: `${layer.name} AI edit mask`, mimeType: "image/png", source, intrinsicSize: { ...asset.intrinsicSize } },
|
||||||
|
maskLayer: {
|
||||||
|
id: maskLayerId,
|
||||||
|
type: "raster",
|
||||||
|
name: `${layer.name} AI edit mask`,
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
opacity: 1,
|
||||||
|
assetId: maskAssetId,
|
||||||
|
transform: { position: { ...layer.transform.position }, scale: { ...layer.transform.scale }, rotation: layer.transform.rotation },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
generate: () => job("generate", "Generating", async (signal) => {
|
generate: () => job("generate", "Generating", async (signal) => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
await dependencies.runGenerate({
|
await dependencies.runGenerate({
|
||||||
@@ -130,6 +159,17 @@ export function createGenerationWorkflow(store: AppStore, dependencies: Generati
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findLayer(layers: readonly Layer[], layerId: string): Layer | undefined {
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.id === layerId) return layer;
|
||||||
|
if (layer.type === "group") {
|
||||||
|
const child = findLayer(layer.children, layerId);
|
||||||
|
if (child) return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function findCandidate(store: AppStore, candidateId: string): GenerationCandidate {
|
function findCandidate(store: AppStore, candidateId: string): GenerationCandidate {
|
||||||
const candidate = store.getState().editor.generation.candidates.find((item) => item.id === candidateId);
|
const candidate = store.getState().editor.generation.candidates.find((item) => item.id === candidateId);
|
||||||
if (!candidate) throw new Error("This generation candidate is no longer available.");
|
if (!candidate) throw new Error("This generation candidate is no longer available.");
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
|||||||
const precondition = workflow.precondition();
|
const precondition = workflow.precondition();
|
||||||
const canGenerate = precondition.ready && !busy;
|
const canGenerate = precondition.ready && !busy;
|
||||||
const preconditionMessage = precondition.ready ? undefined : precondition.message;
|
const preconditionMessage = precondition.ready ? undefined : precondition.message;
|
||||||
|
const repair = precondition.ready ? undefined : precondition.repair;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-w-[calc(100vw-2rem)] flex-wrap items-center justify-center gap-2 px-2">
|
<div className="flex max-w-[calc(100vw-2rem)] flex-wrap items-center justify-center gap-2 px-2">
|
||||||
@@ -34,7 +35,16 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
|||||||
>
|
>
|
||||||
{busy && job?.kind === "generate" ? "Generating..." : "Generate"}
|
{busy && job?.kind === "generate" ? "Generating..." : "Generate"}
|
||||||
</button>
|
</button>
|
||||||
{preconditionMessage ? <span className="max-w-72 text-xs text-white/55" role="status">{preconditionMessage}</span> : null}
|
{preconditionMessage ? (
|
||||||
|
<span className="flex max-w-80 items-center gap-2 rounded-full bg-amber-300/10 px-3 py-2 text-xs text-amber-100/75" role="status">
|
||||||
|
<span>{preconditionMessage}</span>
|
||||||
|
{repair === "add-mask" ? (
|
||||||
|
<button type="button" className="shrink-0 rounded-full bg-white px-3 py-1 font-semibold text-black" onClick={() => void workflow.prepareInpaintMask()}>Add mask</button>
|
||||||
|
) : repair === "set-outpaint-padding" ? (
|
||||||
|
<button type="button" className="shrink-0 rounded-full bg-white px-3 py-1 font-semibold text-black" onClick={() => dispatch(commandIds.toolSetGenerateSettings, { outpaint: { ...settings.outpaint, left: 128, right: 128 } })}>Add padding</button>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<GenerationJobStatus generation={generation} />
|
<GenerationJobStatus generation={generation} />
|
||||||
{busy ? <button type="button" className="h-9 rounded-full bg-white/5 px-3 text-xs font-semibold text-white/70 hover:bg-white/10 hover:text-white" onClick={workflow.cancel}>Cancel</button> : null}
|
{busy ? <button type="button" className="h-9 rounded-full bg-white/5 px-3 text-xs font-semibold text-white/70 hover:bg-white/10 hover:text-white" onClick={workflow.cancel}>Cancel</button> : null}
|
||||||
{candidate ? (
|
{candidate ? (
|
||||||
@@ -56,25 +66,26 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
|||||||
|
|
||||||
function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) {
|
function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) {
|
||||||
return (
|
return (
|
||||||
<div className="subtle-scrollbar flex max-w-[min(28rem,calc(100vw-2rem))] items-center gap-1 overflow-x-auto rounded-full bg-white/[0.04] px-1.5 py-1 ring-1 ring-white/[0.05]" role="group" aria-label="Generation candidates">
|
<div className="subtle-scrollbar flex max-w-[min(34rem,calc(100vw-2rem))] items-stretch gap-2 overflow-x-auto rounded-[1.25rem] bg-black/20 p-2 ring-1 ring-white/[0.08]" role="group" aria-label="Provisional generation candidates">
|
||||||
{generation.candidates.map((candidate) => {
|
{generation.candidates.map((candidate) => {
|
||||||
const selected = candidate.id === (generation.selectedCandidateId ?? generation.candidates[0]?.id);
|
const selected = candidate.id === (generation.selectedCandidateId ?? generation.candidates[0]?.id);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={candidate.id}
|
key={candidate.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`h-9 w-9 overflow-hidden rounded-full ring-2 transition ${selected ? "ring-white" : "ring-white/10 hover:ring-white/40"}`}
|
className={`relative h-16 w-16 shrink-0 overflow-hidden rounded-xl ring-2 transition ${selected ? "ring-white" : "ring-white/10 hover:ring-white/40"}`}
|
||||||
title={`Candidate seed ${candidate.seed}`}
|
title={`Provisional candidate · seed ${candidate.seed}`}
|
||||||
onClick={() => dispatch(commandIds.generationSelectCandidate, { candidateId: candidate.id })}
|
onClick={() => dispatch(commandIds.generationSelectCandidate, { candidateId: candidate.id })}
|
||||||
>
|
>
|
||||||
<img src={candidate.source} alt="" className="h-full w-full object-cover" />
|
<img src={candidate.source} alt="" className="h-full w-full object-cover" />
|
||||||
|
{selected ? <span className="absolute inset-x-1 bottom-1 rounded-full bg-black/70 px-1 py-0.5 text-[0.6rem] font-semibold text-white">Reviewing</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={generation.candidates.length === 0}
|
disabled={generation.candidates.length === 0}
|
||||||
className="h-9 shrink-0 rounded-full px-3 text-xs font-semibold text-white/55 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-35"
|
className="min-h-16 shrink-0 rounded-xl px-3 text-xs font-semibold text-white/55 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-35"
|
||||||
title="Dismiss every candidate and reset comparison"
|
title="Dismiss every candidate and reset comparison"
|
||||||
onClick={() => dispatch(commandIds.generationClearCandidates, undefined)}
|
onClick={() => dispatch(commandIds.generationClearCandidates, undefined)}
|
||||||
>
|
>
|
||||||
@@ -105,11 +116,9 @@ function CandidateControls({
|
|||||||
const disabled = busy;
|
const disabled = busy;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-1 rounded-full bg-white/[0.04] px-2 py-1 ring-1 ring-white/[0.05]">
|
<div className="flex flex-wrap items-center justify-center gap-1 rounded-[1.25rem] bg-white/[0.04] px-2 py-2 ring-1 ring-white/[0.05]">
|
||||||
<CandidatePreview candidate={candidate} />
|
<CandidatePreview candidate={candidate} />
|
||||||
<span className="px-2 text-xs font-medium text-white/55" title={`${candidate.settings.model} · ${candidate.mode}`}>
|
<span className="px-2 text-xs font-medium text-white/55" title={`${candidate.settings.model} · ${candidate.mode}`}><strong className="block font-semibold text-white/75">Provisional result</strong>Seed {candidate.seed}</span>
|
||||||
Seed {candidate.seed}
|
|
||||||
</span>
|
|
||||||
<CandidateCompareControls compareMode={compareMode} disabled={disabled} dispatch={dispatch} />
|
<CandidateCompareControls compareMode={compareMode} disabled={disabled} dispatch={dispatch} />
|
||||||
<CandidateButton disabled={disabled} label="Regenerate" title="Regenerate same mask and crop" onClick={() => rerun("Regenerate", candidate.settings)} />
|
<CandidateButton disabled={disabled} label="Regenerate" title="Regenerate same mask and crop" onClick={() => rerun("Regenerate", candidate.settings)} />
|
||||||
<CandidateButton
|
<CandidateButton
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CaretDown, CaretUp } from "@phosphor-icons/react";
|
|||||||
import { commandIds } from "@commands/ids";
|
import { commandIds } from "@commands/ids";
|
||||||
import type { AppStore } from "@editor/store";
|
import type { AppStore } from "@editor/store";
|
||||||
import type { GenerationResourcesState } from "@editor/state";
|
import type { GenerationResourcesState } from "@editor/state";
|
||||||
import type { GenerateArchitecture, GenerateMode, GenerateSettings } from "@editor/tools";
|
import type { GenerateArchitecture, GenerateIntent, GenerateMode, GenerateSettings } from "@editor/tools";
|
||||||
import { resolveGenerationModeOptions, resolveGenerationModelOptions, resolveGenerationStringOptions, resolveGenerationSupportOptions } from "@operations/generation/options";
|
import { resolveGenerationModeOptions, resolveGenerationModelOptions, resolveGenerationStringOptions, resolveGenerationSupportOptions } from "@operations/generation/options";
|
||||||
import { BottomControlSelectMenu, type BottomControlSelectOption } from "./SelectMenu";
|
import { BottomControlSelectMenu, type BottomControlSelectOption } from "./SelectMenu";
|
||||||
import { BottomControlSlider } from "./Slider";
|
import { BottomControlSlider } from "./Slider";
|
||||||
@@ -74,6 +74,31 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
|||||||
<div className="grid gap-5 pb-2 tabular-nums">
|
<div className="grid gap-5 pb-2 tabular-nums">
|
||||||
{resources.error ? <p className="rounded-full bg-red-500/10 px-3 py-2 text-xs text-red-200">{resources.error}</p> : null}
|
{resources.error ? <p className="rounded-full bg-red-500/10 px-3 py-2 text-xs text-red-200">{resources.error}</p> : null}
|
||||||
|
|
||||||
|
<section className={panelSectionClass()}>
|
||||||
|
<div className="px-1">
|
||||||
|
<h2 className="text-base font-semibold text-white/90">Edit with AI</h2>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-white/45">Choose an outcome. Image Studio configures the matching generation workflow.</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{intentOptions.map((intent) => (
|
||||||
|
<button
|
||||||
|
key={intent.value}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={settings.mode === intent.mode}
|
||||||
|
className={`rounded-[1.25rem] px-3 py-3 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30 ${settings.mode === intent.mode ? "bg-white text-black" : "bg-white/[0.05] text-white/75 hover:bg-white/10"}`}
|
||||||
|
onClick={() => dispatch(commandIds.toolChooseGenerateIntent, { intent: intent.value })}
|
||||||
|
>
|
||||||
|
<span className="block text-sm font-semibold">{intent.label}</span>
|
||||||
|
<span className={`mt-1 block text-xs leading-4 ${settings.mode === intent.mode ? "text-black/55" : "text-white/40"}`}>{intent.description}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button type="button" disabled className="rounded-[1.25rem] bg-white/[0.025] px-3 py-3 text-left text-white/30" title="Dedicated object removal is not supported by the current backend. Use Replace and describe the desired background.">
|
||||||
|
<span className="block text-sm font-semibold">Remove</span>
|
||||||
|
<span className="mt-1 block text-xs leading-4">Use Replace for now; describe what should fill the area.</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className={panelSectionClass()}>
|
<section className={panelSectionClass()}>
|
||||||
<label className="grid gap-2">
|
<label className="grid gap-2">
|
||||||
<span className={panelLabelClass()}>Prompt</span>
|
<span className={panelLabelClass()}>Prompt</span>
|
||||||
@@ -101,6 +126,26 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
|||||||
|
|
||||||
<section className={panelSectionClass()}>
|
<section className={panelSectionClass()}>
|
||||||
<SectionTitle title="Essentials" />
|
<SectionTitle title="Essentials" />
|
||||||
|
<SizeControl refRoot={sizeRef} open={sizeOpen} setOpen={setSizeOpen} settings={settings} dispatch={dispatch} />
|
||||||
|
{settings.mode === "image-to-image" || settings.mode === "inpaint" ? (
|
||||||
|
<div className={panelRowClass()}>
|
||||||
|
<span className={panelLabelClass()}>Influence</span>
|
||||||
|
<BottomControlSlider min={0} max={100} value={settings.strength} className="w-32" aria-label="Generate strength" onValueChange={(strength) => dispatch(commandIds.toolSetGenerateSettings, { strength })} />
|
||||||
|
<span className="w-10 text-right text-sm text-white">{Math.round(settings.strength)}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={panelSectionClass()}>
|
||||||
|
<button type="button" className={sectionToggleClass()} aria-expanded={advancedOpen} aria-controls="generate-advanced-controls" onClick={() => setAdvancedOpen((open) => !open)}>
|
||||||
|
<span>
|
||||||
|
<span className="block text-sm font-semibold text-white/85">Advanced</span>
|
||||||
|
<span className="block text-xs text-white/40">Model, backend, seed, sampling, and workflow internals</span>
|
||||||
|
</span>
|
||||||
|
{advancedOpen ? <CaretUp size={18} weight="bold" /> : <CaretDown size={18} weight="bold" />}
|
||||||
|
</button>
|
||||||
|
<div id="generate-advanced-controls" className={`grid gap-2 overflow-hidden transition-all duration-200 ${advancedOpen ? "max-h-[32rem] pt-2 opacity-100" : "max-h-0 opacity-0"}`}>
|
||||||
|
<PanelSelect label="Mode" value={settings.mode} options={modeOptions} ariaLabel="Generate mode" onValueChange={(mode) => dispatch(commandIds.toolSetGenerateSettings, { mode })} />
|
||||||
<PanelSelect label="Backend" value={settings.architecture} options={architectures} ariaLabel="Generate backend" onValueChange={(architecture) => dispatch(commandIds.toolSetGenerateSettings, { architecture })} />
|
<PanelSelect label="Backend" value={settings.architecture} options={architectures} ariaLabel="Generate backend" onValueChange={(architecture) => dispatch(commandIds.toolSetGenerateSettings, { architecture })} />
|
||||||
<PanelSelect label="Model" value={settings.model} options={modelOptions} ariaLabel="Generate model" onValueChange={(model) => dispatch(commandIds.toolSetGenerateSettings, { model })} />
|
<PanelSelect label="Model" value={settings.model} options={modelOptions} ariaLabel="Generate model" onValueChange={(model) => dispatch(commandIds.toolSetGenerateSettings, { model })} />
|
||||||
{settings.architecture !== "sdxl" ? (
|
{settings.architecture !== "sdxl" ? (
|
||||||
@@ -109,35 +154,17 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
|||||||
<PanelSelect label="VAE" value={settings.vae} options={supportOptions.vaes} ariaLabel="Generate VAE" onValueChange={(vae) => dispatch(commandIds.toolSetGenerateSettings, { vae })} />
|
<PanelSelect label="VAE" value={settings.vae} options={supportOptions.vaes} ariaLabel="Generate VAE" onValueChange={(vae) => dispatch(commandIds.toolSetGenerateSettings, { vae })} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<SizeControl refRoot={sizeRef} open={sizeOpen} setOpen={setSizeOpen} settings={settings} dispatch={dispatch} />
|
|
||||||
<PanelNumber label="Seed" aria-label="Generate seed" min={-1} max={Number.MAX_SAFE_INTEGER} value={settings.seed} onValueChange={(seed) => dispatch(commandIds.toolSetGenerateSettings, { seed })} />
|
<PanelNumber label="Seed" aria-label="Generate seed" min={-1} max={Number.MAX_SAFE_INTEGER} value={settings.seed} onValueChange={(seed) => dispatch(commandIds.toolSetGenerateSettings, { seed })} />
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className={panelSectionClass()}>
|
|
||||||
<button type="button" className={sectionToggleClass()} aria-expanded={advancedOpen} aria-controls="generate-advanced-controls" onClick={() => setAdvancedOpen((open) => !open)}>
|
|
||||||
<span>
|
|
||||||
<span className="block text-sm font-semibold text-white/85">Advanced</span>
|
|
||||||
<span className="block text-xs text-white/40">Mode, sampler, scheduler, steps, CFG, strength</span>
|
|
||||||
</span>
|
|
||||||
{advancedOpen ? <CaretUp size={18} weight="bold" /> : <CaretDown size={18} weight="bold" />}
|
|
||||||
</button>
|
|
||||||
<div id="generate-advanced-controls" className={`grid gap-2 overflow-hidden transition-all duration-200 ${advancedOpen ? "max-h-[32rem] pt-2 opacity-100" : "max-h-0 opacity-0"}`}>
|
|
||||||
<PanelSelect label="Mode" value={settings.mode} options={modeOptions} ariaLabel="Generate mode" onValueChange={(mode) => dispatch(commandIds.toolSetGenerateSettings, { mode })} />
|
|
||||||
<PanelSelect label="Sampler" value={settings.sampler} options={samplerOptions} ariaLabel="Generate sampler" onValueChange={(sampler) => dispatch(commandIds.toolSetGenerateSettings, { sampler })} />
|
<PanelSelect label="Sampler" value={settings.sampler} options={samplerOptions} ariaLabel="Generate sampler" onValueChange={(sampler) => dispatch(commandIds.toolSetGenerateSettings, { sampler })} />
|
||||||
<PanelSelect label="Scheduler" value={settings.scheduler} options={schedulerOptions} ariaLabel="Generate scheduler" onValueChange={(scheduler) => dispatch(commandIds.toolSetGenerateSettings, { scheduler })} />
|
<PanelSelect label="Scheduler" value={settings.scheduler} options={schedulerOptions} ariaLabel="Generate scheduler" onValueChange={(scheduler) => dispatch(commandIds.toolSetGenerateSettings, { scheduler })} />
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<PanelNumber label="Steps" aria-label="Generate steps" value={settings.steps} onValueChange={(steps) => dispatch(commandIds.toolSetGenerateSettings, { steps })} />
|
<PanelNumber label="Steps" aria-label="Generate steps" value={settings.steps} onValueChange={(steps) => dispatch(commandIds.toolSetGenerateSettings, { steps })} />
|
||||||
<PanelNumber label="CFG" aria-label="Generate CFG" value={settings.cfg} onValueChange={(cfg) => dispatch(commandIds.toolSetGenerateSettings, { cfg })} />
|
<PanelNumber label="CFG" aria-label="Generate CFG" value={settings.cfg} onValueChange={(cfg) => dispatch(commandIds.toolSetGenerateSettings, { cfg })} />
|
||||||
</div>
|
</div>
|
||||||
<div className={panelRowClass()}>
|
|
||||||
<span className={panelLabelClass()}>Strength</span>
|
|
||||||
<BottomControlSlider min={0} max={100} value={settings.strength} className="w-32" aria-label="Generate strength" onValueChange={(strength) => dispatch(commandIds.toolSetGenerateSettings, { strength })} />
|
|
||||||
<span className="w-10 text-right text-sm text-white">{Math.round(settings.strength)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={panelSectionClass()}>
|
{settings.mode === "outpaint" ? <section className={panelSectionClass()}>
|
||||||
<button type="button" className={sectionToggleClass()} aria-expanded={outpaintOpen} aria-controls="generate-outpaint-controls" onClick={() => setOutpaintOpen((open) => !open)}>
|
<button type="button" className={sectionToggleClass()} aria-expanded={outpaintOpen} aria-controls="generate-outpaint-controls" onClick={() => setOutpaintOpen((open) => !open)}>
|
||||||
<span>
|
<span>
|
||||||
<span className="block text-sm font-semibold text-white/85">Outpaint</span>
|
<span className="block text-sm font-semibold text-white/85">Outpaint</span>
|
||||||
@@ -154,9 +181,9 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
|||||||
</div>
|
</div>
|
||||||
<PanelNumber label="Feather" aria-label="Outpaint feathering" value={settings.outpaint.feathering} onValueChange={(feathering) => dispatch(commandIds.toolSetGenerateSettings, { outpaint: { ...settings.outpaint, feathering } })} />
|
<PanelNumber label="Feather" aria-label="Outpaint feathering" value={settings.outpaint.feathering} onValueChange={(feathering) => dispatch(commandIds.toolSetGenerateSettings, { outpaint: { ...settings.outpaint, feathering } })} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section> : null}
|
||||||
|
|
||||||
<section className={panelSectionClass()}>
|
{settings.mode === "inpaint" ? <section className={panelSectionClass()}>
|
||||||
<button type="button" className={sectionToggleClass()} aria-expanded={inpaintOpen} aria-controls="generate-inpaint-controls" onClick={() => setInpaintOpen((open) => !open)}>
|
<button type="button" className={sectionToggleClass()} aria-expanded={inpaintOpen} aria-controls="generate-inpaint-controls" onClick={() => setInpaintOpen((open) => !open)}>
|
||||||
<span>
|
<span>
|
||||||
<span className="block text-sm font-semibold text-white/85">Inpaint</span>
|
<span className="block text-sm font-semibold text-white/85">Inpaint</span>
|
||||||
@@ -192,11 +219,18 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
|||||||
<PanelNumber label="Clean" aria-label="Inpaint mask despeckle" value={settings.inpaint.maskDespeckle} max={64} onValueChange={(maskDespeckle) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskDespeckle } })} />
|
<PanelNumber label="Clean" aria-label="Inpaint mask despeckle" value={settings.inpaint.maskDespeckle} max={64} onValueChange={(maskDespeckle) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskDespeckle } })} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intentOptions: ReadonlyArray<{ value: GenerateIntent; mode: GenerateMode; label: string; description: string }> = [
|
||||||
|
{ value: "create", mode: "text-to-image", label: "Create", description: "Make a new image from your prompt." },
|
||||||
|
{ value: "replace", mode: "inpaint", label: "Replace", description: "Regenerate the masked part of one layer." },
|
||||||
|
{ value: "extend", mode: "outpaint", label: "Extend", description: "Grow one selected image beyond its edges." },
|
||||||
|
{ value: "variations", mode: "image-to-image", label: "Variations", description: "Explore alternatives based on one image." },
|
||||||
|
];
|
||||||
|
|
||||||
function SectionTitle({ title }: { title: string }) {
|
function SectionTitle({ title }: { title: string }) {
|
||||||
return <div className="px-1 text-xs font-semibold uppercase tracking-[0.18em] text-white/35">{title}</div>;
|
return <div className="px-1 text-xs font-semibold uppercase tracking-[0.18em] text-white/35">{title}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user