(nodeIds: Iterable<string>, duration = 300)
| 2372 | } |
| 2373 | |
| 2374 | zoomToNodes(nodeIds: Iterable<string>, duration = 300): void { |
| 2375 | const positions: { x: number; y: number }[] = []; |
| 2376 | for (const id of nodeIds) { |
| 2377 | const node = this.nodes.get(id); |
| 2378 | if (!node?.visible) continue; |
| 2379 | // In 3D, use projected positions (rotation paused on click so they're stable) |
| 2380 | if (this.mode3d) { |
| 2381 | positions.push({ |
| 2382 | x: node.sprite.position.x, |
| 2383 | y: node.sprite.position.y, |
| 2384 | }); |
| 2385 | } else { |
| 2386 | positions.push({ x: node.x, y: node.y }); |
| 2387 | } |
| 2388 | } |
| 2389 | if (positions.length === 0) return; |
| 2390 | const bounds = computeBounds(positions); |
| 2391 | const target = fitBounds(bounds, this.width, this.height, 120); |
| 2392 | |
| 2393 | // Focusing on specific nodes is a deliberate user action — stop auto-fit. |
| 2394 | this.hasUserMovedCamera = true; |
| 2395 | |
| 2396 | this.cancelAnimation?.(); |
| 2397 | this.cancelAnimation = animateViewport( |
| 2398 | this.vp, |
| 2399 | target, |
| 2400 | duration, |
| 2401 | (vp) => { |
| 2402 | this.vp = vp; |
| 2403 | }, |
| 2404 | () => { |
| 2405 | this.redrawAllEdges(); |
| 2406 | this.cancelAnimation = null; |
| 2407 | }, |
| 2408 | ); |
| 2409 | } |
| 2410 | |
| 2411 | zoomIn(duration = 200): void { |
| 2412 | // Mark this as a user-driven zoom so the auto-fit follower |
no test coverage detected