@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.
@@ -17,14 +17,15 @@ import {
17
17
  tileflowIconSpriteIndexSchema
18
18
  } from "@tileflow/core";
19
19
  import {
20
- compareCodeUnits as compareCodeUnits4,
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 = partOf(primitive.material);
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 = cross(sub(point(unit, b), point(unit, a)), sub(point(unit, c), point(unit, a)));
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] = clamp((luminance[vertex] - mean) / spread, -1, 1);
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(clamp(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
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 partOf(material) {
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 point(list, vertex) {
1660
+ function point2(list, vertex) {
1367
1661
  return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
1368
1662
  }
1369
- function sub(a, b) {
1663
+ function sub2(a, b) {
1370
1664
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1371
1665
  }
1372
- function cross(a, b) {
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 clamp(value, low, high) {
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 far = name.endsWith(".far.glb");
1431
- const id = name.slice(0, name.length - (far ? 8 : 4));
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[far ? "far" : "near"] = { path, display };
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) => compareCodeUnits4(left.contentHash, right.contentHash)
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(compareCodeUnits4),
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(compareCodeUnits4)
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) => compareCodeUnits4(
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(compareCodeUnits4).map((reference) => ({ kind: "icon-set", reference }));
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
- mapAssets[mapName] = { ...mapAssets[mapName], models: models.models };
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(compareCodeUnits4);
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(compareCodeUnits4);
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) => compareCodeUnits4(left.sequenceKey, right.sequenceKey)
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) => compareCodeUnits4(left.name, right.name));
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) => compareCodeUnits4(left.name, right.name));
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,