cbf4e21e2b
Persist each image-generator node's target resolution in metadata and surface it in the selection frame and node preview. Place new generators without overlapping existing ones, keep local draft generators when a compressed snapshot omits them, and add a dedicated selection-frame variant plus scale-aware preview styling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import type { ComposerImageSettings } from "@/ui/components/CanvasAgentComposer";
|
|
import type { CanvasNode } from "@/domain/design";
|
|
|
|
type StageSize = {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
export type ImageGeneratorTargetSize = {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
const targetMetadataKey = "imageGeneratorTarget";
|
|
|
|
export function imageGeneratorPlaceholderSize(settings: ComposerImageSettings, viewportScale: number, stage: StageSize) {
|
|
const width = Math.max(1, settings.width);
|
|
const height = Math.max(1, settings.height);
|
|
const ratio = width / height;
|
|
const stageShortSide = Math.max(1, Math.min(stage.width, stage.height));
|
|
const screenMaxSide = clamp(stageShortSide * 0.42, 240, 360);
|
|
const worldMaxSide = screenMaxSide / Math.max(viewportScale, 0.12);
|
|
|
|
if (ratio >= 1) {
|
|
return {
|
|
width: Math.round(worldMaxSide),
|
|
height: Math.round(worldMaxSide / ratio)
|
|
};
|
|
}
|
|
|
|
return {
|
|
width: Math.round(worldMaxSide * ratio),
|
|
height: Math.round(worldMaxSide)
|
|
};
|
|
}
|
|
|
|
export function imageGeneratorTargetSizeFromSettings(settings: ComposerImageSettings): ImageGeneratorTargetSize {
|
|
return {
|
|
width: positiveInteger(settings.width, 1024),
|
|
height: positiveInteger(settings.height, 1024)
|
|
};
|
|
}
|
|
|
|
export function imageGeneratorTargetSizeFromNode(node: CanvasNode): ImageGeneratorTargetSize {
|
|
const metadata = parseImageGeneratorTargetMetadata(node.imageAdjustments);
|
|
if (metadata) {
|
|
return {
|
|
width: positiveInteger(metadata.width, 1024),
|
|
height: positiveInteger(metadata.height, 1024)
|
|
};
|
|
}
|
|
const ratio = Math.max(node.width, 1) / Math.max(node.height, 1);
|
|
if (ratio >= 1) {
|
|
return {
|
|
width: positiveInteger(1024 * ratio, 1024),
|
|
height: 1024
|
|
};
|
|
}
|
|
return {
|
|
width: 1024,
|
|
height: positiveInteger(1024 / ratio, 1024)
|
|
};
|
|
}
|
|
|
|
export function serializeImageGeneratorTargetSize(settings: ComposerImageSettings, previous?: string): string {
|
|
const metadata = parseJsonObject(previous);
|
|
metadata[targetMetadataKey] = imageGeneratorTargetSizeFromSettings(settings);
|
|
return JSON.stringify(metadata);
|
|
}
|
|
|
|
function clamp(value: number, min: number, max: number) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function parseImageGeneratorTargetMetadata(value?: string | null): Partial<ImageGeneratorTargetSize> | null {
|
|
const metadata = parseJsonObject(value)[targetMetadataKey];
|
|
if (!metadata || typeof metadata !== "object") return null;
|
|
const source = metadata as Partial<Record<keyof ImageGeneratorTargetSize, unknown>>;
|
|
return {
|
|
width: Number(source.width),
|
|
height: Number(source.height)
|
|
};
|
|
}
|
|
|
|
function parseJsonObject(value?: string | null): Record<string, unknown> {
|
|
if (!value) return {};
|
|
try {
|
|
const parsed = JSON.parse(value) as unknown;
|
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...(parsed as Record<string, unknown>) };
|
|
} catch {
|
|
return {};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function positiveInteger(value: unknown, fallback: number) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric) || numeric <= 0) return Math.max(1, Math.round(fallback));
|
|
return Math.max(1, Math.round(numeric));
|
|
}
|