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

graphty-org / graphty-element / 18770573775

24 Oct 2025 05:27AM UTC coverage: 82.365% (+0.7%) from 81.618%
18770573775

push

github

apowers313
ci: fix ci, take 2

1221 of 1491 branches covered (81.89%)

Branch coverage included in aggregate %.

6588 of 7990 relevant lines covered (82.45%)

601.6 hits per line

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

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

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

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

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

86
    /**
87
     * Get the current render frame count
88
     */
89
    getRenderFrameCount(): number {
2✔
90
        return this.frameCount;
×
91
    }
×
92

93
    /**
94
     * Render a fixed number of frames (for testing)
95
     * This ensures deterministic rendering similar to Babylon.js testing approach
96
     */
97
    renderFixedFrames(count: number): void {
2✔
98
        for (let i = 0; i < count; i++) {
×
99
            this.update();
×
100
        }
×
101
    }
×
102

103
    /**
104
     * Main update method - called by RenderManager each frame
105
     */
106
    private frameCount = 0;
2✔
107

108
    update(): void {
2✔
109
        this.frameCount++;
1,003✔
110

111
        // Always update camera
112
        this.camera.update();
1,003✔
113

114
        // Check if layout is running
115
        if (!this.layoutManager.running) {
1,003✔
116
            // Even if layout is not running, we still need to handle zoom if requested
117
            if (this.needsZoomToFit && !this.hasZoomedToFit) {
546✔
118
                // Check if we have nodes to calculate bounds from
119
                const nodeCount = Array.from(this.layoutManager.nodes).length;
264✔
120
                if (nodeCount > 0) {
264✔
121
                    // Calculate bounding box and update nodes
122
                    const {boundingBoxMin, boundingBoxMax} = this.updateNodes();
20✔
123

124
                    // Update edges
125
                    this.updateEdges();
20✔
126

127
                    // Handle zoom to fit
128
                    this.handleZoomToFit(boundingBoxMin, boundingBoxMax);
20✔
129

130
                    // Update statistics
131
                    this.updateStatistics();
20✔
132
                }
20✔
133
            }
264✔
134

135
            return;
546✔
136
        }
546✔
137

138
        // Normal update when layout is running
139
        // Update layout engine
140
        this.updateLayout();
457✔
141

142
        // Calculate bounding box and update nodes
143
        const {boundingBoxMin, boundingBoxMax} = this.updateNodes();
457✔
144

145
        // Update edges
146
        this.updateEdges();
457✔
147

148
        // Handle zoom to fit if needed
149
        this.handleZoomToFit(boundingBoxMin, boundingBoxMax);
457✔
150

151
        // Update statistics
152
        this.updateStatistics();
457✔
153
    }
1,003✔
154

155
    /**
156
     * Update the layout engine
157
     */
158
    private updateLayout(): void {
2✔
159
        this.statsManager.step();
457✔
160
        this.statsManager.graphStep.beginMonitoring();
457✔
161

162
        const {stepMultiplier} = this.styleManager.getStyles().config.behavior.layout;
457✔
163
        for (let i = 0; i < stepMultiplier; i++) {
457✔
164
            this.layoutManager.step();
457✔
165
            this.layoutStepCount++;
457✔
166
        }
457✔
167

168
        this.statsManager.graphStep.endMonitoring();
457✔
169
    }
457✔
170

171
    /**
172
     * Update all nodes and calculate bounding box
173
     */
174
    private updateNodes(): {boundingBoxMin?: Vector3, boundingBoxMax?: Vector3} {
2✔
175
        let boundingBoxMin: Vector3 | undefined;
477✔
176
        let boundingBoxMax: Vector3 | undefined;
477✔
177

178
        this.statsManager.nodeUpdate.beginMonitoring();
477✔
179

180
        for (const node of this.layoutManager.nodes) {
477✔
181
            node.update();
9,499✔
182

183
            // The mesh position is already updated by node.update()
184

185
            // Update bounding box
186
            const pos = node.mesh.getAbsolutePosition();
9,499✔
187
            const sz = node.size;
9,499✔
188

189
            if (!boundingBoxMin || !boundingBoxMax) {
9,499✔
190
                boundingBoxMin = pos.clone();
389✔
191
                boundingBoxMax = pos.clone();
389✔
192
            }
389✔
193

194
            // Calculate visual bounds including label
195
            // if (node.label?.labelMesh) {
196
            //     // The label mesh position should be current after node.update()
197
            //     // Get the label's bounding info which includes its actual rendered size
198
            //     const labelBoundingInfo = node.label.labelMesh.getBoundingInfo();
199
            //     const labelMin = labelBoundingInfo.boundingBox.minimumWorld;
200
            //     const labelMax = labelBoundingInfo.boundingBox.maximumWorld;
201

202
            //     // Update bounding box to include label bounds
203
            //     if (labelMin.x < boundingBoxMin.x) {
204
            //         boundingBoxMin.x = labelMin.x;
205
            //     }
206

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

211
            //     if (labelMin.y < boundingBoxMin.y) {
212
            //         boundingBoxMin.y = labelMin.y;
213
            //     }
214

215
            //     if (labelMax.y > boundingBoxMax.y) {
216
            //         boundingBoxMax.y = labelMax.y;
217
            //     }
218

219
            //     if (labelMin.z < boundingBoxMin.z) {
220
            //         boundingBoxMin.z = labelMin.z;
221
            //     }
222

223
            //     if (labelMax.z > boundingBoxMax.z) {
224
            //         boundingBoxMax.z = labelMax.z;
225
            //     }
226
            // }
227

228
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "x");
9,499✔
229
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "y");
9,499✔
230
            this.updateBoundingBoxAxis(pos, boundingBoxMin, boundingBoxMax, sz, "z");
9,499✔
231
        }
9,499✔
232

233
        this.statsManager.nodeUpdate.endMonitoring();
477✔
234

235
        return {boundingBoxMin, boundingBoxMax};
477✔
236
    }
477✔
237

238
    /**
239
     * Update bounding box for a single axis
240
     */
241
    private updateBoundingBoxAxis(
2✔
242
        pos: Vector3,
28,497✔
243
        min: Vector3,
28,497✔
244
        max: Vector3,
28,497✔
245
        size: number,
28,497✔
246
        axis: "x" | "y" | "z",
28,497✔
247
    ): void {
28,497✔
248
        const value = pos[axis];
28,497✔
249
        const halfSize = size / 2;
28,497✔
250

251
        min[axis] = Math.min(min[axis], value - halfSize);
28,497✔
252
        max[axis] = Math.max(max[axis], value + halfSize);
28,497✔
253
    }
28,497✔
254

255
    /**
256
     * Update all edges
257
     */
258
    private updateEdges(): void {
2✔
259
        this.statsManager.edgeUpdate.beginMonitoring();
477✔
260

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

264
        // Update individual edges
265
        for (const edge of this.layoutManager.edges) {
477✔
266
            edge.update();
18,580✔
267
        }
18,580✔
268

269
        this.statsManager.edgeUpdate.endMonitoring();
477✔
270
    }
477✔
271

272
    /**
273
     * Handle zoom to fit logic
274
     */
275
    private handleZoomToFit(boundingBoxMin?: Vector3, boundingBoxMax?: Vector3): void {
2✔
276
        if (!this.needsZoomToFit) {
477!
277
            return;
×
278
        }
×
279

280
        if (!boundingBoxMin || !boundingBoxMax) {
477✔
281
            return;
88✔
282
        }
88✔
283

284
        // Check if we should zoom:
285
        // 1. Wait for minimum steps on first zoom
286
        // 2. Zoom every N steps during layout (based on zoomStepInterval)
287
        // 3. Zoom when layout settles
288
        const isSettled = this.layoutManager.layoutEngine?.isSettled ?? false;
477!
289
        const {zoomStepInterval} = this.styleManager.getStyles().config.behavior.layout;
477✔
290
        const shouldZoomPeriodically = this.layoutStepCount > 0 &&
477✔
291
                                      this.layoutStepCount >= this.lastZoomStep + zoomStepInterval;
369✔
292
        const justSettled = isSettled && !this.wasSettled && this.layoutStepCount > 0;
477✔
293

294
        if (!this.hasZoomedToFit && this.layoutManager.running && this.layoutStepCount < this.minLayoutStepsBeforeZoom) {
477✔
295
            // First zoom - wait for minimum steps
296
            return;
234✔
297
        } else if (!this.layoutManager.running && !this.hasZoomedToFit && this.layoutStepCount === 0) {
438✔
298
            // Layout not running and no steps taken - allow immediate zoom
299
        } else if (!shouldZoomPeriodically && !justSettled) {
155!
300
            // Not time for periodic zoom and didn't just settle
301
            return;
×
302
        }
✔
303

304
        // Update settled state for next frame
305
        this.wasSettled = isSettled;
155✔
306

307
        const size = boundingBoxMax.subtract(boundingBoxMin);
155✔
308

309
        if (size.length() > this.config.minBoundingBoxSize) {
155✔
310
            this.camera.zoomToBoundingBox(boundingBoxMin, boundingBoxMax);
155✔
311

312
            this.hasZoomedToFit = true;
155✔
313
            this.lastZoomStep = this.layoutStepCount;
155✔
314

315
            // Only clear needsZoomToFit if layout is settled
316
            if (isSettled) {
155✔
317
                this.needsZoomToFit = false;
20✔
318
            }
20✔
319

320
            // Emit zoom complete event
321
            this.eventManager.emitGraphEvent("zoom-to-fit-complete", {
155✔
322
                boundingBoxMin,
155✔
323
                boundingBoxMax,
155✔
324
            });
155✔
325
        }
155✔
326
    }
477✔
327

328
    /**
329
     * Update statistics
330
     */
331
    private updateStatistics(): void {
2✔
332
        this.statsManager.updateCounts(
477✔
333
            this.dataManager.nodeCache.size,
477✔
334
            this.dataManager.edgeCache.size,
477✔
335
        );
477✔
336

337
        // Update mesh cache stats
338
        const meshCache = this.graphContext.getMeshCache();
477✔
339
        this.statsManager.updateCacheStats(meshCache.hits, meshCache.misses);
477✔
340
    }
477✔
341

342
    /**
343
     * Check if zoom to fit has been completed
344
     */
345
    get zoomToFitCompleted(): boolean {
2✔
346
        return this.hasZoomedToFit;
×
347
    }
×
348

349
    /**
350
     * Update configuration
351
     */
352
    updateConfig(config: Partial<UpdateManagerConfig>): void {
2✔
353
        Object.assign(this.config, config);
×
354
    }
×
355
}
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