Skip to content

Commit 3835543

Browse files
committed
fix(app): dynamic camera framing to comfortably fit selected node and all connected neighbors
1 parent 152bb79 commit 3835543

1 file changed

Lines changed: 32 additions & 8 deletions

File tree

‎app/src/components/GraphCanvas.jsx‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,48 @@ export default function GraphCanvas({
104104
stateRef.current.camera.y = -selNode.y;
105105
stateRef.current.camera.targetX = -selNode.x;
106106
stateRef.current.camera.targetY = -selNode.y;
107-
stateRef.current.camera.scale = 1.35;
108-
stateRef.current.camera.targetScale = 1.35;
107+
stateRef.current.camera.scale = 0.95;
108+
stateRef.current.camera.targetScale = 0.95;
109109
}
110110
}
111111
}, [graphData]);
112112

113113
// Center on selected node when selection changes or panel opens/closes
114-
// When sidebar opens, offset the camera so the node remains centered in the visible left viewport
114+
// Calculates optimal zoom scale so the selected node AND all its connected neighbor nodes fit comfortably in the visible viewport
115115
useEffect(() => {
116116
if (!selectedNodeId) return;
117117
const node = stateRef.current.nodeMap?.get(selectedNodeId);
118118
if (node) {
119-
const targetScale = 1.35;
120-
119+
const links = stateRef.current.links || [];
120+
const neighborNodes = [];
121+
links.forEach(l => {
122+
if (l.source.id === node.id && l.target) neighborNodes.push(l.target);
123+
if (l.target.id === node.id && l.source) neighborNodes.push(l.source);
124+
});
125+
126+
let maxDist = 0;
127+
neighborNodes.forEach(n => {
128+
const d = Math.hypot(n.x - node.x, n.y - node.y);
129+
if (d > maxDist) maxDist = d;
130+
});
131+
132+
const winW = typeof window !== 'undefined' ? window.innerWidth : 1280;
133+
const winH = typeof window !== 'undefined' ? window.innerHeight : 800;
134+
const sidebarWidth = (isPanelOpen && winW >= 640) ? (winW >= 1024 ? 560 : 500) : 0;
135+
const visibleWidth = winW - sidebarWidth;
136+
const visibleHeight = winH;
137+
138+
// Ensure all neighbors fit with breathing room for node badges and labels
139+
const safeRadius = Math.max(160, maxDist + 80);
140+
const idealScaleX = (visibleWidth * 0.82) / (safeRadius * 2);
141+
const idealScaleY = (visibleHeight * 0.82) / (safeRadius * 2);
142+
const idealScale = Math.min(idealScaleX, idealScaleY);
143+
144+
// Clamp between 0.65 and 0.95 (never over-zoom, never excessively shrink)
145+
const targetScale = Math.max(0.65, Math.min(0.95, idealScale));
146+
121147
let offsetX = 0;
122-
if (isPanelOpen && typeof window !== 'undefined' && window.innerWidth >= 640) {
123-
const sidebarWidth = window.innerWidth >= 1024 ? 560 : 500;
124-
// Shift camera so the node is centered in (window.innerWidth - sidebarWidth)
148+
if (sidebarWidth > 0) {
125149
offsetX = (sidebarWidth / 2) / targetScale;
126150
}
127151

0 commit comments

Comments
 (0)