Files
lemonspace_app/hooks/use-pipeline-preview.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

134 lines
4.1 KiB
TypeScript

"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { hashPipeline, type PipelineStep } from "@/lib/image-pipeline/contracts";
import { emptyHistogram, type HistogramData } from "@/lib/image-pipeline/histogram";
import {
renderPreview,
type PreviewRenderResult,
} from "@/lib/image-pipeline/preview-renderer";
type UsePipelinePreviewOptions = {
sourceUrl: string | null;
steps: readonly PipelineStep[];
nodeWidth: number;
previewScale?: number;
maxPreviewWidth?: number;
};
function computePreviewWidth(nodeWidth: number, previewScale: number, maxPreviewWidth: number): number {
const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
return Math.max(1, Math.round(Math.min(nodeWidth * dpr * previewScale, maxPreviewWidth)));
}
export function usePipelinePreview(options: UsePipelinePreviewOptions): {
canvasRef: React.RefObject<HTMLCanvasElement | null>;
histogram: HistogramData;
isRendering: boolean;
hasSource: boolean;
previewAspectRatio: number;
error: string | null;
} {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [histogram, setHistogram] = useState<HistogramData>(() => emptyHistogram());
const [isRendering, setIsRendering] = useState(false);
const [previewAspectRatio, setPreviewAspectRatio] = useState(1);
const [error, setError] = useState<string | null>(null);
const runIdRef = useRef(0);
const previewScale = useMemo(() => {
if (typeof options.previewScale !== "number" || !Number.isFinite(options.previewScale)) {
return 1;
}
return Math.max(0.2, Math.min(1, options.previewScale));
}, [options.previewScale]);
const maxPreviewWidth = useMemo(() => {
if (typeof options.maxPreviewWidth !== "number" || !Number.isFinite(options.maxPreviewWidth)) {
return 1024;
}
return Math.max(128, Math.round(options.maxPreviewWidth));
}, [options.maxPreviewWidth]);
const previewWidth = useMemo(
() => computePreviewWidth(options.nodeWidth, previewScale, maxPreviewWidth),
[maxPreviewWidth, options.nodeWidth, previewScale],
);
const pipelineHash = useMemo(() => {
if (!options.sourceUrl) {
return "no-source";
}
return hashPipeline(options.sourceUrl, options.steps);
}, [options.sourceUrl, options.steps]);
useEffect(() => {
const sourceUrl = options.sourceUrl;
if (!sourceUrl) {
const frameId = window.requestAnimationFrame(() => {
setHistogram(emptyHistogram());
setError(null);
setIsRendering(false);
});
return () => {
window.cancelAnimationFrame(frameId);
};
}
const currentRun = runIdRef.current + 1;
runIdRef.current = currentRun;
const timer = window.setTimeout(() => {
setIsRendering(true);
setError(null);
void renderPreview({
sourceUrl,
steps: options.steps,
previewWidth,
})
.then((result: PreviewRenderResult) => {
if (runIdRef.current !== currentRun) return;
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = result.width;
canvas.height = result.height;
const context = canvas.getContext("2d");
if (!context) {
setError("Preview context unavailable");
return;
}
context.putImageData(result.imageData, 0, 0);
setHistogram(result.histogram);
setPreviewAspectRatio(result.width / result.height);
})
.catch((renderError: unknown) => {
if (runIdRef.current !== currentRun) return;
const message =
renderError instanceof Error
? renderError.message
: "Preview rendering failed";
setError(message);
})
.finally(() => {
if (runIdRef.current !== currentRun) return;
setIsRendering(false);
});
}, 16);
return () => {
window.clearTimeout(timer);
};
}, [options.sourceUrl, options.steps, pipelineHash, previewWidth]);
return {
canvasRef,
histogram,
isRendering,
hasSource: Boolean(options.sourceUrl),
previewAspectRatio,
error,
};
}