Files
image-studio/editor/tools.ts
syntaxbullet 38565382c3 feat: add feather brush tool with adjustable settings and blending functionality
- Implemented feather brush tool in the brushRaster module, allowing for feathered edges in brush strokes.
- Added new FeatherControls component for UI adjustments of feather settings including size, radius, strength, and smoothing.
- Updated brush preview logic to accommodate feather tool alongside existing brush and eraser tools.
- Enhanced layer rendering to support feather mask previews and interactions.
- Introduced blending logic for feathered strokes to mix blurred mask values with original pixels.
- Added unit tests for feather blending functionality and tool keybindings.
- Updated cursor handling to reflect feather tool usage.
2026-07-11 22:08:25 +02:00

174 lines
6.4 KiB
TypeScript

export const availableToolIds = ["select", "brush", "eraser", "feather", "magicWand", "semanticSelect", "maskLasso", "maskRectangle", "pan"] as const;
export const availableOperationIds = ["generate", "chromaKey"] as const;
export type ToolId = (typeof availableToolIds)[number];
export type OperationId = (typeof availableOperationIds)[number];
export type InteractionMode =
| { type: "tool"; tool: ToolId }
| { type: "temporary-pan"; previousTool: ToolId };
export type BrushSettings = {
color: string;
size: number;
hardness: number;
opacity: number;
flow: number;
smoothing: number;
pressureSize: boolean;
};
export type FeatherSettings = {
size: number;
radius: number;
strength: number;
smoothing: number;
pressureSize: boolean;
};
export type ChromaKeySettings = {
color: string;
tolerance: number;
softness: number;
feather: number;
choke: number;
despeckle: number;
spill: number;
};
export type MagicWandMode = "replace" | "add" | "subtract";
export type GenerateMode = "text-to-image" | "image-to-image" | "inpaint" | "outpaint";
export type GenerateIntent = "create" | "replace" | "remove" | "extend" | "variations";
export const generateArchitectures = ["sdxl", "z-image", "z-image-turbo", "anima"] as const;
export type GenerateArchitecture = (typeof generateArchitectures)[number];
export type GenerateModel = string;
export type InpaintMaskedContent = "neutral" | "original" | "originalColor" | "edges";
export type InpaintProfile = "remove" | "replace" | "repair" | "material" | "reshape" | "custom";
export type InpaintStructureControl = "none" | "canny" | "depth" | "pose";
export type GenerateSettings = {
architecture: GenerateArchitecture;
mode: GenerateMode;
model: GenerateModel;
textEncoder: string;
vae: string;
prompt: string;
negativePrompt: string;
strength: number;
steps: number;
cfg: number;
seed: number;
sampler: string;
scheduler: string;
width: number;
height: number;
batchSize: number;
refinePass: boolean;
refineStrength: number;
outpaint: {
left: number;
top: number;
right: number;
bottom: number;
feathering: number;
};
inpaint: {
profile: InpaintProfile;
maskedAreaOnly: boolean;
cropPadding: number;
maskPolarity: "hidden" | "revealed";
maskedContent: InpaintMaskedContent;
growMaskBy: number;
maskExpand: number;
maskFeather: number;
maskBlur: number;
maskDespeckle: number;
structureControl: InpaintStructureControl;
controlStrength: number;
controlModel: string;
colorMatch: boolean;
};
};
export const generateArchitectureDefaults: Record<GenerateArchitecture, {
model: GenerateModel;
textEncoder: string;
vae: string;
steps: number;
cfg: number;
sampler: string;
scheduler: string;
supportedModes: readonly GenerateMode[];
}> = {
sdxl: { model: "auto", textEncoder: "auto", vae: "auto", steps: 30, cfg: 7, sampler: "euler", scheduler: "normal", supportedModes: ["text-to-image", "image-to-image", "inpaint", "outpaint"] },
"z-image": { model: "auto", textEncoder: "qwen_3_4b.safetensors", vae: "ae.safetensors", steps: 30, cfg: 4, sampler: "res_multistep", scheduler: "simple", supportedModes: ["text-to-image"] },
"z-image-turbo": { model: "auto", textEncoder: "qwen_3_4b.safetensors", vae: "ae.safetensors", steps: 8, cfg: 1, sampler: "res_multistep", scheduler: "simple", supportedModes: ["text-to-image"] },
anima: { model: "auto", textEncoder: "qwen_3_06b_base.safetensors", vae: "qwen_image_vae.safetensors", steps: 30, cfg: 4, sampler: "er_sde", scheduler: "simple", supportedModes: ["text-to-image"] },
};
export type MagicWandSettings = {
tolerance: number;
feather: number;
choke: number;
despeckle: number;
contiguous: boolean;
mode: MagicWandMode;
};
export type ToolState = {
activeTool: ToolId;
interactionMode: InteractionMode;
brush: BrushSettings;
feather: FeatherSettings;
chromaKey: ChromaKeySettings;
magicWand: MagicWandSettings;
generate: GenerateSettings;
};
export const initialToolState: ToolState = {
activeTool: "select",
interactionMode: { type: "tool", tool: "select" },
brush: { color: "#111827", size: 8, hardness: 100, opacity: 100, flow: 100, smoothing: 20, pressureSize: true },
feather: { size: 96, radius: 16, strength: 65, smoothing: 25, pressureSize: true },
chromaKey: { color: "#00ff00", tolerance: 32, softness: 24, feather: 0, choke: 0, despeckle: 0, spill: 50 },
magicWand: { tolerance: 32, feather: 0, choke: 0, despeckle: 0, contiguous: true, mode: "replace" },
generate: {
architecture: "sdxl",
mode: "text-to-image",
model: "auto",
textEncoder: "auto",
vae: "auto",
prompt: "",
negativePrompt: "",
strength: 75,
steps: 30,
cfg: 7,
seed: -1,
sampler: "euler",
scheduler: "normal",
width: 1024,
height: 1024,
batchSize: 4,
refinePass: true,
refineStrength: 20,
outpaint: { left: 128, top: 128, right: 128, bottom: 128, feathering: 32 },
inpaint: { profile: "replace", maskedAreaOnly: true, cropPadding: 128, maskPolarity: "revealed", maskedContent: "neutral", growMaskBy: 6, maskExpand: 8, maskFeather: 3, maskBlur: 0, maskDespeckle: 0, structureControl: "none", controlStrength: 0.55, controlModel: "auto", colorMatch: true },
},
};
export const inpaintProfileDefaults: Record<Exclude<InpaintProfile, "custom">, Omit<Partial<GenerateSettings>, "inpaint"> & { inpaint: Partial<GenerateSettings["inpaint"]> }> = {
remove: { strength: 100, inpaint: { maskedContent: "neutral", cropPadding: 160, maskExpand: 12, maskFeather: 4, structureControl: "none" } },
replace: { strength: 85, inpaint: { maskedContent: "neutral", cropPadding: 128, maskExpand: 8, maskFeather: 3, structureControl: "none" } },
repair: { strength: 45, refinePass: true, refineStrength: 15, inpaint: { maskedContent: "original", cropPadding: 96, maskExpand: 4, maskFeather: 2, structureControl: "none" } },
material: { strength: 65, refinePass: true, refineStrength: 20, inpaint: { maskedContent: "edges", cropPadding: 128, maskExpand: 6, maskFeather: 3, structureControl: "canny", controlStrength: 0.55, colorMatch: false } },
reshape: { strength: 90, inpaint: { maskedContent: "neutral", cropPadding: 160, maskExpand: 10, maskFeather: 4, structureControl: "depth", controlStrength: 0.4 } },
};
export function isPanInteractionMode(interactionMode: InteractionMode): boolean {
return interactionMode.type === "temporary-pan" || (interactionMode.type === "tool" && interactionMode.tool === "pan");
}