import { commandIds } from "@commands/ids"; import type { GenerationCandidate, GenerationJobKind } from "@editor/state"; import type { AppStore } from "@editor/store"; import type { GenerateSettings } from "@editor/tools"; import { createRefinementMask } from "@operations/masks/rasterActions"; import { createMaskedPixelReplacementSource } from "./candidateActions"; import { runGenerationJob } from "./generationJob"; import { loadGenerationResources } from "./loadResources"; import { runGenerate, runGenerateFromCandidate } from "./runGenerate"; import { checkGenerationPreconditions } from "./preconditions"; export type GenerationWorkflow = ReturnType; export type GenerationWorkflowDependencies = { runGenerate: typeof runGenerate; runGenerateFromCandidate: typeof runGenerateFromCandidate; createMaskedPixelReplacementSource: typeof createMaskedPixelReplacementSource; createRefinementMask: typeof createRefinementMask; loadGenerationResources: typeof loadGenerationResources; createId(): string; }; const defaultDependencies: GenerationWorkflowDependencies = { runGenerate, runGenerateFromCandidate, createMaskedPixelReplacementSource, createRefinementMask, loadGenerationResources, createId: () => crypto.randomUUID(), }; export function createGenerationWorkflow(store: AppStore, dependencies: GenerationWorkflowDependencies = defaultDependencies) { const job = (kind: GenerationJobKind, label: string, task: () => Promise) => runGenerationJob({ kind, label, dispatch: store.dispatch, task }); return { precondition: () => { const state = store.getState(); return checkGenerationPreconditions(state.document, state.editor.selection, state.editor.tools.generate); }, loadResources: () => dependencies.loadGenerationResources(store), generate: () => job("generate", "Generating", async () => { const state = store.getState(); await dependencies.runGenerate({ document: state.document, selection: state.editor.selection, viewport: state.editor.viewport, settings: state.editor.tools.generate, dispatch: store.dispatch, }); }), regenerate: (candidateId: string, settings?: GenerateSettings, label = "Regenerate") => job("regenerate", label, async () => { const candidate = findCandidate(store, candidateId); const nextSettings = settings ?? candidate.settings; store.dispatch(commandIds.toolSetGenerateSettings, nextSettings); await dependencies.runGenerateFromCandidate({ candidate, settings: nextSettings, dispatch: store.dispatch }); }), applyCandidateAsLayer: (candidateId: string) => { store.dispatch(commandIds.generationApplyCandidateAsLayer, { candidateId, assetId: dependencies.createId(), layerId: dependencies.createId(), }); }, applyCandidateAsRefinementLayer: (candidateId: string) => job("refine", "Adding refinement mask", async () => { const candidate = findCandidate(store, candidateId); const layerId = dependencies.createId(); const maskAssetId = dependencies.createId(); const width = Math.max(1, Math.round(candidate.intrinsicSize.w)); const height = Math.max(1, Math.round(candidate.intrinsicSize.h)); const source = await dependencies.createRefinementMask(width, height); store.dispatch(commandIds.generationApplyCandidateAsLayer, { candidateId, assetId: dependencies.createId(), layerId, }); store.dispatch(commandIds.documentAddLayerMask, { layerId, asset: { id: maskAssetId, name: `${candidate.placement.layerName} refinement mask`, mimeType: "image/png", source, intrinsicSize: { w: width, h: height }, }, maskLayer: { id: dependencies.createId(), 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, }, }, }); store.dispatch(commandIds.toolSetActive, { tool: "eraser" }); }), replaceCandidatePixels: (candidateId: string) => job("replace", "Replacing pixels", async () => { const candidate = findCandidate(store, candidateId); const source = await dependencies.createMaskedPixelReplacementSource(store.getState().document, candidate); store.dispatch(commandIds.generationReplaceCandidatePixels, { candidateId, source, mimeType: "image/png" }); }), }; } function findCandidate(store: AppStore, candidateId: string): GenerationCandidate { const candidate = store.getState().editor.generation.candidates.find((item) => item.id === candidateId); if (!candidate) throw new Error("This generation candidate is no longer available."); return candidate; }