From 76f5eaa4467110858e1846d147e6db638df136f8 Mon Sep 17 00:00:00 2001 From: Vorflux AI Date: Fri, 27 Mar 2026 18:37:06 +0000 Subject: [PATCH] Fix zoom display regression: split viewport change concerns Pass popoverVisible flag from graph-canvas tick loop to the onViewportChange callback. The handler always updates zoomDisplay (so NavigationControls shows correct zoom %) but only increments viewportVersion (triggering popover repositioning) when a node is actually selected or hovered. This avoids 60fps React reconciliation during plain panning while keeping the zoom indicator responsive. --- .../memory-graph/src/components/graph-canvas.tsx | 15 ++++++--------- .../memory-graph/src/components/memory-graph.tsx | 16 ++++++++++++---- packages/memory-graph/src/types.ts | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx index e1a66129..f195e0ba 100644 --- a/packages/memory-graph/src/components/graph-canvas.tsx +++ b/packages/memory-graph/src/components/graph-canvas.tsx @@ -234,17 +234,14 @@ export const GraphCanvas = memo(function GraphCanvas({ return renderNeeded.current = false - // Report viewport changes so popover positions update. - // Only fire when a popover is actually visible (selected or hovered node) - // to avoid triggering React re-renders at 60fps during plain panning. + // Report viewport changes so zoom display and popover positions update. const vpChanged = vp.panX !== prevVpX || vp.panY !== prevVpY || vp.zoom !== prevVpZoom - if ( - (vpChanged || simActive) && - (cur.selectedNodeId || cur.hoveredNodeId) && - cb.current.onViewportChange - ) { - cb.current.onViewportChange(vp.zoom) + if ((vpChanged || simActive) && cb.current.onViewportChange) { + cb.current.onViewportChange( + vp.zoom, + !!(cur.selectedNodeId || cur.hoveredNodeId), + ) } if (vpChanged) { prevVpX = vp.panX diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 301236e6..c7a59c95 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -170,10 +170,18 @@ export function MemoryGraph({ // Drag end handled by InputHandler }, []) - const handleViewportChange = useCallback((zoom: number) => { - setZoomDisplay(Math.round(zoom * 100)) - setViewportVersion((v) => v + 1) - }, []) + const handleViewportChange = useCallback( + (zoom: number, popoverVisible: boolean) => { + setZoomDisplay(Math.round(zoom * 100)) + // Only increment viewportVersion (which triggers popover repositioning + // via activePopoverPosition useMemo) when a popover is actually visible. + // This avoids 60fps React reconciliation during plain panning/zooming. + if (popoverVisible) { + setViewportVersion((v) => v + 1) + } + }, + [], + ) // Navigation const handleAutoFit = useCallback(() => { diff --git a/packages/memory-graph/src/types.ts b/packages/memory-graph/src/types.ts index d95ab070..1e564b98 100644 --- a/packages/memory-graph/src/types.ts +++ b/packages/memory-graph/src/types.ts @@ -163,7 +163,7 @@ export interface GraphCanvasProps { onNodeClick: (nodeId: string | null) => void onNodeDragStart: (nodeId: string) => void onNodeDragEnd: () => void - onViewportChange?: (zoom: number) => void + onViewportChange?: (zoom: number, popoverVisible: boolean) => void canvasRef?: React.RefObject simulation?: import("./canvas/simulation").ForceSimulation viewportRef?: React.RefObject<