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.
This commit is contained in:
133
hooks/use-pipeline-preview.ts
Normal file
133
hooks/use-pipeline-preview.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
"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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user