import { commandIds } from "@commands/ids"; import type { ImageDocument } from "@core/document"; import type { GenerationCandidate, GenerationCompareMode, GenerationState, SelectionState, ViewportState } from "@editor/state"; import type { GenerateSettings } from "@editor/tools"; import type { AppStore } from "@editor/store"; import { createMaskedPixelReplacementSource } from "@operations/generation/candidateActions"; import { runGenerate, runGenerateFromCandidate } from "@operations/generation/runGenerate"; import { runGenerationJob } from "@operations/generation/generationJob"; import { currentGenerationJob, GenerationJobStatus } from "../GenerationJobStatus"; import { createRefinementMask } from "@operations/masks/rasterActions"; import { checkGenerationPreconditions } from "@operations/generation/preconditions"; export type GenerateActionControlsProps = { document: ImageDocument; selection: SelectionState; viewport: ViewportState; settings: GenerateSettings; generation: GenerationState; dispatch: AppStore["dispatch"]; }; export function GenerateActionControls({ document, selection, viewport, settings, generation, dispatch }: GenerateActionControlsProps) { const job = currentGenerationJob(generation); const busy = job?.status === "running"; const candidate = selectedCandidate(generation); const precondition = checkGenerationPreconditions(document, selection, settings); const canGenerate = precondition.ready && !busy; const preconditionMessage = precondition.ready ? undefined : precondition.message; return (
{preconditionMessage ? {preconditionMessage} : null} {candidate ? ( <> ) : null}
); } function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) { if (generation.candidates.length < 2) return null; return (
{generation.candidates.map((candidate) => { const selected = candidate.id === (generation.selectedCandidateId ?? generation.candidates[0]?.id); return ( ); })}
); } function CandidateControls({ document, candidate, compareMode, settings, busy, dispatch, }: { document: ImageDocument; candidate: GenerationCandidate; compareMode: GenerationCompareMode; settings: GenerateSettings; busy: boolean; dispatch: AppStore["dispatch"]; }) { const rerun = (label: string, nextSettings: GenerateSettings) => { dispatch(commandIds.toolSetGenerateSettings, nextSettings); void runGenerationJob({ kind: "regenerate", label, dispatch, task: () => runGenerateFromCandidate({ candidate, settings: nextSettings, dispatch }) }); }; const disabled = busy; return (
Seed {candidate.seed} rerun("Regenerate", candidate.settings)} /> rerun("Lower", { ...candidate.settings, strength: Math.max(0, candidate.settings.strength - 10), seed: candidate.seed })} /> rerun("Reuse seed", { ...candidate.settings, seed: candidate.seed })} /> rerun("New seed", { ...candidate.settings, seed: -1 })} /> applyCandidateAsLayer(candidate, dispatch)} /> { void runGenerationJob({ kind: "refine", label: "Adding refinement mask", dispatch, task: () => applyCandidateAsRefinementLayer(candidate, dispatch) }); }} /> { void runGenerationJob({ kind: "replace", label: "Replacing pixels", dispatch, task: async () => { const source = await createMaskedPixelReplacementSource(document, candidate); dispatch(commandIds.generationReplaceCandidatePixels, { candidateId: candidate.id, source, mimeType: "image/png" }); } }); }} /> { if (!candidate.inpaint) return; dispatch(commandIds.selectionSet, { artboardId: candidate.placement.artboardId, layerIds: [candidate.inpaint.targetLayerId] }); dispatch(commandIds.toolEnterMaskEdit, { targetLayerId: candidate.inpaint.targetLayerId, maskLayerId: candidate.inpaint.maskLayerId }); }} /> dispatch(commandIds.generationRemoveCandidate, { candidateId: candidate.id })} /> {settings.seed !== candidate.seed ? null : Current settings reuse this seed}
); } function CandidateCompareControls({ compareMode, disabled, dispatch }: { compareMode: GenerationCompareMode; disabled: boolean; dispatch: AppStore["dispatch"] }) { return ( {generationCompareOptions.map((option) => { const active = compareMode === option.mode; return ( ); })} ); } const generationCompareOptions: Array<{ mode: GenerationCompareMode; label: string; title: string }> = [ { mode: "result", label: "After", title: "Show the generated result over the document" }, { mode: "before", label: "Before", title: "Hide the generated result and show the source document" }, { mode: "split", label: "Split", title: "Compare source on the left with result on the right" }, ]; function CandidatePreview({ candidate }: { candidate: GenerationCandidate }) { if (!candidate.inputImage) { return ; } return ( {candidate.maskImage ? : null} ); } function CandidateButton({ label, title, disabled, busy, onClick }: { label: string; title: string; disabled?: boolean; busy?: boolean; onClick: () => void }) { return ( ); } function selectedCandidate(generation: GenerationState): GenerationCandidate | undefined { return generation.candidates.find((candidate) => candidate.id === generation.selectedCandidateId) ?? generation.candidates[0]; } function applyCandidateAsLayer(candidate: GenerationCandidate, dispatch: AppStore["dispatch"]) { applyCandidateAsLayerWithIds(candidate, { layerId: crypto.randomUUID(), assetId: crypto.randomUUID() }, dispatch); } async function applyCandidateAsRefinementLayer(candidate: GenerationCandidate, dispatch: AppStore["dispatch"]) { const layerId = crypto.randomUUID(); const maskLayerId = crypto.randomUUID(); const maskAssetId = crypto.randomUUID(); applyCandidateAsLayerWithIds(candidate, { layerId, assetId: crypto.randomUUID() }, dispatch); const width = Math.max(1, Math.round(candidate.intrinsicSize.w)); const height = Math.max(1, Math.round(candidate.intrinsicSize.h)); const source = await createRefinementMask(width, height); dispatch(commandIds.documentAddLayerMask, { layerId, asset: { id: maskAssetId, name: `${candidate.placement.layerName} refinement mask`, mimeType: "image/png", source, intrinsicSize: { w: width, h: height }, }, maskLayer: { id: maskLayerId, type: "raster", name: `${candidate.placement.layerName} refinement mask`, visible: true, locked: false, opacity: 1, assetId: maskAssetId, transform: { position: { ...candidate.placement.transform.position }, scale: { ...candidate.placement.transform.scale }, rotation: candidate.placement.transform.rotation, }, }, }); dispatch(commandIds.toolSetActive, { tool: "eraser" }); } function applyCandidateAsLayerWithIds(candidate: GenerationCandidate, ids: { layerId: string; assetId: string }, dispatch: AppStore["dispatch"]) { dispatch(commandIds.generationApplyCandidateAsLayer, { candidateId: candidate.id, assetId: ids.assetId, layerId: ids.layerId, }); }