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:
2026-04-02 16:12:56 +02:00
parent 3fa686d60d
commit 8703387617
7 changed files with 669 additions and 123 deletions

View File

@@ -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,