Files
image-studio/operations/generation/preconditions.ts

79 lines
3.7 KiB
TypeScript

import type { ImageDocument } from "@core/document";
import { getLayerMask } from "@core/layer-mask-utils";
import { createDocumentReadIndex, resolveIndexedLayerBounds } from "@editor/document-indexes";
import type { SelectionState } from "@editor/state";
import type { GenerateSettings } from "@editor/tools";
export type GenerationPrecondition =
| { ready: true }
| { ready: false; message: string };
export function checkGenerationPreconditions(
document: ImageDocument,
selection: SelectionState,
settings: GenerateSettings,
): GenerationPrecondition {
if (!settings.prompt.trim()) return missing("Enter a prompt to generate an image.");
const artboard = selection.artboardId
? document.artboards.find((candidate) => candidate.id === selection.artboardId)
: document.artboards[0];
if (!artboard) return missing("Create an artboard before generating an image.");
if (settings.mode === "text-to-image") return { ready: true };
if (selection.layerIds.length !== 1 || !selection.layerIds[0]) {
return missing(modeSelectionMessage(settings.mode));
}
const index = createDocumentReadIndex(document);
const layerInfo = index.layerInfoById.get(selection.layerIds[0]);
if (!layerInfo || layerInfo.artboardId !== artboard.id || layerInfo.layer.type === "group") {
return missing(modeSelectionMessage(settings.mode));
}
const asset = index.assetById.get(layerInfo.layer.assetId);
if (!asset) return missing("The selected layer is missing its source image.");
if (asset.intrinsicSize.w <= 0 || asset.intrinsicSize.h <= 0) return missing("The selected image has an invalid size.");
if (settings.mode === "outpaint") {
const padding = settings.outpaint;
if (padding.left + padding.top + padding.right + padding.bottom <= 0) {
return missing("Add outpaint padding on at least one side.");
}
}
if (settings.mode !== "inpaint") return { ready: true };
const layerMask = getLayerMask(layerInfo.layer);
if (!layerMask?.enabled) return missing("Add and enable a layer mask on the selected image before inpainting.");
const maskLayer = index.layerById.get(layerMask.maskLayerId);
if (!maskLayer || maskLayer.type === "group") return missing("The selected layer mask is missing.");
const maskAsset = index.assetById.get(maskLayer.assetId);
if (!maskAsset) return missing("The selected layer mask is missing its image data.");
if (Math.round(asset.intrinsicSize.w) !== Math.round(maskAsset.intrinsicSize.w) || Math.round(asset.intrinsicSize.h) !== Math.round(maskAsset.intrinsicSize.h)) {
return missing("The selected layer and mask image sizes must match.");
}
const layerBounds = resolveIndexedLayerBounds(index, layerInfo.layer);
const maskBounds = resolveIndexedLayerBounds(index, maskLayer);
if (!layerBounds || !maskBounds || !rectsAligned(layerBounds, maskBounds) || Math.abs(layerInfo.layer.transform.rotation - maskLayer.transform.rotation) > 0.001) {
return missing("Align the selected layer and its mask before inpainting.");
}
return { ready: true };
}
function modeSelectionMessage(mode: GenerateSettings["mode"]): string {
if (mode === "image-to-image") return "Select exactly one image or raster layer for image-to-image generation.";
if (mode === "inpaint") return "Select exactly one masked image or raster layer to inpaint.";
return "Select exactly one image or raster layer to outpaint.";
}
function missing(message: string): GenerationPrecondition {
return { ready: false, message };
}
function rectsAligned(a: { x: number; y: number; w: number; h: number }, b: { x: number; y: number; w: number; h: number }): boolean {
return Math.abs(a.x - b.x) <= 0.5 && Math.abs(a.y - b.y) <= 0.5 && Math.abs(a.w - b.w) <= 0.5 && Math.abs(a.h - b.h) <= 0.5;
}