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

iTowns / itowns / 22103702551

17 Feb 2026 03:06PM UTC coverage: 88.172% (+0.05%) from 88.125%
22103702551

Pull #2675

github

web-flow
Merge 8e4388f0d into bbdd27d9e
Pull Request #2675: refactor: migrate MainLoop to TypeScript

2779 of 3591 branches covered (77.39%)

Branch coverage included in aggregate %.

119 of 127 new or added lines in 4 files covered. (93.7%)

2 existing lines in 1 file now uncovered.

28456 of 31834 relevant lines covered (89.39%)

930.28 hits per line

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

87.63
/packages/Main/src/Core/MainLoop.ts
1
import type { GeometryLayer, Layer, View } from 'Main';
1✔
2
import type { Object3D, Camera as ThreeCamera } from 'three';
1✔
3
import { EventDispatcher } from 'three';
1✔
4
import type Camera from 'Renderer/Camera';
1✔
5
import type c3DEngine from 'Renderer/c3DEngine';
1✔
6
import type Scheduler from './Scheduler/Scheduler';
1✔
7

1✔
8
export const RENDERING_PAUSED = 0;
1✔
9
export const RENDERING_SCHEDULED = 1;
1✔
10

1✔
11
/**
1✔
12
 * MainLoop's update events list that are fired using
1✔
13
 * {@link View#execFrameRequesters}.
1✔
14
 */
1✔
15
export const MAIN_LOOP_EVENTS = {
1✔
16
    /** Fired at the start of the update */
1✔
17
    UPDATE_START: 'update_start' as const,
1✔
18
    /** Fired before the camera update */
1✔
19
    BEFORE_CAMERA_UPDATE: 'before_camera_update' as const,
1✔
20
    /** Fired after the camera update */
1✔
21
    AFTER_CAMERA_UPDATE: 'after_camera_update' as const,
1✔
22
    /** Fired before the layer update */
1✔
23
    BEFORE_LAYER_UPDATE: 'before_layer_update' as const,
1✔
24
    /** Fired after the layer update */
1✔
25
    AFTER_LAYER_UPDATE: 'after_layer_update' as const,
1✔
26
    /** Fired before the render */
1✔
27
    BEFORE_RENDER: 'before_render' as const,
1✔
28
    /** Fired after the render */
1✔
29
    AFTER_RENDER: 'after_render' as const,
1✔
30
    /** Fired at the end of the update */
1✔
31
    UPDATE_END: 'update_end' as const,
1✔
32
};
1✔
33

1✔
34
type Context = {
1✔
35
    camera: Camera,
1✔
36
    engine: c3DEngine,
1✔
37
    scheduler: Scheduler,
1✔
38
    view: View,
1✔
39
};
1✔
40

1✔
41
type UpdatableGeometryLayer<T> = GeometryLayer & {
1✔
42
    update(context: Context, layer: Layer, node: T, parent?: T): Array<T> | undefined
1✔
43
};
1✔
44

1✔
45
type UpdateSource = Layer | ThreeCamera | { layer: Layer };
1✔
46

1✔
47
type MainLoopEvents = {
1✔
48
    // An unknown body indicates an empty event
1✔
49
    'command-queue-empty': object;
1✔
50
};
1✔
51

1✔
52
function updateElements<T extends Object3D>(
6✔
53
    context: Context,
6✔
54
    geometryLayer: UpdatableGeometryLayer<T>,
6✔
55
    elements?: Array<T>,
6✔
56
) {
6✔
57
    if (!elements) {
6!
58
        return;
×
59
    }
×
60
    for (const element of elements) {
6✔
61
        // update element
4✔
62
        // TODO: find a way to notify attachedLayers when geometryLayer deletes
4✔
63
        // some elements and then update Debug.js:addGeometryLayerDebugFeatures
4✔
64
        const newElementsToUpdate = geometryLayer.update(context, geometryLayer, element);
4✔
65

4✔
66
        const sub = geometryLayer.getObjectToUpdateForAttachedLayers(element);
4✔
67

4✔
68
        if (sub) {
4!
NEW
69
            for (let i = 0; i < sub.elements.length; i++) {
×
NEW
70
                if (!(sub.elements[i].isObject3D)) {
×
NEW
71
                    throw new Error(`
×
72
                            Invalid object for attached layer to update.
×
73
                            Must be a THREE.Object and have a THREE.Material`);
×
74
                }
×
75
                // update attached layers
×
76
                for (const attachedLayer of geometryLayer.attachedLayers) {
×
77
                    if (attachedLayer.ready) {
×
NEW
78
                        (attachedLayer as UpdatableGeometryLayer<T>).update(context,
×
NEW
79
                            attachedLayer,
×
NEW
80
                            sub.elements[i],
×
NEW
81
                            sub.parent);
×
82
                    }
×
83
                }
×
84
            }
×
85
        }
×
86

4✔
87
        updateElements(context, geometryLayer, newElementsToUpdate);
4✔
88
    }
4✔
89
}
6✔
90

1✔
91
function filterChangeSources(
2✔
92
    updateSources: Set<UpdateSource>,
2✔
93
    geometryLayer: GeometryLayer,
2✔
94
): Set<UpdateSource> {
2✔
95
    let fullUpdate = false;
2✔
96
    const filtered = new Set<UpdateSource>();
2✔
97
    updateSources.forEach((src) => {
2✔
98
        if (src === geometryLayer || 'isCamera' in src) {
2!
99
            geometryLayer.info.clear();
2✔
100
            fullUpdate = true;
2✔
101
        } else if ((src as { layer: Layer }).layer === geometryLayer) {
2!
102
            filtered.add(src);
×
103
        }
×
104
    });
2✔
105
    return fullUpdate ? new Set([geometryLayer]) : filtered;
2!
106
}
2✔
107

1✔
108
class MainLoop extends EventDispatcher<MainLoopEvents> {
1✔
109
    private _needsRedraw = false;
1✔
110
    private _updateLoopRestarted = true;
45✔
111
    private _lastTimestamp = 0;
1✔
112

1✔
113
    public renderingState: typeof RENDERING_PAUSED | typeof RENDERING_SCHEDULED;
1✔
114
    public scheduler: Scheduler;
1✔
115
    public gfxEngine: c3DEngine;
1✔
116

1✔
117
    constructor(scheduler: Scheduler, engine: c3DEngine) {
1✔
118
        super();
45✔
119
        this.renderingState = RENDERING_PAUSED;
45✔
120
        this.scheduler = scheduler;
45✔
121
        this.gfxEngine = engine; // TODO: remove me
45✔
122
    }
45✔
123

1✔
124
    public scheduleViewUpdate(view: View, forceRedraw: boolean) {
1✔
125
        this._needsRedraw ||= forceRedraw;
183✔
126

183✔
127
        if (this.renderingState !== RENDERING_SCHEDULED) {
183✔
128
            this.renderingState = RENDERING_SCHEDULED;
40✔
129

40✔
130
            if (__DEBUG__) {
40✔
131
                document.title += ' ⌛';
40✔
132
            }
40✔
133

40✔
134
            // TODO: Fix asynchronization between xr and MainLoop render loops.
40✔
135
            // WebGLRenderer#setAnimationLoop must be used for WebXR projects.
40✔
136
            // (see WebXR#initializeWebXR).
40✔
137
            if (!this.gfxEngine.renderer.xr.isPresenting) {
40✔
138
                requestAnimationFrame((timestamp) => { this.step(view, timestamp); });
40✔
139
            }
40✔
140
        }
40✔
141
    }
183✔
142

1✔
143
    private _update(view: View, updateSources: Set<UpdateSource>, dt: number) {
1✔
144
        const context: Context = {
2✔
145
            camera: view.camera,
2✔
146
            engine: this.gfxEngine,
2✔
147
            scheduler: this.scheduler,
2✔
148
            view,
2✔
149
        };
2✔
150

2✔
151
        // replace layer with their parent where needed
2✔
152
        updateSources.forEach((src: UpdateSource) => {
2✔
153
            // @ts-expect-error True JS shenanigans
2✔
154
            const layer = src.layer || src;
2✔
155
            if (layer.isLayer && layer.parent) {
2!
156
                updateSources.add(layer.parent);
×
157
            }
×
158
        });
2✔
159

2✔
160
        for (const geometryLayer of view.getLayers((_, y) => !y)) {
2✔
161
            if (geometryLayer.ready && geometryLayer.visible && !geometryLayer.frozen) {
2✔
162
                view.execFrameRequesters(
2✔
163
                    MAIN_LOOP_EVENTS.BEFORE_LAYER_UPDATE, dt, this._updateLoopRestarted,
2✔
164
                    geometryLayer,
2✔
165
                );
2✔
166

2✔
167
                // Filter updateSources that are relevant for the geometryLayer
2✔
168
                const srcs = filterChangeSources(updateSources, geometryLayer);
2✔
169
                if (srcs.size > 0) {
2✔
170
                    // pre update attached layer
2✔
171
                    for (const attachedLayer of geometryLayer.attachedLayers) {
2!
172
                        if (attachedLayer.ready && attachedLayer.preUpdate) {
×
173
                            attachedLayer.preUpdate(context, srcs);
×
174
                        }
×
175
                    }
×
176
                    // `preUpdate` returns an array of elements to update
2✔
177
                    const elementsToUpdate = geometryLayer.preUpdate(context, srcs);
2✔
178
                    // `update` is called in `updateElements`.
2✔
179
                    updateElements(context, geometryLayer, elementsToUpdate);
2✔
180
                    // `postUpdate` is called when this geom layer update
2✔
181
                    // process is finished
2✔
182
                    geometryLayer.postUpdate(context, geometryLayer, updateSources);
2✔
183
                }
2✔
184

2✔
185
                // Clear the cache of expired resources
2✔
186

2✔
187
                view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_LAYER_UPDATE,
2✔
188
                    dt, this._updateLoopRestarted, geometryLayer);
2✔
189
            }
2✔
190
        }
2✔
191
    }
2✔
192

1✔
193
    public step(view: View, timestamp: number) {
1✔
194
        const dt = timestamp - this._lastTimestamp;
2✔
195
        view._executeFrameRequestersRemovals();
2✔
196

2✔
197
        view.execFrameRequesters(MAIN_LOOP_EVENTS.UPDATE_START, dt, this._updateLoopRestarted);
2✔
198

2✔
199
        const willRedraw = this._needsRedraw;
2✔
200
        this._lastTimestamp = timestamp;
2✔
201

2✔
202
        // Reset internal state before calling _update (so future calls to
2✔
203
        // View.notifyChange() can properly change it)
2✔
204
        this._needsRedraw = false;
2✔
205
        this.renderingState = RENDERING_PAUSED;
2✔
206
        const updateSources: Set<UpdateSource> = new Set(view._changeSources);
2✔
207
        view._changeSources.clear();
2✔
208

2✔
209
        view.execFrameRequesters(MAIN_LOOP_EVENTS.BEFORE_CAMERA_UPDATE,
2✔
210
            dt, this._updateLoopRestarted);
2✔
211
        view.camera.update();
2✔
212
        view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
2✔
213
            dt, this._updateLoopRestarted);
2✔
214

2✔
215
        // Disable camera's matrix auto update to make sure the camera's
2✔
216
        // world matrix is never updated mid-update.
2✔
217
        // Otherwise inconsistencies can appear because object visibility
2✔
218
        // testing and object drawing could be performed using different
2✔
219
        // camera matrixWorld.
2✔
220
        // Note: this is required at least because WEBGLRenderer calls
2✔
221
        // camera.updateMatrixWorld()
2✔
222
        const oldAutoUpdate = view.camera3D.matrixAutoUpdate;
2✔
223
        view.camera3D.matrixAutoUpdate = false;
2✔
224

2✔
225
        // update data-structure
2✔
226
        this._update(view, updateSources, dt);
2✔
227

2✔
228
        if (this.scheduler.commandsWaitingExecutionCount() == 0) {
2✔
229
            this.dispatchEvent({ type: 'command-queue-empty' });
2✔
230
        }
2✔
231

2✔
232
        // Redraw *only* if needed.
2✔
233
        // (redraws only happen when this.#needsRedraw is true, which in turn
2✔
234
        // only happens when view.notifyChange() is called with redraw=true)
2✔
235
        // As such there's no continuous update-loop, instead we use an ad-hoc
2✔
236
        // update/render mechanism.
2✔
237
        if (willRedraw) {
2✔
238
            this._renderView(view, dt);
1✔
239
        }
1✔
240

2✔
241
        // next time, we'll consider that we've just started the loop if we are
2✔
242
        // still PAUSED now
2✔
243
        this._updateLoopRestarted = this.renderingState === RENDERING_PAUSED;
2✔
244

2✔
245
        if (__DEBUG__) {
2✔
246
            document.title = document.title.substring(0, document.title.length - 2);
2✔
247
        }
2✔
248

2✔
249
        view.camera3D.matrixAutoUpdate = oldAutoUpdate;
2✔
250

2✔
251
        view.execFrameRequesters(MAIN_LOOP_EVENTS.UPDATE_END, dt, this._updateLoopRestarted);
2✔
252
    }
2✔
253

1✔
254
    private _renderView(view: View, dt: number) {
1✔
255
        view.execFrameRequesters(MAIN_LOOP_EVENTS.BEFORE_RENDER, dt, this._updateLoopRestarted);
1✔
256

1✔
257
        if (view.render) {
1!
258
            view.render();
×
259
        } else {
1✔
260
            // use default rendering method
1✔
261
            this.gfxEngine.renderView(view);
1✔
262
        }
1✔
263

1✔
264
        view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_RENDER, dt, this._updateLoopRestarted);
1✔
265
    }
1✔
266
}
1✔
267

1✔
268
export default MainLoop;
1✔
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