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

iTowns / itowns / 11341675326

15 Oct 2024 07:53AM UTC coverage: 86.851% (-0.1%) from 86.948%
11341675326

Pull #2435

github

web-flow
Merge 93963e8fd into cfb9d0f51
Pull Request #2435: fix(OGC3DTilesLayer): handle multiple views

2794 of 3710 branches covered (75.31%)

Branch coverage included in aggregate %.

36 of 41 new or added lines in 2 files covered. (87.8%)

148 existing lines in 3 files now uncovered.

24366 of 27562 relevant lines covered (88.4%)

1022.16 hits per line

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

83.82
/src/Layer/C3DTilesLayer.js
1
import * as THREE from 'three';
1✔
2
import GeometryLayer from 'Layer/GeometryLayer';
1✔
3
import { init3dTilesLayer, pre3dTilesUpdate, process3dTilesNode } from 'Process/3dTilesProcessing';
1✔
4
import C3DTileset from 'Core/3DTiles/C3DTileset';
1✔
5
import C3DTExtensions from 'Core/3DTiles/C3DTExtensions';
1✔
6
import { PNTS_MODE, PNTS_SHAPE, PNTS_SIZE_MODE } from 'Renderer/PointsMaterial';
1✔
7
// eslint-disable-next-line no-unused-vars
1✔
8
import Style from 'Core/Style';
1✔
9
import C3DTFeature from 'Core/3DTiles/C3DTFeature';
1✔
10
import { optimizeGeometryGroups } from 'Utils/ThreeUtils';
1✔
11

1✔
12
export const C3DTILES_LAYER_EVENTS = {
1✔
13
    /**
1✔
14
     * Fires when a tile content has been loaded
1✔
15
     * @event C3DTilesLayer#on-tile-content-loaded
1✔
16
     * @type {object}
1✔
17
     * @property {THREE.Object3D} tileContent - object3D of the tile
1✔
18
     */
1✔
19
    ON_TILE_CONTENT_LOADED: 'on-tile-content-loaded',
1✔
20
    /**
1✔
21
     * Fires when a tile is requested
1✔
22
     * @event C3DTilesLayer#on-tile-requested
1✔
23
     * @type {object}
1✔
24
     * @property {object} metadata - tile
1✔
25
     */
1✔
26
    ON_TILE_REQUESTED: 'on-tile-requested',
1✔
27
};
1✔
28

1✔
29
const update = process3dTilesNode();
1✔
30

1✔
31
/**
1✔
32
 * Find tileId of object
1✔
33
 * @param {THREE.Object3D} object - object
1✔
34
 *
1✔
35
 * @returns {number} tileId
1✔
36
 */
1✔
37
function findTileID(object) {
65✔
38
    let currentObject = object;
65✔
39
    let result = currentObject.tileId;
65✔
40
    while (isNaN(result) && currentObject.parent) {
65!
41
        currentObject = currentObject.parent;
×
42
        result = currentObject.tileId;
×
43
    }
×
44
    return result;
65✔
45
}
65✔
46

1✔
47
/**
1✔
48
 * Check if object3d has feature
1✔
49
 * @param {THREE.Object3D} object3d - object3d to check
1✔
50
 *
1✔
51
 * @returns {boolean} - true if object3d has feature
1✔
52
 */
1✔
53
function object3DHasFeature(object3d) {
100✔
54
    return object3d.geometry && object3d.geometry.attributes._BATCHID;
100✔
55
}
100✔
56

1✔
57
class C3DTilesLayer extends GeometryLayer {
3✔
58
    #fillColorMaterialsBuffer;
3✔
59
    /**
3✔
60
     * @deprecated Deprecated 3D Tiles layer. Use {@link OGC3DTilesLayer} instead.
3✔
61
     * @extends GeometryLayer
3✔
62
     *
3✔
63
     * @example
3✔
64
     * // Create a new 3d-tiles layer from a web server
3✔
65
     * const l3dt = new C3DTilesLayer('3dtiles', {
3✔
66
     *      name: '3dtl',
3✔
67
     *      source: new C3DTilesSource({
3✔
68
     *           url: 'https://tileset.json'
3✔
69
     *      })
3✔
70
     * }, view);
3✔
71
     * View.prototype.addLayer.call(view, l3dt);
3✔
72
     *
3✔
73
     * // Create a new 3d-tiles layer from a Cesium ion server
3✔
74
     * const l3dt = new C3DTilesLayer('3dtiles', {
3✔
75
     *      name: '3dtl',
3✔
76
     *      source: new C3DTilesIonSource({
3✔
77
     *              accessToken: 'myAccessToken',
3✔
78
                    assetId: 12
3✔
79
     *      })
3✔
80
     * }, view);
3✔
81
     * View.prototype.addLayer.call(view, l3dt);
3✔
82
     *
3✔
83
     * @param      {string}  id - The id of the layer, that should be unique.
3✔
84
     *     It is not mandatory, but an error will be emitted if this layer is
3✔
85
     *     added a
3✔
86
     * {@link View} that already has a layer going by that id.
3✔
87
     * @param      {object}  config   configuration, all elements in it
3✔
88
     * will be merged as is in the layer.
3✔
89
     * @param {C3TilesSource} config.source The source of 3d Tiles.
3✔
90
     *
3✔
91
     * name.
3✔
92
     * @param {Number} [config.sseThreshold=16] The [Screen Space Error](https://github.com/CesiumGS/3d-tiles/blob/main/specification/README.md#geometric-error)
3✔
93
     * threshold at which child nodes of the current node will be loaded and added to the scene.
3✔
94
     * @param {Number} [config.cleanupDelay=1000] The time (in ms) after which a tile content (and its children) are
3✔
95
     * removed from the scene.
3✔
96
     * @param {C3DTExtensions} [config.registeredExtensions] 3D Tiles extensions managers registered for this tileset.
3✔
97
     * @param {String} [config.pntsMode= PNTS_MODE.COLOR] {@link PointsMaterial} Point cloud coloring mode.
3✔
98
     *      Only 'COLOR' or 'CLASSIFICATION' are possible. COLOR uses RGB colors of the points,
3✔
99
     *      CLASSIFICATION uses a classification property of the batch table to color points.
3✔
100
     * @param {String} [config.pntsShape= PNTS_SHAPE.CIRCLE] Point cloud point shape. Only 'CIRCLE' or 'SQUARE' are possible.
3✔
101
     * @param {String} [config.pntsSizeMode= PNTS_SIZE_MODE.VALUE] {@link PointsMaterial} Point cloud size mode. Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance from point to camera.
3✔
102
     * @param {Number} [config.pntsMinAttenuatedSize=3] Minimum scale used by 'ATTENUATED' size mode
3✔
103
     * @param {Number} [config.pntsMaxAttenuatedSize=10] Maximum scale used by 'ATTENUATED' size mode
3✔
104
     * @param {Style} [config.style=null] - style used for this layer
3✔
105
     * @param  {View}  view  The view
3✔
106
     */
3✔
107
    constructor(id, config, view) {
3✔
108
        console.warn('C3DTilesLayer is deprecated and will be removed in iTowns 3.0 version. Use OGC3DTilesLayer instead.');
3✔
109
        super(id, new THREE.Group(), { source: config.source });
3✔
110
        this.isC3DTilesLayer = true;
3✔
111
        this.sseThreshold = config.sseThreshold || 16;
3✔
112
        this.cleanupDelay = config.cleanupDelay || 1000;
3✔
113
        this.protocol = '3d-tiles';
3✔
114
        this.name = config.name;
3✔
115
        this.registeredExtensions = config.registeredExtensions || new C3DTExtensions();
3✔
116

3✔
117
        this.pntsMode = PNTS_MODE.COLOR;
3✔
118
        this.pntsShape = PNTS_SHAPE.CIRCLE;
3✔
119
        this.classification = config.classification;
3✔
120
        this.pntsSizeMode = PNTS_SIZE_MODE.VALUE;
3✔
121
        this.pntsMinAttenuatedSize = config.pntsMinAttenuatedSize || 1;
3✔
122
        this.pntsMaxAttenuatedSize = config.pntsMaxAttenuatedSize || 7;
3✔
123
        if (config.pntsMode) {
3!
124
            const exists = Object.values(PNTS_MODE).includes(config.pntsMode);
×
125
            if (!exists) {
×
126
                console.warn("The points cloud mode doesn't exist. Use 'COLOR' or 'CLASSIFICATION' instead.");
×
127
            } else {
×
128
                this.pntsMode = config.pntsMode;
×
129
            }
×
UNCOV
130
        }
×
131

3✔
132
        if (config.pntsShape) {
3!
133
            const exists = Object.values(PNTS_SHAPE).includes(config.pntsShape);
×
134
            if (!exists) {
×
135
                console.warn("The points cloud point shape doesn't exist. Use 'CIRCLE' or 'SQUARE' instead.");
×
136
            } else {
×
137
                this.pntsShape = config.pntsShape;
×
138
            }
×
UNCOV
139
        }
×
140

3✔
141
        if (config.pntsSizeMode) {
3!
142
            const exists = Object.values(PNTS_SIZE_MODE).includes(config.pntsSizeMode);
×
143
            if (!exists) { console.warn("The points cloud size mode doesn't exist. Use 'VALUE' or 'ATTENUATED' instead."); } else { this.pntsSizeMode = config.pntsSizeMode; }
×
UNCOV
144
        }
×
145

3✔
146
        /** @type {Style} */
3✔
147
        this.style = config.style || null;
3✔
148

3✔
149
        /** @type {Map<string, THREE.MeshStandardMaterial>} */
3✔
150
        this.#fillColorMaterialsBuffer = new Map();
3✔
151

3✔
152
        /**
3✔
153
         * Map all C3DTFeature of the layer according their tileId and their batchId
3✔
154
         * Map< tileId, Map< batchId, C3DTFeature>>
3✔
155
         *
3✔
156
         * @type {Map<number, Map<number,C3DTFeature>>}
3✔
157
         */
3✔
158
        this.tilesC3DTileFeatures = new Map();
3✔
159

3✔
160
        if (config.onTileContentLoaded) {
3!
161
            console.warn('DEPRECATED onTileContentLoaded should not be passed at the contruction, use C3DTILES_LAYER_EVENTS.ON_TILE_CONTENT_LOADED event instead');
×
162
            this.addEventListener(C3DTILES_LAYER_EVENTS.ON_TILE_CONTENT_LOADED, config.onTileContentLoaded);
×
UNCOV
163
        }
×
164

3✔
165
        if (config.overrideMaterials) {
3!
166
            console.warn('overrideMaterials is deprecated, use style API instead');
×
167
            this.overrideMaterials = config.overrideMaterials;
×
UNCOV
168
        }
×
169

3✔
170
        this._cleanableTiles = [];
3✔
171

3✔
172
        const resolve = this.addInitializationStep();
3✔
173

3✔
174
        this.source.whenReady.then((tileset) => {
3✔
175
            this.tileset = new C3DTileset(tileset, this.source.baseUrl, this.registeredExtensions);
3✔
176
            // Verify that extensions of the tileset have been registered in the layer
3✔
177
            if (this.tileset.extensionsUsed) {
3✔
178
                for (const extensionUsed of this.tileset.extensionsUsed) {
1✔
179
                    // if current extension is not registered
1✔
180
                    if (!this.registeredExtensions.isExtensionRegistered(extensionUsed)) {
1!
181
                        // if it is required to load the tileset
×
182
                        if (this.tileset.extensionsRequired &&
×
183
                            this.tileset.extensionsRequired.includes(extensionUsed)) {
×
184
                            console.error(
×
185
                                `3D Tiles tileset required extension "${extensionUsed}" must be registered to the 3D Tiles layer of iTowns to be parsed and used.`);
×
186
                        } else {
×
187
                            console.warn(
×
188
                                `3D Tiles tileset used extension "${extensionUsed}" must be registered to the 3D Tiles layer of iTowns to be parsed and used.`);
×
189
                        }
×
UNCOV
190
                    }
×
191
                }
1✔
192
            }
1✔
193
            // TODO: Move all init3dTilesLayer code to constructor
2✔
194
            init3dTilesLayer(view, view.mainLoop.scheduler, this, tileset.root).then(resolve);
2✔
195
        });
3✔
196
    }
3✔
197

3✔
198
    preUpdate(context) {
3✔
199
        return pre3dTilesUpdate.bind(this)(context);
1✔
200
    }
1✔
201

3✔
202
    update(context, layer, node) {
3✔
203
        return update(context, layer, node);
1✔
204
    }
1✔
205

3✔
206
    getObjectToUpdateForAttachedLayers(meta) {
3✔
207
        if (meta.content) {
1✔
208
            const result = [];
1✔
209
            meta.content.traverse((obj) => {
1✔
210
                if (obj.isObject3D && obj.material && obj.layer == meta.layer) {
4✔
211
                    result.push(obj);
3✔
212
                }
3✔
213
            });
1✔
214
            const p = meta.parent;
1✔
215
            if (p && p.content) {
1!
216
                return {
×
217
                    elements: result,
×
218
                    parent: p.content,
×
UNCOV
219
                };
×
220
            } else {
1✔
221
                return {
1✔
222
                    elements: result,
1✔
223
                };
1✔
224
            }
1✔
225
        }
1✔
226
    }
1✔
227

3✔
228
    /**
3✔
229
     * Get the closest c3DTileFeature of an intersects array.
3✔
230
     * @param {Array} intersects - @return An array containing all
3✔
231
     * targets picked under specified coordinates. Intersects can be
3✔
232
     * computed with view.pickObjectsAt(..). See fillHTMLWithPickingInfo()
3✔
233
     * in 3dTilesHelper.js for an example.
3✔
234
     *
3✔
235
     * @returns {C3DTileFeature} - the closest C3DTileFeature of the intersects array
3✔
236
     */
3✔
237
    getC3DTileFeatureFromIntersectsArray(intersects) {
3✔
238
        // find closest intersect with an attributes _BATCHID + face != undefined
×
239
        let closestIntersect = null;
×
240

×
241
        for (let index = 0; index < intersects.length; index++) {
×
242
            const i = intersects[index];
×
243
            if (i.object.geometry &&
×
244
                i.object.geometry.attributes._BATCHID &&
×
245
                i.face && // need face to get batch id
×
246
                i.layer == this // just to be sure that the right layer intersected
×
247
            ) {
×
248
                closestIntersect = i;
×
249
                break;
×
250
            }
×
251
        }
×
252

×
253
        if (!closestIntersect) {
×
254
            return null;
×
255
        }
×
256

×
257
        const tileId = findTileID(closestIntersect.object);
×
258
        // face is a Face3 object of THREE which is a
×
259
        // triangular face. face.a is its first vertex
×
260
        const vertex = closestIntersect.face.a;
×
261
        const batchID = closestIntersect.object.geometry.attributes._BATCHID.getX(vertex);
×
262

×
263
        return this.tilesC3DTileFeatures.get(tileId).get(batchID);
×
UNCOV
264
    }
×
265

3✔
266
    /**
3✔
267
     * Called when a tile content is loaded
3✔
268
     * @param {THREE.Object3D} tileContent - tile as THREE.Object3D
3✔
269
     */
3✔
270
    onTileContentLoaded(tileContent) {
3✔
271
        this.initC3DTileFeatures(tileContent);
13✔
272

13✔
273
        // notify observer
13✔
274
        this.dispatchEvent({ type: C3DTILES_LAYER_EVENTS.ON_TILE_CONTENT_LOADED, tileContent });
13✔
275

13✔
276
        // only update style of tile features
13✔
277
        this.updateStyle([tileContent.tileId]);
13✔
278
    }
13✔
279

3✔
280
    /**
3✔
281
     * Initialize C3DTileFeatures from tileContent
3✔
282
     * @param {THREE.Object3D} tileContent - tile as THREE.Object3D
3✔
283
     */
3✔
284
    initC3DTileFeatures(tileContent) {
3✔
285
        this.tilesC3DTileFeatures.set(tileContent.tileId, new Map()); // initialize
13✔
286
        tileContent.traverse((child) => {
13✔
287
            if (object3DHasFeature(child)) {
23✔
288
                const batchIdAttribute = child.geometry.getAttribute('_BATCHID');
11✔
289
                let currentBatchId = batchIdAttribute.getX(0);
11✔
290
                let start = 0;
11✔
291
                let count = 0;
11✔
292

11✔
293
                const registerBatchIdGroup = () => {
11✔
294
                    if (this.tilesC3DTileFeatures.get(tileContent.tileId).has(currentBatchId)) {
2,796✔
295
                        // already created
2,260✔
296
                        const c3DTileFeature = this.tilesC3DTileFeatures.get(tileContent.tileId).get(currentBatchId);
2,260✔
297
                        // add new group
2,260✔
298
                        c3DTileFeature.groups.push({
2,260✔
299
                            start,
2,260✔
300
                            count,
2,260✔
301
                        });
2,260✔
302
                    } else {
2,796✔
303
                        // first occurence
536✔
304
                        const c3DTileFeature = new C3DTFeature(
536✔
305
                            tileContent.tileId,
536✔
306
                            currentBatchId,
536✔
307
                            [{ start, count }], // initialize with current group
536✔
308
                            {},
536✔
309
                            child,
536✔
310
                        );
536✔
311
                        this.tilesC3DTileFeatures.get(tileContent.tileId).set(currentBatchId, c3DTileFeature);
536✔
312
                    }
536✔
313
                };
11✔
314

11✔
315
                // TODO: Could be simplified by incrementing of 1 and stopping the iteration at positionAttributeSize.count
11✔
316
                // See https://github.com/iTowns/itowns/pull/2266#discussion_r1483285122
11✔
317
                const positionAttribute = child.geometry.getAttribute('position');
11✔
318
                const positionAttributeSize = positionAttribute.count * positionAttribute.itemSize;
11✔
319
                for (let index = 0; index < positionAttributeSize; index += positionAttribute.itemSize) {
11✔
320
                    const batchIndex = index / positionAttribute.itemSize;
7,650✔
321
                    const batchId = batchIdAttribute.getX(batchIndex);
7,650✔
322

7,650✔
323
                    // check if batchId is currentBatchId
7,650✔
324
                    if (currentBatchId !== batchId) {
7,650✔
325
                        registerBatchIdGroup();
2,785✔
326

2,785✔
327
                        // reset
2,785✔
328
                        currentBatchId = batchId;
2,785✔
329
                        start = batchIndex;
2,785✔
330
                        count = 0;
2,785✔
331
                    }
2,785✔
332

7,650✔
333
                    // record this position in current C3DTileFeature
7,650✔
334
                    count++;
7,650✔
335

7,650✔
336
                    // check if end of the array
7,650✔
337
                    if (index + positionAttribute.itemSize >= positionAttributeSize) {
7,650✔
338
                        registerBatchIdGroup();
11✔
339
                    }
11✔
340
                }
7,650✔
341
            }
11✔
342
        });
13✔
343
    }
13✔
344

3✔
345
    /**
3✔
346
     * Update style of the C3DTFeatures, an allowList of tile id can be passed to only update certain tile.
3✔
347
     * Note that this function only update THREE.Object3D materials, in order to see style changes you should call view.notifyChange()
3✔
348
     * @param {Array<number>|null} [allowTileIdList] - tile ids to allow in updateStyle computation if null all tiles are updated
3✔
349
     *
3✔
350
     * @returns {boolean} true if style updated false otherwise
3✔
351
     */
3✔
352
    updateStyle(allowTileIdList = null) {
3✔
353
        if (!this._style) {
21✔
354
            return false;
6✔
355
        }
6✔
356
        if (!this.object3d) {
21✔
357
            return false;
3✔
358
        }
3✔
359

12✔
360
        const currentMaterials = [];// list materials used for this update
12✔
361

12✔
362
        const mapObjects3d = new Map();
12✔
363
        this.object3d.traverse((child) => {
12✔
364
            if (object3DHasFeature(child)) {
77✔
365
                const tileId = findTileID(child);
65✔
366

65✔
367
                if (allowTileIdList && !allowTileIdList.includes(tileId)) {
65✔
368
                    return; // this tileId is not updated
45✔
369
                }
45✔
370

20✔
371
                // push for update style
20✔
372
                if (!mapObjects3d.has(tileId)) {
20✔
373
                    mapObjects3d.set(tileId, []);
20✔
374
                }
20✔
375
                mapObjects3d.get(tileId).push(child);
20✔
376
            }
20✔
377
        });
12✔
378

12✔
379
        for (const [tileId, objects3d] of mapObjects3d) {
21✔
380
            const c3DTileFeatures = this.tilesC3DTileFeatures.get(tileId); // features of this tile
20✔
381
            objects3d.forEach((object3d) => {
20✔
382
                // clear
20✔
383
                object3d.geometry.clearGroups();
20✔
384
                object3d.material = [];
20✔
385

20✔
386
                for (const [, c3DTileFeature] of c3DTileFeatures) {
20✔
387
                    if (c3DTileFeature.object3d != object3d) {
1,012!
388
                        continue;// this feature do not belong to object3d
×
UNCOV
389
                    }
×
390

1,012✔
391
                    this._style.context.setGeometry({
1,012✔
392
                        properties: c3DTileFeature,
1,012✔
393
                    });
1,012✔
394

1,012✔
395
                    /** @type {THREE.Color} */
1,012✔
396
                    const color = new THREE.Color(this._style.fill.color);
1,012✔
397

1,012✔
398
                    /** @type {number} */
1,012✔
399
                    const opacity = this._style.fill.opacity;
1,012✔
400

1,012✔
401
                    const materialId = color.getHexString() + opacity;
1,012✔
402

1,012✔
403
                    let material = null;
1,012✔
404
                    if (this.#fillColorMaterialsBuffer.has(materialId)) {
1,012✔
405
                        material = this.#fillColorMaterialsBuffer.get(materialId);
1,008✔
406
                    } else {
1,012✔
407
                        material = new THREE.MeshStandardMaterial({ color, opacity, transparent: opacity < 1, alphaTest: 0.09 });
4✔
408
                        this.#fillColorMaterialsBuffer.set(materialId, material);// bufferize
4✔
409
                    }
4✔
410

1,012✔
411
                    // compute materialIndex
1,012✔
412
                    let materialIndex = -1;
1,012✔
413
                    for (let index = 0; index < object3d.material.length; index++) {
1,012✔
414
                        const childMaterial = object3d.material[index];
1,266✔
415
                        if (material.uuid === childMaterial.uuid) {
1,266✔
416
                            materialIndex = index;
959✔
417
                            break;
959✔
418
                        }
959✔
419
                    }
1,266✔
420
                    if (materialIndex < 0) {
1,012✔
421
                        // not in object3d.material add it
53✔
422
                        object3d.material.push(material);
53✔
423
                        materialIndex = object3d.material.length - 1;
53✔
424
                    }
53✔
425

1,012✔
426
                    // materialIndex groups is computed
1,012✔
427
                    c3DTileFeature.groups.forEach((group) => {
1,012✔
428
                        object3d.geometry.addGroup(group.start, group.count, materialIndex);
5,532✔
429
                    });
1,012✔
430
                }
1,012✔
431

20✔
432
                optimizeGeometryGroups(object3d);
20✔
433

20✔
434
                // record material(s) used in object3d
20✔
435
                if (object3d.material instanceof Array) {
20✔
436
                    object3d.material.forEach((material) => {
20✔
437
                        if (!currentMaterials.includes(material)) {
53✔
438
                            currentMaterials.push(material);
24✔
439
                        }
24✔
440
                    });
20✔
441
                } else if (!currentMaterials.includes(object3d.material)) {
20!
442
                    currentMaterials.push(object3d.material);
×
UNCOV
443
                }
×
444
            });
20✔
445
        }
20✔
446

12✔
447
        // remove buffered materials not in currentMaterials
12✔
448
        for (const [id, fillMaterial] of this.#fillColorMaterialsBuffer) {
21✔
449
            if (!currentMaterials.includes(fillMaterial)) {
24!
450
                fillMaterial.dispose();
×
451
                this.#fillColorMaterialsBuffer.delete(id);
×
UNCOV
452
            }
×
453
        }
24✔
454

12✔
455
        return true;
12✔
456
    }
12✔
457

3✔
458
    get materialCount() {
3✔
459
        return this.#fillColorMaterialsBuffer.size;
1✔
460
    }
1✔
461

3✔
462
    set style(value) {
3✔
463
        if (value instanceof Style) {
7✔
464
            this._style = value;
3✔
465
        } else if (!value) {
7✔
466
            this._style = null;
3✔
467
        } else {
4✔
468
            this._style = new Style(value);
1✔
469
        }
1✔
470
        this.updateStyle();
7✔
471
    }
7✔
472

3✔
473
    get style() {
3✔
474
        return this._style;
×
UNCOV
475
    }
×
476
}
3✔
477

1✔
478
export default C3DTilesLayer;
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