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

iTowns / itowns / 21940175993

12 Feb 2026 09:05AM UTC coverage: 88.167% (+0.04%) from 88.125%
21940175993

Pull #2675

github

web-flow
Merge 91464fec3 into b02b8fc7f
Pull Request #2675: refactor: migrate MainLoop to TypeScript

2781 of 3593 branches covered (77.4%)

Branch coverage included in aggregate %.

94 of 104 new or added lines in 4 files covered. (90.38%)

87 existing lines in 1 file now uncovered.

28452 of 31832 relevant lines covered (89.38%)

930.31 hits per line

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

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

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

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

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

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

1✔
44
function updateElements<T extends Object3D>(
6✔
45
    context: Context,
6✔
46
    geometryLayer: UpdatableGeometryLayer<T>,
6✔
47
    elements?: Array<T>,
6✔
48
) {
6✔
49
    if (!elements) {
6!
50
        return;
×
51
    }
×
52
    for (const element of elements) {
6✔
53
        // update element
4✔
54
        // TODO: find a way to notify attachedLayers when geometryLayer deletes
4✔
55
        // some elements and then update Debug.js:addGeometryLayerDebugFeatures
4✔
56
        const newElementsToUpdate = geometryLayer.update(context, geometryLayer, element);
4✔
57

4✔
58
        const sub = geometryLayer.getObjectToUpdateForAttachedLayers(element);
4✔
59

4✔
60
        if (sub) {
4!
NEW
61
            for (let i = 0; i < sub.elements.length; i++) {
×
NEW
62
                if (!(sub.elements[i].isObject3D)) {
×
NEW
63
                    throw new Error(`
×
64
                            Invalid object for attached layer to update.
×
65
                            Must be a THREE.Object and have a THREE.Material`);
×
66
                }
×
67
                // update attached layers
×
68
                for (const attachedLayer of geometryLayer.attachedLayers) {
×
69
                    if (attachedLayer.ready) {
×
NEW
70
                        // @ts-expect-error Updatable layer
×
NEW
71
                        attachedLayer.update(context,
×
NEW
72
                            attachedLayer,
×
NEW
73
                            sub.elements[i],
×
NEW
74
                            sub.parent);
×
75
                    }
×
76
                }
×
77
            }
×
78
        }
×
79

4✔
80
        updateElements(context, geometryLayer, newElementsToUpdate);
4✔
81
    }
4✔
82
}
6✔
83

1✔
84
// TODO: Figure out what that last type actually is
1✔
85
type UpdateSource = Layer | ThreeCamera | { layer: Layer };
1✔
86

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

1✔
104
type MainLoopEvents = {
1✔
105
    // An unknown body indicates an empty event
1✔
106
    'command-queue-empty': unknown;
1✔
107
};
1✔
108

1✔
109
class MainLoop extends EventDispatcher<MainLoopEvents> {
1✔
110
    #needsRedraw = false;
1✔
111
    #updateLoopRestarted = true;
45✔
112
    #lastTimestamp = 0;
1✔
113

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

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

1✔
125
    public scheduleViewUpdate(view: View, forceRedraw: boolean) {
1✔
126
        this.#needsRedraw ||= forceRedraw;
182✔
127

182✔
128
        if (this.renderingState !== RENDERING_SCHEDULED) {
182✔
129
            this.renderingState = RENDERING_SCHEDULED;
40✔
130

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

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

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

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

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

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

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

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

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

2✔
198
        view.execFrameRequesters(MAIN_LOOP_EVENTS.UPDATE_START, dt, this.#updateLoopRestarted);
2✔
199

2✔
200
        const willRedraw = this.#needsRedraw;
2✔
201
        this.#lastTimestamp = timestamp;
2✔
202

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

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

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

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

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

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

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

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

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

2✔
252
        view.execFrameRequesters(MAIN_LOOP_EVENTS.UPDATE_END, dt, this.#updateLoopRestarted);
2✔
253
    }
2✔
254

1✔
255
    #renderView(view: View, dt: number) {
1✔
256
        view.execFrameRequesters(MAIN_LOOP_EVENTS.BEFORE_RENDER, dt, this.#updateLoopRestarted);
1✔
257

1✔
258
        // @ts-expect-error Checking dynamically-added method
1✔
259
        if (view.render) {
1!
NEW
260
            // @ts-expect-error Result of the above
×
261
            view.render();
×
262
        } else {
1✔
263
            // use default rendering method
1✔
264
            this.gfxEngine.renderView(view);
1✔
265
        }
1✔
266

1✔
267
        view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_RENDER, dt, this.#updateLoopRestarted);
1✔
268
    }
1✔
269
}
1✔
270

1✔
271
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