Enhance canvas functionality with storage URL resolution and debugging improvements
- 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.
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { Handle, Position, useStore, type NodeProps } from "@xyflow/react";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import CompareSurface from "./compare-surface";
|
||||
import {
|
||||
resolveRenderPipelineHash,
|
||||
resolveRenderPreviewInput,
|
||||
type RenderPreviewInput,
|
||||
} from "@/lib/canvas-render-preview";
|
||||
|
||||
interface CompareNodeData {
|
||||
leftUrl?: string;
|
||||
@@ -12,13 +18,140 @@ interface CompareNodeData {
|
||||
rightLabel?: string;
|
||||
}
|
||||
|
||||
export default function CompareNode({ data, selected }: NodeProps) {
|
||||
type CompareSide = "left" | "right";
|
||||
|
||||
type CompareSideState = {
|
||||
finalUrl?: string;
|
||||
label?: string;
|
||||
previewInput?: RenderPreviewInput;
|
||||
isStaleRenderOutput: boolean;
|
||||
};
|
||||
|
||||
type CompareDisplayMode = "render" | "preview";
|
||||
|
||||
export default function CompareNode({ id, data, selected, width }: NodeProps) {
|
||||
const nodeData = data as CompareNodeData;
|
||||
const nodes = useStore((state) => state.nodes);
|
||||
const edges = useStore((state) => state.edges);
|
||||
const [sliderX, setSliderX] = useState(50);
|
||||
const [manualDisplayMode, setManualDisplayMode] = useState<CompareDisplayMode | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const hasLeft = !!nodeData.leftUrl;
|
||||
const hasRight = !!nodeData.rightUrl;
|
||||
const pipelineNodes = useMemo(
|
||||
() => nodes.map((node) => ({ id: node.id, type: node.type ?? "", data: node.data })),
|
||||
[nodes],
|
||||
);
|
||||
const pipelineEdges = useMemo(
|
||||
() => edges.map((edge) => ({ source: edge.source, target: edge.target })),
|
||||
[edges],
|
||||
);
|
||||
|
||||
const nodesById = useMemo(() => new Map(nodes.map((node) => [node.id, node])), [nodes]);
|
||||
const incomingEdges = useMemo(
|
||||
() =>
|
||||
edges.filter(
|
||||
(edge) =>
|
||||
edge.target === id &&
|
||||
edge.className !== "temp" &&
|
||||
(edge.targetHandle === "left" || edge.targetHandle === "right"),
|
||||
),
|
||||
[edges, id],
|
||||
);
|
||||
|
||||
const resolvedSides = useMemo(() => {
|
||||
const resolveSide = (
|
||||
side: CompareSide,
|
||||
finalUrl: string | undefined,
|
||||
finalLabel: string | undefined,
|
||||
defaultLabel: string,
|
||||
): CompareSideState => {
|
||||
const incomingEdge = incomingEdges.find((edge) => edge.targetHandle === side);
|
||||
const sourceNode = incomingEdge ? nodesById.get(incomingEdge.source) : undefined;
|
||||
const sourceData = (sourceNode?.data ?? {}) as Record<string, unknown>;
|
||||
const sourceLabel =
|
||||
typeof sourceData.label === "string" && sourceData.label.length > 0
|
||||
? sourceData.label
|
||||
: sourceNode?.type;
|
||||
|
||||
const label = finalLabel ?? sourceLabel ?? defaultLabel;
|
||||
|
||||
let previewInput: RenderPreviewInput | undefined;
|
||||
let isStaleRenderOutput = false;
|
||||
|
||||
if (sourceNode && sourceNode.type === "render") {
|
||||
const preview = resolveRenderPreviewInput({
|
||||
nodeId: sourceNode.id,
|
||||
nodes: pipelineNodes,
|
||||
edges: pipelineEdges,
|
||||
});
|
||||
|
||||
if (preview.sourceUrl) {
|
||||
previewInput = {
|
||||
sourceUrl: preview.sourceUrl,
|
||||
steps: preview.steps,
|
||||
};
|
||||
|
||||
const sourceLastUploadedHash =
|
||||
typeof sourceData.lastUploadedHash === "string"
|
||||
? sourceData.lastUploadedHash
|
||||
: undefined;
|
||||
const sourceLastRenderedHash =
|
||||
typeof sourceData.lastRenderedHash === "string"
|
||||
? sourceData.lastRenderedHash
|
||||
: undefined;
|
||||
const sourcePersistedOutputHash =
|
||||
sourceLastUploadedHash ?? sourceLastRenderedHash;
|
||||
const sourceCurrentHash = resolveRenderPipelineHash({
|
||||
sourceUrl: preview.sourceUrl,
|
||||
steps: preview.steps,
|
||||
data: sourceData,
|
||||
});
|
||||
|
||||
isStaleRenderOutput =
|
||||
Boolean(finalUrl) &&
|
||||
Boolean(sourceCurrentHash) &&
|
||||
Boolean(sourcePersistedOutputHash) &&
|
||||
sourceCurrentHash !== sourcePersistedOutputHash;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalUrl) {
|
||||
return { finalUrl, label, previewInput, isStaleRenderOutput };
|
||||
}
|
||||
|
||||
return { label, previewInput, isStaleRenderOutput };
|
||||
};
|
||||
|
||||
return {
|
||||
left: resolveSide("left", nodeData.leftUrl, nodeData.leftLabel, "Before"),
|
||||
right: resolveSide("right", nodeData.rightUrl, nodeData.rightLabel, "After"),
|
||||
};
|
||||
}, [
|
||||
incomingEdges,
|
||||
nodeData.leftLabel,
|
||||
nodeData.leftUrl,
|
||||
nodeData.rightLabel,
|
||||
nodeData.rightUrl,
|
||||
nodesById,
|
||||
pipelineEdges,
|
||||
pipelineNodes,
|
||||
]);
|
||||
|
||||
const hasLeft = Boolean(resolvedSides.left.finalUrl || resolvedSides.left.previewInput);
|
||||
const hasRight = Boolean(resolvedSides.right.finalUrl || resolvedSides.right.previewInput);
|
||||
const hasConnectedRenderInput = useMemo(
|
||||
() =>
|
||||
incomingEdges.some((edge) => {
|
||||
const sourceNode = nodesById.get(edge.source);
|
||||
return sourceNode?.type === "render";
|
||||
}),
|
||||
[incomingEdges, nodesById],
|
||||
);
|
||||
const shouldDefaultToPreview =
|
||||
resolvedSides.left.isStaleRenderOutput || resolvedSides.right.isStaleRenderOutput;
|
||||
const effectiveDisplayMode =
|
||||
manualDisplayMode ?? (shouldDefaultToPreview ? "preview" : "render");
|
||||
const previewNodeWidth = Math.max(240, Math.min(640, Math.round(width ?? 500)));
|
||||
|
||||
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
@@ -86,7 +219,33 @@ export default function CompareNode({ data, selected }: NodeProps) {
|
||||
/>
|
||||
|
||||
<div className="grid h-full min-h-0 w-full grid-cols-1 grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="px-3 py-2 text-xs font-medium text-muted-foreground">⚖️ Compare</div>
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2">
|
||||
<div className="text-xs font-medium text-muted-foreground">⚖️ Compare</div>
|
||||
{hasConnectedRenderInput && (
|
||||
<div className="nodrag inline-flex rounded-md border border-border bg-background/80 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-2 py-0.5 text-[10px] font-medium ${effectiveDisplayMode === "render" ? "bg-muted text-foreground" : "text-muted-foreground"}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setManualDisplayMode("render");
|
||||
}}
|
||||
>
|
||||
Render
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-2 py-0.5 text-[10px] font-medium ${effectiveDisplayMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground"}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setManualDisplayMode("preview");
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -94,81 +253,76 @@ export default function CompareNode({ data, selected }: NodeProps) {
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
>
|
||||
{!hasLeft && !hasRight && (
|
||||
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-muted-foreground">
|
||||
<ImageIcon className="h-10 w-10 opacity-30" />
|
||||
<p className="px-8 text-center text-xs opacity-60">
|
||||
Connect two image nodes - left handle (blue) and right handle (green)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasRight && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={nodeData.rightUrl}
|
||||
alt={nodeData.rightLabel ?? "Right"}
|
||||
className="pointer-events-none absolute inset-0 h-full w-full object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasLeft && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
style={{ width: `${sliderX}%` }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={nodeData.leftUrl}
|
||||
alt={nodeData.leftLabel ?? "Left"}
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
style={{ width: "100%", maxWidth: "none" }}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasLeft && hasRight && (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute bottom-0 top-0 z-10 w-0.5 bg-white shadow-md"
|
||||
style={{ left: `${sliderX}%` }}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 z-20 -translate-x-1/2 -translate-y-1/2"
|
||||
style={{ left: `${sliderX}%` }}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-border bg-white shadow-lg">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M5 8H1M11 8H15M5 5L2 8L5 11M11 5L14 8L11 11"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{!hasLeft && !hasRight && (
|
||||
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-muted-foreground">
|
||||
<ImageIcon className="h-10 w-10 opacity-30" />
|
||||
<p className="px-8 text-center text-xs opacity-60">
|
||||
Connect two image nodes - left handle (blue) and right handle (green)
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{hasLeft && (
|
||||
<div className="pointer-events-none absolute left-2 top-2 z-10">
|
||||
<span className="rounded bg-blue-500/80 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">
|
||||
{nodeData.leftLabel ?? "Before"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasRight && (
|
||||
<CompareSurface
|
||||
finalUrl={resolvedSides.right.finalUrl}
|
||||
label={resolvedSides.right.label}
|
||||
previewInput={resolvedSides.right.previewInput}
|
||||
nodeWidth={previewNodeWidth}
|
||||
preferPreview={effectiveDisplayMode === "preview"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasRight && (
|
||||
<div className="pointer-events-none absolute right-2 top-2 z-10">
|
||||
<span className="rounded bg-emerald-500/80 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">
|
||||
{nodeData.rightLabel ?? "After"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasLeft && (
|
||||
<CompareSurface
|
||||
finalUrl={resolvedSides.left.finalUrl}
|
||||
label={resolvedSides.left.label}
|
||||
previewInput={resolvedSides.left.previewInput}
|
||||
nodeWidth={previewNodeWidth}
|
||||
clipWidthPercent={sliderX}
|
||||
preferPreview={effectiveDisplayMode === "preview"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasLeft && hasRight && (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute bottom-0 top-0 z-10 w-0.5 bg-white shadow-md"
|
||||
style={{ left: `${sliderX}%` }}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 z-20 -translate-x-1/2 -translate-y-1/2"
|
||||
style={{ left: `${sliderX}%` }}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-border bg-white shadow-lg">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M5 8H1M11 8H15M5 5L2 8L5 11M11 5L14 8L11 11"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasLeft && (
|
||||
<div className="pointer-events-none absolute left-2 top-2 z-10">
|
||||
<span className="rounded bg-blue-500/80 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">
|
||||
{resolvedSides.left.label ?? "Before"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasRight && (
|
||||
<div className="pointer-events-none absolute right-2 top-2 z-10">
|
||||
<span className="rounded bg-emerald-500/80 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">
|
||||
{resolvedSides.right.label ?? "After"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNodeWrapper>
|
||||
|
||||
77
components/canvas/nodes/compare-surface.tsx
Normal file
77
components/canvas/nodes/compare-surface.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { usePipelinePreview } from "@/hooks/use-pipeline-preview";
|
||||
import type { RenderPreviewInput } from "@/lib/canvas-render-preview";
|
||||
|
||||
const EMPTY_STEPS: RenderPreviewInput["steps"] = [];
|
||||
|
||||
type CompareSurfaceProps = {
|
||||
finalUrl?: string;
|
||||
label?: string;
|
||||
previewInput?: RenderPreviewInput;
|
||||
nodeWidth: number;
|
||||
clipWidthPercent?: number;
|
||||
preferPreview?: boolean;
|
||||
};
|
||||
|
||||
export default function CompareSurface({
|
||||
finalUrl,
|
||||
label,
|
||||
previewInput,
|
||||
nodeWidth,
|
||||
clipWidthPercent,
|
||||
preferPreview,
|
||||
}: CompareSurfaceProps) {
|
||||
const usePreview = Boolean(previewInput && (preferPreview || !finalUrl));
|
||||
const previewSourceUrl = usePreview ? previewInput?.sourceUrl ?? null : null;
|
||||
const previewSteps = usePreview ? previewInput?.steps ?? EMPTY_STEPS : EMPTY_STEPS;
|
||||
const visibleFinalUrl = usePreview ? undefined : finalUrl;
|
||||
|
||||
const { canvasRef, isRendering, error } = usePipelinePreview({
|
||||
sourceUrl: previewSourceUrl,
|
||||
steps: previewSteps,
|
||||
nodeWidth,
|
||||
previewScale: 0.7,
|
||||
maxPreviewWidth: 960,
|
||||
});
|
||||
|
||||
const hasPreview = Boolean(usePreview && previewInput);
|
||||
const clipStyle =
|
||||
typeof clipWidthPercent === "number"
|
||||
? {
|
||||
clipPath: `inset(0 ${100 - clipWidthPercent}% 0 0)`,
|
||||
WebkitClipPath: `inset(0 ${100 - clipWidthPercent}% 0 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0" style={clipStyle}>
|
||||
{visibleFinalUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={visibleFinalUrl}
|
||||
alt={label ?? "Comparison image"}
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
) : hasPreview ? (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{hasPreview ? (
|
||||
<div className="absolute bottom-2 left-2 rounded bg-amber-500/85 px-1.5 py-0.5 text-[10px] font-medium text-black/90 backdrop-blur-sm">
|
||||
{isRendering ? "Live Preview..." : "Live Preview"}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasPreview && error ? (
|
||||
<div className="absolute bottom-2 right-2 rounded bg-destructive/85 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">
|
||||
Preview error
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,14 +12,10 @@ import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { usePipelinePreview } from "@/hooks/use-pipeline-preview";
|
||||
import { resolveRenderPreviewInput } from "@/lib/canvas-render-preview";
|
||||
import { resolveMediaAspectRatio } from "@/lib/canvas-utils";
|
||||
import { parseAspectRatioString } from "@/lib/image-formats";
|
||||
import {
|
||||
collectPipeline,
|
||||
getSourceImage,
|
||||
hashPipeline,
|
||||
type PipelineStep,
|
||||
} from "@/lib/image-pipeline/contracts";
|
||||
import { getSourceImage, hashPipeline } from "@/lib/image-pipeline/contracts";
|
||||
import { bridge } from "@/lib/image-pipeline/bridge";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
@@ -112,7 +108,13 @@ const RENDER_MIN_HEIGHT = 300;
|
||||
const ASPECT_RATIO_TOLERANCE = 0.015;
|
||||
const SIZE_TOLERANCE_PX = 1;
|
||||
|
||||
const RENDER_PIPELINE_TYPES = new Set(["curves", "color-adjust", "light-adjust", "detail-adjust"]);
|
||||
function logRenderDebug(event: string, payload: Record<string, unknown>): void {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("[RenderNode debug]", event, payload);
|
||||
}
|
||||
|
||||
function readPositiveNumber(value: unknown): number | null {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
@@ -217,21 +219,6 @@ function toRatioConstrainedSize(args: {
|
||||
return widthDistance <= heightDistance ? widthCandidate : heightCandidate;
|
||||
}
|
||||
|
||||
function resolveNodeImageUrl(node: Node): string | null {
|
||||
const nodeData = (node.data ?? {}) as Record<string, unknown>;
|
||||
const directUrl = typeof nodeData.url === "string" ? nodeData.url : null;
|
||||
if (directUrl && directUrl.length > 0) {
|
||||
return directUrl;
|
||||
}
|
||||
|
||||
const previewUrl = typeof nodeData.previewUrl === "string" ? nodeData.previewUrl : null;
|
||||
if (previewUrl && previewUrl.length > 0) {
|
||||
return previewUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeDimension(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return undefined;
|
||||
@@ -497,22 +484,40 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
[edges],
|
||||
);
|
||||
|
||||
const sourceUrl = useMemo(
|
||||
const renderPreviewInput = useMemo(
|
||||
() =>
|
||||
getSourceImage({
|
||||
resolveRenderPreviewInput({
|
||||
nodeId: id,
|
||||
nodes: pipelineNodes,
|
||||
edges: pipelineEdges,
|
||||
isSourceNode: (node) =>
|
||||
node.type === "image" || node.type === "ai-image" || node.type === "asset",
|
||||
getSourceImageFromNode: (node) => {
|
||||
const sourceNode = nodes.find((candidate) => candidate.id === node.id);
|
||||
return sourceNode ? resolveNodeImageUrl(sourceNode) : null;
|
||||
},
|
||||
}),
|
||||
[id, nodes, pipelineEdges, pipelineNodes],
|
||||
[id, pipelineEdges, pipelineNodes],
|
||||
);
|
||||
|
||||
const sourceUrl = renderPreviewInput.sourceUrl;
|
||||
|
||||
useEffect(() => {
|
||||
logRenderDebug("node-data-updated", {
|
||||
nodeId: id,
|
||||
hasSourceUrl: typeof sourceUrl === "string" && sourceUrl.length > 0,
|
||||
storageId: data.storageId ?? null,
|
||||
lastUploadStorageId: data.lastUploadStorageId ?? null,
|
||||
hasResolvedUrl: typeof data.url === "string" && data.url.length > 0,
|
||||
lastUploadedAt: data.lastUploadedAt ?? null,
|
||||
lastUploadedHash: data.lastUploadedHash ?? null,
|
||||
lastRenderedHash: data.lastRenderedHash ?? null,
|
||||
});
|
||||
}, [
|
||||
data.lastRenderedHash,
|
||||
data.lastUploadStorageId,
|
||||
data.lastUploadedAt,
|
||||
data.lastUploadedHash,
|
||||
data.storageId,
|
||||
data.url,
|
||||
id,
|
||||
sourceUrl,
|
||||
]);
|
||||
|
||||
const sourceNode = useMemo<SourceNodeDescriptor | null>(
|
||||
() =>
|
||||
getSourceImage({
|
||||
@@ -526,16 +531,7 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
[id, pipelineEdges, pipelineNodes],
|
||||
);
|
||||
|
||||
const steps = useMemo(
|
||||
() =>
|
||||
collectPipeline({
|
||||
nodeId: id,
|
||||
nodes: pipelineNodes,
|
||||
edges: pipelineEdges,
|
||||
isPipelineNode: (node) => RENDER_PIPELINE_TYPES.has(node.type ?? ""),
|
||||
}) as PipelineStep[],
|
||||
[id, pipelineEdges, pipelineNodes],
|
||||
);
|
||||
const steps = renderPreviewInput.steps;
|
||||
|
||||
const renderFingerprint = useMemo(
|
||||
() => ({
|
||||
@@ -741,7 +737,16 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
};
|
||||
|
||||
const handleRender = async (mode: "download" | "upload") => {
|
||||
if (!sourceUrl || !currentPipelineHash) return;
|
||||
if (!sourceUrl || !currentPipelineHash) {
|
||||
logRenderDebug("render-aborted-prerequisites", {
|
||||
nodeId: id,
|
||||
mode,
|
||||
hasSourceUrl: Boolean(sourceUrl),
|
||||
hasPipelineHash: Boolean(currentPipelineHash),
|
||||
isOffline: status.isOffline,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
localData.outputResolution === "custom" &&
|
||||
@@ -762,6 +767,17 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
|
||||
try {
|
||||
const activeData = localDataRef.current;
|
||||
logRenderDebug("render-start", {
|
||||
nodeId: id,
|
||||
mode,
|
||||
pipelineHash: currentPipelineHash,
|
||||
resolution: activeData.outputResolution,
|
||||
customWidth: activeData.customWidth ?? null,
|
||||
customHeight: activeData.customHeight ?? null,
|
||||
format: activeData.format,
|
||||
jpegQuality: activeData.format === "jpeg" ? activeData.jpegQuality : null,
|
||||
});
|
||||
|
||||
const renderResult = await bridge.renderFull({
|
||||
sourceUrl,
|
||||
steps,
|
||||
@@ -784,6 +800,20 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
|
||||
if (runId !== renderRunIdRef.current) return;
|
||||
|
||||
logRenderDebug("render-success", {
|
||||
nodeId: id,
|
||||
mode,
|
||||
pipelineHash: currentPipelineHash,
|
||||
width: renderResult.width,
|
||||
height: renderResult.height,
|
||||
sourceWidth: renderResult.sourceWidth,
|
||||
sourceHeight: renderResult.sourceHeight,
|
||||
format: renderResult.format,
|
||||
mimeType: renderResult.mimeType,
|
||||
sizeBytes: renderResult.sizeBytes,
|
||||
wasSizeClamped: renderResult.wasSizeClamped,
|
||||
});
|
||||
|
||||
const filename = `lemonspace-render-${Date.now()}.${extensionForFormat(renderResult.format)}`;
|
||||
|
||||
if (mode === "download") {
|
||||
@@ -816,7 +846,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
lastRenderErrorHash: undefined,
|
||||
};
|
||||
|
||||
if (mode === "download") {
|
||||
const shouldUploadAfterRender = mode === "upload" || !status.isOffline;
|
||||
|
||||
if (!shouldUploadAfterRender) {
|
||||
await persistImmediately(renderNext);
|
||||
return;
|
||||
}
|
||||
@@ -825,6 +857,15 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
logRenderDebug("upload-start", {
|
||||
nodeId: id,
|
||||
pipelineHash: currentPipelineHash,
|
||||
triggerMode: mode,
|
||||
filename,
|
||||
mimeType: renderResult.mimeType,
|
||||
sizeBytes: renderResult.sizeBytes,
|
||||
});
|
||||
|
||||
const uploadUrl = await generateUploadUrl();
|
||||
if (runId !== renderRunIdRef.current) return;
|
||||
|
||||
@@ -836,6 +877,14 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
|
||||
if (runId !== renderRunIdRef.current) return;
|
||||
|
||||
logRenderDebug("upload-success", {
|
||||
nodeId: id,
|
||||
pipelineHash: currentPipelineHash,
|
||||
triggerMode: mode,
|
||||
storageId,
|
||||
filename,
|
||||
});
|
||||
|
||||
const uploadNext: PersistedRenderData = {
|
||||
...renderNext,
|
||||
storageId,
|
||||
@@ -861,6 +910,12 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
if (runId !== renderRunIdRef.current) return;
|
||||
|
||||
const message = uploadError instanceof Error ? uploadError.message : "Upload failed";
|
||||
logRenderDebug("upload-error", {
|
||||
nodeId: id,
|
||||
pipelineHash: currentPipelineHash,
|
||||
triggerMode: mode,
|
||||
error: message,
|
||||
});
|
||||
await persistImmediately({
|
||||
...renderNext,
|
||||
lastUploadError: message,
|
||||
@@ -875,6 +930,12 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
if (runId !== renderRunIdRef.current) return;
|
||||
|
||||
const message = error instanceof Error ? error.message : "Render failed";
|
||||
logRenderDebug("render-error", {
|
||||
nodeId: id,
|
||||
mode,
|
||||
pipelineHash: currentPipelineHash,
|
||||
error: message,
|
||||
});
|
||||
const next: PersistedRenderData = {
|
||||
...localDataRef.current,
|
||||
lastRenderError: message,
|
||||
|
||||
Reference in New Issue
Block a user