- Added a fallback mechanism for resolving storage URLs in `canvas-helpers.ts`, improving reliability when URLs are not directly available. - Introduced new utility functions in `canvas.tsx` for summarizing update and resize payloads, enhancing debugging capabilities during canvas operations. - Updated `compare-node.tsx` to improve state management and rendering logic, allowing for better handling of incoming edges and display modes. - Refactored `render-node.tsx` to streamline the rendering process and include detailed logging for debugging render operations. - Updated `.gitignore` to exclude `.kilo` files, ensuring cleaner repository management.
143 lines
3.7 KiB
TypeScript
143 lines
3.7 KiB
TypeScript
import {
|
|
collectPipeline,
|
|
getSourceImage,
|
|
hashPipeline,
|
|
type PipelineStep,
|
|
} from "@/lib/image-pipeline/contracts";
|
|
|
|
export type RenderPreviewGraphNode = {
|
|
id: string;
|
|
type: string;
|
|
data?: unknown;
|
|
};
|
|
|
|
export type RenderPreviewGraphEdge = {
|
|
source: string;
|
|
target: string;
|
|
};
|
|
|
|
export type RenderPreviewInput = {
|
|
sourceUrl: string;
|
|
steps: PipelineStep[];
|
|
};
|
|
|
|
type RenderResolutionOption = "original" | "2x" | "custom";
|
|
type RenderFormatOption = "png" | "jpeg" | "webp";
|
|
|
|
const DEFAULT_OUTPUT_RESOLUTION: RenderResolutionOption = "original";
|
|
const DEFAULT_FORMAT: RenderFormatOption = "png";
|
|
const DEFAULT_JPEG_QUALITY = 90;
|
|
const MIN_CUSTOM_DIMENSION = 1;
|
|
const MAX_CUSTOM_DIMENSION = 16_384;
|
|
|
|
function sanitizeDimension(value: unknown): number | undefined {
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
const rounded = Math.round(value);
|
|
if (rounded < MIN_CUSTOM_DIMENSION || rounded > MAX_CUSTOM_DIMENSION) {
|
|
return undefined;
|
|
}
|
|
|
|
return rounded;
|
|
}
|
|
|
|
const SOURCE_NODE_TYPES = new Set(["image", "ai-image", "asset"]);
|
|
|
|
export const RENDER_PREVIEW_PIPELINE_TYPES = new Set([
|
|
"curves",
|
|
"color-adjust",
|
|
"light-adjust",
|
|
"detail-adjust",
|
|
]);
|
|
|
|
export function resolveRenderFingerprint(data: unknown): {
|
|
resolution: RenderResolutionOption;
|
|
customWidth?: number;
|
|
customHeight?: number;
|
|
format: RenderFormatOption;
|
|
jpegQuality?: number;
|
|
} {
|
|
const record = (data ?? {}) as Record<string, unknown>;
|
|
const resolution: RenderResolutionOption =
|
|
record.outputResolution === "2x" || record.outputResolution === "custom"
|
|
? record.outputResolution
|
|
: DEFAULT_OUTPUT_RESOLUTION;
|
|
|
|
const format: RenderFormatOption =
|
|
record.format === "jpeg" || record.format === "webp"
|
|
? record.format
|
|
: DEFAULT_FORMAT;
|
|
|
|
const jpegQuality =
|
|
typeof record.jpegQuality === "number" && Number.isFinite(record.jpegQuality)
|
|
? Math.max(1, Math.min(100, Math.round(record.jpegQuality)))
|
|
: DEFAULT_JPEG_QUALITY;
|
|
|
|
return {
|
|
resolution,
|
|
customWidth: resolution === "custom" ? sanitizeDimension(record.customWidth) : undefined,
|
|
customHeight: resolution === "custom" ? sanitizeDimension(record.customHeight) : undefined,
|
|
format,
|
|
jpegQuality: format === "jpeg" ? jpegQuality : undefined,
|
|
};
|
|
}
|
|
|
|
export function resolveRenderPipelineHash(args: {
|
|
sourceUrl: string | null;
|
|
steps: PipelineStep[];
|
|
data: unknown;
|
|
}): string | null {
|
|
if (!args.sourceUrl) {
|
|
return null;
|
|
}
|
|
|
|
return hashPipeline(
|
|
{ sourceUrl: args.sourceUrl, render: resolveRenderFingerprint(args.data) },
|
|
args.steps,
|
|
);
|
|
}
|
|
|
|
export function resolveNodeImageUrl(data: unknown): string | null {
|
|
const record = (data ?? {}) as Record<string, unknown>;
|
|
const directUrl = typeof record.url === "string" ? record.url : null;
|
|
if (directUrl && directUrl.length > 0) {
|
|
return directUrl;
|
|
}
|
|
|
|
const previewUrl =
|
|
typeof record.previewUrl === "string" ? record.previewUrl : null;
|
|
if (previewUrl && previewUrl.length > 0) {
|
|
return previewUrl;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function resolveRenderPreviewInput(args: {
|
|
nodeId: string;
|
|
nodes: readonly RenderPreviewGraphNode[];
|
|
edges: readonly RenderPreviewGraphEdge[];
|
|
}): { sourceUrl: string | null; steps: PipelineStep[] } {
|
|
const sourceUrl = getSourceImage({
|
|
nodeId: args.nodeId,
|
|
nodes: args.nodes,
|
|
edges: args.edges,
|
|
isSourceNode: (node) => SOURCE_NODE_TYPES.has(node.type ?? ""),
|
|
getSourceImageFromNode: (node) => resolveNodeImageUrl(node.data),
|
|
});
|
|
|
|
const steps = collectPipeline({
|
|
nodeId: args.nodeId,
|
|
nodes: args.nodes,
|
|
edges: args.edges,
|
|
isPipelineNode: (node) => RENDER_PREVIEW_PIPELINE_TYPES.has(node.type ?? ""),
|
|
}) as PipelineStep[];
|
|
|
|
return {
|
|
sourceUrl,
|
|
steps,
|
|
};
|
|
}
|