mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-31 21:51:15 +08:00
feat(markdown-node): improve rendering performance and user interaction by caching parsed Markdown and optimizing DOM updates
This commit is contained in:
@@ -43,7 +43,7 @@ import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
|
||||
import { useAgentStore } from "@/stores/use-agent-store";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { buildCanvasResourceReferences, buildNodeMentionReferences } from "@/lib/canvas/canvas-resource-references";
|
||||
import { buildCanvasResourceReferences, buildNodeMentionReferences, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { getNodeDefinition, isBuiltinNodeType as isBuiltinType, listNodeDefinitions, useNodeRegistryVersion } from "@/lib/canvas/node-registry";
|
||||
import { buildNodeContext } from "@/lib/canvas/plugin-node-context";
|
||||
import { ensurePluginsLoaded } from "@/lib/canvas/plugin-loader";
|
||||
@@ -102,6 +102,8 @@ type CanvasGenerationRequest = {
|
||||
|
||||
const VIDEO_NODE_MAX_WIDTH = 420;
|
||||
const VIDEO_NODE_MAX_HEIGHT = 420;
|
||||
// 稳定的空引用数组:避免每次渲染 `... || []` 产生新数组引用而击穿 CanvasNode 的 React.memo
|
||||
const EMPTY_REFERENCES: CanvasResourceReference[] = [];
|
||||
const CONNECTION_HANDLE_HIT_RADIUS = 40;
|
||||
const CONNECTION_NODE_HIT_PADDING = 32;
|
||||
const NODE_STATUS_IDLE = "idle" as const;
|
||||
@@ -2701,6 +2703,68 @@ function InfiniteCanvasPage() {
|
||||
[insertAssistantImage, insertAssistantText, screenToCanvas, size.height, size.width],
|
||||
);
|
||||
|
||||
// --- 传给 CanvasNode 的回调/渲染函数统一 memo 化 ---
|
||||
// CanvasNode 是 React.memo,但只要这些 prop 每次渲染都是新引用,memo 就失效,
|
||||
// 导致点击/悬停/移动视角时全部节点跟着重渲染(markdown 尤其明显)。全部 useCallback 后,
|
||||
// 未变化的节点不再重渲染。依赖里的 map/handler 均已 memo 化,纯交互时保持稳定。
|
||||
const handleNodeHoverStart = useCallback((nodeId: string) => {
|
||||
if (nodeDraggingRef.current) return;
|
||||
setHoveredNodeId(nodeId);
|
||||
}, []);
|
||||
const handleNodeHoverEnd = useCallback((nodeId: string) => {
|
||||
setHoveredNodeId((current) => (current === nodeId ? null : current));
|
||||
}, []);
|
||||
const handleNodeViewImage = useCallback((node: CanvasNodeData) => setPreviewNodeId(node.id), []);
|
||||
const handleNodeRetry = useCallback((node: CanvasNodeData) => void handleRetryNode(node), [handleRetryNode]);
|
||||
const handleNodeContextMenu = useCallback((event: ReactMouseEvent, nodeId: string) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenu({ type: "node", x: event.clientX, y: event.clientY, nodeId });
|
||||
}, []);
|
||||
|
||||
const renderNodePanel = useCallback(
|
||||
(panelNode: CanvasNodeData) =>
|
||||
getNodeDefinition(panelNode.type)?.Panel ? (
|
||||
renderPluginPanel(panelNode)
|
||||
) : panelNode.type === CanvasNodeType.Config ? (
|
||||
<CanvasConfigComposer value={panelNode.metadata?.composerContent ?? panelNode.metadata?.prompt ?? ""} inputs={configInputsById.get(panelNode.id) || []} onChange={(composerContent) => handleConfigNodeChange(panelNode.id, { composerContent })} onClose={() => setDialogNodeId(null)} />
|
||||
) : (
|
||||
<CanvasNodePromptPanel
|
||||
node={panelNode}
|
||||
isRunning={runningNodeId === panelNode.id}
|
||||
mentionReferences={mentionReferencesByNodeId.get(panelNode.id) || EMPTY_REFERENCES}
|
||||
onPromptChange={handleNodePromptChange}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onGenerate={handleGenerateNode}
|
||||
onStop={confirmStopGeneration}
|
||||
modeOverride={getNodeDefinition(panelNode.type)?.useBuiltinPanel?.mode}
|
||||
onImageSettingsOpenChange={(open) => {
|
||||
setNodeImageSettingsOpen(open);
|
||||
if (open) setToolbarNodeId(null);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[configInputsById, confirmStopGeneration, handleConfigNodeChange, handleGenerateNode, handleNodePromptChange, mentionReferencesByNodeId, renderPluginPanel, runningNodeId],
|
||||
);
|
||||
|
||||
const renderNodeContentPanel = useCallback(
|
||||
(contentNode: CanvasNodeData) => (
|
||||
<CanvasConfigNodePanel
|
||||
node={contentNode}
|
||||
isRunning={runningNodeId === contentNode.id}
|
||||
inputSummary={getInputSummary(configInputsById.get(contentNode.id) || [])}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onComposerToggle={() => setDialogNodeId((current) => (current === contentNode.id ? null : contentNode.id))}
|
||||
onStop={confirmStopGeneration}
|
||||
onGenerate={(nodeId) => {
|
||||
const target = nodesRef.current.find((item) => item.id === nodeId);
|
||||
void handleGenerateNode(nodeId, target?.metadata?.generationMode || "image", target?.metadata?.composerContent ?? target?.metadata?.prompt ?? "");
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[configInputsById, confirmStopGeneration, handleConfigNodeChange, handleGenerateNode, runningNodeId],
|
||||
);
|
||||
|
||||
if (!projectLoaded) return <CanvasRefreshShell />;
|
||||
|
||||
return (
|
||||
@@ -2804,73 +2868,25 @@ function InfiniteCanvasPage() {
|
||||
batchMotion={batchMotionById.get(node.id)}
|
||||
showImageInfo={showImageInfo}
|
||||
resourceLabel={resourceReferenceByNodeId.get(node.id)}
|
||||
mentionReferences={mentionReferencesByNodeId.get(node.id) || []}
|
||||
mentionReferences={mentionReferencesByNodeId.get(node.id) || EMPTY_REFERENCES}
|
||||
pluginHost={pluginHost}
|
||||
registryVersion={nodeRegistryVersion}
|
||||
renderPanel={(panelNode) =>
|
||||
getNodeDefinition(panelNode.type)?.Panel ? (
|
||||
renderPluginPanel(panelNode)
|
||||
) : panelNode.type === CanvasNodeType.Config ? (
|
||||
<CanvasConfigComposer
|
||||
value={panelNode.metadata?.composerContent ?? panelNode.metadata?.prompt ?? ""}
|
||||
inputs={configInputsById.get(panelNode.id) || []}
|
||||
onChange={(composerContent) => handleConfigNodeChange(panelNode.id, { composerContent })}
|
||||
onClose={() => setDialogNodeId(null)}
|
||||
/>
|
||||
) : (
|
||||
<CanvasNodePromptPanel
|
||||
node={panelNode}
|
||||
isRunning={runningNodeId === panelNode.id}
|
||||
mentionReferences={mentionReferencesByNodeId.get(panelNode.id) || []}
|
||||
onPromptChange={handleNodePromptChange}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onGenerate={handleGenerateNode}
|
||||
onStop={confirmStopGeneration}
|
||||
modeOverride={getNodeDefinition(panelNode.type)?.useBuiltinPanel?.mode}
|
||||
onImageSettingsOpenChange={(open) => {
|
||||
setNodeImageSettingsOpen(open);
|
||||
if (open) setToolbarNodeId(null);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
renderNodeContent={(contentNode) => (
|
||||
<CanvasConfigNodePanel
|
||||
node={contentNode}
|
||||
isRunning={runningNodeId === contentNode.id}
|
||||
inputSummary={getInputSummary(configInputsById.get(contentNode.id) || [])}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onComposerToggle={() => setDialogNodeId((current) => (current === contentNode.id ? null : contentNode.id))}
|
||||
onStop={confirmStopGeneration}
|
||||
onGenerate={(nodeId) => {
|
||||
const target = nodesRef.current.find((item) => item.id === nodeId);
|
||||
void handleGenerateNode(nodeId, target?.metadata?.generationMode || "image", target?.metadata?.composerContent ?? target?.metadata?.prompt ?? "");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
renderPanel={renderNodePanel}
|
||||
renderNodeContent={renderNodeContentPanel}
|
||||
onMouseDown={handleNodeMouseDown}
|
||||
onSelectCapture={handleNodeSelectCapture}
|
||||
onHoverStart={(nodeId) => {
|
||||
if (nodeDraggingRef.current) return;
|
||||
setHoveredNodeId(nodeId);
|
||||
}}
|
||||
onHoverEnd={(nodeId) => {
|
||||
setHoveredNodeId((current) => (current === nodeId ? null : current));
|
||||
}}
|
||||
onHoverStart={handleNodeHoverStart}
|
||||
onHoverEnd={handleNodeHoverEnd}
|
||||
onConnectStart={handleConnectStart}
|
||||
onResize={handleNodeResize}
|
||||
onContentChange={handleNodeContentChange}
|
||||
onTitleChange={handleNodeTitleChange}
|
||||
onToggleBatch={toggleBatchExpanded}
|
||||
onSetBatchPrimary={setBatchPrimary}
|
||||
onRetry={(node) => void handleRetryNode(node)}
|
||||
onRetry={handleNodeRetry}
|
||||
onGenerateImage={generateImageFromTextNode}
|
||||
onViewImage={(node) => setPreviewNodeId(node.id)}
|
||||
onContextMenu={(event, id) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenu({ type: "node", x: event.clientX, y: event.clientY, nodeId: id });
|
||||
}}
|
||||
onViewImage={handleNodeViewImage}
|
||||
onContextMenu={handleNodeContextMenu}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user