import { useEffect, useState } from "react"; import { commandIds } from "@commands/ids"; import type { ImageDocument } from "@core/document"; import type { GenerationCandidate, GenerationState, SelectionState, ViewportState } from "@editor/state"; import type { GenerateSettings } from "@editor/tools"; import type { AppStore } from "@editor/store"; import { createMaskedPixelReplacementSource } from "../generate/candidateActions"; import { runGenerate, runGenerateFromCandidate } from "../generate/runGenerate"; import { createSolidMaskSource } from "../mask/maskRaster"; 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 [busy, setBusy] = useState(); const [error, setError] = useState(); const [elapsedSeconds, setElapsedSeconds] = useState(0); const candidate = selectedCandidate(generation); const canGenerate = Boolean(settings.prompt.trim()) && !busy; useEffect(() => { if (!busy) { setElapsedSeconds(0); return; } setElapsedSeconds(0); const startedAt = Date.now(); const interval = window.setInterval(() => { setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000)); }, 1000); return () => window.clearInterval(interval); }, [busy]); return (
{busy ? {busy} {formatElapsed(elapsedSeconds)} : null} {candidate ? ( <> ) : null} {error ? {error} : null}
); } function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) { if (generation.candidates.length < 2) return null; return (
{generation.candidates.slice(0, 6).map((candidate) => { const selected = candidate.id === (generation.selectedCandidateId ?? generation.candidates[0]?.id); return ( ); })}
); } function CandidateControls({ document, candidate, settings, busy, setBusy, setError, dispatch, }: { document: ImageDocument; candidate: GenerationCandidate; settings: GenerateSettings; busy?: string; setBusy: (busy: string | undefined) => void; setError: (error: string | undefined) => void; dispatch: AppStore["dispatch"]; }) { const rerun = (label: string, nextSettings: GenerateSettings) => { setBusy(label); setError(undefined); dispatch(commandIds.toolSetGenerateSettings, nextSettings); void runGenerateFromCandidate({ candidate, settings: nextSettings, dispatch }) .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : `${label} failed`)) .finally(() => setBusy(undefined)); }; const disabled = Boolean(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, false, dispatch)} /> { setBusy("Refine"); setError(undefined); void applyCandidateAsRefinementLayer(candidate, dispatch) .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : "Refine setup failed")) .finally(() => setBusy(undefined)); }} /> applyCandidateAsLayer(candidate, true, dispatch)} /> { setBusy("Replace"); setError(undefined); void createMaskedPixelReplacementSource(document, candidate) .then((source) => dispatch(commandIds.generationReplaceCandidatePixels, { candidateId: candidate.id, source, mimeType: "image/png" })) .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : "Replace failed")) .finally(() => setBusy(undefined)); }} /> { 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 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, variant: boolean, dispatch: AppStore["dispatch"]) { applyCandidateAsLayerWithIds(candidate, { layerId: crypto.randomUUID(), assetId: crypto.randomUUID() }, variant, 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() }, true, dispatch); const width = Math.max(1, Math.round(candidate.intrinsicSize.w)); const height = Math.max(1, Math.round(candidate.intrinsicSize.h)); const source = await createSolidMaskSource(width, height, "white"); 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 }, variant: boolean, dispatch: AppStore["dispatch"]) { dispatch(commandIds.generationApplyCandidateAsLayer, { candidateId: candidate.id, assetId: ids.assetId, layerId: ids.layerId, variant, }); } function formatElapsed(seconds: number) { const minutes = Math.floor(seconds / 60); const remainder = seconds % 60; return `${minutes}:${remainder.toString().padStart(2, "0")}`; }