From 8e1506e4c4f3fe9cab2da5671bd7f1c9ac9bb50a Mon Sep 17 00:00:00 2001 From: liangxu Date: Thu, 16 Jul 2026 20:46:12 +0800 Subject: [PATCH] feat(canvas): produce and consume transparent renderContent textures Rasterize imported SVG/CAD sources to a transparent WebP render texture and carry it as an optional renderContent alongside the original content through the domain node, snapshot normalization, and reference images. Add canvasNodeRenderContent and prefer it for artboard, layer, generated-file, and project thumbnail previews while keeping content as the crop/download/ model-reference source. Generalize the SVG rasterizer with a render purpose and add upload/reference coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/domain/design.ts | 1 + .../designGateway/canvasSnapshot.ts | 3 +- .../ui/components/ArtboardPreview/index.tsx | 12 +++--- .../src/ui/components/CanvasPanels/index.tsx | 6 +-- .../src/ui/components/ProjectThumb/index.tsx | 4 +- frontend/src/ui/lib/cadImport.ts | 20 +++++++--- frontend/src/ui/lib/canvasImportUpload.ts | 31 +++++++++++++++ frontend/src/ui/lib/imageDelivery.ts | 6 +++ frontend/src/ui/lib/modelImageReferences.ts | 9 +++-- .../canvas/nodeReferenceImages.ts | 2 +- frontend/tests/canvasImportUpload.test.mjs | 38 +++++++++++++++++++ frontend/tests/nodeReferenceImages.test.mjs | 26 +++++++++++++ 12 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 frontend/src/ui/lib/canvasImportUpload.ts create mode 100644 frontend/tests/canvasImportUpload.test.mjs create mode 100644 frontend/tests/nodeReferenceImages.test.mjs diff --git a/frontend/src/domain/design.ts b/frontend/src/domain/design.ts index 14181dc..a31676a 100644 --- a/frontend/src/domain/design.ts +++ b/frontend/src/domain/design.ts @@ -15,6 +15,7 @@ export type CanvasNode = { width: number; height: number; content: string; + renderContent?: string; tone: string; status: string; parentId?: string; diff --git a/frontend/src/infrastructure/designGateway/canvasSnapshot.ts b/frontend/src/infrastructure/designGateway/canvasSnapshot.ts index 47f77d4..8ef0620 100644 --- a/frontend/src/infrastructure/designGateway/canvasSnapshot.ts +++ b/frontend/src/infrastructure/designGateway/canvasSnapshot.ts @@ -166,6 +166,7 @@ function normalizeCanvasNode(node: Partial | null | undefined, index width: numberValue(fieldValue(source, "width", "Width"), 320), height: numberValue(fieldValue(source, "height", "Height"), 240), content: stringValue(fieldValue(source, "content", "Content")), + renderContent: stringValue(fieldValue(source, "renderContent", "RenderContent")) || undefined, tone: stringValue(fieldValue(source, "tone", "Tone")), status: stringValue(fieldValue(source, "status", "Status")) || "success", parentId: stringValue(fieldValue(source, "parentId", "ParentID")) || undefined, @@ -264,7 +265,7 @@ function base64ToBytes(value: string) { function projectCoverListFromNodes(nodes: CanvasNode[]) { return nodes .filter((node) => (node.type === "image" || node.type === "frame") && node.status === "success" && isCanvasCoverUrl(node.content)) - .map((node) => node.content.trim()) + .map((node) => node.renderContent?.trim() || node.content.trim()) .slice(0, 12); } diff --git a/frontend/src/ui/components/ArtboardPreview/index.tsx b/frontend/src/ui/components/ArtboardPreview/index.tsx index 9885fc8..3431f18 100644 --- a/frontend/src/ui/components/ArtboardPreview/index.tsx +++ b/frontend/src/ui/components/ArtboardPreview/index.tsx @@ -7,7 +7,7 @@ import { useI18n } from "@/i18n/i18n"; import { CanvasWorkOverlay } from "@/ui/components/CanvasWorkOverlay"; import { LayerMergeSkeleton } from "@/ui/components/LayerMergeSkeleton"; import { imageAdjustmentImageStyle, imageAdjustmentLayerStyle, isDefaultImageAdjustments, parseImageAdjustments } from "@/ui/lib/imageAdjustments"; -import { canvasImageUrl } from "@/ui/lib/imageDelivery"; +import { canvasImageUrl, canvasNodeRenderContent } from "@/ui/lib/imageDelivery"; import { isImageUrl } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps"; import { framePreviewStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle"; import { imageGeneratorTargetSizeFromNode, type ImageGeneratorTargetSize } from "@/ui/pages/CanvasWorkspace/canvas/imageGeneratorSizing"; @@ -20,7 +20,7 @@ export function ArtboardPreview({ node, viewportScale = 1, imageGeneratorTargetS if (node.status === "generating" && node.layerRole === "merge-layer") { return (
- {isImageUrl(node.content) && {node.title}} + {isImageUrl(node.content) && {node.title}}
); @@ -58,7 +58,7 @@ export function ArtboardPreview({ node, viewportScale = 1, imageGeneratorTargetS if (node.layerRole === "background-removed" || node.tone === "transparent-image") { return ; } - return {node.title}; + return {node.title}; } if (node.type === "image" && node.layerRole === "image-generator") { @@ -78,14 +78,14 @@ export function ArtboardPreview({ node, viewportScale = 1, imageGeneratorTargetS function TransparentImagePreview({ node }: { node: CanvasNode }) { return (
- {node.title} + {node.title}
); } function AdjustedImagePreview({ node }: { node: CanvasNode }) { const adjustments = parseImageAdjustments(node.imageAdjustments); - const source = canvasImageUrl(node.content); + const source = canvasImageUrl(canvasNodeRenderContent(node)); return (
{node.title} @@ -148,7 +148,7 @@ function ImageGeneratorPreview({ node, viewportScale, targetSize }: { node: Canv function WorkingImagePreview({ node, label, style }: { node: CanvasNode; label: string; style?: CSSProperties }) { return ( - {node.title} + {node.title} ); } diff --git a/frontend/src/ui/components/CanvasPanels/index.tsx b/frontend/src/ui/components/CanvasPanels/index.tsx index 6bfc05c..faee3ec 100644 --- a/frontend/src/ui/components/CanvasPanels/index.tsx +++ b/frontend/src/ui/components/CanvasPanels/index.tsx @@ -4,7 +4,7 @@ import { ChevronDown, ChevronRight, ChevronUp, Download, Eye, EyeOff, Frame, Gri import { Fragment, useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent } from "react"; import type { AgentMessage, CanvasNode, CanvasViewport } from "@/domain/design"; import { useI18n } from "@/i18n/i18n"; -import { thumbnailImageUrl } from "@/ui/lib/imageDelivery"; +import { canvasNodeRenderContent, thumbnailImageUrl } from "@/ui/lib/imageDelivery"; import { isFrameContainerNode, type LayerReorderItem, type LayerReorderPlacement, type LayerReorderRequest } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps"; import { ShapeNodePreview } from "@/ui/pages/CanvasWorkspace/canvas/ShapeNodePreview"; import { isBooleanShapeGroupNode, isBooleanShapeOperandNode, isShapeNode } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode"; @@ -95,7 +95,7 @@ export function CanvasGeneratedFilesPanel({ }} > - {node.title} + {node.title} {layerTitle(node, t)}