Files
image-studio/operations/generation/outputPlacement.ts
2026-07-11 12:38:34 +02:00

106 lines
3.8 KiB
TypeScript

import type { ImageDocument } from "@core/document";
import type { Size, Transform } from "@core/geometry";
import type { ArtboardId } from "@core/id";
import { createDocumentReadIndex } from "@editor/document-indexes";
import type { GenerationCandidate, SelectionState } from "@editor/state";
import type { GenerateSettings } from "@editor/tools";
import type { InpaintBundle } from "./inpaintPrep";
export type GeneratedOutputPlacement = GenerationCandidate["placement"];
export function resolveGeneratedOutputPlacement(options: {
document: ImageDocument;
selection: SelectionState;
settings: GenerateSettings;
intrinsicSize: Size;
inpaintBundle?: InpaintBundle;
}): GeneratedOutputPlacement {
const { document, selection, settings, intrinsicSize, inpaintBundle } = options;
if (inpaintBundle) {
return {
artboardId: inpaintBundle.placement.artboardId,
layerName: inpaintBundle.placement.layerName,
transform: scaleInpaintPlacement(inpaintBundle, intrinsicSize),
};
}
const artboard = selection.artboardId
? document.artboards.find((candidate) => candidate.id === selection.artboardId)
: document.artboards[0];
if (!artboard) throw new Error("Create an artboard before placing generated output.");
if (settings.mode === "text-to-image") {
return {
artboardId: artboard.id,
layerName: "Generated image",
transform: {
position: {
x: artboard.bounds.x + (artboard.bounds.w - intrinsicSize.w) / 2,
y: artboard.bounds.y + (artboard.bounds.h - intrinsicSize.h) / 2,
},
scale: { x: 1, y: 1 },
rotation: 0,
},
};
}
const target = resolveTarget(document, selection, artboard.id);
const sourceScale = {
x: target.layer.transform.scale.x * target.asset.intrinsicSize.w / Math.max(1, intrinsicSize.w),
y: target.layer.transform.scale.y * target.asset.intrinsicSize.h / Math.max(1, intrinsicSize.h),
};
if (settings.mode === "image-to-image") {
return {
artboardId: artboard.id,
layerName: "Generated image",
transform: {
position: { ...target.layer.transform.position },
scale: sourceScale,
rotation: target.layer.transform.rotation,
},
};
}
if (settings.mode === "outpaint") {
const pixelScale = target.layer.transform.scale;
return {
artboardId: artboard.id,
layerName: "Generated image",
transform: {
position: {
x: target.layer.transform.position.x - settings.outpaint.left * pixelScale.x,
y: target.layer.transform.position.y - settings.outpaint.top * pixelScale.y,
},
scale: { ...pixelScale },
rotation: target.layer.transform.rotation,
},
};
}
throw new Error("Inpaint output requires an inpaint placement bundle.");
}
function resolveTarget(document: ImageDocument, selection: SelectionState, artboardId: ArtboardId) {
const layerId = selection.layerIds[0];
const index = createDocumentReadIndex(document);
const layerInfo = layerId ? index.layerInfoById.get(layerId) : undefined;
if (!layerInfo || layerInfo.artboardId !== artboardId || (layerInfo.layer.type !== "image" && layerInfo.layer.type !== "raster")) {
throw new Error("Select one image or raster layer before placing generated output.");
}
const asset = index.assetById.get(layerInfo.layer.assetId);
if (!asset) throw new Error("The selected layer is missing its source image.");
return { layer: layerInfo.layer, asset };
}
function scaleInpaintPlacement(bundle: InpaintBundle, intrinsicSize: Size): Transform {
return {
position: { ...bundle.placement.transform.position },
scale: {
x: bundle.placement.transform.scale.x * bundle.width / Math.max(1, intrinsicSize.w),
y: bundle.placement.transform.scale.y * bundle.height / Math.max(1, intrinsicSize.h),
},
rotation: bundle.placement.transform.rotation,
};
}