(duration = 300)
| 2246 | } |
| 2247 | |
| 2248 | zoomToFit(duration = 300): void { |
| 2249 | if (this.nodes.size === 0) return; |
| 2250 | const positions = Array.from(this.nodes.values()) |
| 2251 | .filter((n) => n.visible) |
| 2252 | .map((n) => |
| 2253 | this.mode3d |
| 2254 | ? { x: n.sprite.position.x, y: n.sprite.position.y } |
| 2255 | : { x: n.x, y: n.y }, |
| 2256 | ); |
| 2257 | if (positions.length === 0) return; |
| 2258 | const bounds = computeBounds(positions); |
| 2259 | const target = fitBounds(bounds, this.width, this.height); |
| 2260 | |
| 2261 | // Hand the camera off from any in-progress auto-fit follower to |
| 2262 | // this explicit one-shot animation (Fix #9). |
| 2263 | this.autoFitTarget = null; |
| 2264 | |
| 2265 | if (duration <= 0) { |
| 2266 | this.vp = target; |
| 2267 | this.applyCounterScale(); |
| 2268 | return; |
| 2269 | } |
| 2270 | |
| 2271 | this.cancelAnimation?.(); |
| 2272 | this.cancelAnimation = animateViewport( |
| 2273 | this.vp, |
| 2274 | target, |
| 2275 | duration, |
| 2276 | (vp) => { |
| 2277 | this.vp = vp; |
| 2278 | }, |
| 2279 | () => { |
| 2280 | this.redrawAllEdges(); |
| 2281 | this.cancelAnimation = null; |
| 2282 | }, |
| 2283 | ); |
| 2284 | } |
| 2285 | |
| 2286 | /** |
| 2287 | * Continuous easing auto-fit — refreshes the follower's target from live |
no test coverage detected