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

iTowns / itowns / 6979781676

24 Nov 2023 10:44AM UTC coverage: 77.111% (+0.1%) from 77.004%
6979781676

Pull #2223

github

web-flow
Merge 23836a3b7 into 1d10290b5
Pull Request #2223: Fix base alti for mesh 3d

4051 of 5992 branches covered (0.0%)

Branch coverage included in aggregate %.

216 of 238 new or added lines in 9 files covered. (90.76%)

9 existing lines in 4 files now uncovered.

7986 of 9618 relevant lines covered (83.03%)

1640.25 hits per line

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

88.98
/src/Converter/Feature2Mesh.js
1
import * as THREE from 'three';
1✔
2
import Earcut from 'earcut';
1✔
3
import { FEATURE_TYPES } from 'Core/Feature';
1✔
4
import ReferLayerProperties from 'Layer/ReferencingLayerProperties';
1✔
5
import { deprecatedFeature2MeshOptions } from 'Core/Deprecated/Undeprecator';
1✔
6
import Extent from 'Core/Geographic/Extent';
1✔
7
import Crs from 'Core/Geographic/Crs';
1✔
8
import OrientationUtils from 'Utils/OrientationUtils';
1✔
9
import Coordinates from 'Core/Geographic/Coordinates';
1✔
10
import Style, { StyleContext } from 'Core/Style';
2,213!
11

12
const coord = new Coordinates('EPSG:4326', 0, 0, 0);
1✔
13
const context = new StyleContext();
1✔
14
let userStyle = new Style();
1✔
15

16
const dim_ref = new THREE.Vector2();
1✔
17
const dim = new THREE.Vector2();
1✔
18
const normal = new THREE.Vector3();
1✔
19
const base = new THREE.Vector3();
1✔
20
const extrusion = new THREE.Vector3();
1✔
21
const inverseScale = new THREE.Vector3();
1✔
22
const extent = new Extent('EPSG:4326', 0, 0, 0, 0);
1✔
23

24
const _color = new THREE.Color();
1✔
25
const maxValueUint8 = 2 ** 8 - 1;
1✔
26
const maxValueUint16 = 2 ** 16 - 1;
1✔
27
const maxValueUint32 = 2 ** 32 - 1;
1✔
28
const crsWGS84 = 'EPSG:4326';
4✔
29

30
class FeatureMesh extends THREE.Group {
2✔
31
    #currentCrs;
32
    #originalCrs;
33
    #collection = new THREE.Group();
14✔
34
    #place = new THREE.Group();
35
    constructor(meshes, collection) {
14✔
36
        super();
28✔
37

38
        this.meshes = new THREE.Group().add(...meshes);
14✔
39

40
        this.#collection = new THREE.Group().add(this.meshes);
14✔
41
        this.#collection.quaternion.copy(collection.quaternion);
14✔
42
        this.#collection.position.copy(collection.position);
14✔
43

44
        this.#collection.scale.copy(collection.scale);
14✔
45
        this.#collection.updateMatrix();
14✔
46

47
        this.#originalCrs = collection.crs;
14✔
48
        this.#currentCrs = this.#originalCrs;
14✔
49
        this.extent = collection.extent;
14✔
50

51
        this.add(this.#place.add(this.#collection));
14✔
52
    }
1✔
53

54
    as(crs) {
55
        if (this.#currentCrs !== crs) {
8✔
56
            this.#currentCrs = crs;
4✔
57
            if (crs == this.#originalCrs) {
4!
58
                // reset transformation
59
                this.place.position.set(0, 0, 0);
×
60
                this.position.set(0, 0, 0);
×
61
                this.scale.set(1, 1, 1);
×
62
                this.quaternion.identity();
×
63
            } else {
64
                // calculate the scale transformation to transform the feature.extent
65
                // to feature.extent.as(crs)
66
                coord.crs = Crs.formatToEPSG(this.#originalCrs);
4✔
67
                extent.copy(this.extent).applyMatrix4(this.#collection.matrix);
4✔
68
                extent.as(coord.crs, extent);
4✔
69
                extent.spatialEuclideanDimensions(dim_ref);
4✔
70
                extent.planarDimensions(dim);
4✔
71
                if (dim.x && dim.y) {
4!
72
                    this.scale.copy(dim_ref).divide(dim).setZ(1);
4✔
73
                }
74

75
                // Position and orientation
76
                // remove original position
77
                this.#place.position.copy(this.#collection.position).negate();
4✔
78

79
                // get mesh coordinate
80
                coord.setFromVector3(this.#collection.position);
4✔
81

82
                // get method to calculate orientation
83
                const crsInput = this.#originalCrs == 'EPSG:3857' ? crsWGS84 : this.#originalCrs;
4!
84
                const crs2crs = OrientationUtils.quaternionFromCRSToCRS(crsInput, crs);
4✔
85
                // calculate orientation to crs
86
                crs2crs(coord.as(crsWGS84), this.quaternion);
4✔
87

88
                // transform position to crs
89
                coord.as(crs, coord).toVector3(this.position);
4✔
90
            }
91
        }
92

93
        return this;
8✔
94
    }
1✔
95
}
96

97
function toColor(color) {
98
    if (color) {
3,640✔
99
        if (color.type == 'Color') {
3,605!
100
            return color;
×
101
        } else {
102
            return _color.set(color);
3,605✔
103
        }
104
    } else {
105
        return _color.set(Math.random() * 0xffffff);
35✔
106
    }
107
}
108

109
function getIntArrayFromSize(data, size) {
110
    if (size <= maxValueUint8) {
20✔
111
        return new Uint8Array(data);
18✔
112
    } else if (size <= maxValueUint16) {
2!
113
        return new Uint16Array(data);
2✔
114
    } else {
115
        return new Uint32Array(data);
×
116
    }
117
}
118

119
function separateMeshes(object3D) {
120
    const meshes = [];
3✔
121
    object3D.updateMatrixWorld();
3✔
122
    object3D.traverse((element) => {
3✔
123
        if (element instanceof THREE.Mesh) {
9✔
124
            element.updateMatrixWorld();
6✔
125
            element.geometry.applyMatrix4(element.matrixWorld);
6✔
126
            meshes.push(element);
6✔
127
        }
128
    });
129

130
    return meshes;
3✔
131
}
132

133
/**
134
 * Add indices for the side faces.
135
 * We loop over the contour and create a side face made of two triangles.
136
 *
137
 * For a ring made of (n) coordinates, there are (n*2) vertices.
138
 * The (n) first vertices are on the roof, the (n) other vertices are on the floor.
139
 *
140
 * If index (i) is on the roof, index (i+length) is on the floor.
141
 *
142
 * @param {number[]} indices - Array of indices to push to
143
 * @param {number} length - Total vertices count in the geom (excluding the extrusion ones)
144
 * @param {number} offset
145
 * @param {number} count
146
 * @param {boolean} isClockWise - Wrapping direction
147
 */
148
function addExtrudedPolygonSideFaces(indices, length, offset, count, isClockWise) {
149
    // loop over contour length, and for each point of the contour,
150
    // add indices to make two triangle, that make the side face
151
    const startIndice = indices.length;
19✔
152
    indices.length += (count - 1) * 6;
19✔
153
    for (let i = offset, j = startIndice; i < offset + count - 1; ++i, ++j) {
19✔
154
        if (isClockWise) {
3,586✔
155
            // first triangle indices
156
            indices[j] = i;
3,574✔
157
            indices[++j] = i + length;
3,574✔
158
            indices[++j] = i + 1;
3,574✔
159
            // second triangle indices
160
            indices[++j] = i + 1;
3,574✔
161
            indices[++j] = i + length;
3,574✔
162
            indices[++j] = i + length + 1;
3,574✔
163
        } else {
164
            // first triangle indices
165
            indices[j] = i + length;
12✔
166
            indices[++j] = i;
12✔
167
            indices[++j] = i + length + 1;
12✔
168
            // second triangle indices
169
            indices[++j] = i + length + 1;
12✔
170
            indices[++j] = i;
12✔
171
            indices[++j] = i + 1;
12✔
172
        }
173
    }
174
}
175

176
function featureToPoint(feature, options) {
177
    const ptsIn = feature.vertices;
3✔
178
    const colors = new Uint8Array(ptsIn.length);
3✔
179
    const batchIds = new Uint32Array(ptsIn.length);
3✔
180
    const batchId = options.batchId || ((p, id) => id);
3✔
181

182
    let featureId = 0;
3✔
183
    const vertices = new Float32Array(ptsIn);
3✔
184
    inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse);
3✔
185
    normal.set(0, 0, 1).multiply(inverseScale);
3✔
186

187
    const pointMaterialSize = [];
3✔
188
    context.setFeature(feature);
3✔
189

190
    for (const geometry of feature.geometries) {
3✔
191
        const start = geometry.indices[0].offset;
3✔
192
        const count = geometry.indices[0].count;
3✔
193
        const end = start + count;
3✔
194
        const id = batchId(geometry.properties, featureId);
3✔
195
        context.setGeometry(geometry);
3✔
196

197
        for (let v = start * 3, j = start; j < end; v += 3, j += 1) {
3✔
198
            if (feature.normals) {
3!
199
                normal.fromArray(feature.normals, v).multiply(inverseScale);
×
200
            }
201

202
            const localCoord = context.setLocalCoordinatesFromArray(feature.vertices, v);
3✔
203
            userStyle.setContext(context);
3✔
204
            const { base_altitude, color, radius } = userStyle.point;
3✔
205

206
            coord.copy(localCoord)
3✔
207
                .applyMatrix4(context.collection.matrixWorld)
208
                .as('EPSG:4326', coord);
209

210
            // populate vertices
211
            base.copy(normal)
3✔
212
                .multiplyScalar(base_altitude - coord.z).add(localCoord)
213
                .toArray(vertices, v);
214

215
            toColor(color).multiplyScalar(255).toArray(colors, v);
3✔
216

217
            if (!pointMaterialSize.includes(radius)) {
3!
218
                pointMaterialSize.push(radius);
3✔
219
            }
220
            batchIds[j] = id;
3✔
221
        }
222
        featureId++;
3✔
223
    }
3✔
224

225
    const geom = new THREE.BufferGeometry();
3✔
226
    geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
3✔
227
    geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
3✔
228
    geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1));
3✔
229

230
    options.pointMaterial.size = pointMaterialSize[0];
3✔
231
    if (pointMaterialSize.length > 1) {
3!
232
        // TODO CREATE material for each feature
NEW
233
        console.warn('Too many differents point.radius, only the first one will be used');
×
234
    }
235

236
    return new THREE.Points(geom, options.pointMaterial);
3✔
237
}
238

239
function featureToLine(feature, options) {
240
    const ptsIn = feature.vertices;
1✔
241
    const colors = new Uint8Array(ptsIn.length);
1✔
242
    const count = ptsIn.length / 3;
1✔
243

244
    const batchIds = new Uint32Array(count);
1✔
245
    const batchId = options.batchId || ((p, id) => id);
1✔
246
    let featureId = 0;
1✔
247

248
    const vertices = new Float32Array(ptsIn.length);
1✔
249
    const geom = new THREE.BufferGeometry();
1✔
250

251
    const lineMaterialWidth = [];
1✔
252
    context.setFeature(feature);
1✔
253

254
    const countIndices = (count - feature.geometries.length) * 2;
1✔
255
    const indices = getIntArrayFromSize(countIndices, count);
1✔
256

257
    let i = 0;
1✔
258
    inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse);
1✔
259
    normal.set(0, 0, 1).multiply(inverseScale);
1✔
260
    // Multi line case
1✔
261
    for (const geometry of feature.geometries) {
1✔
262
        context.setGeometry(geometry);
1✔
263
        const id = batchId(geometry.properties, featureId);
1✔
264

265
        const start = geometry.indices[0].offset;
1✔
266
        // To avoid integer overflow with indice value (16 bits)
267
        if (start > 0xffff) {
1!
268
            console.warn('Feature to Line: integer overflow, too many points in lines');
×
269
            break;
×
270
        }
271
        const count = geometry.indices[0].count;
1✔
272
        const end = start + count;
1✔
273

274
        for (let v = start * 3, j = start; j < end; v += 3, j += 1) {
1✔
275
            if (j < end - 1) {
2✔
276
                if (j < 0xffff) {
1!
277
                    indices[i++] = j;
1✔
278
                    indices[i++] = j + 1;
1✔
279
                } else {
280
                    break;
×
281
                }
282
            }
283
            if (feature.normals) {
2!
284
                normal.fromArray(feature.normals, v).multiply(inverseScale);
×
285
            }
286

287
            const localCoord = context.setLocalCoordinatesFromArray(feature.vertices, v);
2✔
288
            userStyle.setContext(context);
2✔
289
            const { base_altitude, color, width } = userStyle.stroke;
2✔
290

291
            coord.copy(localCoord)
2✔
292
                .applyMatrix4(context.collection.matrixWorld)
293
                .as('EPSG:4326', coord);
294

295
            // populate vertices
296
            base.copy(normal)
2✔
297
                .multiplyScalar(base_altitude - coord.z).add(localCoord)
298
                .toArray(vertices, v);
299

300
            toColor(color).multiplyScalar(255).toArray(colors, v);
2✔
301

302
            if (!lineMaterialWidth.includes(width)) {
2✔
303
                lineMaterialWidth.push(width);
1✔
304
            }
305
            batchIds[j] = id;
2✔
306
        }
307
        featureId++;
1✔
308
    }
1✔
309
    options.lineMaterial.linewidth = lineMaterialWidth[0];
1✔
310
    if (lineMaterialWidth.length > 1) {
1!
311
        // TODO CREATE material for each feature
NEW
312
        console.warn('Too many differents stroke.width, only the first one will be used');
×
313
    }
314
    geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
1✔
315
    geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
1✔
316
    geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1));
1✔
317
    geom.setIndex(new THREE.BufferAttribute(indices, 1));
1✔
318

319
    return new THREE.LineSegments(geom, options.lineMaterial);
1✔
320
}
321

322
function featureToPolygon(feature, options) {
323
    const vertices = new Float32Array(feature.vertices);
6✔
324
    const colors = new Uint8Array(feature.vertices.length);
6✔
325
    const indices = [];
6✔
326

327
    const batchIds = new Uint32Array(vertices.length / 3);
6✔
328
    const batchId = options.batchId || ((p, id) => id);
6✔
329
    context.setFeature(feature);
6✔
330

331
    inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse);
6✔
332
    normal.set(0, 0, 1).multiply(inverseScale);
6✔
333
    let featureId = 0;
6✔
334

335
    for (const geometry of feature.geometries) {
6!
336
        const start = geometry.indices[0].offset;
6✔
337
        // To avoid integer overflow with index value (32 bits)
338
        if (start > maxValueUint32) {
6!
339
            console.warn('Feature to Polygon: integer overflow, too many points in polygons');
×
340
            break;
×
341
        }
342
        context.setGeometry(geometry);
6✔
343

344
        const lastIndice = geometry.indices.slice(-1)[0];
6✔
345
        const end = lastIndice.offset + lastIndice.count;
6✔
346
        const count = end - start;
6✔
347
        const startIn = start * 3;
6✔
348
        const endIn = startIn + count * 3;
6✔
349
        const id = batchId(geometry.properties, featureId);
6✔
350

351
        for (let i = startIn, b = start; i < endIn; i += 3, b += 1) {
6✔
352
            if (feature.normals) {
30✔
353
                normal.fromArray(feature.normals, i).multiply(inverseScale);
15✔
354
            }
355

356
            const localCoord = context.setLocalCoordinatesFromArray(feature.vertices, i);
30✔
357
            userStyle.setContext(context);
30✔
358
            const { base_altitude, color } = userStyle.fill;
30✔
359

360
            coord.copy(localCoord)
30✔
361
                .applyMatrix4(context.collection.matrixWorld)
362
                .as('EPSG:4326', coord);
363

364
            // populate geometry buffers
365
            base.copy(normal)
30✔
366
                .multiplyScalar(base_altitude - coord.z).add(localCoord)
367
                .toArray(vertices, i);
368

369
            toColor(color).multiplyScalar(255).toArray(colors, i);
30✔
370
            batchIds[b] = id;
30✔
371
        }
372
        featureId++;
6✔
373

374
        const geomVertices = vertices.slice(start * 3, end * 3);
6✔
375
        const holesOffsets = geometry.indices.map(i => i.offset - start).slice(1);
6✔
376
        const triangles = Earcut(geomVertices, holesOffsets, 3);
6✔
377

378
        const startIndice = indices.length;
6✔
379
        indices.length += triangles.length;
6✔
380

381
        for (let i = 0; i < triangles.length; i++) {
6✔
382
            indices[startIndice + i] = triangles[i] + start;
36✔
383
        }
384
    }
6✔
385

386
    const geom = new THREE.BufferGeometry();
6✔
387
    geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
6✔
388
    geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
6✔
389
    geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1));
6✔
390
    geom.setIndex(new THREE.BufferAttribute(getIntArrayFromSize(indices, vertices.length / 3), 1));
6✔
391

392
    return new THREE.Mesh(geom, options.polygonMaterial);
6✔
393
}
394

395
function area(contour, offset, count) {
396
    offset *= 3;
×
397
    const n = offset + count * 3;
×
398
    let a = 0.0;
×
399

400
    for (let p = n - 3, q = offset; q < n; p = q, q += 3) {
×
401
        a += contour[p] * contour[q + 1] - contour[q] * contour[p + 1];
×
402
    }
403

404
    return a * 0.5;
×
405
}
406

407
function featureToExtrudedPolygon(feature, options) {
408
    const ptsIn = feature.vertices;
13✔
409
    const vertices = new Float32Array(ptsIn.length * 2);
13✔
410
    const totalVertices = ptsIn.length / 3;
13✔
411

412
    const colors = new Uint8Array(ptsIn.length * 2);
13✔
413

414
    const indices = [];
13✔
415

416
    const batchIds = new Uint32Array(vertices.length / 3);
13✔
417
    const batchId = options.batchId || ((p, id) => id);
15✔
418

419
    let featureId = 0;
13✔
420

421
    context.setFeature(feature);
13✔
422
    inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse);
13✔
423
    normal.set(0, 0, 1).multiply(inverseScale);
13✔
424
    coord.setCrs(context.collection.crs);
13✔
425

426
    for (const geometry of feature.geometries) {
28✔
427
        context.setGeometry(geometry);
15✔
428

429
        const start = geometry.indices[0].offset;
15✔
430
        const lastIndice = geometry.indices.slice(-1)[0];
15✔
431
        const end = lastIndice.offset + lastIndice.count;
15✔
432
        const count = end - start;
15✔
433
        const isClockWise = geometry.indices[0].ccw ?? (area(ptsIn, start, count) < 0);
15!
434

435
        const startIn = start * 3;
15✔
436
        const startTop = start + totalVertices;
15✔
437
        const endIn = startIn + count * 3;
15✔
438
        const id = batchId(geometry.properties, featureId);
15✔
439

440
        for (let i = startIn, t = startIn + ptsIn.length, b = start; i < endIn; i += 3, t += 3, b += 1) {
15✔
441
            if (feature.normals) {
3,605✔
442
                normal.fromArray(feature.normals, i).multiply(inverseScale);
1,768✔
443
            }
444

445
            const localCoord = context.setLocalCoordinatesFromArray(ptsIn, i);
3,605✔
446
            userStyle.setContext(context);
3,605✔
447
            const { base_altitude, extrusion_height, color } = userStyle.fill;
3,605✔
448

449
            coord.copy(localCoord)
3,605✔
450
                .applyMatrix4(context.collection.matrixWorld)
451
                .as('EPSG:4326', coord);
452

453
            // populate base geometry buffers
454
            base.copy(normal)
3,605✔
455
                .multiplyScalar(base_altitude - coord.z).add(localCoord)
456
                .toArray(vertices, i);
457
            batchIds[b] = id;
3,605✔
458

459
            // populate top geometry buffers
460
            extrusion.copy(normal)
3,605✔
461
                .multiplyScalar(extrusion_height).add(base)
462
                .toArray(vertices, t);
463
            batchIds[b + totalVertices] = id;
3,605✔
464

465
            // coloring base and top mesh
466
            const meshColor = toColor(color).multiplyScalar(255);
3,605✔
467
            meshColor.toArray(colors, t); // top
3,605✔
468
            meshColor.multiplyScalar(0.5).toArray(colors, i); // base is half dark
3,605✔
469
        }
470

471
        featureId++;
15✔
472

473
        const endTop = end + totalVertices;
15✔
474

475
        const geomVertices = vertices.slice(startTop * 3, endTop * 3);
15✔
476
        const holesOffsets = geometry.indices.map(i => i.offset - start).slice(1);
19✔
477
        const triangles = Earcut(geomVertices, holesOffsets, 3);
15✔
478

479
        const startIndice = indices.length;
15✔
480
        indices.length += triangles.length;
15✔
481

482
        for (let i = 0; i < triangles.length; i++) {
15✔
483
            indices[startIndice + i] = triangles[i] + startTop;
10,740✔
484
        }
485

486
        // add extruded contour
487
        addExtrudedPolygonSideFaces(
15✔
488
            indices,
489
            totalVertices,
490
            geometry.indices[0].offset,
491
            geometry.indices[0].count,
492
            isClockWise);
493

494
        // add extruded holes
495
        for (let i = 1; i < geometry.indices.length; i++) {
15✔
496
            const indice = geometry.indices[i];
4✔
497
            addExtrudedPolygonSideFaces(
4✔
498
                indices,
499
                totalVertices,
500
                indice.offset,
501
                indice.count,
502
                !(indice.ccw ?? isClockWise));
12!
503
        }
504
    }
13✔
505

506
    const geom = new THREE.BufferGeometry();
13✔
507
    geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
13✔
508
    geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
13✔
509
    geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1));
13✔
510

511
    geom.setIndex(new THREE.BufferAttribute(getIntArrayFromSize(indices, vertices.length / 3), 1));
13✔
512

513
    return new THREE.Mesh(geom, options.polygonMaterial);
13✔
514
}
515

516
/**
517
 * Created Instanced object from mesh
518
 *
519
 * @param {THREE.MESH} mesh Model 3D to instanciate
520
 * @param {*} count number of instances to create (int)
521
 * @param {*} ptsIn positions of instanced (array double)
522
 * @returns {THREE.InstancedMesh} Instanced mesh
523
 */
524
function createInstancedMesh(mesh, count, ptsIn) {
525
    const instancedMesh = new THREE.InstancedMesh(mesh.geometry, mesh.material, count);
6✔
526
    let index = 0;
6✔
527
    for (let i = 0; i < count * 3; i += 3) {
6✔
528
        const mat = new THREE.Matrix4();
14✔
529
        mat.setPosition(ptsIn[i], ptsIn[i + 1], ptsIn[i + 2]);
14✔
530
        instancedMesh.setMatrixAt(index, mat);
14✔
531
        index++;
14✔
532
    }
533

534
    instancedMesh.instanceMatrix.needsUpdate = true;
6✔
535

536
    return instancedMesh;
6✔
537
}
538

539
/**
540
 * Convert a [Feature]{@link Feature} of type POINT to a Instanced meshes
541
 *
542
 * @param {Object} feature
543
 * @param {Object} options - options controlling the conversion
544
 * @returns {THREE.Mesh} mesh or GROUP of THREE.InstancedMesh
545
 */
546
function pointsToInstancedMeshes(feature, options) {
547
    const ptsIn = feature.vertices;
3✔
548
    const count = feature.geometries.length;
3✔
549
    const modelObject = options.style.point.model.object;
3✔
550

551
    if (modelObject instanceof THREE.Mesh) {
3!
552
        return createInstancedMesh(modelObject, count, ptsIn);
×
553
    } else if (modelObject instanceof THREE.Object3D) {
3!
554
        const group = new THREE.Group();
3✔
555
        // Get independent meshes from more complexe object
556
        const meshes = separateMeshes(modelObject);
3✔
557
        meshes.forEach(mesh => group.add(createInstancedMesh(mesh, count, ptsIn)));
6✔
558
        return group;
3✔
559
    } else {
NEW
560
        throw new Error('The format of the model object provided in the style (layer.style.point.model.object) is not supported. Only THREE.Mesh or THREE.Object3D are supported.');
×
561
    }
562
}
563

564
/**
565
 * Convert a [Feature]{@link Feature} to a Mesh
566
 *
567
 * @param {Feature} feature - the feature to convert
568
 * @param {Object} options - options controlling the conversion
569
 * @return {THREE.Mesh} mesh or GROUP of THREE.InstancedMesh
570
 */
571
function featureToMesh(feature, options) {
572
    if (!feature.vertices) {
26!
573
        return;
×
574
    }
575

576
    let mesh;
577
    switch (feature.type) {
26!
578
        case FEATURE_TYPES.POINT:
579
            if (userStyle.point?.model?.object) {
6✔
580
                try {
3✔
581
                    mesh = pointsToInstancedMeshes(feature, options);
3✔
582
                    mesh.isInstancedMesh = true;
3✔
583
                } catch (e) {
584
                    mesh = featureToPoint(feature, options);
×
585
                }
586
            } else {
587
                mesh = featureToPoint(feature, options);
3✔
588
            }
589
            break;
6✔
590
        case FEATURE_TYPES.LINE:
591
            mesh = featureToLine(feature, options);
1✔
592
            break;
1✔
593
        case FEATURE_TYPES.POLYGON:
594
            if (userStyle.fill && Object.keys(userStyle.fill).includes('extrusion_height')) {
19✔
595
                mesh = featureToExtrudedPolygon(feature, options);
13✔
596
            } else {
597
                mesh = featureToPolygon(feature, options);
6✔
598
            }
599
            break;
19✔
600
        default:
601
    }
602

603
    if (!mesh.isInstancedMesh) {
26✔
604
        mesh.material.vertexColors = true;
23✔
605
        mesh.material.color = new THREE.Color(0xffffff);
23✔
606
    }
607
    mesh.feature = feature;
26✔
608

609
    return mesh;
26✔
610
}
611

612
/**
613
 * @module Feature2Mesh
614
 */
615
export default {
1✔
616
    /**
617
     * Return a function that converts [Features]{@link module:GeoJsonParser} to Meshes. Feature collection will be converted to a
618
     * a THREE.Group.
619
     *
620
     * @param {Object} options - options controlling the conversion
621
     * @param {function} [options.batchId] - optional function to create batchId attribute.
622
     * It is passed the feature property and the feature index. As the batchId is using an unsigned int structure on 32 bits,
623
     * the batchId could be between 0 and 4,294,967,295.
624
     * @return {function}
625
     * @example <caption>Example usage of batchId with featureId.</caption>
626
     * view.addLayer({
627
     *     id: 'WFS Buildings',
628
     *     type: 'geometry',
629
     *     update: itowns.FeatureProcessing.update,
630
     *     convert: itowns.Feature2Mesh.convert({
631
     *         batchId: (property, featureId) => featureId,
632
     *     }),
633
     *     filter: acceptFeature,
634
     *     source,
635
     * });
636
     *
637
     * @example <caption>Example usage of batchId with property.</caption>
638
     * view.addLayer({
639
     *     id: 'WFS Buildings',
640
     *     type: 'geometry',
641
     *     update: itowns.FeatureProcessing.update,
642
     *     convert: itowns.Feature2Mesh.convert({
643
     *         batchId: (property, featureId) => property.house ? 10 : featureId,
644
     *         }),
645
     *     filter: acceptFeature,
646
     *     source,
647
     * });
648
     */
649
    convert(options = {}) {
13✔
650
        deprecatedFeature2MeshOptions(options);
13✔
651
        return function _convert(collection) {
14✔
652
            if (!collection) { return; }
14!
653

654
            if (!options.pointMaterial) {
14✔
655
                // Opacity and wireframe refered with layer properties
656
                // TODO :next step is move these properties to Style
657
                options.pointMaterial = ReferLayerProperties(new THREE.PointsMaterial(), this);
13✔
658
                options.lineMaterial = ReferLayerProperties(new THREE.LineBasicMaterial(), this);
13✔
659
                options.polygonMaterial = ReferLayerProperties(new THREE.MeshBasicMaterial(), this);
13✔
660
                // In the case we didn't instanciate the layer (this) before the convert, we can directly
661
                // pass a style using options.style.
662
                // This is usually done in some tests and if you want to use Feature2Mesh.convert()
663
                // as in examples/source_file_gpx_3d.html.
664
                options.style = options.style || (this ? this.style : undefined);
13✔
665
            }
666
            userStyle = options.style || userStyle;
14✔
667

668
            context.setCollection(collection);
14✔
669

670
            const features = collection.features;
14✔
671
            if (!features || features.length == 0) { return; }
14!
672

673
            const meshes = features.map((feature) => {
14✔
674
                const mesh = featureToMesh(feature, options);
26✔
675
                mesh.layer = this;
26✔
676
                return mesh;
26✔
677
            });
678
            const featureNode = new FeatureMesh(meshes, collection);
14✔
679

680
            return featureNode;
14✔
681
        };
682
    },
683
};
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

© 2025 Coveralls, Inc