perf(canvas): 画布渲染 - 降低大画布节点开销
This commit is contained in:
@@ -5,6 +5,7 @@ import { AlertCircle, BookOpenCheck, CheckCircle2, ChevronRight, Clapperboard, C
|
||||
import { useCanvasNodeActions } from "./canvas-node-action-context";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import type { CanvasNodeRenderLOD } from "@/lib/canvas/canvas-node-lod";
|
||||
import { canvasConnectionTilt } from "@/lib/canvas/canvas-connection-tilt";
|
||||
import { storyboardMinNodeHeight } from "@/lib/canvas/canvas-storyboard-layout";
|
||||
import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } from "@/lib/canvas/resource-storage-status";
|
||||
@@ -20,6 +21,7 @@ type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
|
||||
type CanvasNodeProps = {
|
||||
data: CanvasNodeData;
|
||||
renderLOD?: CanvasNodeRenderLOD;
|
||||
dragOffset?: Position;
|
||||
scale: number;
|
||||
isSelected: boolean;
|
||||
@@ -68,6 +70,7 @@ type CanvasNodeProps = {
|
||||
|
||||
export const CanvasNode = React.memo(function CanvasNode({
|
||||
data,
|
||||
renderLOD = "full",
|
||||
dragOffset,
|
||||
scale,
|
||||
isSelected,
|
||||
@@ -128,6 +131,8 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
const isBatchChild = data.type === CanvasNodeType.Image && Boolean(data.metadata?.batchRootId);
|
||||
const showStatusTrack = Boolean(resourceLabel || data.metadata?.locked || isBatchRoot || (isBatchChild && !readOnly) || (hasMediaContent && !readOnly));
|
||||
const isActive = isConnectionTarget || isSelected || isFocusRelated;
|
||||
const effectiveRenderLOD: CanvasNodeRenderLOD = renderLOD === "full" || hovered || isEditingContent || isEditingTitle || mediaActive ? "full" : renderLOD;
|
||||
const showChrome = effectiveRenderLOD === "full";
|
||||
const nodeState = isFocusRelated ? "focus" : isConnectionTarget ? "target" : isSelected ? "selected" : isRelated && !isBatchChild ? "related" : "idle";
|
||||
const showOutputConnection = getNodeDefinition(data.type)?.showOutputConnection !== false;
|
||||
const assetTags = data.metadata?.assetTags?.filter((tag) => tag.trim()) || [];
|
||||
@@ -272,6 +277,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
return (
|
||||
<div
|
||||
data-node-id={data.id}
|
||||
data-node-lod={effectiveRenderLOD}
|
||||
className={`node-element absolute flex select-none flex-col ${dragOffset ? "cursor-grabbing" : data.type === CanvasNodeType.Drawing ? "cursor-pointer" : "cursor-default"} ${isSelected && data.type === CanvasNodeType.Video ? "z-[var(--z-node-toolbar)]" : isSelected || isFocusRelated || isConnectionTarget ? "z-[var(--z-node-active)]" : "z-[var(--z-node)]"}`}
|
||||
style={{
|
||||
transform: `translate(${data.position.x + (dragOffset?.x || 0)}px, ${data.position.y + (dragOffset?.y || 0)}px)`,
|
||||
@@ -289,7 +295,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
}}
|
||||
onContextMenu={(event) => onContextMenu(event, data.id)}
|
||||
>
|
||||
<NodeExternalHeader
|
||||
{showChrome ? <NodeExternalHeader
|
||||
node={data}
|
||||
scale={scale}
|
||||
dimensionLabel={mediaDimensionLabel}
|
||||
@@ -303,7 +309,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
onEdit={() => setIsEditingTitle(true)}
|
||||
onCommit={commitTitle}
|
||||
onCancel={() => { setTitleDraft(data.title); setIsEditingTitle(false); }}
|
||||
/>
|
||||
/> : null}
|
||||
<div
|
||||
className="canvas-node-shell relative h-full w-full overflow-visible rounded-[var(--node-radius)]"
|
||||
data-node-state={nodeState}
|
||||
@@ -313,7 +319,8 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
background: hasImageContent || hasVideoContent ? "transparent" : theme.node.fill,
|
||||
// 固定占位但不绘制描边,避免聚焦切换时边框宽度变化造成白边跳动。
|
||||
border: isComposerNode ? "0" : "1px solid transparent",
|
||||
boxShadow: isComposerNode ? "none" : isSelected || isFocusRelated ? theme.node.hoverShadow : theme.node.shadow,
|
||||
boxShadow: isComposerNode || !showChrome ? "none" : isSelected || isFocusRelated ? theme.node.hoverShadow : theme.node.shadow,
|
||||
outline: !showChrome && isActive ? `1px solid ${theme.node.activeStroke}` : undefined,
|
||||
"--connection-tilt-x": `${connectionTilt?.rotateX || 0}deg`,
|
||||
"--connection-tilt-y": `${connectionTilt?.rotateY || 0}deg`,
|
||||
transformOrigin: connectionTilt?.origin,
|
||||
@@ -364,12 +371,13 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
}
|
||||
>
|
||||
{/* 节点状态徽章(对应 #97 决策2:左上角 loading/success/error,近距离确认信号)*/}
|
||||
{data.metadata?.status && data.metadata.status !== "idle" && data.type !== CanvasNodeType.Frame ? (
|
||||
{showChrome && data.metadata?.status && data.metadata.status !== "idle" && data.type !== CanvasNodeType.Frame ? (
|
||||
<NodeStatusBadge status={data.metadata.status} />
|
||||
) : null}
|
||||
<CanvasNodeContent
|
||||
node={data}
|
||||
theme={theme}
|
||||
renderLOD={effectiveRenderLOD}
|
||||
isEditingContent={isEditingContent}
|
||||
textareaRef={textareaRef}
|
||||
isBatchRoot={isBatchRoot}
|
||||
@@ -393,7 +401,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.type === CanvasNodeType.Text && data.metadata?.workflowKind !== "character" && !readOnly ? (
|
||||
{showChrome && data.type === CanvasNodeType.Text && data.metadata?.workflowKind !== "character" && !readOnly ? (
|
||||
<div
|
||||
className={`absolute bottom-[10%] left-1/2 z-[var(--node-z-overlay)] -translate-x-1/2 motion-safe:transition motion-safe:duration-200 ${isSelected ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0"}`}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
@@ -412,7 +420,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{data.metadata?.versionLabel ? (
|
||||
{showChrome && data.metadata?.versionLabel ? (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute left-3 top-3 z-[var(--node-z-overlay)] grid size-7 place-items-center rounded-[var(--r-full)] border p-0.5 text-[var(--node-badge-fs)] font-semibold leading-none backdrop-blur-md transition-[transform,background,border-color,box-shadow] hover:-translate-y-px focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:hover:translate-y-0"
|
||||
@@ -430,7 +438,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
{data.metadata.versionLabel}
|
||||
</button>
|
||||
) : null}
|
||||
{showStatusTrack ? (
|
||||
{showChrome && showStatusTrack ? (
|
||||
<div className={`absolute right-3 top-3 z-[var(--node-z-overlay)] flex min-w-0 items-center justify-end gap-1 ${data.metadata?.versionLabel ? "max-w-[calc(100%-104px)]" : "max-w-[calc(100%-24px)]"}`}>
|
||||
{resourceLabel && data.type !== CanvasNodeType.Image ? <ResourceLabelBadge reference={resourceLabel} theme={theme} /> : null}
|
||||
{hasMediaContent && !readOnly ? <ResourceStorageBadge storageKey={data.metadata?.storageKey} active={isActive} theme={theme} /> : null}
|
||||
@@ -440,7 +448,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
</div>
|
||||
) : null}
|
||||
{/* 批次子图操作条:成功子项提供下载/副本/设为主图,失败子项提供重试/删除 */}
|
||||
{isBatchChild && !readOnly && (hasImageContent || data.metadata?.status === "error") && (hovered || isSelected) ? (
|
||||
{showChrome && isBatchChild && !readOnly && (hasImageContent || data.metadata?.status === "error") && (hovered || isSelected) ? (
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-2 z-[var(--node-z-overlay)] flex justify-center"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
@@ -459,19 +467,19 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
</div>
|
||||
) : null}
|
||||
{/* 批次主图位(折叠根节点封面)常驻下载按钮 */}
|
||||
{isBatchRoot && hasImageContent && !readOnly ? (
|
||||
{showChrome && isBatchRoot && hasImageContent && !readOnly ? (
|
||||
<div className="absolute bottom-2 right-2 z-[var(--node-z-overlay)]" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<BatchChildActionButton theme={theme} label="下载主图" icon={<Download className="size-3.5" />} onClick={() => downloadNode?.(data)} />
|
||||
</div>
|
||||
) : null}
|
||||
{assetTags.length || (showImageInfo && hasImageContent) ? (
|
||||
{showChrome && (assetTags.length || (showImageInfo && hasImageContent)) ? (
|
||||
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-[var(--node-z-overlay)] flex items-end justify-between gap-2">
|
||||
{assetTags.length ? <AssetTagBadges tags={assetTags} theme={theme} /> : null}
|
||||
{showImageInfo && hasImageContent ? <CanvasNodeImageInfo node={data} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!readOnly && !data.metadata?.locked && (isSelected || hovered) ? <>
|
||||
{showChrome && !readOnly && !data.metadata?.locked && (isSelected || hovered) ? <>
|
||||
<ResizeHandle corner="top-left" onMouseDown={handleResizeMouseDown} />
|
||||
<ResizeHandle corner="top-right" onMouseDown={handleResizeMouseDown} />
|
||||
<ResizeHandle corner="bottom-left" onMouseDown={handleResizeMouseDown} />
|
||||
@@ -479,8 +487,8 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
</> : null}
|
||||
</div>
|
||||
|
||||
{!readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.BatchTable ? <ConnectionSideRail side="left" scale={scale} theme={theme} visible={hovered || forceInputVisible} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "target", undefined, anchorRatio)} /> : null}
|
||||
{!readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.Config && showOutputConnection ? <ConnectionSideRail side="right" scale={scale} theme={theme} visible={hovered} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "source", undefined, anchorRatio)} /> : null}
|
||||
{(showChrome || forceInputVisible) && !readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.BatchTable ? <ConnectionSideRail side="left" scale={scale} theme={theme} visible={hovered || forceInputVisible} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "target", undefined, anchorRatio)} /> : null}
|
||||
{showChrome && !readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.Config && showOutputConnection ? <ConnectionSideRail side="right" scale={scale} theme={theme} visible={hovered} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "source", undefined, anchorRatio)} /> : null}
|
||||
|
||||
</div>
|
||||
);
|
||||
@@ -489,6 +497,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
function areCanvasNodePropsEqual(previous: CanvasNodeProps, next: CanvasNodeProps) {
|
||||
return (
|
||||
previous.data === next.data &&
|
||||
previous.renderLOD === next.renderLOD &&
|
||||
previous.dragOffset?.x === next.dragOffset?.x &&
|
||||
previous.dragOffset?.y === next.dragOffset?.y &&
|
||||
previous.scale === next.scale &&
|
||||
|
||||
Reference in New Issue
Block a user