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

graphty-org / graphty-element / 16571675313

28 Jul 2025 02:22PM UTC coverage: 81.618% (+11.7%) from 69.879%
16571675313

push

github

apowers313
test: fix flaky tests

987 of 1241 branches covered (79.53%)

Branch coverage included in aggregate %.

5802 of 7077 relevant lines covered (81.98%)

705.34 hits per line

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

93.0
/src/managers/UpdateManager.ts
1
import type {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,
124✔
48
        private statsManager: StatsManager,
124✔
49
        private layoutManager: LayoutManager,
124✔
50
        private dataManager: DataManager,
124✔
51
        private styleManager: StyleManager,
124✔
52
        private camera: CameraManager,
124✔
53
        private graphContext: GraphContext,
124✔
54
        config: UpdateManagerConfig = {},
124✔
55
    ) {
124✔
56
        this.config = {
124✔
57
            layoutStepMultiplier: config.layoutStepMultiplier ?? 1,
124!
58
            autoZoomToFit: config.autoZoomToFit ?? true,
124!
59
            minBoundingBoxSize: config.minBoundingBoxSize ?? 0.1,
124✔
60
        };
124✔
61
    }
124✔
62

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

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

72
    /**
73
     * Enable zoom to fit on next update
74
     */
75
    enableZoomToFit(): void {
2✔
76
        this.needsZoomToFit = true;
415✔
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) {
415✔
80
            this.layoutStepCount = 0;
415✔
81
            this.lastZoomStep = 0;
415✔
82
            this.wasSettled = false;
415✔
83
        }
415✔
84
    }
415✔
85

86
    /**
87
     * Main update method - called by RenderManager each frame
88
     */
89
    private frameCount = 0;
2✔
90

91
    update(): void {
2✔
92
        this.frameCount++;
872✔
93

94
        // Always update camera
95
        this.camera.update();
872✔
96

97
        // Check if layout is running
98
        if (!this.layoutManager.running) {
872✔
99
            // Even if layout is not running, we still need to handle zoom if requested
100
            if (this.needsZoomToFit && !this.hasZoomedToFit) {
377✔
101
                // Check if we have nodes to calculate bounds from
102
                const nodeCount = Array.from(this.layoutManager.nodes).length;
150✔
103
                if (nodeCount > 0) {
150✔
104
                    // Calculate bounding box and update nodes
105
                    const {boundingBoxMin, boundingBoxMax} = this.updateNodes();
18✔
106

107
                    // Update edges
108
                    this.updateEdges();
18✔
109

110
                    // Handle zoom to fit
111
                    this.handleZoomToFit(boundingBoxMin, boundingBoxMax);
18✔
112

113
                    // Update statistics
114
                    this.updateStatistics();
18✔
115
                }
18✔
116
            }
150✔
117

118
            return;
377✔
119
        }
377✔
120

121
        // Normal update when layout is running
122
        // Update layout engine
123
        this.updateLayout();
495✔
124

125
        // Calculate bounding box and update nodes
126
        const {boundingBoxMin, boundingBoxMax} = this.updateNodes();
495✔
127

128
        // Update edges
129
        this.updateEdges();
495✔
130

131
        // Handle zoom to fit if needed
132
        this.handleZoomToFit(boundingBoxMin, boundingBoxMax);
495✔
133

134
        // Update statistics
135
        this.updateStatistics();
495✔
136
    }
872✔
137

138
    /**
139
     * Update the layout engine
140
     */
141
    private updateLayout(): void {
2✔
142
        this.statsManager.step();
495✔
143
        this.statsManager.graphStep.beginMonitoring();
495✔
144

145
        const {stepMultiplier} = this.styleManager.getStyles().config.behavior.layout;
495✔
146
        for (let i = 0; i < stepMultiplier; i++) {
495✔
147
            this.layoutManager.step();
495✔
148
            this.layoutStepCount++;
495✔
149
        }
495✔
150

151
        this.statsManager.graphStep.endMonitoring();
495✔
152
    }
495✔
153

154
    /**
155
     * Update all nodes and calculate bounding box
156
     */
157
    private updateNodes(): {boundingBoxMin?: Vector3, boundingBoxMax?: Vector3} {
2✔
158
        let boundingBoxMin: Vector3 | undefined;
513✔
159
        let boundingBoxMax: Vector3 | undefined;
513✔
160

161
        this.statsManager.nodeUpdate.beginMonitoring();
513✔
162

163
        for (const node of this.layoutManager.nodes) {
513✔
164
            node.update();
10,856✔
165

166
            // The mesh position is already updated by node.update()
167

168
            // Update bounding box
169
            const pos = node.mesh.getAbsolutePosition();
10,856✔
170
            const sz = node.size;
10,856✔
171

172
            if (!boundingBoxMin || !boundingBoxMax) {
10,856✔
173
                boundingBoxMin = pos.clone();
449✔
174
                boundingBoxMax = pos.clone();
449✔
175
            }
449✔
176

177
            // Calculate visual bounds including label
178
            // if (node.label?.labelMesh) {
179
            //     // The label mesh position should be current after node.update()
180
            //     // Get the label's bounding info which includes its actual rendered size
181
            //     const labelBoundingInfo = node.label.labelMesh.getBoundingInfo();
182
            //     const labelMin = labelBoundingInfo.boundingBox.minimumWorld;
183
            //     const labelMax = labelBoundingInfo.boundingBox.maximumWorld;
184

185
            //     // Update bounding box to include label bounds
186
            //     if (labelMin.x < boundingBoxMin.x) {
187
            //         boundingBoxMin.x = labelMin.x;
188
            //     }
189

190
            //     if (labelMax.x > boundingBoxMax.x) {
191
            //         boundingBoxMax.x = labelMax.x;
192
            //     }
193

194
            //     if (labelMin.y < boundingBoxMin.y) {
195
            //         boundingBoxMin.y = labelMin.y;
196
            //     }
197

198
            //     if (labelMax.y > boundingBoxMax.y) {
199
            //         boundingBoxMax.y = labelMax.y;
200
            //     }
201

202
            //     if (labelMin.z < boundingBoxMin.z) {
203
            //         boundingBoxMin.z = labelMin.z;
204
            //     }
205

206
            //     if (labelMax.z > boundingBoxMax.z) {
207
            //         boundingBoxMax.z = labelMax.z;
208
            //     }
209
            // }
210

211
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "x");
10,856✔
212
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "y");
10,856✔
213
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "z");
10,856✔
214
        }
10,856✔
215

216
        this.statsManager.nodeUpdate.endMonitoring();
513✔
217

218
        return {boundingBoxMin, boundingBoxMax};
513✔
219
    }
513✔
220

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

234
        min[axis] = Math.min(min[axis], value - halfSize);
32,568✔
235
        max[axis] = Math.max(max[axis], value + halfSize);
32,568✔
236
    }
32,568✔
237

238
    /**
239
     * Update all edges
240
     */
241
    private updateEdges(): void {
2✔
242
        this.statsManager.edgeUpdate.beginMonitoring();
513✔
243

244
        // Update rays for all edges (static method on Edge class)
245
        Edge.updateRays(this.graphContext);
513✔
246

247
        // Update individual edges
248
        for (const edge of this.layoutManager.edges) {
513✔
249
            edge.update();
21,028✔
250
        }
21,028✔
251

252
        this.statsManager.edgeUpdate.endMonitoring();
513✔
253
    }
513✔
254

255
    /**
256
     * Handle zoom to fit logic
257
     */
258
    private handleZoomToFit(boundingBoxMin?: Vector3, boundingBoxMax?: Vector3): void {
2✔
259
        if (!this.needsZoomToFit) {
513!
260
            return;
×
261
        }
×
262

263
        if (!boundingBoxMin || !boundingBoxMax) {
513✔
264
            return;
64✔
265
        }
64✔
266

267
        // Check if we should zoom:
268
        // 1. Wait for minimum steps on first zoom
269
        // 2. Zoom every N steps during layout (based on zoomStepInterval)
270
        // 3. Zoom when layout settles
271
        const isSettled = this.layoutManager.layoutEngine?.isSettled ?? false;
513!
272
        const {zoomStepInterval} = this.styleManager.getStyles().config.behavior.layout;
513✔
273
        const shouldZoomPeriodically = this.layoutStepCount > 0 &&
513✔
274
                                      this.layoutStepCount >= this.lastZoomStep + zoomStepInterval;
431✔
275
        const justSettled = isSettled && !this.wasSettled && this.layoutStepCount > 0;
513✔
276

277
        if (!this.hasZoomedToFit && this.layoutManager.running && this.layoutStepCount < this.minLayoutStepsBeforeZoom) {
513✔
278
            // First zoom - wait for minimum steps
279
            return;
259✔
280
        } else if (!this.layoutManager.running && !this.hasZoomedToFit && this.layoutStepCount === 0) {
494✔
281
            // Layout not running and no steps taken - allow immediate zoom
282
        } else if (!shouldZoomPeriodically && !justSettled) {
190!
283
            // Not time for periodic zoom and didn't just settle
284
            return;
×
285
        }
✔
286

287
        // Update settled state for next frame
288
        this.wasSettled = isSettled;
190✔
289

290
        const size = boundingBoxMax.subtract(boundingBoxMin);
190✔
291

292
        if (size.length() > this.config.minBoundingBoxSize) {
190✔
293
            this.camera.zoomToBoundingBox(boundingBoxMin, boundingBoxMax);
190✔
294

295
            this.hasZoomedToFit = true;
190✔
296
            this.lastZoomStep = this.layoutStepCount;
190✔
297

298
            // Only clear needsZoomToFit if layout is settled
299
            if (isSettled) {
190✔
300
                this.needsZoomToFit = false;
18✔
301
            }
18✔
302

303
            // Emit zoom complete event
304
            this.eventManager.emitGraphEvent("zoom-to-fit-complete", {
190✔
305
                boundingBoxMin,
190✔
306
                boundingBoxMax,
190✔
307
            });
190✔
308
        }
190✔
309
    }
513✔
310

311
    /**
312
     * Update statistics
313
     */
314
    private updateStatistics(): void {
2✔
315
        this.statsManager.updateCounts(
513✔
316
            this.dataManager.nodeCache.size,
513✔
317
            this.dataManager.edgeCache.size,
513✔
318
        );
513✔
319

320
        // Update mesh cache stats
321
        const meshCache = this.graphContext.getMeshCache();
513✔
322
        this.statsManager.updateCacheStats(meshCache.hits, meshCache.misses);
513✔
323
    }
513✔
324

325
    /**
326
     * Check if zoom to fit has been completed
327
     */
328
    get zoomToFitCompleted(): boolean {
2✔
329
        return this.hasZoomedToFit;
×
330
    }
×
331

332
    /**
333
     * Update configuration
334
     */
335
    updateConfig(config: Partial<UpdateManagerConfig>): void {
2✔
336
        Object.assign(this.config, config);
×
337
    }
×
338
}
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