Files
lemonspace_app/lib/image-pipeline/render-size.ts
Matthias f3c5c2d8f1 Enhance canvas functionality with new node types and validation
- Added support for new canvas node types: curves, color-adjust, light-adjust, detail-adjust, and render.
- Implemented validation for adjustment nodes to restrict incoming edges to one.
- Updated canvas connection validation to improve user feedback on invalid connections.
- Enhanced node creation and rendering logic to accommodate new node types and their properties.
- Refactored related components and utilities for better maintainability and performance.
2026-04-02 11:39:05 +02:00

109 lines
3.1 KiB
TypeScript

import type {
RenderOptions,
RenderSizeLimits,
ResolvedRenderSize,
} from "@/lib/image-pipeline/render-types";
const DEFAULT_MAX_DIMENSION = 8192;
const DEFAULT_MAX_PIXELS = 33_554_432;
function sanitizeLimit(name: string, value: number | undefined, fallback: number): number {
if (value === undefined) {
return fallback;
}
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`Invalid render limit '${name}'. Expected a positive finite number.`);
}
return Math.max(1, Math.floor(value));
}
function sanitizeDimension(name: string, value: number): number {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`Invalid ${name}. Expected a positive finite number.`);
}
return Math.max(1, Math.round(value));
}
function scaleDimensions(
width: number,
height: number,
factor: number,
): {
width: number;
height: number;
} {
return {
width: Math.max(1, Math.floor(width * factor)),
height: Math.max(1, Math.floor(height * factor)),
};
}
export function resolveRenderSize(options: {
sourceWidth: number;
sourceHeight: number;
render: RenderOptions;
limits?: RenderSizeLimits;
}): ResolvedRenderSize {
const sourceWidth = sanitizeDimension("sourceWidth", options.sourceWidth);
const sourceHeight = sanitizeDimension("sourceHeight", options.sourceHeight);
const maxDimension = sanitizeLimit(
"maxDimension",
options.limits?.maxDimension,
DEFAULT_MAX_DIMENSION,
);
const maxPixels = sanitizeLimit("maxPixels", options.limits?.maxPixels, DEFAULT_MAX_PIXELS);
let targetWidth = sourceWidth;
let targetHeight = sourceHeight;
if (options.render.resolution === "2x") {
targetWidth = sourceWidth * 2;
targetHeight = sourceHeight * 2;
} else if (options.render.resolution === "custom") {
if (!options.render.customSize) {
throw new Error("Invalid render options: resolution 'custom' requires customSize.");
}
targetWidth = sanitizeDimension("customSize.width", options.render.customSize.width);
targetHeight = sanitizeDimension("customSize.height", options.render.customSize.height);
} else if (options.render.resolution !== "original") {
throw new Error(`Unsupported render resolution '${options.render.resolution}'.`);
}
targetWidth = sanitizeDimension("targetWidth", targetWidth);
targetHeight = sanitizeDimension("targetHeight", targetHeight);
let scaleFactor = 1;
let wasClamped = false;
const dimensionScale = Math.min(1, maxDimension / Math.max(targetWidth, targetHeight));
if (dimensionScale < 1) {
const scaled = scaleDimensions(targetWidth, targetHeight, dimensionScale);
targetWidth = scaled.width;
targetHeight = scaled.height;
scaleFactor *= dimensionScale;
wasClamped = true;
}
const pixelCount = targetWidth * targetHeight;
if (pixelCount > maxPixels) {
const pixelScale = Math.sqrt(maxPixels / pixelCount);
const scaled = scaleDimensions(targetWidth, targetHeight, pixelScale);
targetWidth = scaled.width;
targetHeight = scaled.height;
scaleFactor *= pixelScale;
wasClamped = true;
}
return {
width: targetWidth,
height: targetHeight,
scaleFactor,
wasClamped,
};
}