• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 19792929756

30 Nov 2025 02:57AM UTC coverage: 86.308% (+3.9%) from 82.377%
19792929756

push

github

apowers313
docs: fix stories for chromatic

3676 of 4303 branches covered (85.43%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 2 files covered. (100.0%)

1093 existing lines in 30 files now uncovered.

17371 of 20083 relevant lines covered (86.5%)

7075.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.29
/src/managers/UpdateManager.ts
1
import type {Mesh, Vector3} from "@babylonjs/core";
2

3
import type {CameraManager} from "../cameras/CameraManager";
4
import {Edge} from "../Edge";
2✔
5
import type {DataManager} from "./DataManager";
6
import type {EventManager} from "./EventManager";
7
import type {GraphContext} from "./GraphContext";
8
import type {Manager} from "./interfaces";
9
import type {LayoutManager} from "./LayoutManager";
10
import type {StatsManager} from "./StatsManager";
11
import type {StyleManager} from "./StyleManager";
12

13
/**
14
 * Configuration for the UpdateManager
15
 */
16
export interface UpdateManagerConfig {
17
    /**
18
     * Number of layout steps to perform per update
19
     */
20
    layoutStepMultiplier?: number;
21

22
    /**
23
     * Whether to automatically zoom to fit on first load
24
     */
25
    autoZoomToFit?: boolean;
26

27
    /**
28
     * Minimum bounding box size to trigger zoom to fit
29
     */
30
    minBoundingBoxSize?: number;
31
}
32

33
/**
34
 * Manages the update loop logic for the graph
35
 * Coordinates updates across nodes, edges, layout, and camera
36
 */
37
export class UpdateManager implements Manager {
2✔
38
    private needsZoomToFit = false;
2✔
39
    private hasZoomedToFit = false;
2✔
40
    private config: Required<UpdateManagerConfig>;
41
    private layoutStepCount = 0;
2✔
42
    private minLayoutStepsBeforeZoom = 10;
2✔
43
    private lastZoomStep = 0;
2✔
44
    private wasSettled = false;
2✔
45

46
    constructor(
2✔
47
        private eventManager: EventManager,
572✔
48
        private statsManager: StatsManager,
572✔
49
        private layoutManager: LayoutManager,
572✔
50
        private dataManager: DataManager,
572✔
51
        private styleManager: StyleManager,
572✔
52
        private camera: CameraManager,
572✔
53
        private graphContext: GraphContext,
572✔
54
        config: UpdateManagerConfig = {},
572✔
55
    ) {
572✔
56
        this.config = {
572✔
57
            layoutStepMultiplier: config.layoutStepMultiplier ?? 1,
572!
58
            autoZoomToFit: config.autoZoomToFit ?? true,
572!
59
            minBoundingBoxSize: config.minBoundingBoxSize ?? 0.1,
572✔
60
        };
572✔
61
    }
572✔
62

63
    async init(): Promise<void> {
2✔
64
        // UpdateManager doesn't need async initialization
65
        return Promise.resolve();
510✔
66
    }
510✔
67

68
    dispose(): void {
2✔
69
        // UpdateManager doesn't hold resources to dispose
70
    }
469✔
71

72
    /**
73
     * Enable zoom to fit on next update
74
     */
75
    enableZoomToFit(): void {
2✔
76
        this.needsZoomToFit = true;
2,891✔
77
        // Only reset the layout step count if we haven't zoomed yet
78
        // This prevents the counter from being reset when enableZoomToFit is called multiple times
79
        if (!this.hasZoomedToFit) {
2,891✔
80
            this.layoutStepCount = 0;
2,882✔
81
            this.lastZoomStep = 0;
2,882✔
82
            this.wasSettled = false;
2,882✔
83
        }
2,882✔
84
    }
2,891✔
85

86
    /**
87
     * Disable zoom to fit
88
     */
89
    disableZoomToFit(): void {
2✔
90
        this.needsZoomToFit = false;
×
91
    }
×
92

93
    /**
94
     * Get current zoom to fit state
95
     */
96
    isZoomToFitEnabled(): boolean {
2✔
97
        return this.needsZoomToFit;
4✔
98
    }
4✔
99

100
    /**
101
     * Get the current render frame count
102
     */
103
    getRenderFrameCount(): number {
2✔
104
        return this.frameCount;
4✔
105
    }
4✔
106

107
    /**
108
     * Render a fixed number of frames (for testing)
109
     * This ensures deterministic rendering similar to Babylon.js testing approach
110
     */
111
    renderFixedFrames(count: number): void {
2✔
112
        for (let i = 0; i < count; i++) {
2✔
113
            this.update();
10✔
114
        }
10✔
115
    }
2✔
116

117
    /**
118
     * Main update method - called by RenderManager each frame
119
     */
120
    private frameCount = 0;
2✔
121

122
    update(): void {
2✔
123
        this.frameCount++;
7,608✔
124

125
        // Always update camera
126
        this.camera.update();
7,608✔
127

128
        // Check if layout is running
129
        if (!this.layoutManager.running) {
7,608✔
130
            // Even if layout is not running, we still need to handle zoom if requested
131
            if (this.needsZoomToFit && !this.hasZoomedToFit) {
5,585✔
132
                // Check if we have nodes to calculate bounds from
133
                const nodeCount = Array.from(this.layoutManager.nodes).length;
1,839✔
134
                if (nodeCount > 0) {
1,839✔
135
                    // Calculate bounding box and update nodes
136
                    const {boundingBoxMin, boundingBoxMax} = this.updateNodes();
214✔
137

138
                    // Update edges (also expands bounding box for edge labels)
139
                    this.updateEdges(boundingBoxMin, boundingBoxMax);
214✔
140

141
                    // Handle zoom to fit
142
                    this.handleZoomToFit(boundingBoxMin, boundingBoxMax);
214✔
143

144
                    // Update statistics
145
                    this.updateStatistics();
214✔
146
                }
214✔
147
            }
1,839✔
148

149
            return;
5,585✔
150
        }
5,585✔
151

152
        // Update layout engine (step the force-directed algorithm)
153
        this.updateLayout();
2,023✔
154

155
        // Update nodes and edges
156
        const {boundingBoxMin, boundingBoxMax} = this.updateNodes();
2,023✔
157

158
        // Update edges (also expands bounding box for edge labels)
159
        this.updateEdges(boundingBoxMin, boundingBoxMax);
2,023✔
160

161
        // Handle zoom to fit if needed
162
        this.handleZoomToFit(boundingBoxMin, boundingBoxMax);
2,023✔
163

164
        // Update statistics
165
        this.updateStatistics();
2,023✔
166
    }
7,608✔
167

168
    /**
169
     * Update the layout engine
170
     */
171
    private updateLayout(): void {
2✔
172
        this.statsManager.step();
2,023✔
173
        this.statsManager.graphStep.beginMonitoring();
2,023✔
174

175
        const {stepMultiplier} = this.styleManager.getStyles().config.behavior.layout;
2,023✔
176
        for (let i = 0; i < stepMultiplier; i++) {
2,023✔
177
            this.layoutManager.step();
2,023✔
178
            this.layoutStepCount++;
2,023✔
179
        }
2,023✔
180

181
        this.statsManager.graphStep.endMonitoring();
2,023✔
182
    }
2,023✔
183

184
    /**
185
     * Update all nodes and calculate bounding box
186
     */
187
    private updateNodes(): {boundingBoxMin?: Vector3, boundingBoxMax?: Vector3} {
2✔
188
        let boundingBoxMin: Vector3 | undefined;
2,237✔
189
        let boundingBoxMax: Vector3 | undefined;
2,237✔
190

191
        this.statsManager.nodeUpdate.beginMonitoring();
2,237✔
192

193
        for (const node of this.layoutManager.nodes) {
2,237✔
194
            node.update();
44,480✔
195

196
            // The mesh position is already updated by node.update()
197

198
            // Update bounding box
199
            const pos = node.mesh.getAbsolutePosition();
44,480✔
200
            const sz = node.size;
44,480✔
201

202
            if (!boundingBoxMin || !boundingBoxMax) {
44,480✔
203
                boundingBoxMin = pos.clone();
2,062✔
204
                boundingBoxMax = pos.clone();
2,062✔
205
            }
2,062✔
206

207
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "x");
44,480✔
208
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "y");
44,480✔
209
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "z");
44,480✔
210

211
            // Include node label in bounding box
212
            if (node.label?.labelMesh) {
44,480✔
213
                this.expandBoundingBoxForLabel(node.label.labelMesh, boundingBoxMin, boundingBoxMax);
5,394✔
214
            }
5,394✔
215
        }
44,480✔
216

217
        this.statsManager.nodeUpdate.endMonitoring();
2,237✔
218

219
        return {boundingBoxMin, boundingBoxMax};
2,237✔
220
    }
2,237✔
221

222
    /**
223
     * Update bounding box for a single axis
224
     */
225
    private updateBoundingBoxAxis(
2✔
226
        pos: Vector3,
133,440✔
227
        min: Vector3,
133,440✔
228
        max: Vector3,
133,440✔
229
        size: number,
133,440✔
230
        axis: "x" | "y" | "z",
133,440✔
231
    ): void {
133,440✔
232
        const value = pos[axis];
133,440✔
233
        const halfSize = size / 2;
133,440✔
234

235
        min[axis] = Math.min(min[axis], value - halfSize);
133,440✔
236
        max[axis] = Math.max(max[axis], value + halfSize);
133,440✔
237
    }
133,440✔
238

239
    /**
240
     * Expand bounding box to include a label mesh
241
     */
242
    private expandBoundingBoxForLabel(labelMesh: Mesh, min: Vector3, max: Vector3): void {
2✔
243
        const labelBoundingInfo = labelMesh.getBoundingInfo();
5,492✔
244
        const labelMin = labelBoundingInfo.boundingBox.minimumWorld;
5,492✔
245
        const labelMax = labelBoundingInfo.boundingBox.maximumWorld;
5,492✔
246

247
        min.x = Math.min(min.x, labelMin.x);
5,492✔
248
        min.y = Math.min(min.y, labelMin.y);
5,492✔
249
        min.z = Math.min(min.z, labelMin.z);
5,492✔
250
        max.x = Math.max(max.x, labelMax.x);
5,492✔
251
        max.y = Math.max(max.y, labelMax.y);
5,492✔
252
        max.z = Math.max(max.z, labelMax.z);
5,492✔
253
    }
5,492✔
254

255
    /**
256
     * Update all edges and expand bounding box for edge labels
257
     */
258
    private updateEdges(boundingBoxMin?: Vector3, boundingBoxMax?: Vector3): void {
2✔
259
        this.statsManager.edgeUpdate.beginMonitoring();
2,237✔
260

261
        // Update rays for all edges (static method on Edge class)
262
        Edge.updateRays(this.graphContext);
2,237✔
263

264
        // Update individual edges
265
        for (const edge of this.layoutManager.edges) {
2,237✔
266
            edge.update();
77,313✔
267

268
            // Include edge labels in bounding box if we have one
269
            if (boundingBoxMin && boundingBoxMax) {
77,313✔
270
                // Edge label (at midpoint)
271
                if (edge.label?.labelMesh) {
77,313✔
272
                    this.expandBoundingBoxForLabel(edge.label.labelMesh, boundingBoxMin, boundingBoxMax);
42✔
273
                }
42✔
274

275
                // Arrow head text label
276
                if (edge.arrowHeadText?.labelMesh) {
77,313✔
277
                    this.expandBoundingBoxForLabel(edge.arrowHeadText.labelMesh, boundingBoxMin, boundingBoxMax);
54✔
278
                }
54✔
279

280
                // Arrow tail text label
281
                if (edge.arrowTailText?.labelMesh) {
77,313✔
282
                    this.expandBoundingBoxForLabel(edge.arrowTailText.labelMesh, boundingBoxMin, boundingBoxMax);
2✔
283
                }
2✔
284
            }
77,313✔
285
        }
77,313✔
286

287
        this.statsManager.edgeUpdate.endMonitoring();
2,237✔
288
    }
2,237✔
289

290
    /**
291
     * Handle zoom to fit logic
292
     */
293
    private handleZoomToFit(boundingBoxMin?: Vector3, boundingBoxMax?: Vector3): void {
2✔
294
        if (!this.needsZoomToFit) {
2,237!
UNCOV
295
            return;
×
UNCOV
296
        }
×
297

298
        if (!boundingBoxMin || !boundingBoxMax) {
2,237✔
299
            return;
175✔
300
        }
175✔
301

302
        // Check if we should zoom:
303
        // 1. Wait for minimum steps on first zoom
304
        // 2. Zoom every N steps during layout (based on zoomStepInterval)
305
        // 3. Zoom when layout settles
306
        const isSettled = this.layoutManager.layoutEngine?.isSettled ?? false;
2,237!
307
        const {zoomStepInterval} = this.styleManager.getStyles().config.behavior.layout;
2,237✔
308
        const shouldZoomPeriodically = this.layoutStepCount > 0 &&
2,237✔
309
                                      this.layoutStepCount >= this.lastZoomStep + zoomStepInterval;
1,848✔
310
        const justSettled = isSettled && !this.wasSettled && this.layoutStepCount > 0;
2,237✔
311

312
        if (!this.hasZoomedToFit && this.layoutManager.running && this.layoutStepCount < this.minLayoutStepsBeforeZoom) {
2,237✔
313
            // First zoom - wait for minimum steps
314
            return;
872✔
315
        } else if (!this.layoutManager.running && !this.hasZoomedToFit && this.layoutStepCount === 0) {
2,039✔
316
            // Layout not running and no steps taken - allow immediate zoom
317
        } else if (!shouldZoomPeriodically && !justSettled) {
1,190!
318
            // Not time for periodic zoom and didn't just settle
UNCOV
319
            return;
×
UNCOV
320
        }
✔
321

322
        // Update settled state for next frame
323
        this.wasSettled = isSettled;
1,190✔
324

325
        const size = boundingBoxMax.subtract(boundingBoxMin);
1,190✔
326

327
        if (size.length() > this.config.minBoundingBoxSize) {
1,190✔
328
            this.camera.zoomToBoundingBox(boundingBoxMin, boundingBoxMax);
1,190✔
329

330
            this.hasZoomedToFit = true;
1,190✔
331
            this.lastZoomStep = this.layoutStepCount;
1,190✔
332

333
            // Only clear needsZoomToFit if layout is settled
334
            if (isSettled) {
1,190✔
335
                this.needsZoomToFit = false;
219✔
336
            }
219✔
337

338
            // Emit zoom complete event
339
            this.eventManager.emitGraphEvent("zoom-to-fit-complete", {
1,190✔
340
                boundingBoxMin,
1,190✔
341
                boundingBoxMax,
1,190✔
342
            });
1,190✔
343
        }
1,190✔
344
    }
2,237✔
345

346
    /**
347
     * Update statistics
348
     */
349
    private updateStatistics(): void {
2✔
350
        this.statsManager.updateCounts(
2,237✔
351
            this.dataManager.nodeCache.size,
2,237✔
352
            this.dataManager.edgeCache.size,
2,237✔
353
        );
2,237✔
354

355
        // Update mesh cache stats
356
        const meshCache = this.graphContext.getMeshCache();
2,237✔
357
        this.statsManager.updateCacheStats(meshCache.hits, meshCache.misses);
2,237✔
358
    }
2,237✔
359

360
    /**
361
     * Check if zoom to fit has been completed
362
     */
363
    get zoomToFitCompleted(): boolean {
2✔
364
        return this.hasZoomedToFit;
2✔
365
    }
2✔
366

367
    /**
368
     * Update configuration
369
     */
370
    updateConfig(config: Partial<UpdateManagerConfig>): void {
2✔
371
        Object.assign(this.config, config);
1✔
372
    }
1✔
373
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc