@tileflow/dev 0.1.0-alpha.31 → 0.1.0-alpha.33
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/README.md +67 -2
- package/THIRD_PARTY_NOTICES.md +225 -3
- package/dist/artifacts.d.ts +6 -2
- package/dist/artifacts.js +17 -2
- package/dist/{chunk-IR22ZTQW.js → chunk-QYU4OW5A.js} +481 -27
- package/dist/{chunk-2QXKVQ6O.js → chunk-WJOF6USY.js} +47 -6
- package/dist/devtools-browser.js +16914 -0
- package/dist/devtools-observer.js +95 -0
- package/dist/fonts.d.ts +1 -1
- package/dist/icons-BdOehQmB.d.ts +424 -0
- package/dist/icons.d.ts +3 -339
- package/dist/icons.js +5 -1
- package/dist/{preview-assets-MVUJJ5XR.js → preview-assets-KGUICQHG.js} +6 -0
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +250 -108
- package/dist/server.d.ts +11 -4
- package/dist/server.js +1843 -280
- package/docs/beacon-map-audits.md +145 -0
- package/docs/bounded-vector-feature-inspection.md +3 -4
- package/docs/layers-inspection.md +92 -0
- package/docs/local-comparison-workbench-and-compiler-inspection.md +74 -171
- package/docs/map-design-diagnostics.md +76 -0
- package/docs/native-artifacts.md +3 -3
- package/package.json +7 -5
- package/ui-fonts/hubot-sans-OFL.txt +93 -0
- package/ui-fonts/hubot-sans.woff2 +0 -0
- package/ui-fonts/inter-OFL.txt +92 -0
- package/ui-fonts/inter.woff2 +0 -0
- package/ui-fonts/jetbrains-mono-OFL.txt +93 -0
- package/ui-fonts/jetbrains-mono.woff2 +0 -0
- package/ui-fonts/mona-sans-OFL.txt +93 -0
- package/ui-fonts/mona-sans.woff2 +0 -0
|
@@ -17,14 +17,15 @@ import {
|
|
|
17
17
|
tileflowIconSpriteIndexSchema
|
|
18
18
|
} from "@tileflow/core";
|
|
19
19
|
import {
|
|
20
|
-
compareCodeUnits as
|
|
20
|
+
compareCodeUnits as compareCodeUnits5,
|
|
21
21
|
hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels3,
|
|
22
22
|
parseResolvedTileflowMap,
|
|
23
23
|
parseTileflowIconJson as parseTileflowIconJson2,
|
|
24
24
|
serializeCanonicalJson as serializeCanonicalJson3,
|
|
25
25
|
sha256Hex as sha256Hex3,
|
|
26
26
|
tileflowIconIdPattern,
|
|
27
|
-
tileflowIconPackageLimits as tileflowIconPackageLimits4
|
|
27
|
+
tileflowIconPackageLimits as tileflowIconPackageLimits4,
|
|
28
|
+
tileflowModelContentType as tileflowModelContentType2
|
|
28
29
|
} from "@tileflow/core";
|
|
29
30
|
|
|
30
31
|
// src/icon-composition.ts
|
|
@@ -940,14 +941,26 @@ function transform(grid, width, height) {
|
|
|
940
941
|
}
|
|
941
942
|
|
|
942
943
|
// src/tree-models.ts
|
|
944
|
+
import { MeshoptEncoder } from "meshoptimizer/encoder";
|
|
943
945
|
import { createHash } from "crypto";
|
|
944
946
|
import { lstat as lstat3, readdir as readdir2, readFile } from "fs/promises";
|
|
945
947
|
import { join as join3 } from "path";
|
|
946
948
|
import {
|
|
949
|
+
compareCodeUnits as compareCodeUnits4,
|
|
950
|
+
encodeTileflowModels,
|
|
947
951
|
encodeTileflowTreeModel,
|
|
952
|
+
hashTileflowModelBundleManifest,
|
|
953
|
+
serializeTileflowLandmarkRegion,
|
|
954
|
+
tileflowLandmarkTileOf,
|
|
955
|
+
tileflowModelBundleFileName,
|
|
956
|
+
tileflowModelBundleLimits,
|
|
957
|
+
tileflowModelContentType,
|
|
948
958
|
tileflowTreeModelLimits as tileflowTreeModelLimits2
|
|
949
959
|
} from "@tileflow/core";
|
|
950
960
|
|
|
961
|
+
// src/building-model-mesh.ts
|
|
962
|
+
import { tileflowModelLimits, tileflowModelParts } from "@tileflow/core";
|
|
963
|
+
|
|
951
964
|
// src/gltf-reader.ts
|
|
952
965
|
var TileflowGltfError = class extends Error {
|
|
953
966
|
name = "TileflowGltfError";
|
|
@@ -1243,6 +1256,287 @@ function vector(value, fallback) {
|
|
|
1243
1256
|
return list.length === fallback.length && list.every(Number.isFinite) ? list : fallback;
|
|
1244
1257
|
}
|
|
1245
1258
|
|
|
1259
|
+
// src/building-model-mesh.ts
|
|
1260
|
+
var tileflowBuildingModelPartNames = Object.keys(tileflowModelParts);
|
|
1261
|
+
function isTileflowBuildingModel(primitives) {
|
|
1262
|
+
const drawn = primitives.filter((primitive) => !isFootprint(primitive));
|
|
1263
|
+
return drawn.length > 0 && drawn.every((primitive) => partOf(primitive.material) !== void 0);
|
|
1264
|
+
}
|
|
1265
|
+
function tileflowBuildingModelDeclaredFootprint(primitives) {
|
|
1266
|
+
const positions = [];
|
|
1267
|
+
const indices = [];
|
|
1268
|
+
for (const primitive of primitives) {
|
|
1269
|
+
if (!isFootprint(primitive)) continue;
|
|
1270
|
+
const offset = positions.length / 3;
|
|
1271
|
+
positions.push(...primitive.positions);
|
|
1272
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
1273
|
+
}
|
|
1274
|
+
return indices.length > 0 ? { positions, indices } : void 0;
|
|
1275
|
+
}
|
|
1276
|
+
function prepareTileflowBuildingModelMesh(bytes) {
|
|
1277
|
+
const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes) : bytes;
|
|
1278
|
+
const positions = [];
|
|
1279
|
+
const normals = [];
|
|
1280
|
+
const parts = [];
|
|
1281
|
+
const luminance = [];
|
|
1282
|
+
const indices = [];
|
|
1283
|
+
for (const primitive of primitives) {
|
|
1284
|
+
if (isFootprint(primitive)) continue;
|
|
1285
|
+
const part = partOf(primitive.material);
|
|
1286
|
+
if (part === void 0)
|
|
1287
|
+
throw new TileflowGltfError(
|
|
1288
|
+
primitive.material === void 0 ? "has a mesh without a material; name each material surface, secondary, glass, accent or roof" : `has a material "${primitive.material}" that names no part of a building; name each material surface, secondary, glass, accent or roof`
|
|
1289
|
+
);
|
|
1290
|
+
const offset = positions.length / 3;
|
|
1291
|
+
const count2 = primitive.positions.length / 3;
|
|
1292
|
+
positions.push(...primitive.positions);
|
|
1293
|
+
for (let vertex = 0; vertex < count2; vertex += 1) {
|
|
1294
|
+
parts.push(part);
|
|
1295
|
+
normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
|
|
1296
|
+
const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
|
|
1297
|
+
luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
1298
|
+
}
|
|
1299
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
1300
|
+
}
|
|
1301
|
+
const count = positions.length / 3;
|
|
1302
|
+
const triangles2 = indices.length / 3;
|
|
1303
|
+
if (triangles2 > tileflowModelLimits.triangles)
|
|
1304
|
+
throw new TileflowGltfError(
|
|
1305
|
+
`has ${triangles2.toLocaleString("en")} triangles; a building model takes at most ${tileflowModelLimits.triangles.toLocaleString("en")} a level. Simplify it and export it again`
|
|
1306
|
+
);
|
|
1307
|
+
if (count > tileflowModelLimits.vertices)
|
|
1308
|
+
throw new TileflowGltfError(
|
|
1309
|
+
`has ${count.toLocaleString("en")} vertices; a building model takes at most ${tileflowModelLimits.vertices.toLocaleString("en")} a level. Merge vertices by distance and export it again`
|
|
1310
|
+
);
|
|
1311
|
+
let low = Infinity;
|
|
1312
|
+
let high = -Infinity;
|
|
1313
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1314
|
+
low = Math.min(low, positions[vertex * 3 + 2]);
|
|
1315
|
+
high = Math.max(high, positions[vertex * 3 + 2]);
|
|
1316
|
+
}
|
|
1317
|
+
if (low < -0.5)
|
|
1318
|
+
throw new TileflowGltfError(
|
|
1319
|
+
`reaches ${(-low).toFixed(1)} m below its origin; put the origin on the ground where the building stands, with the building above it`
|
|
1320
|
+
);
|
|
1321
|
+
if (!(high > 0)) throw new TileflowGltfError("has no height above its origin");
|
|
1322
|
+
const sums = new Array(count * 3).fill(0);
|
|
1323
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1324
|
+
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
1325
|
+
const n = cross(
|
|
1326
|
+
sub(point(positions, b), point(positions, a)),
|
|
1327
|
+
sub(point(positions, c), point(positions, a))
|
|
1328
|
+
);
|
|
1329
|
+
for (const vertex of [a, b, c])
|
|
1330
|
+
for (let axis = 0; axis < 3; axis += 1) sums[vertex * 3 + axis] += n[axis];
|
|
1331
|
+
}
|
|
1332
|
+
const outNormals = [];
|
|
1333
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1334
|
+
const n = normals[vertex] ?? sums.slice(vertex * 3, vertex * 3 + 3);
|
|
1335
|
+
const size = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
1336
|
+
outNormals.push(n[0] / size, n[1] / size, n[2] / size);
|
|
1337
|
+
}
|
|
1338
|
+
const shades = new Array(count).fill(0);
|
|
1339
|
+
for (const part of new Set(parts)) {
|
|
1340
|
+
const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
|
|
1341
|
+
const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
|
|
1342
|
+
const spread = Math.max(0.06, mean * 0.6);
|
|
1343
|
+
for (const vertex of members)
|
|
1344
|
+
shades[vertex] = clamp((luminance[vertex] - mean) / spread, -1, 1);
|
|
1345
|
+
}
|
|
1346
|
+
return {
|
|
1347
|
+
positions,
|
|
1348
|
+
normals: outNormals,
|
|
1349
|
+
parts,
|
|
1350
|
+
shades,
|
|
1351
|
+
occlusion: new Array(count).fill(1),
|
|
1352
|
+
indices
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function tileflowBuildingModelFootprint(mesh) {
|
|
1356
|
+
const { positions, indices } = mesh;
|
|
1357
|
+
let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1358
|
+
for (let at = 0; at < positions.length; at += 3) {
|
|
1359
|
+
minX = Math.min(minX, positions[at]);
|
|
1360
|
+
maxX = Math.max(maxX, positions[at]);
|
|
1361
|
+
minY = Math.min(minY, positions[at + 1]);
|
|
1362
|
+
maxY = Math.max(maxY, positions[at + 1]);
|
|
1363
|
+
}
|
|
1364
|
+
const cell = Math.max(1, Math.max(maxX - minX, maxY - minY) / 1024);
|
|
1365
|
+
const columns = Math.ceil((maxX - minX) / cell) + 5;
|
|
1366
|
+
const rows = Math.ceil((maxY - minY) / cell) + 5;
|
|
1367
|
+
const x0 = minX - 2 * cell;
|
|
1368
|
+
const y0 = minY - 2 * cell;
|
|
1369
|
+
const filled = new Uint8Array(columns * rows);
|
|
1370
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1371
|
+
const corners = [indices[at], indices[at + 1], indices[at + 2]].map((vertex) => [
|
|
1372
|
+
(positions[vertex * 3] - x0) / cell,
|
|
1373
|
+
(positions[vertex * 3 + 1] - y0) / cell
|
|
1374
|
+
]);
|
|
1375
|
+
for (const [cx, cy] of corners) filled[Math.floor(cy) * columns + Math.floor(cx)] = 1;
|
|
1376
|
+
const left = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[0]))));
|
|
1377
|
+
const right = Math.min(columns - 1, Math.ceil(Math.max(...corners.map((c2) => c2[0]))));
|
|
1378
|
+
const top = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[1]))));
|
|
1379
|
+
const bottom = Math.min(rows - 1, Math.ceil(Math.max(...corners.map((c2) => c2[1]))));
|
|
1380
|
+
const [a, b, c] = corners;
|
|
1381
|
+
const area = edge(a, b, c);
|
|
1382
|
+
if (Math.abs(area) < 1e-9) continue;
|
|
1383
|
+
for (let y = top; y <= bottom; y += 1)
|
|
1384
|
+
for (let x = left; x <= right; x += 1) {
|
|
1385
|
+
const p = [x + 0.5, y + 0.5];
|
|
1386
|
+
const w0 = edge(b, c, p) / area;
|
|
1387
|
+
const w1 = edge(c, a, p) / area;
|
|
1388
|
+
const w2 = edge(a, b, p) / area;
|
|
1389
|
+
if (w0 >= 0 && w1 >= 0 && w2 >= 0) filled[y * columns + x] = 1;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const grown = new Uint8Array(filled);
|
|
1393
|
+
for (let y = 1; y < rows - 1; y += 1)
|
|
1394
|
+
for (let x = 1; x < columns - 1; x += 1) {
|
|
1395
|
+
if (filled[y * columns + x]) continue;
|
|
1396
|
+
for (let dy = -1; dy <= 1 && !grown[y * columns + x]; dy += 1)
|
|
1397
|
+
for (let dx = -1; dx <= 1; dx += 1)
|
|
1398
|
+
if (filled[(y + dy) * columns + x + dx]) {
|
|
1399
|
+
grown[y * columns + x] = 1;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const outside = new Uint8Array(columns * rows);
|
|
1404
|
+
const stack = [0];
|
|
1405
|
+
outside[0] = 1;
|
|
1406
|
+
while (stack.length) {
|
|
1407
|
+
const at = stack.pop();
|
|
1408
|
+
const x = at % columns;
|
|
1409
|
+
const y = (at - x) / columns;
|
|
1410
|
+
for (const [dx, dy] of [
|
|
1411
|
+
[1, 0],
|
|
1412
|
+
[-1, 0],
|
|
1413
|
+
[0, 1],
|
|
1414
|
+
[0, -1]
|
|
1415
|
+
]) {
|
|
1416
|
+
const nx = x + dx;
|
|
1417
|
+
const ny = y + dy;
|
|
1418
|
+
if (nx < 0 || ny < 0 || nx >= columns || ny >= rows) continue;
|
|
1419
|
+
const next2 = ny * columns + nx;
|
|
1420
|
+
if (outside[next2] || grown[next2]) continue;
|
|
1421
|
+
outside[next2] = 1;
|
|
1422
|
+
stack.push(next2);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const covered = (x, y) => x >= 0 && y >= 0 && x < columns && y < rows && !outside[y * columns + x];
|
|
1426
|
+
const next = /* @__PURE__ */ new Map();
|
|
1427
|
+
const add = (from, to) => {
|
|
1428
|
+
const key = `${from[0]},${from[1]}`;
|
|
1429
|
+
const list = next.get(key) ?? [];
|
|
1430
|
+
list.push(to);
|
|
1431
|
+
next.set(key, list);
|
|
1432
|
+
};
|
|
1433
|
+
for (let y = 0; y < rows; y += 1)
|
|
1434
|
+
for (let x = 0; x < columns; x += 1) {
|
|
1435
|
+
if (!covered(x, y)) continue;
|
|
1436
|
+
if (!covered(x, y - 1)) add([x, y], [x + 1, y]);
|
|
1437
|
+
if (!covered(x + 1, y)) add([x + 1, y], [x + 1, y + 1]);
|
|
1438
|
+
if (!covered(x, y + 1)) add([x + 1, y + 1], [x, y + 1]);
|
|
1439
|
+
if (!covered(x - 1, y)) add([x, y + 1], [x, y]);
|
|
1440
|
+
}
|
|
1441
|
+
const rings = [];
|
|
1442
|
+
for (const [startKey, targets] of next) {
|
|
1443
|
+
while (targets.length) {
|
|
1444
|
+
const start = startKey.split(",").map(Number);
|
|
1445
|
+
const ring = [start];
|
|
1446
|
+
let current = targets.pop();
|
|
1447
|
+
while (`${current[0]},${current[1]}` !== startKey) {
|
|
1448
|
+
ring.push(current);
|
|
1449
|
+
const outs = next.get(`${current[0]},${current[1]}`);
|
|
1450
|
+
if (!outs?.length) break;
|
|
1451
|
+
current = outs.pop();
|
|
1452
|
+
}
|
|
1453
|
+
const simplified = simplify(ring, 0.75);
|
|
1454
|
+
if (simplified.length >= 3)
|
|
1455
|
+
rings.push(simplified.flatMap(([x, y]) => [x0 + x * cell, y0 + y * cell]));
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
rings.sort((a, b) => Math.abs(ringArea(b)) - Math.abs(ringArea(a)));
|
|
1459
|
+
const kept = [];
|
|
1460
|
+
let points = 0;
|
|
1461
|
+
for (const ring of rings) {
|
|
1462
|
+
if (points + ring.length / 2 > tileflowModelLimits.footprintPoints) break;
|
|
1463
|
+
kept.push(ring);
|
|
1464
|
+
points += ring.length / 2;
|
|
1465
|
+
}
|
|
1466
|
+
return kept;
|
|
1467
|
+
}
|
|
1468
|
+
function isFootprint(primitive) {
|
|
1469
|
+
return primitive.material?.trim().toLowerCase().replace(/\.\d+$/u, "") === "footprint";
|
|
1470
|
+
}
|
|
1471
|
+
function partOf(material) {
|
|
1472
|
+
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
1473
|
+
const part = tileflowBuildingModelPartNames.find((candidate) => candidate === name);
|
|
1474
|
+
return part === void 0 ? void 0 : tileflowModelParts[part];
|
|
1475
|
+
}
|
|
1476
|
+
function simplify(ring, tolerance) {
|
|
1477
|
+
if (ring.length <= 4) return [...ring];
|
|
1478
|
+
let far = 0;
|
|
1479
|
+
let distance = -1;
|
|
1480
|
+
for (const [index, [x, y]] of ring.entries()) {
|
|
1481
|
+
const d = Math.hypot(x - ring[0][0], y - ring[0][1]);
|
|
1482
|
+
if (d > distance) {
|
|
1483
|
+
distance = d;
|
|
1484
|
+
far = index;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const half = (points) => {
|
|
1488
|
+
if (points.length <= 2) return [...points];
|
|
1489
|
+
const [a, b] = [points[0], points[points.length - 1]];
|
|
1490
|
+
let index = 0;
|
|
1491
|
+
let most = -1;
|
|
1492
|
+
for (let at = 1; at < points.length - 1; at += 1) {
|
|
1493
|
+
const d = segmentDistance(points[at], a, b);
|
|
1494
|
+
if (d > most) {
|
|
1495
|
+
most = d;
|
|
1496
|
+
index = at;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (most <= tolerance) return [a, b];
|
|
1500
|
+
const left = half(points.slice(0, index + 1));
|
|
1501
|
+
return [...left.slice(0, -1), ...half(points.slice(index))];
|
|
1502
|
+
};
|
|
1503
|
+
const first = half(ring.slice(0, far + 1));
|
|
1504
|
+
const second = half([...ring.slice(far), ring[0]]);
|
|
1505
|
+
return [...first.slice(0, -1), ...second.slice(0, -1)];
|
|
1506
|
+
}
|
|
1507
|
+
function segmentDistance(p, a, b) {
|
|
1508
|
+
const dx = b[0] - a[0];
|
|
1509
|
+
const dy = b[1] - a[1];
|
|
1510
|
+
const length = dx * dx + dy * dy;
|
|
1511
|
+
const t = length > 0 ? clamp(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
|
|
1512
|
+
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
|
1513
|
+
}
|
|
1514
|
+
function ringArea(ring) {
|
|
1515
|
+
let area = 0;
|
|
1516
|
+
for (let a = 0, b = ring.length - 2; a < ring.length; b = a, a += 2)
|
|
1517
|
+
area += ring[b] * ring[a + 1] - ring[a] * ring[b + 1];
|
|
1518
|
+
return area / 2;
|
|
1519
|
+
}
|
|
1520
|
+
function edge(a, b, p) {
|
|
1521
|
+
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
1522
|
+
}
|
|
1523
|
+
function point(list, vertex) {
|
|
1524
|
+
return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
|
|
1525
|
+
}
|
|
1526
|
+
function sub(a, b) {
|
|
1527
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1528
|
+
}
|
|
1529
|
+
function cross(a, b) {
|
|
1530
|
+
return [
|
|
1531
|
+
a[1] * b[2] - a[2] * b[1],
|
|
1532
|
+
a[2] * b[0] - a[0] * b[2],
|
|
1533
|
+
a[0] * b[1] - a[1] * b[0]
|
|
1534
|
+
];
|
|
1535
|
+
}
|
|
1536
|
+
function clamp(value, low, high) {
|
|
1537
|
+
return Math.min(high, Math.max(low, value));
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1246
1540
|
// src/tree-model-mesh.ts
|
|
1247
1541
|
import {
|
|
1248
1542
|
tileflowTreeModelLimits,
|
|
@@ -1257,7 +1551,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
|
|
|
1257
1551
|
const luminance = [];
|
|
1258
1552
|
const indices = [];
|
|
1259
1553
|
for (const primitive of primitives) {
|
|
1260
|
-
const part =
|
|
1554
|
+
const part = partOf2(primitive.material);
|
|
1261
1555
|
const offset = positions.length / 3;
|
|
1262
1556
|
const count2 = primitive.positions.length / 3;
|
|
1263
1557
|
positions.push(...primitive.positions);
|
|
@@ -1304,7 +1598,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
|
|
|
1304
1598
|
const faceSums = new Array(count * 3).fill(0);
|
|
1305
1599
|
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1306
1600
|
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
1307
|
-
const n =
|
|
1601
|
+
const n = cross2(sub2(point2(unit, b), point2(unit, a)), sub2(point2(unit, c), point2(unit, a)));
|
|
1308
1602
|
for (const vertex of [a, b, c])
|
|
1309
1603
|
for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
|
|
1310
1604
|
}
|
|
@@ -1321,7 +1615,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
|
|
|
1321
1615
|
const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
|
|
1322
1616
|
const spread = Math.max(0.06, mean * 0.6);
|
|
1323
1617
|
for (const vertex of members)
|
|
1324
|
-
shades[vertex] =
|
|
1618
|
+
shades[vertex] = clamp2((luminance[vertex] - mean) / spread, -1, 1);
|
|
1325
1619
|
}
|
|
1326
1620
|
let crownLow = 1;
|
|
1327
1621
|
let crownHigh = 0;
|
|
@@ -1339,7 +1633,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
|
|
|
1339
1633
|
}
|
|
1340
1634
|
const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
|
|
1341
1635
|
const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
|
|
1342
|
-
occlusion.push(
|
|
1636
|
+
occlusion.push(clamp2(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
|
|
1343
1637
|
}
|
|
1344
1638
|
return {
|
|
1345
1639
|
// The author's proportions: the trees it stands keep them around the sizes the data gives.
|
|
@@ -1352,7 +1646,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
|
|
|
1352
1646
|
indices
|
|
1353
1647
|
};
|
|
1354
1648
|
}
|
|
1355
|
-
function
|
|
1649
|
+
function partOf2(material) {
|
|
1356
1650
|
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
1357
1651
|
const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
|
|
1358
1652
|
if (part) return tileflowTreeParts[part];
|
|
@@ -1363,20 +1657,20 @@ function partOf(material) {
|
|
|
1363
1657
|
function isCrown(part) {
|
|
1364
1658
|
return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
|
|
1365
1659
|
}
|
|
1366
|
-
function
|
|
1660
|
+
function point2(list, vertex) {
|
|
1367
1661
|
return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
|
|
1368
1662
|
}
|
|
1369
|
-
function
|
|
1663
|
+
function sub2(a, b) {
|
|
1370
1664
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1371
1665
|
}
|
|
1372
|
-
function
|
|
1666
|
+
function cross2(a, b) {
|
|
1373
1667
|
return [
|
|
1374
1668
|
a[1] * b[2] - a[2] * b[1],
|
|
1375
1669
|
a[2] * b[0] - a[0] * b[2],
|
|
1376
1670
|
a[0] * b[1] - a[1] * b[0]
|
|
1377
1671
|
];
|
|
1378
1672
|
}
|
|
1379
|
-
function
|
|
1673
|
+
function clamp2(value, low, high) {
|
|
1380
1674
|
return Math.min(high, Math.max(low, value));
|
|
1381
1675
|
}
|
|
1382
1676
|
|
|
@@ -1396,6 +1690,97 @@ var TileflowTreeModelError = class extends Error {
|
|
|
1396
1690
|
this.issues = issues;
|
|
1397
1691
|
}
|
|
1398
1692
|
};
|
|
1693
|
+
var tileflowBuildingModelLimit = 64;
|
|
1694
|
+
async function prepareTileflowBuildingModel(id, levels) {
|
|
1695
|
+
const model = buildingModelEntry(id, levels);
|
|
1696
|
+
await MeshoptEncoder.ready;
|
|
1697
|
+
const bytes = encodeTileflowModels([model], MeshoptEncoder);
|
|
1698
|
+
return { bytes, sha256: sha256Of(bytes) };
|
|
1699
|
+
}
|
|
1700
|
+
async function packageTileflowLandmarkTile(landmarks) {
|
|
1701
|
+
if (landmarks.length === 0) throw new RangeError("A landmark tile holds at least one landmark.");
|
|
1702
|
+
if (new Set(landmarks.map((landmark) => landmark.id)).size !== landmarks.length)
|
|
1703
|
+
throw new RangeError("Every landmark has an id of its own.");
|
|
1704
|
+
let tile;
|
|
1705
|
+
const models = [];
|
|
1706
|
+
for (const landmark of [...landmarks].sort((a, b) => compareCodeUnits4(a.id, b.id))) {
|
|
1707
|
+
if (!(landmark.heading >= 0 && landmark.heading < 360))
|
|
1708
|
+
throw new RangeError(`Landmark ${landmark.id} is turned 0 to 360 degrees.`);
|
|
1709
|
+
let own;
|
|
1710
|
+
try {
|
|
1711
|
+
own = tileflowLandmarkTileOf(landmark.at);
|
|
1712
|
+
} catch {
|
|
1713
|
+
throw new RangeError(`Landmark ${landmark.id} stands at no place on the map.`);
|
|
1714
|
+
}
|
|
1715
|
+
tile ??= own;
|
|
1716
|
+
if (own[0] !== tile[0] || own[1] !== tile[1])
|
|
1717
|
+
throw new RangeError(
|
|
1718
|
+
`Landmark ${landmark.id} stands in tile ${own.join("/")}, not ${tile.join("/")}.`
|
|
1719
|
+
);
|
|
1720
|
+
let model;
|
|
1721
|
+
try {
|
|
1722
|
+
model = buildingModelEntry(landmark.id, landmark.levels);
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
throw new RangeError(`Landmark ${landmark.id}: ${error.message}`);
|
|
1725
|
+
}
|
|
1726
|
+
models.push({ ...model, at: [landmark.at[0], landmark.at[1]], heading: landmark.heading });
|
|
1727
|
+
}
|
|
1728
|
+
await MeshoptEncoder.ready;
|
|
1729
|
+
const bytes = encodeTileflowModels(models, MeshoptEncoder);
|
|
1730
|
+
return { x: tile[0], y: tile[1], bytes, sha256: sha256Of(bytes) };
|
|
1731
|
+
}
|
|
1732
|
+
function encodeTileflowLandmarkRegion(region) {
|
|
1733
|
+
const bytes = serializeTileflowLandmarkRegion(region);
|
|
1734
|
+
return { bytes, sha256: sha256Of(bytes) };
|
|
1735
|
+
}
|
|
1736
|
+
function buildingModelEntry(id, levels) {
|
|
1737
|
+
const primitives = readTileflowGltfTriangles(levels.near);
|
|
1738
|
+
if (!isTileflowBuildingModel(primitives))
|
|
1739
|
+
throw new TileflowGltfError(
|
|
1740
|
+
"is not a building model; name each material surface, secondary, glass, accent or roof"
|
|
1741
|
+
);
|
|
1742
|
+
const meshes = [prepareTileflowBuildingModelMesh(primitives)];
|
|
1743
|
+
for (const level of [levels.mid, levels.far])
|
|
1744
|
+
if (level) meshes.push(prepareTileflowBuildingModelMesh(level));
|
|
1745
|
+
return {
|
|
1746
|
+
id,
|
|
1747
|
+
levels: meshes,
|
|
1748
|
+
footprint: tileflowBuildingModelFootprint(
|
|
1749
|
+
tileflowBuildingModelDeclaredFootprint(primitives) ?? meshes[0]
|
|
1750
|
+
)
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
async function createTileflowModelBundle(buildings) {
|
|
1754
|
+
const entries = Object.entries(buildings).sort(([a], [b]) => compareCodeUnits4(a, b));
|
|
1755
|
+
if (entries.length === 0) return void 0;
|
|
1756
|
+
const totalBytes = entries.reduce((total, [, model]) => total + model.bytes.byteLength, 0);
|
|
1757
|
+
if (totalBytes > tileflowModelBundleLimits.maxPackageBytes)
|
|
1758
|
+
throw new RangeError(
|
|
1759
|
+
`The map's building models together are ${totalBytes} bytes once prepared; a hosted map takes at most ${tileflowModelBundleLimits.maxPackageBytes}. Simplify or remove models.`
|
|
1760
|
+
);
|
|
1761
|
+
const manifest = {
|
|
1762
|
+
format: "tileflow-model-bundle-v1",
|
|
1763
|
+
models: entries.map(([id, model]) => ({
|
|
1764
|
+
byteLength: model.bytes.byteLength,
|
|
1765
|
+
contentType: tileflowModelContentType,
|
|
1766
|
+
file: model.fileName,
|
|
1767
|
+
id,
|
|
1768
|
+
sha256: model.sha256
|
|
1769
|
+
}))
|
|
1770
|
+
};
|
|
1771
|
+
return {
|
|
1772
|
+
contentHash: await hashTileflowModelBundleManifest(manifest),
|
|
1773
|
+
manifest,
|
|
1774
|
+
files: entries.map(([, model]) => ({
|
|
1775
|
+
contentType: tileflowModelContentType,
|
|
1776
|
+
fileName: model.fileName,
|
|
1777
|
+
source: model.bytes
|
|
1778
|
+
}))
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
function sha256Of(bytes) {
|
|
1782
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1783
|
+
}
|
|
1399
1784
|
var fileLimit = 8 * 1024 * 1024;
|
|
1400
1785
|
var modelId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1401
1786
|
async function prepareTileflowMapTreeModels(directories, options) {
|
|
@@ -1427,8 +1812,8 @@ async function prepareTileflowMapTreeModels(directories, options) {
|
|
|
1427
1812
|
continue;
|
|
1428
1813
|
}
|
|
1429
1814
|
if (!lower.endsWith(".glb")) continue;
|
|
1430
|
-
const
|
|
1431
|
-
const id = name.slice(0, name.length - (
|
|
1815
|
+
const level = name.endsWith(".far.glb") ? "far" : name.endsWith(".mid.glb") ? "mid" : "near";
|
|
1816
|
+
const id = name.slice(0, name.length - (level === "near" ? 4 : 8));
|
|
1432
1817
|
if (!modelId.test(id) || id.length > 64) {
|
|
1433
1818
|
issues.push({
|
|
1434
1819
|
message: "a model's file name is its id: lowercase letters, digits and hyphens, up to 64, such as royal-palm.glb",
|
|
@@ -1447,15 +1832,63 @@ async function prepareTileflowMapTreeModels(directories, options) {
|
|
|
1447
1832
|
continue;
|
|
1448
1833
|
}
|
|
1449
1834
|
const entry = files.get(id) ?? {};
|
|
1450
|
-
entry[
|
|
1835
|
+
entry[level] = { path, display };
|
|
1451
1836
|
files.set(id, entry);
|
|
1452
1837
|
}
|
|
1453
1838
|
}
|
|
1454
1839
|
const models = {};
|
|
1840
|
+
const buildings = {};
|
|
1455
1841
|
const identities = [];
|
|
1456
1842
|
for (const [id, entry] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
1457
1843
|
if (!entry.near) {
|
|
1458
|
-
issues.push({ message: `has no ${id}.glb beside it`, path: entry.far.display });
|
|
1844
|
+
issues.push({ message: `has no ${id}.glb beside it`, path: (entry.mid ?? entry.far).display });
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
let nearBytes;
|
|
1848
|
+
let primitives;
|
|
1849
|
+
try {
|
|
1850
|
+
nearBytes = new Uint8Array(await readFile(entry.near.path));
|
|
1851
|
+
primitives = readTileflowGltfTriangles(nearBytes);
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
issues.push({
|
|
1854
|
+
message: error instanceof TileflowGltfError ? error.message : "cannot be read",
|
|
1855
|
+
path: entry.near.display
|
|
1856
|
+
});
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
if (isTileflowBuildingModel(primitives)) {
|
|
1860
|
+
const files2 = { near: nearBytes };
|
|
1861
|
+
let failing = entry.near;
|
|
1862
|
+
try {
|
|
1863
|
+
for (const level of ["near", "mid", "far"]) {
|
|
1864
|
+
const file = entry[level];
|
|
1865
|
+
if (!file) continue;
|
|
1866
|
+
failing = file;
|
|
1867
|
+
const bytes = level === "near" ? nearBytes : new Uint8Array(await readFile(file.path));
|
|
1868
|
+
files2[level] = bytes;
|
|
1869
|
+
if (level !== "near") prepareTileflowBuildingModelMesh(bytes);
|
|
1870
|
+
identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
|
|
1871
|
+
}
|
|
1872
|
+
failing = entry.near;
|
|
1873
|
+
const prepared2 = await prepareTileflowBuildingModel(id, files2);
|
|
1874
|
+
buildings[id] = {
|
|
1875
|
+
fileName: tileflowModelBundleFileName(id, prepared2.sha256),
|
|
1876
|
+
bytes: prepared2.bytes,
|
|
1877
|
+
sha256: prepared2.sha256
|
|
1878
|
+
};
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
issues.push({
|
|
1881
|
+
message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
|
|
1882
|
+
path: failing.display
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
if (entry.mid) {
|
|
1888
|
+
issues.push({
|
|
1889
|
+
message: "is a tree model; trees have a near model and a far one (<id>.far.glb), not a mid one",
|
|
1890
|
+
path: entry.mid.display
|
|
1891
|
+
});
|
|
1459
1892
|
continue;
|
|
1460
1893
|
}
|
|
1461
1894
|
const prepared = {};
|
|
@@ -1481,12 +1914,18 @@ async function prepareTileflowMapTreeModels(directories, options) {
|
|
|
1481
1914
|
}
|
|
1482
1915
|
if (Object.keys(models).length > tileflowTreeModelLimits2.models)
|
|
1483
1916
|
issues.push({
|
|
1484
|
-
message: `holds ${Object.keys(models).length} models; a map takes at most ${tileflowTreeModelLimits2.models}`,
|
|
1917
|
+
message: `holds ${Object.keys(models).length} tree models; a map takes at most ${tileflowTreeModelLimits2.models}`,
|
|
1918
|
+
path: options.configPath
|
|
1919
|
+
});
|
|
1920
|
+
if (Object.keys(buildings).length > tileflowBuildingModelLimit)
|
|
1921
|
+
issues.push({
|
|
1922
|
+
message: `holds ${Object.keys(buildings).length} building models; a map takes at most ${tileflowBuildingModelLimit}`,
|
|
1485
1923
|
path: options.configPath
|
|
1486
1924
|
});
|
|
1487
1925
|
if (issues.length > 0) throw new TileflowTreeModelError(issues);
|
|
1488
1926
|
return {
|
|
1489
1927
|
models,
|
|
1928
|
+
buildings,
|
|
1490
1929
|
identities,
|
|
1491
1930
|
watchPaths: resolved.filter((directory) => directory.watch).map((directory) => directory.realPath)
|
|
1492
1931
|
};
|
|
@@ -1558,19 +1997,19 @@ async function compileTileflowIconPackages(project, options) {
|
|
|
1558
1997
|
bindings,
|
|
1559
1998
|
compositions,
|
|
1560
1999
|
packages: [...packagesByHash.values()].sort(
|
|
1561
|
-
(left, right) =>
|
|
2000
|
+
(left, right) => compareCodeUnits5(left.contentHash, right.contentHash)
|
|
1562
2001
|
),
|
|
1563
2002
|
sourceIdentities,
|
|
1564
2003
|
watchFiles: uniqueStrings(
|
|
1565
2004
|
result.mapRequests.flatMap(
|
|
1566
2005
|
(request) => result.composedBySequence.get(request.sequenceKey)?.watchFiles ?? []
|
|
1567
2006
|
)
|
|
1568
|
-
).sort(
|
|
2007
|
+
).sort(compareCodeUnits5),
|
|
1569
2008
|
watchPaths: uniqueStrings(
|
|
1570
2009
|
result.mapRequests.flatMap(
|
|
1571
2010
|
(request) => result.composedBySequence.get(request.sequenceKey)?.watchPaths ?? []
|
|
1572
2011
|
)
|
|
1573
|
-
).sort(
|
|
2012
|
+
).sort(compareCodeUnits5)
|
|
1574
2013
|
};
|
|
1575
2014
|
}
|
|
1576
2015
|
async function inspectTileflowIconCatalogs(project, options) {
|
|
@@ -1663,7 +2102,7 @@ async function inspectTileflowIconCatalogs(project, options) {
|
|
|
1663
2102
|
)
|
|
1664
2103
|
);
|
|
1665
2104
|
catalogs.sort(
|
|
1666
|
-
(left, right) =>
|
|
2105
|
+
(left, right) => compareCodeUnits5(
|
|
1667
2106
|
left.contributors.map((contributor) => contributor.label).join("\0"),
|
|
1668
2107
|
right.contributors.map((contributor) => contributor.label).join("\0")
|
|
1669
2108
|
)
|
|
@@ -1715,7 +2154,7 @@ async function readWorkspaceIconSetLock(requests, baseDirectory) {
|
|
|
1715
2154
|
references.add(reference);
|
|
1716
2155
|
}
|
|
1717
2156
|
if (references.size === 0) return void 0;
|
|
1718
|
-
const sources = [...references].sort(
|
|
2157
|
+
const sources = [...references].sort(compareCodeUnits5).map((reference) => ({ kind: "icon-set", reference }));
|
|
1719
2158
|
return readTileflowIconsLockfile(baseDirectory, sources);
|
|
1720
2159
|
}
|
|
1721
2160
|
function selectMapIconSetLock(lock, sources) {
|
|
@@ -1773,7 +2212,17 @@ async function prepareTileflowCatalogIcons(project, options) {
|
|
|
1773
2212
|
cwd: options.cwd,
|
|
1774
2213
|
target: "local"
|
|
1775
2214
|
});
|
|
1776
|
-
|
|
2215
|
+
const buildingModels = {};
|
|
2216
|
+
for (const [id, model] of Object.entries(models.buildings)) {
|
|
2217
|
+
const fileName = `models/${mapName}/${model.fileName}`;
|
|
2218
|
+
assets.push({ contentType: tileflowModelContentType2, fileName, source: model.bytes });
|
|
2219
|
+
buildingModels[id] = joinUrl(options.assetBaseUrl, fileName);
|
|
2220
|
+
}
|
|
2221
|
+
mapAssets[mapName] = {
|
|
2222
|
+
...mapAssets[mapName],
|
|
2223
|
+
models: models.models,
|
|
2224
|
+
...Object.keys(buildingModels).length > 0 ? { buildingModels } : {}
|
|
2225
|
+
};
|
|
1777
2226
|
mapModelSources[mapName] = models.identities;
|
|
1778
2227
|
modelWatchPaths.push(...models.watchPaths);
|
|
1779
2228
|
}
|
|
@@ -1814,10 +2263,10 @@ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
|
|
|
1814
2263
|
}
|
|
1815
2264
|
}
|
|
1816
2265
|
}
|
|
1817
|
-
return [...paths].sort(
|
|
2266
|
+
return [...paths].sort(compareCodeUnits5);
|
|
1818
2267
|
}
|
|
1819
2268
|
function getMapIconRequests(project, mapNames) {
|
|
1820
|
-
const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(
|
|
2269
|
+
const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits5);
|
|
1821
2270
|
return selectedNames.map((mapName) => {
|
|
1822
2271
|
const mapConfig = project.maps[mapName];
|
|
1823
2272
|
if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
|
|
@@ -1827,7 +2276,7 @@ function getMapIconRequests(project, mapNames) {
|
|
|
1827
2276
|
}
|
|
1828
2277
|
function uniqueSequences(requests) {
|
|
1829
2278
|
return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
|
|
1830
|
-
(left, right) =>
|
|
2279
|
+
(left, right) => compareCodeUnits5(left.sequenceKey, right.sequenceKey)
|
|
1831
2280
|
);
|
|
1832
2281
|
}
|
|
1833
2282
|
function describeComposedContributor(contributor) {
|
|
@@ -1888,7 +2337,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1888
2337
|
});
|
|
1889
2338
|
return null;
|
|
1890
2339
|
}
|
|
1891
|
-
entries.sort((left, right) =>
|
|
2340
|
+
entries.sort((left, right) => compareCodeUnits5(left.name, right.name));
|
|
1892
2341
|
const candidates = [];
|
|
1893
2342
|
const names = /* @__PURE__ */ new Set();
|
|
1894
2343
|
let metadata = {
|
|
@@ -1976,7 +2425,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1976
2425
|
size: entryStat.size
|
|
1977
2426
|
});
|
|
1978
2427
|
}
|
|
1979
|
-
candidates.sort((left, right) =>
|
|
2428
|
+
candidates.sort((left, right) => compareCodeUnits5(left.name, right.name));
|
|
1980
2429
|
for (const id of Object.keys(metadata.icons)) {
|
|
1981
2430
|
if (!names.has(id))
|
|
1982
2431
|
issues.push({
|
|
@@ -2381,6 +2830,11 @@ export {
|
|
|
2381
2830
|
readTileflowIconsLockfile,
|
|
2382
2831
|
writeTileflowIconsLockfile,
|
|
2383
2832
|
composeTileflowIconSources,
|
|
2833
|
+
prepareTileflowBuildingModel,
|
|
2834
|
+
packageTileflowLandmarkTile,
|
|
2835
|
+
encodeTileflowLandmarkRegion,
|
|
2836
|
+
createTileflowModelBundle,
|
|
2837
|
+
prepareTileflowMapTreeModels,
|
|
2384
2838
|
renderTileflowIconAtlasPreview,
|
|
2385
2839
|
TileflowIconCompilationError,
|
|
2386
2840
|
iconFileExtensions,
|