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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/docs/api.md +66 -0
- package/docs/architecture.md +87 -0
- package/docs/cartography.md +77 -0
- package/docs/cesium.md +107 -0
- package/docs/openlayers.md +98 -0
- package/docs/styles.md +103 -0
- package/package.json +51 -0
- package/packages/cesium/package.json +13 -0
- package/packages/cesium/src/index.js +405 -0
- package/packages/ol/package.json +14 -0
- package/packages/ol/src/index.js +1705 -0
- package/packages/ol/src/labels.js +977 -0
- package/src/3dtiles/b3dm.js +38 -0
- package/src/3dtiles/clipper-surfaces.js +317 -0
- package/src/3dtiles/export.js +768 -0
- package/src/3dtiles/extrude.js +301 -0
- package/src/3dtiles/flat.js +531 -0
- package/src/3dtiles/glb.js +178 -0
- package/src/3dtiles/gpkg-buildings.js +240 -0
- package/src/3dtiles/gpkg-features.js +157 -0
- package/src/3dtiles/tileset.js +75 -0
- package/src/build.js +134 -0
- package/src/cli.js +656 -0
- package/src/export-pmtiles.js +962 -0
- package/src/geometry-read.js +50 -0
- package/src/gpkg-read.js +460 -0
- package/src/gpkg.js +567 -0
- package/src/html.js +593 -0
- package/src/layers.js +357 -0
- package/src/manifest.js +29 -0
- package/src/mvt.js +2593 -0
- package/src/ol.js +5 -0
- package/src/osm.js +2110 -0
- package/src/pmtiles-worker.js +70 -0
- package/src/pmtiles.js +260 -0
- package/src/server.js +720 -0
- package/src/style-command.js +78 -0
- package/src/style-filters.js +76 -0
- package/src/style-presets.js +93 -0
- package/src/style-themes.js +235 -0
- package/src/style.js +13 -0
- package/src/tile-cache.js +59 -0
- package/src/utils.js +222 -0
- package/styles/presets/light.json +4655 -0
- package/styles/presets/monochrome.json +4655 -0
- package/styles/presets/neon-dark-3d.json +90 -0
- package/styles/presets/neon-dark.json +4690 -0
- package/styles/presets/tactical.json +4690 -0
- 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
|
+
}
|