map-zero 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +220 -0
  4. package/docs/api.md +66 -0
  5. package/docs/architecture.md +87 -0
  6. package/docs/cartography.md +77 -0
  7. package/docs/cesium.md +107 -0
  8. package/docs/openlayers.md +98 -0
  9. package/docs/styles.md +103 -0
  10. package/package.json +51 -0
  11. package/packages/cesium/package.json +13 -0
  12. package/packages/cesium/src/index.js +405 -0
  13. package/packages/ol/package.json +14 -0
  14. package/packages/ol/src/index.js +1705 -0
  15. package/packages/ol/src/labels.js +977 -0
  16. package/src/3dtiles/b3dm.js +38 -0
  17. package/src/3dtiles/clipper-surfaces.js +317 -0
  18. package/src/3dtiles/export.js +768 -0
  19. package/src/3dtiles/extrude.js +301 -0
  20. package/src/3dtiles/flat.js +531 -0
  21. package/src/3dtiles/glb.js +178 -0
  22. package/src/3dtiles/gpkg-buildings.js +240 -0
  23. package/src/3dtiles/gpkg-features.js +157 -0
  24. package/src/3dtiles/tileset.js +75 -0
  25. package/src/build.js +134 -0
  26. package/src/cli.js +656 -0
  27. package/src/export-pmtiles.js +962 -0
  28. package/src/geometry-read.js +50 -0
  29. package/src/gpkg-read.js +460 -0
  30. package/src/gpkg.js +567 -0
  31. package/src/html.js +593 -0
  32. package/src/layers.js +357 -0
  33. package/src/manifest.js +29 -0
  34. package/src/mvt.js +2593 -0
  35. package/src/ol.js +5 -0
  36. package/src/osm.js +2110 -0
  37. package/src/pmtiles-worker.js +70 -0
  38. package/src/pmtiles.js +260 -0
  39. package/src/server.js +720 -0
  40. package/src/style-command.js +78 -0
  41. package/src/style-filters.js +76 -0
  42. package/src/style-presets.js +93 -0
  43. package/src/style-themes.js +235 -0
  44. package/src/style.js +13 -0
  45. package/src/tile-cache.js +59 -0
  46. package/src/utils.js +222 -0
  47. package/styles/presets/light.json +4655 -0
  48. package/styles/presets/monochrome.json +4655 -0
  49. package/styles/presets/neon-dark-3d.json +90 -0
  50. package/styles/presets/neon-dark.json +4690 -0
  51. package/styles/presets/tactical.json +4690 -0
  52. package/styles/themes/neon-dark.theme.json +20 -0
@@ -0,0 +1,531 @@
1
+ import earcut from 'earcut';
2
+
3
+ import { cleanRing, wgs84SurfaceNormal, wgs84ToEcef } from './extrude.js';
4
+
5
+ /**
6
+ * @typedef {{
7
+ * positions: Float32Array,
8
+ * normals: Float32Array,
9
+ * indices: Uint16Array,
10
+ * min: [number, number, number],
11
+ * max: [number, number, number],
12
+ * bbox: [number, number, number, number],
13
+ * maxHeight: number,
14
+ * featureCount: number
15
+ * }} FlatMesh
16
+ */
17
+
18
+ const DEFAULT_RIBBON_JOIN = 'round';
19
+ const DEFAULT_RIBBON_CAP = 'round';
20
+ const DEFAULT_MITER_LIMIT = 3.5;
21
+
22
+ /**
23
+ * @param {string} layerId
24
+ * @param {Array<{ geometry: Record<string, unknown>, properties: Record<string, unknown> }>} features
25
+ * @param {{
26
+ * height?: number,
27
+ * lineWidthMeters?: number,
28
+ * join?: 'miter' | 'bevel' | 'round',
29
+ * cap?: 'butt' | 'round',
30
+ * roundSegments?: number,
31
+ * miterLimit?: number
32
+ * }} options
33
+ * @returns {FlatMesh | null}
34
+ */
35
+ export function buildFlatLayerMesh(layerId, features, options = {}) {
36
+ if (lineLayer(layerId)) {
37
+ return buildLineRibbonMesh(features, {
38
+ height: options.height ?? 0.8,
39
+ widthMeters: options.lineWidthMeters ?? defaultLineWidth(layerId),
40
+ join: options.join ?? defaultJoin(layerId),
41
+ cap: options.cap ?? defaultCap(layerId),
42
+ roundSegments: options.roundSegments ?? defaultRoundSegments(layerId),
43
+ miterLimit: options.miterLimit ?? DEFAULT_MITER_LIMIT
44
+ });
45
+ }
46
+
47
+ return buildPolygonSurfaceMesh(features, {
48
+ height: options.height ?? 0.2
49
+ });
50
+ }
51
+
52
+ /**
53
+ * @param {Array<{ geometry: Record<string, unknown> }>} features
54
+ * @param {{
55
+ * height: number,
56
+ * widthMeters: number,
57
+ * join: 'miter' | 'bevel' | 'round',
58
+ * cap: 'butt' | 'round',
59
+ * roundSegments: number,
60
+ * miterLimit: number
61
+ * }} options
62
+ * @returns {FlatMesh | null}
63
+ */
64
+ function buildLineRibbonMesh(features, options) {
65
+ const positions = [];
66
+ const normals = [];
67
+ const bboxes = [];
68
+ let featureCount = 0;
69
+
70
+ for (const feature of features) {
71
+ const lines = linesFromGeometry(feature.geometry);
72
+ for (const line of lines) {
73
+ const clean = cleanLine(line);
74
+ if (clean.length < 2) {
75
+ continue;
76
+ }
77
+
78
+ const ribbon = buildContinuousRibbon(clean, options);
79
+ if (!ribbon) {
80
+ continue;
81
+ }
82
+ addRibbonPolygon(positions, normals, ribbon, options.height);
83
+
84
+ bboxes.push(expandBboxMeters(lineBbox(clean), options.widthMeters / 2));
85
+ featureCount++;
86
+ }
87
+ }
88
+
89
+ return finishMesh(positions, normals, bboxes, options.height, featureCount);
90
+ }
91
+
92
+ /**
93
+ * @param {Array<{ geometry: Record<string, unknown> }>} features
94
+ * @param {{ height: number }} options
95
+ * @returns {FlatMesh | null}
96
+ */
97
+ export function buildPolygonSurfaceMesh(features, options) {
98
+ const positions = [];
99
+ const normals = [];
100
+ const indices = [];
101
+ const bboxes = [];
102
+ let featureCount = 0;
103
+
104
+ for (const feature of features) {
105
+ const polygons = polygonsFromGeometry(feature.geometry);
106
+ for (const polygon of polygons) {
107
+ const ring = cleanRing(polygon[0] ?? []);
108
+ if (ring.length < 3) {
109
+ continue;
110
+ }
111
+
112
+ const centroid = polygonCentroid(ring);
113
+ const projected = projectRing(ring, centroid);
114
+ const triangles = earcut(projected.flat, null, 2);
115
+ if (triangles.length === 0) {
116
+ continue;
117
+ }
118
+
119
+ const normal = wgs84SurfaceNormal(centroid[0], centroid[1]);
120
+ const ecef = ring.map(([lon, lat]) => wgs84ToEcef(lon, lat, options.height));
121
+ const vertexOffset = positions.length / 3;
122
+ for (const point of ecef) {
123
+ positions.push(...point);
124
+ normals.push(...normal);
125
+ }
126
+ for (const index of triangles) {
127
+ indices.push(vertexOffset + index);
128
+ }
129
+ bboxes.push(lineBbox(ring));
130
+ featureCount++;
131
+ }
132
+ }
133
+
134
+ return finishMesh(positions, normals, bboxes, options.height, featureCount, indices);
135
+ }
136
+
137
+ /**
138
+ * @param {number[]} positions
139
+ * @param {number[]} normals
140
+ * @param {[number, number][]} ribbon
141
+ * @param {number} height
142
+ */
143
+ function addRibbonPolygon(positions, normals, ribbon, height) {
144
+ if (ribbon.length < 3) {
145
+ return;
146
+ }
147
+
148
+ const centroid = polygonCentroid(ribbon);
149
+ const projected = projectRing(ribbon, centroid);
150
+ const triangles = earcut(projected.flat, null, 2);
151
+ if (triangles.length === 0) {
152
+ return;
153
+ }
154
+
155
+ const normal = wgs84SurfaceNormal(centroid[0], centroid[1]);
156
+ const ecef = ribbon.map(([lon, lat]) => wgs84ToEcef(lon, lat, height));
157
+ for (let i = 0; i < triangles.length; i += 3) {
158
+ addTriangle(
159
+ positions,
160
+ normals,
161
+ ecef[triangles[i]],
162
+ ecef[triangles[i + 1]],
163
+ ecef[triangles[i + 2]],
164
+ normal
165
+ );
166
+ }
167
+ }
168
+
169
+ function addTriangle(positions, normals, a, b, c, normal) {
170
+ positions.push(...a, ...b, ...c);
171
+ normals.push(...normal, ...normal, ...normal);
172
+ }
173
+
174
+ function finishMesh(positions, normals, bboxes, height, featureCount, indices = []) {
175
+ if (positions.length === 0 || featureCount === 0) {
176
+ return null;
177
+ }
178
+
179
+ const positionArray = new Float32Array(positions);
180
+ const normalArray = new Float32Array(normals);
181
+ const indexArray = indices.length > 0
182
+ ? positionArray.length / 3 > 65535
183
+ ? new Uint32Array(indices)
184
+ : new Uint16Array(indices)
185
+ : new Uint16Array(0);
186
+ const bounds = minMaxVec3(positionArray);
187
+ return {
188
+ positions: positionArray,
189
+ normals: normalArray,
190
+ indices: indexArray,
191
+ min: bounds.min,
192
+ max: bounds.max,
193
+ bbox: mergeBboxes(bboxes),
194
+ maxHeight: height,
195
+ featureCount
196
+ };
197
+ }
198
+
199
+ function linesFromGeometry(geometry) {
200
+ if (geometry.type === 'LineString' && Array.isArray(geometry.coordinates)) {
201
+ return [geometry.coordinates];
202
+ }
203
+ if (geometry.type === 'MultiLineString' && Array.isArray(geometry.coordinates)) {
204
+ return geometry.coordinates;
205
+ }
206
+ if (geometry.type === 'Polygon' && Array.isArray(geometry.coordinates)) {
207
+ return [geometry.coordinates[0] ?? []];
208
+ }
209
+ if (geometry.type === 'MultiPolygon' && Array.isArray(geometry.coordinates)) {
210
+ return geometry.coordinates.map((polygon) => polygon[0] ?? []);
211
+ }
212
+ return [];
213
+ }
214
+
215
+ function polygonsFromGeometry(geometry) {
216
+ if (geometry.type === 'Polygon' && Array.isArray(geometry.coordinates)) {
217
+ return [geometry.coordinates];
218
+ }
219
+ if (geometry.type === 'MultiPolygon' && Array.isArray(geometry.coordinates)) {
220
+ return geometry.coordinates;
221
+ }
222
+ return [];
223
+ }
224
+
225
+ function cleanLine(line) {
226
+ return line
227
+ .map((point) => [Number(point?.[0]), Number(point?.[1])])
228
+ .filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
229
+ }
230
+
231
+ function lineLayer(layerId) {
232
+ return layerId === 'roads' || layerId === 'railways' || layerId === 'boundaries';
233
+ }
234
+
235
+ function defaultLineWidth(layerId) {
236
+ if (layerId === 'roads') return 6;
237
+ if (layerId === 'railways') return 3;
238
+ return 2;
239
+ }
240
+
241
+ function defaultJoin(layerId) {
242
+ return layerId === 'boundaries' ? 'bevel' : DEFAULT_RIBBON_JOIN;
243
+ }
244
+
245
+ function defaultCap(layerId) {
246
+ return layerId === 'boundaries' ? 'butt' : DEFAULT_RIBBON_CAP;
247
+ }
248
+
249
+ function defaultRoundSegments(layerId) {
250
+ if (layerId === 'roads') return 5;
251
+ if (layerId === 'railways') return 4;
252
+ return 0;
253
+ }
254
+
255
+ /**
256
+ * @param {[number, number][]} line
257
+ * @param {{
258
+ * widthMeters: number,
259
+ * join: 'miter' | 'bevel' | 'round',
260
+ * cap: 'butt' | 'round',
261
+ * roundSegments: number,
262
+ * miterLimit: number
263
+ * }} options
264
+ * @returns {[number, number][] | null}
265
+ */
266
+ function buildContinuousRibbon(line, options) {
267
+ const local = toLocalLine(line);
268
+ const closed = local.closed;
269
+ const points = local.points;
270
+ if (points.length < 2) {
271
+ return null;
272
+ }
273
+
274
+ const half = options.widthMeters / 2;
275
+ /** @type {Array<[number, number]>} */
276
+ const left = [];
277
+ /** @type {Array<[number, number]>} */
278
+ const right = [];
279
+ const count = points.length;
280
+
281
+ for (let i = 0; i < count; i++) {
282
+ const current = points[i];
283
+ const prev = i > 0 ? points[i - 1] : closed ? points[count - 1] : null;
284
+ const next = i < count - 1 ? points[i + 1] : closed ? points[0] : null;
285
+
286
+ if (!prev || !next) {
287
+ const direction = next ? normalize2(sub2(next, current)) : normalize2(sub2(current, prev));
288
+ const normal = leftNormal(direction);
289
+ left.push(add2(current, scale2(normal, half)));
290
+ right.push(add2(current, scale2(normal, -half)));
291
+ continue;
292
+ }
293
+
294
+ const join = ribbonJoinPoints(prev, current, next, half, options);
295
+ left.push(...join.left);
296
+ right.push(...join.right);
297
+ }
298
+
299
+ if (!closed && options.cap === 'round' && options.roundSegments >= 3) {
300
+ const startArc = capArc(points[0], points[1], half, options.roundSegments, true);
301
+ const endArc = capArc(points[count - 1], points[count - 2], half, options.roundSegments, false);
302
+ return localPointsToLonLat([...left, ...endArc, ...right.reverse(), ...startArc], local);
303
+ }
304
+
305
+ return localPointsToLonLat([...left, ...right.reverse()], local);
306
+ }
307
+
308
+ function ribbonJoinPoints(prev, current, next, half, options) {
309
+ const inDir = normalize2(sub2(current, prev));
310
+ const outDir = normalize2(sub2(next, current));
311
+ if (length2(inDir) === 0 || length2(outDir) === 0) {
312
+ return {
313
+ left: [[current[0], current[1] + half]],
314
+ right: [[current[0], current[1] - half]]
315
+ };
316
+ }
317
+
318
+ const inNormal = leftNormal(inDir);
319
+ const outNormal = leftNormal(outDir);
320
+ const leftPrev = add2(current, scale2(inNormal, half));
321
+ const leftNext = add2(current, scale2(outNormal, half));
322
+ const rightPrev = add2(current, scale2(inNormal, -half));
323
+ const rightNext = add2(current, scale2(outNormal, -half));
324
+
325
+ const leftMiter = lineIntersection(leftPrev, inDir, leftNext, outDir);
326
+ const rightMiter = lineIntersection(rightPrev, inDir, rightNext, outDir);
327
+ const maxMiter = half * options.miterLimit;
328
+ const mild = leftMiter && rightMiter &&
329
+ distance2(leftMiter, current) <= maxMiter &&
330
+ distance2(rightMiter, current) <= maxMiter;
331
+
332
+ if (options.join === 'miter' || (options.join === 'round' && mild)) {
333
+ if (mild) {
334
+ return { left: [leftMiter], right: [rightMiter] };
335
+ }
336
+ }
337
+
338
+ if (options.join === 'round' && options.roundSegments >= 3) {
339
+ return {
340
+ left: arcAround(current, leftPrev, leftNext, options.roundSegments),
341
+ right: arcAround(current, rightNext, rightPrev, options.roundSegments)
342
+ };
343
+ }
344
+
345
+ return {
346
+ left: [leftPrev, leftNext],
347
+ right: [rightNext, rightPrev]
348
+ };
349
+ }
350
+
351
+ function capArc(endpoint, neighbor, half, segments, start) {
352
+ const dir = normalize2(sub2(endpoint, neighbor));
353
+ const normal = leftNormal(dir);
354
+ const a = add2(endpoint, scale2(normal, start ? -half : half));
355
+ const b = add2(endpoint, scale2(normal, start ? half : -half));
356
+ return arcAround(endpoint, a, b, Math.max(3, segments));
357
+ }
358
+
359
+ function arcAround(center, from, to, segments) {
360
+ const start = Math.atan2(from[1] - center[1], from[0] - center[0]);
361
+ let end = Math.atan2(to[1] - center[1], to[0] - center[0]);
362
+ while (end < start) {
363
+ end += Math.PI * 2;
364
+ }
365
+ if (end - start > Math.PI) {
366
+ end -= Math.PI * 2;
367
+ }
368
+
369
+ const steps = Math.max(1, Math.ceil(Math.abs(end - start) / Math.PI * segments));
370
+ const radius = distance2(from, center);
371
+ const points = [];
372
+ for (let i = 0; i <= steps; i++) {
373
+ const t = i / steps;
374
+ const angle = start + (end - start) * t;
375
+ points.push([
376
+ center[0] + Math.cos(angle) * radius,
377
+ center[1] + Math.sin(angle) * radius
378
+ ]);
379
+ }
380
+ return points;
381
+ }
382
+
383
+ function toLocalLine(line) {
384
+ const closed = line.length > 2 && samePoint(line[0], line[line.length - 1]);
385
+ const points = closed ? line.slice(0, -1) : line;
386
+ const origin = polygonCentroid(points);
387
+ const meanLat = origin[1] * Math.PI / 180;
388
+ const metersPerLon = Math.max(1, 111320 * Math.cos(meanLat));
389
+ const metersPerLat = 110540;
390
+ return {
391
+ origin,
392
+ metersPerLon,
393
+ metersPerLat,
394
+ closed,
395
+ points: points.map(([lon, lat]) => [
396
+ (lon - origin[0]) * metersPerLon,
397
+ (lat - origin[1]) * metersPerLat
398
+ ])
399
+ };
400
+ }
401
+
402
+ function localPointsToLonLat(points, local) {
403
+ return points.map(([x, y]) => [
404
+ local.origin[0] + x / local.metersPerLon,
405
+ local.origin[1] + y / local.metersPerLat
406
+ ]);
407
+ }
408
+
409
+ function lineIntersection(pointA, dirA, pointB, dirB) {
410
+ const cross = cross2(dirA, dirB);
411
+ if (Math.abs(cross) < 1e-6) {
412
+ return null;
413
+ }
414
+
415
+ const diff = sub2(pointB, pointA);
416
+ const t = cross2(diff, dirB) / cross;
417
+ return add2(pointA, scale2(dirA, t));
418
+ }
419
+
420
+ function samePoint(a, b) {
421
+ return Math.abs(a[0] - b[0]) < 1e-12 && Math.abs(a[1] - b[1]) < 1e-12;
422
+ }
423
+
424
+ function add2(a, b) {
425
+ return [a[0] + b[0], a[1] + b[1]];
426
+ }
427
+
428
+ function sub2(a, b) {
429
+ return [a[0] - b[0], a[1] - b[1]];
430
+ }
431
+
432
+ function scale2(a, scale) {
433
+ return [a[0] * scale, a[1] * scale];
434
+ }
435
+
436
+ function length2(a) {
437
+ return Math.hypot(a[0], a[1]);
438
+ }
439
+
440
+ function distance2(a, b) {
441
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
442
+ }
443
+
444
+ function normalize2(a) {
445
+ const length = length2(a);
446
+ return length > 0 ? [a[0] / length, a[1] / length] : [0, 0];
447
+ }
448
+
449
+ function leftNormal(direction) {
450
+ return [-direction[1], direction[0]];
451
+ }
452
+
453
+ function cross2(a, b) {
454
+ return a[0] * b[1] - a[1] * b[0];
455
+ }
456
+
457
+ function projectRing(points, centroid) {
458
+ const meanLat = centroid[1] * Math.PI / 180;
459
+ const metersPerLon = Math.max(1, 111320 * Math.cos(meanLat));
460
+ const metersPerLat = 110540;
461
+ const flat = [];
462
+ for (const [lon, lat] of points) {
463
+ flat.push((lon - centroid[0]) * metersPerLon, (lat - centroid[1]) * metersPerLat);
464
+ }
465
+ return { flat };
466
+ }
467
+
468
+ function polygonCentroid(points) {
469
+ let lon = 0;
470
+ let lat = 0;
471
+ for (const point of points) {
472
+ lon += point[0];
473
+ lat += point[1];
474
+ }
475
+ return [lon / points.length, lat / points.length];
476
+ }
477
+
478
+ function lineBbox(points) {
479
+ let minLon = Infinity;
480
+ let minLat = Infinity;
481
+ let maxLon = -Infinity;
482
+ let maxLat = -Infinity;
483
+ for (const [lon, lat] of points) {
484
+ minLon = Math.min(minLon, lon);
485
+ minLat = Math.min(minLat, lat);
486
+ maxLon = Math.max(maxLon, lon);
487
+ maxLat = Math.max(maxLat, lat);
488
+ }
489
+ return [minLon, minLat, maxLon, maxLat];
490
+ }
491
+
492
+ function expandBboxMeters(bbox, meters) {
493
+ const meanLat = ((bbox[1] + bbox[3]) / 2) * Math.PI / 180;
494
+ const metersPerLon = Math.max(1, 111320 * Math.cos(meanLat));
495
+ const lonPad = meters / metersPerLon;
496
+ const latPad = meters / 110540;
497
+ return [
498
+ bbox[0] - lonPad,
499
+ bbox[1] - latPad,
500
+ bbox[2] + lonPad,
501
+ bbox[3] + latPad
502
+ ];
503
+ }
504
+
505
+ function mergeBboxes(bboxes) {
506
+ let minLon = Infinity;
507
+ let minLat = Infinity;
508
+ let maxLon = -Infinity;
509
+ let maxLat = -Infinity;
510
+ for (const bbox of bboxes) {
511
+ minLon = Math.min(minLon, bbox[0]);
512
+ minLat = Math.min(minLat, bbox[1]);
513
+ maxLon = Math.max(maxLon, bbox[2]);
514
+ maxLat = Math.max(maxLat, bbox[3]);
515
+ }
516
+ return [minLon, minLat, maxLon, maxLat];
517
+ }
518
+
519
+ function minMaxVec3(positions) {
520
+ const min = [Infinity, Infinity, Infinity];
521
+ const max = [-Infinity, -Infinity, -Infinity];
522
+ for (let i = 0; i < positions.length; i += 3) {
523
+ min[0] = Math.min(min[0], positions[i]);
524
+ min[1] = Math.min(min[1], positions[i + 1]);
525
+ min[2] = Math.min(min[2], positions[i + 2]);
526
+ max[0] = Math.max(max[0], positions[i]);
527
+ max[1] = Math.max(max[1], positions[i + 1]);
528
+ max[2] = Math.max(max[2], positions[i + 2]);
529
+ }
530
+ return { min, max };
531
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @typedef {{
3
+ * positions: Float32Array,
4
+ * normals?: Float32Array,
5
+ * indices: Uint16Array | Uint32Array,
6
+ * min: [number, number, number],
7
+ * max: [number, number, number]
8
+ * }} Mesh
9
+ */
10
+
11
+ /**
12
+ * Build a minimal unlit GLB for one merged mesh.
13
+ *
14
+ * @param {Mesh} mesh
15
+ * @param {{ color?: [number, number, number, number], generator?: string, includeNormals?: boolean }} [options]
16
+ * @returns {Buffer}
17
+ */
18
+ export function buildGlbFromMesh(mesh, options = {}) {
19
+ const chunks = [];
20
+ const bufferViews = [];
21
+ const accessors = [];
22
+ const attributes = {};
23
+
24
+ const positionView = appendBuffer(chunks, bufferFromTypedArray(mesh.positions), 4);
25
+ bufferViews.push({ buffer: 0, byteOffset: positionView.byteOffset, byteLength: positionView.byteLength, target: 34962 });
26
+ attributes.POSITION = accessors.length;
27
+ accessors.push({
28
+ bufferView: bufferViews.length - 1,
29
+ byteOffset: 0,
30
+ componentType: 5126,
31
+ count: mesh.positions.length / 3,
32
+ type: 'VEC3',
33
+ min: mesh.min,
34
+ max: mesh.max
35
+ });
36
+
37
+ const includeNormals = options.includeNormals === true
38
+ && mesh.normals
39
+ && mesh.normals.length === mesh.positions.length;
40
+ if (includeNormals) {
41
+ const normalView = appendBuffer(chunks, bufferFromTypedArray(mesh.normals), 4);
42
+ bufferViews.push({ buffer: 0, byteOffset: normalView.byteOffset, byteLength: normalView.byteLength, target: 34962 });
43
+ attributes.NORMAL = accessors.length;
44
+ accessors.push({
45
+ bufferView: bufferViews.length - 1,
46
+ byteOffset: 0,
47
+ componentType: 5126,
48
+ count: mesh.normals.length / 3,
49
+ type: 'VEC3'
50
+ });
51
+ }
52
+
53
+ const hasIndices = mesh.indices.length > 0;
54
+ let indexAccessor = null;
55
+ if (hasIndices) {
56
+ const indexView = appendBuffer(chunks, bufferFromTypedArray(mesh.indices), 4);
57
+ bufferViews.push({ buffer: 0, byteOffset: indexView.byteOffset, byteLength: indexView.byteLength, target: 34963 });
58
+ indexAccessor = accessors.length;
59
+ accessors.push({
60
+ bufferView: bufferViews.length - 1,
61
+ byteOffset: 0,
62
+ componentType: mesh.indices instanceof Uint32Array ? 5125 : 5123,
63
+ count: mesh.indices.length,
64
+ type: 'SCALAR'
65
+ });
66
+ }
67
+
68
+ const bin = Buffer.concat(chunks);
69
+ const primitive = {
70
+ attributes,
71
+ material: 0
72
+ };
73
+ if (indexAccessor !== null) {
74
+ primitive.indices = indexAccessor;
75
+ }
76
+
77
+ const gltf = {
78
+ asset: {
79
+ version: '2.0',
80
+ generator: options.generator ?? 'map-zero'
81
+ },
82
+ extensionsUsed: ['KHR_materials_unlit'],
83
+ scene: 0,
84
+ scenes: [{ nodes: [0] }],
85
+ nodes: [{ mesh: 0 }],
86
+ meshes: [{
87
+ primitives: [primitive]
88
+ }],
89
+ materials: [{
90
+ pbrMetallicRoughness: {
91
+ baseColorFactor: options.color ?? [0, 1, 1, 1],
92
+ metallicFactor: 0,
93
+ roughnessFactor: 1
94
+ },
95
+ extensions: {
96
+ KHR_materials_unlit: {}
97
+ },
98
+ doubleSided: true
99
+ }],
100
+ buffers: [{ byteLength: bin.length }],
101
+ bufferViews,
102
+ accessors
103
+ };
104
+
105
+ return buildGlb(gltf, bin);
106
+ }
107
+
108
+ /**
109
+ * @param {ArrayBufferView} typedArray
110
+ * @returns {Buffer}
111
+ */
112
+ function bufferFromTypedArray(typedArray) {
113
+ return Buffer.from(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
114
+ }
115
+
116
+ /**
117
+ * @param {Record<string, unknown>} gltf
118
+ * @param {Buffer} binBuffer
119
+ * @returns {Buffer}
120
+ */
121
+ export function buildGlb(gltf, binBuffer) {
122
+ const jsonChunk = padBuffer(Buffer.from(JSON.stringify(gltf), 'utf8'), 4, 0x20);
123
+ const binChunk = padBuffer(binBuffer, 4, 0);
124
+ const totalLength = 12 + 8 + jsonChunk.length + 8 + binChunk.length;
125
+ const header = Buffer.alloc(12);
126
+ header.writeUInt32LE(0x46546c67, 0);
127
+ header.writeUInt32LE(2, 4);
128
+ header.writeUInt32LE(totalLength, 8);
129
+ const jsonHeader = Buffer.alloc(8);
130
+ jsonHeader.writeUInt32LE(jsonChunk.length, 0);
131
+ jsonHeader.writeUInt32LE(0x4e4f534a, 4);
132
+ const binHeader = Buffer.alloc(8);
133
+ binHeader.writeUInt32LE(binChunk.length, 0);
134
+ binHeader.writeUInt32LE(0x004e4942, 4);
135
+ return Buffer.concat([header, jsonHeader, jsonChunk, binHeader, binChunk]);
136
+ }
137
+
138
+ /**
139
+ * @param {Buffer[]} chunks
140
+ * @param {Buffer} buffer
141
+ * @param {number} alignment
142
+ * @returns {{ byteOffset: number, byteLength: number }}
143
+ */
144
+ function appendBuffer(chunks, buffer, alignment) {
145
+ const byteOffset = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
146
+ const paddedOffset = align(byteOffset, alignment);
147
+ if (paddedOffset > byteOffset) {
148
+ chunks.push(Buffer.alloc(paddedOffset - byteOffset));
149
+ }
150
+
151
+ chunks.push(buffer);
152
+ return {
153
+ byteOffset: paddedOffset,
154
+ byteLength: buffer.length
155
+ };
156
+ }
157
+
158
+ /**
159
+ * @param {Buffer} buffer
160
+ * @param {number} alignment
161
+ * @param {number} fill
162
+ * @returns {Buffer}
163
+ */
164
+ function padBuffer(buffer, alignment, fill) {
165
+ const targetLength = align(buffer.length, alignment);
166
+ return targetLength === buffer.length
167
+ ? buffer
168
+ : Buffer.concat([buffer, Buffer.alloc(targetLength - buffer.length, fill)]);
169
+ }
170
+
171
+ /**
172
+ * @param {number} value
173
+ * @param {number} alignment
174
+ * @returns {number}
175
+ */
176
+ function align(value, alignment) {
177
+ return Math.ceil(value / alignment) * alignment;
178
+ }