@tileflow/dev 0.1.0-alpha.31 → 0.1.0-alpha.32

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
28
29
  } from "@tileflow/core";
29
30
 
30
31
  // src/icon-composition.ts
@@ -940,14 +941,22 @@ 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
+ serializeTileflowLandmarkRegion,
953
+ tileflowLandmarkTileOf,
948
954
  tileflowTreeModelLimits as tileflowTreeModelLimits2
949
955
  } from "@tileflow/core";
950
956
 
957
+ // src/building-model-mesh.ts
958
+ import { tileflowModelLimits, tileflowModelParts } from "@tileflow/core";
959
+
951
960
  // src/gltf-reader.ts
952
961
  var TileflowGltfError = class extends Error {
953
962
  name = "TileflowGltfError";
@@ -1243,6 +1252,287 @@ function vector(value, fallback) {
1243
1252
  return list.length === fallback.length && list.every(Number.isFinite) ? list : fallback;
1244
1253
  }
1245
1254
 
1255
+ // src/building-model-mesh.ts
1256
+ var tileflowBuildingModelPartNames = Object.keys(tileflowModelParts);
1257
+ function isTileflowBuildingModel(primitives) {
1258
+ const drawn = primitives.filter((primitive) => !isFootprint(primitive));
1259
+ return drawn.length > 0 && drawn.every((primitive) => partOf(primitive.material) !== void 0);
1260
+ }
1261
+ function tileflowBuildingModelDeclaredFootprint(primitives) {
1262
+ const positions = [];
1263
+ const indices = [];
1264
+ for (const primitive of primitives) {
1265
+ if (!isFootprint(primitive)) continue;
1266
+ const offset = positions.length / 3;
1267
+ positions.push(...primitive.positions);
1268
+ for (const index of primitive.indices) indices.push(offset + index);
1269
+ }
1270
+ return indices.length > 0 ? { positions, indices } : void 0;
1271
+ }
1272
+ function prepareTileflowBuildingModelMesh(bytes) {
1273
+ const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes) : bytes;
1274
+ const positions = [];
1275
+ const normals = [];
1276
+ const parts = [];
1277
+ const luminance = [];
1278
+ const indices = [];
1279
+ for (const primitive of primitives) {
1280
+ if (isFootprint(primitive)) continue;
1281
+ const part = partOf(primitive.material);
1282
+ if (part === void 0)
1283
+ throw new TileflowGltfError(
1284
+ 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`
1285
+ );
1286
+ const offset = positions.length / 3;
1287
+ const count2 = primitive.positions.length / 3;
1288
+ positions.push(...primitive.positions);
1289
+ for (let vertex = 0; vertex < count2; vertex += 1) {
1290
+ parts.push(part);
1291
+ normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
1292
+ const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
1293
+ luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
1294
+ }
1295
+ for (const index of primitive.indices) indices.push(offset + index);
1296
+ }
1297
+ const count = positions.length / 3;
1298
+ const triangles2 = indices.length / 3;
1299
+ if (triangles2 > tileflowModelLimits.triangles)
1300
+ throw new TileflowGltfError(
1301
+ `has ${triangles2.toLocaleString("en")} triangles; a building model takes at most ${tileflowModelLimits.triangles.toLocaleString("en")} a level. Simplify it and export it again`
1302
+ );
1303
+ if (count > tileflowModelLimits.vertices)
1304
+ throw new TileflowGltfError(
1305
+ `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`
1306
+ );
1307
+ let low = Infinity;
1308
+ let high = -Infinity;
1309
+ for (let vertex = 0; vertex < count; vertex += 1) {
1310
+ low = Math.min(low, positions[vertex * 3 + 2]);
1311
+ high = Math.max(high, positions[vertex * 3 + 2]);
1312
+ }
1313
+ if (low < -0.5)
1314
+ throw new TileflowGltfError(
1315
+ `reaches ${(-low).toFixed(1)} m below its origin; put the origin on the ground where the building stands, with the building above it`
1316
+ );
1317
+ if (!(high > 0)) throw new TileflowGltfError("has no height above its origin");
1318
+ const sums = new Array(count * 3).fill(0);
1319
+ for (let at = 0; at + 2 < indices.length; at += 3) {
1320
+ const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
1321
+ const n = cross(
1322
+ sub(point(positions, b), point(positions, a)),
1323
+ sub(point(positions, c), point(positions, a))
1324
+ );
1325
+ for (const vertex of [a, b, c])
1326
+ for (let axis = 0; axis < 3; axis += 1) sums[vertex * 3 + axis] += n[axis];
1327
+ }
1328
+ const outNormals = [];
1329
+ for (let vertex = 0; vertex < count; vertex += 1) {
1330
+ const n = normals[vertex] ?? sums.slice(vertex * 3, vertex * 3 + 3);
1331
+ const size = Math.hypot(n[0], n[1], n[2]) || 1;
1332
+ outNormals.push(n[0] / size, n[1] / size, n[2] / size);
1333
+ }
1334
+ const shades = new Array(count).fill(0);
1335
+ for (const part of new Set(parts)) {
1336
+ const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
1337
+ const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
1338
+ const spread = Math.max(0.06, mean * 0.6);
1339
+ for (const vertex of members)
1340
+ shades[vertex] = clamp((luminance[vertex] - mean) / spread, -1, 1);
1341
+ }
1342
+ return {
1343
+ positions,
1344
+ normals: outNormals,
1345
+ parts,
1346
+ shades,
1347
+ occlusion: new Array(count).fill(1),
1348
+ indices
1349
+ };
1350
+ }
1351
+ function tileflowBuildingModelFootprint(mesh) {
1352
+ const { positions, indices } = mesh;
1353
+ let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity];
1354
+ for (let at = 0; at < positions.length; at += 3) {
1355
+ minX = Math.min(minX, positions[at]);
1356
+ maxX = Math.max(maxX, positions[at]);
1357
+ minY = Math.min(minY, positions[at + 1]);
1358
+ maxY = Math.max(maxY, positions[at + 1]);
1359
+ }
1360
+ const cell = Math.max(1, Math.max(maxX - minX, maxY - minY) / 1024);
1361
+ const columns = Math.ceil((maxX - minX) / cell) + 5;
1362
+ const rows = Math.ceil((maxY - minY) / cell) + 5;
1363
+ const x0 = minX - 2 * cell;
1364
+ const y0 = minY - 2 * cell;
1365
+ const filled = new Uint8Array(columns * rows);
1366
+ for (let at = 0; at + 2 < indices.length; at += 3) {
1367
+ const corners = [indices[at], indices[at + 1], indices[at + 2]].map((vertex) => [
1368
+ (positions[vertex * 3] - x0) / cell,
1369
+ (positions[vertex * 3 + 1] - y0) / cell
1370
+ ]);
1371
+ for (const [cx, cy] of corners) filled[Math.floor(cy) * columns + Math.floor(cx)] = 1;
1372
+ const left = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[0]))));
1373
+ const right = Math.min(columns - 1, Math.ceil(Math.max(...corners.map((c2) => c2[0]))));
1374
+ const top = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[1]))));
1375
+ const bottom = Math.min(rows - 1, Math.ceil(Math.max(...corners.map((c2) => c2[1]))));
1376
+ const [a, b, c] = corners;
1377
+ const area = edge(a, b, c);
1378
+ if (Math.abs(area) < 1e-9) continue;
1379
+ for (let y = top; y <= bottom; y += 1)
1380
+ for (let x = left; x <= right; x += 1) {
1381
+ const p = [x + 0.5, y + 0.5];
1382
+ const w0 = edge(b, c, p) / area;
1383
+ const w1 = edge(c, a, p) / area;
1384
+ const w2 = edge(a, b, p) / area;
1385
+ if (w0 >= 0 && w1 >= 0 && w2 >= 0) filled[y * columns + x] = 1;
1386
+ }
1387
+ }
1388
+ const grown = new Uint8Array(filled);
1389
+ for (let y = 1; y < rows - 1; y += 1)
1390
+ for (let x = 1; x < columns - 1; x += 1) {
1391
+ if (filled[y * columns + x]) continue;
1392
+ for (let dy = -1; dy <= 1 && !grown[y * columns + x]; dy += 1)
1393
+ for (let dx = -1; dx <= 1; dx += 1)
1394
+ if (filled[(y + dy) * columns + x + dx]) {
1395
+ grown[y * columns + x] = 1;
1396
+ break;
1397
+ }
1398
+ }
1399
+ const outside = new Uint8Array(columns * rows);
1400
+ const stack = [0];
1401
+ outside[0] = 1;
1402
+ while (stack.length) {
1403
+ const at = stack.pop();
1404
+ const x = at % columns;
1405
+ const y = (at - x) / columns;
1406
+ for (const [dx, dy] of [
1407
+ [1, 0],
1408
+ [-1, 0],
1409
+ [0, 1],
1410
+ [0, -1]
1411
+ ]) {
1412
+ const nx = x + dx;
1413
+ const ny = y + dy;
1414
+ if (nx < 0 || ny < 0 || nx >= columns || ny >= rows) continue;
1415
+ const next2 = ny * columns + nx;
1416
+ if (outside[next2] || grown[next2]) continue;
1417
+ outside[next2] = 1;
1418
+ stack.push(next2);
1419
+ }
1420
+ }
1421
+ const covered = (x, y) => x >= 0 && y >= 0 && x < columns && y < rows && !outside[y * columns + x];
1422
+ const next = /* @__PURE__ */ new Map();
1423
+ const add = (from, to) => {
1424
+ const key = `${from[0]},${from[1]}`;
1425
+ const list = next.get(key) ?? [];
1426
+ list.push(to);
1427
+ next.set(key, list);
1428
+ };
1429
+ for (let y = 0; y < rows; y += 1)
1430
+ for (let x = 0; x < columns; x += 1) {
1431
+ if (!covered(x, y)) continue;
1432
+ if (!covered(x, y - 1)) add([x, y], [x + 1, y]);
1433
+ if (!covered(x + 1, y)) add([x + 1, y], [x + 1, y + 1]);
1434
+ if (!covered(x, y + 1)) add([x + 1, y + 1], [x, y + 1]);
1435
+ if (!covered(x - 1, y)) add([x, y + 1], [x, y]);
1436
+ }
1437
+ const rings = [];
1438
+ for (const [startKey, targets] of next) {
1439
+ while (targets.length) {
1440
+ const start = startKey.split(",").map(Number);
1441
+ const ring = [start];
1442
+ let current = targets.pop();
1443
+ while (`${current[0]},${current[1]}` !== startKey) {
1444
+ ring.push(current);
1445
+ const outs = next.get(`${current[0]},${current[1]}`);
1446
+ if (!outs?.length) break;
1447
+ current = outs.pop();
1448
+ }
1449
+ const simplified = simplify(ring, 0.75);
1450
+ if (simplified.length >= 3)
1451
+ rings.push(simplified.flatMap(([x, y]) => [x0 + x * cell, y0 + y * cell]));
1452
+ }
1453
+ }
1454
+ rings.sort((a, b) => Math.abs(ringArea(b)) - Math.abs(ringArea(a)));
1455
+ const kept = [];
1456
+ let points = 0;
1457
+ for (const ring of rings) {
1458
+ if (points + ring.length / 2 > tileflowModelLimits.footprintPoints) break;
1459
+ kept.push(ring);
1460
+ points += ring.length / 2;
1461
+ }
1462
+ return kept;
1463
+ }
1464
+ function isFootprint(primitive) {
1465
+ return primitive.material?.trim().toLowerCase().replace(/\.\d+$/u, "") === "footprint";
1466
+ }
1467
+ function partOf(material) {
1468
+ const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
1469
+ const part = tileflowBuildingModelPartNames.find((candidate) => candidate === name);
1470
+ return part === void 0 ? void 0 : tileflowModelParts[part];
1471
+ }
1472
+ function simplify(ring, tolerance) {
1473
+ if (ring.length <= 4) return [...ring];
1474
+ let far = 0;
1475
+ let distance = -1;
1476
+ for (const [index, [x, y]] of ring.entries()) {
1477
+ const d = Math.hypot(x - ring[0][0], y - ring[0][1]);
1478
+ if (d > distance) {
1479
+ distance = d;
1480
+ far = index;
1481
+ }
1482
+ }
1483
+ const half = (points) => {
1484
+ if (points.length <= 2) return [...points];
1485
+ const [a, b] = [points[0], points[points.length - 1]];
1486
+ let index = 0;
1487
+ let most = -1;
1488
+ for (let at = 1; at < points.length - 1; at += 1) {
1489
+ const d = segmentDistance(points[at], a, b);
1490
+ if (d > most) {
1491
+ most = d;
1492
+ index = at;
1493
+ }
1494
+ }
1495
+ if (most <= tolerance) return [a, b];
1496
+ const left = half(points.slice(0, index + 1));
1497
+ return [...left.slice(0, -1), ...half(points.slice(index))];
1498
+ };
1499
+ const first = half(ring.slice(0, far + 1));
1500
+ const second = half([...ring.slice(far), ring[0]]);
1501
+ return [...first.slice(0, -1), ...second.slice(0, -1)];
1502
+ }
1503
+ function segmentDistance(p, a, b) {
1504
+ const dx = b[0] - a[0];
1505
+ const dy = b[1] - a[1];
1506
+ const length = dx * dx + dy * dy;
1507
+ const t = length > 0 ? clamp(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
1508
+ return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
1509
+ }
1510
+ function ringArea(ring) {
1511
+ let area = 0;
1512
+ for (let a = 0, b = ring.length - 2; a < ring.length; b = a, a += 2)
1513
+ area += ring[b] * ring[a + 1] - ring[a] * ring[b + 1];
1514
+ return area / 2;
1515
+ }
1516
+ function edge(a, b, p) {
1517
+ return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1518
+ }
1519
+ function point(list, vertex) {
1520
+ return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
1521
+ }
1522
+ function sub(a, b) {
1523
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1524
+ }
1525
+ function cross(a, b) {
1526
+ return [
1527
+ a[1] * b[2] - a[2] * b[1],
1528
+ a[2] * b[0] - a[0] * b[2],
1529
+ a[0] * b[1] - a[1] * b[0]
1530
+ ];
1531
+ }
1532
+ function clamp(value, low, high) {
1533
+ return Math.min(high, Math.max(low, value));
1534
+ }
1535
+
1246
1536
  // src/tree-model-mesh.ts
1247
1537
  import {
1248
1538
  tileflowTreeModelLimits,
@@ -1257,7 +1547,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
1257
1547
  const luminance = [];
1258
1548
  const indices = [];
1259
1549
  for (const primitive of primitives) {
1260
- const part = partOf(primitive.material);
1550
+ const part = partOf2(primitive.material);
1261
1551
  const offset = positions.length / 3;
1262
1552
  const count2 = primitive.positions.length / 3;
1263
1553
  positions.push(...primitive.positions);
@@ -1304,7 +1594,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
1304
1594
  const faceSums = new Array(count * 3).fill(0);
1305
1595
  for (let at = 0; at + 2 < indices.length; at += 3) {
1306
1596
  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)));
1597
+ const n = cross2(sub2(point2(unit, b), point2(unit, a)), sub2(point2(unit, c), point2(unit, a)));
1308
1598
  for (const vertex of [a, b, c])
1309
1599
  for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
1310
1600
  }
@@ -1321,7 +1611,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
1321
1611
  const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
1322
1612
  const spread = Math.max(0.06, mean * 0.6);
1323
1613
  for (const vertex of members)
1324
- shades[vertex] = clamp((luminance[vertex] - mean) / spread, -1, 1);
1614
+ shades[vertex] = clamp2((luminance[vertex] - mean) / spread, -1, 1);
1325
1615
  }
1326
1616
  let crownLow = 1;
1327
1617
  let crownHigh = 0;
@@ -1339,7 +1629,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
1339
1629
  }
1340
1630
  const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
1341
1631
  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));
1632
+ occlusion.push(clamp2(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
1343
1633
  }
1344
1634
  return {
1345
1635
  // The author's proportions: the trees it stands keep them around the sizes the data gives.
@@ -1352,7 +1642,7 @@ function prepareTileflowTreeModelMesh(bytes, level) {
1352
1642
  indices
1353
1643
  };
1354
1644
  }
1355
- function partOf(material) {
1645
+ function partOf2(material) {
1356
1646
  const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
1357
1647
  const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
1358
1648
  if (part) return tileflowTreeParts[part];
@@ -1363,20 +1653,20 @@ function partOf(material) {
1363
1653
  function isCrown(part) {
1364
1654
  return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
1365
1655
  }
1366
- function point(list, vertex) {
1656
+ function point2(list, vertex) {
1367
1657
  return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
1368
1658
  }
1369
- function sub(a, b) {
1659
+ function sub2(a, b) {
1370
1660
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1371
1661
  }
1372
- function cross(a, b) {
1662
+ function cross2(a, b) {
1373
1663
  return [
1374
1664
  a[1] * b[2] - a[2] * b[1],
1375
1665
  a[2] * b[0] - a[0] * b[2],
1376
1666
  a[0] * b[1] - a[1] * b[0]
1377
1667
  ];
1378
1668
  }
1379
- function clamp(value, low, high) {
1669
+ function clamp2(value, low, high) {
1380
1670
  return Math.min(high, Math.max(low, value));
1381
1671
  }
1382
1672
 
@@ -1396,6 +1686,69 @@ var TileflowTreeModelError = class extends Error {
1396
1686
  this.issues = issues;
1397
1687
  }
1398
1688
  };
1689
+ var tileflowBuildingModelLimit = 64;
1690
+ async function prepareTileflowBuildingModel(id, levels) {
1691
+ const model = buildingModelEntry(id, levels);
1692
+ await MeshoptEncoder.ready;
1693
+ const bytes = encodeTileflowModels([model], MeshoptEncoder);
1694
+ return { bytes, sha256: sha256Of(bytes) };
1695
+ }
1696
+ async function packageTileflowLandmarkTile(landmarks) {
1697
+ if (landmarks.length === 0) throw new RangeError("A landmark tile holds at least one landmark.");
1698
+ if (new Set(landmarks.map((landmark) => landmark.id)).size !== landmarks.length)
1699
+ throw new RangeError("Every landmark has an id of its own.");
1700
+ let tile;
1701
+ const models = [];
1702
+ for (const landmark of [...landmarks].sort((a, b) => compareCodeUnits4(a.id, b.id))) {
1703
+ if (!(landmark.heading >= 0 && landmark.heading < 360))
1704
+ throw new RangeError(`Landmark ${landmark.id} is turned 0 to 360 degrees.`);
1705
+ let own;
1706
+ try {
1707
+ own = tileflowLandmarkTileOf(landmark.at);
1708
+ } catch {
1709
+ throw new RangeError(`Landmark ${landmark.id} stands at no place on the map.`);
1710
+ }
1711
+ tile ??= own;
1712
+ if (own[0] !== tile[0] || own[1] !== tile[1])
1713
+ throw new RangeError(
1714
+ `Landmark ${landmark.id} stands in tile ${own.join("/")}, not ${tile.join("/")}.`
1715
+ );
1716
+ let model;
1717
+ try {
1718
+ model = buildingModelEntry(landmark.id, landmark.levels);
1719
+ } catch (error) {
1720
+ throw new RangeError(`Landmark ${landmark.id}: ${error.message}`);
1721
+ }
1722
+ models.push({ ...model, at: [landmark.at[0], landmark.at[1]], heading: landmark.heading });
1723
+ }
1724
+ await MeshoptEncoder.ready;
1725
+ const bytes = encodeTileflowModels(models, MeshoptEncoder);
1726
+ return { x: tile[0], y: tile[1], bytes, sha256: sha256Of(bytes) };
1727
+ }
1728
+ function encodeTileflowLandmarkRegion(region) {
1729
+ const bytes = serializeTileflowLandmarkRegion(region);
1730
+ return { bytes, sha256: sha256Of(bytes) };
1731
+ }
1732
+ function buildingModelEntry(id, levels) {
1733
+ const primitives = readTileflowGltfTriangles(levels.near);
1734
+ if (!isTileflowBuildingModel(primitives))
1735
+ throw new TileflowGltfError(
1736
+ "is not a building model; name each material surface, secondary, glass, accent or roof"
1737
+ );
1738
+ const meshes = [prepareTileflowBuildingModelMesh(primitives)];
1739
+ for (const level of [levels.mid, levels.far])
1740
+ if (level) meshes.push(prepareTileflowBuildingModelMesh(level));
1741
+ return {
1742
+ id,
1743
+ levels: meshes,
1744
+ footprint: tileflowBuildingModelFootprint(
1745
+ tileflowBuildingModelDeclaredFootprint(primitives) ?? meshes[0]
1746
+ )
1747
+ };
1748
+ }
1749
+ function sha256Of(bytes) {
1750
+ return createHash("sha256").update(bytes).digest("hex");
1751
+ }
1399
1752
  var fileLimit = 8 * 1024 * 1024;
1400
1753
  var modelId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1401
1754
  async function prepareTileflowMapTreeModels(directories, options) {
@@ -1427,8 +1780,8 @@ async function prepareTileflowMapTreeModels(directories, options) {
1427
1780
  continue;
1428
1781
  }
1429
1782
  if (!lower.endsWith(".glb")) continue;
1430
- const far = name.endsWith(".far.glb");
1431
- const id = name.slice(0, name.length - (far ? 8 : 4));
1783
+ const level = name.endsWith(".far.glb") ? "far" : name.endsWith(".mid.glb") ? "mid" : "near";
1784
+ const id = name.slice(0, name.length - (level === "near" ? 4 : 8));
1432
1785
  if (!modelId.test(id) || id.length > 64) {
1433
1786
  issues.push({
1434
1787
  message: "a model's file name is its id: lowercase letters, digits and hyphens, up to 64, such as royal-palm.glb",
@@ -1447,15 +1800,62 @@ async function prepareTileflowMapTreeModels(directories, options) {
1447
1800
  continue;
1448
1801
  }
1449
1802
  const entry = files.get(id) ?? {};
1450
- entry[far ? "far" : "near"] = { path, display };
1803
+ entry[level] = { path, display };
1451
1804
  files.set(id, entry);
1452
1805
  }
1453
1806
  }
1454
1807
  const models = {};
1808
+ const buildings = {};
1455
1809
  const identities = [];
1456
1810
  for (const [id, entry] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1457
1811
  if (!entry.near) {
1458
- issues.push({ message: `has no ${id}.glb beside it`, path: entry.far.display });
1812
+ issues.push({ message: `has no ${id}.glb beside it`, path: (entry.mid ?? entry.far).display });
1813
+ continue;
1814
+ }
1815
+ let nearBytes;
1816
+ let primitives;
1817
+ try {
1818
+ nearBytes = new Uint8Array(await readFile(entry.near.path));
1819
+ primitives = readTileflowGltfTriangles(nearBytes);
1820
+ } catch (error) {
1821
+ issues.push({
1822
+ message: error instanceof TileflowGltfError ? error.message : "cannot be read",
1823
+ path: entry.near.display
1824
+ });
1825
+ continue;
1826
+ }
1827
+ if (isTileflowBuildingModel(primitives)) {
1828
+ const files2 = { near: nearBytes };
1829
+ let failing = entry.near;
1830
+ try {
1831
+ for (const level of ["near", "mid", "far"]) {
1832
+ const file = entry[level];
1833
+ if (!file) continue;
1834
+ failing = file;
1835
+ const bytes = level === "near" ? nearBytes : new Uint8Array(await readFile(file.path));
1836
+ files2[level] = bytes;
1837
+ if (level !== "near") prepareTileflowBuildingModelMesh(bytes);
1838
+ identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
1839
+ }
1840
+ failing = entry.near;
1841
+ const prepared2 = await prepareTileflowBuildingModel(id, files2);
1842
+ buildings[id] = {
1843
+ fileName: `${id}-${prepared2.sha256.slice(0, 16)}.glb`,
1844
+ bytes: prepared2.bytes
1845
+ };
1846
+ } catch (error) {
1847
+ issues.push({
1848
+ message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
1849
+ path: failing.display
1850
+ });
1851
+ }
1852
+ continue;
1853
+ }
1854
+ if (entry.mid) {
1855
+ issues.push({
1856
+ message: "is a tree model; trees have a near model and a far one (<id>.far.glb), not a mid one",
1857
+ path: entry.mid.display
1858
+ });
1459
1859
  continue;
1460
1860
  }
1461
1861
  const prepared = {};
@@ -1481,12 +1881,18 @@ async function prepareTileflowMapTreeModels(directories, options) {
1481
1881
  }
1482
1882
  if (Object.keys(models).length > tileflowTreeModelLimits2.models)
1483
1883
  issues.push({
1484
- message: `holds ${Object.keys(models).length} models; a map takes at most ${tileflowTreeModelLimits2.models}`,
1884
+ message: `holds ${Object.keys(models).length} tree models; a map takes at most ${tileflowTreeModelLimits2.models}`,
1885
+ path: options.configPath
1886
+ });
1887
+ if (Object.keys(buildings).length > tileflowBuildingModelLimit)
1888
+ issues.push({
1889
+ message: `holds ${Object.keys(buildings).length} building models; a map takes at most ${tileflowBuildingModelLimit}`,
1485
1890
  path: options.configPath
1486
1891
  });
1487
1892
  if (issues.length > 0) throw new TileflowTreeModelError(issues);
1488
1893
  return {
1489
1894
  models,
1895
+ buildings,
1490
1896
  identities,
1491
1897
  watchPaths: resolved.filter((directory) => directory.watch).map((directory) => directory.realPath)
1492
1898
  };
@@ -1558,19 +1964,19 @@ async function compileTileflowIconPackages(project, options) {
1558
1964
  bindings,
1559
1965
  compositions,
1560
1966
  packages: [...packagesByHash.values()].sort(
1561
- (left, right) => compareCodeUnits4(left.contentHash, right.contentHash)
1967
+ (left, right) => compareCodeUnits5(left.contentHash, right.contentHash)
1562
1968
  ),
1563
1969
  sourceIdentities,
1564
1970
  watchFiles: uniqueStrings(
1565
1971
  result.mapRequests.flatMap(
1566
1972
  (request) => result.composedBySequence.get(request.sequenceKey)?.watchFiles ?? []
1567
1973
  )
1568
- ).sort(compareCodeUnits4),
1974
+ ).sort(compareCodeUnits5),
1569
1975
  watchPaths: uniqueStrings(
1570
1976
  result.mapRequests.flatMap(
1571
1977
  (request) => result.composedBySequence.get(request.sequenceKey)?.watchPaths ?? []
1572
1978
  )
1573
- ).sort(compareCodeUnits4)
1979
+ ).sort(compareCodeUnits5)
1574
1980
  };
1575
1981
  }
1576
1982
  async function inspectTileflowIconCatalogs(project, options) {
@@ -1663,7 +2069,7 @@ async function inspectTileflowIconCatalogs(project, options) {
1663
2069
  )
1664
2070
  );
1665
2071
  catalogs.sort(
1666
- (left, right) => compareCodeUnits4(
2072
+ (left, right) => compareCodeUnits5(
1667
2073
  left.contributors.map((contributor) => contributor.label).join("\0"),
1668
2074
  right.contributors.map((contributor) => contributor.label).join("\0")
1669
2075
  )
@@ -1715,7 +2121,7 @@ async function readWorkspaceIconSetLock(requests, baseDirectory) {
1715
2121
  references.add(reference);
1716
2122
  }
1717
2123
  if (references.size === 0) return void 0;
1718
- const sources = [...references].sort(compareCodeUnits4).map((reference) => ({ kind: "icon-set", reference }));
2124
+ const sources = [...references].sort(compareCodeUnits5).map((reference) => ({ kind: "icon-set", reference }));
1719
2125
  return readTileflowIconsLockfile(baseDirectory, sources);
1720
2126
  }
1721
2127
  function selectMapIconSetLock(lock, sources) {
@@ -1773,7 +2179,17 @@ async function prepareTileflowCatalogIcons(project, options) {
1773
2179
  cwd: options.cwd,
1774
2180
  target: "local"
1775
2181
  });
1776
- mapAssets[mapName] = { ...mapAssets[mapName], models: models.models };
2182
+ const buildingModels = {};
2183
+ for (const [id, model] of Object.entries(models.buildings)) {
2184
+ const fileName = `models/${mapName}/${model.fileName}`;
2185
+ assets.push({ contentType: tileflowModelContentType, fileName, source: model.bytes });
2186
+ buildingModels[id] = joinUrl(options.assetBaseUrl, fileName);
2187
+ }
2188
+ mapAssets[mapName] = {
2189
+ ...mapAssets[mapName],
2190
+ models: models.models,
2191
+ ...Object.keys(buildingModels).length > 0 ? { buildingModels } : {}
2192
+ };
1777
2193
  mapModelSources[mapName] = models.identities;
1778
2194
  modelWatchPaths.push(...models.watchPaths);
1779
2195
  }
@@ -1814,10 +2230,10 @@ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
1814
2230
  }
1815
2231
  }
1816
2232
  }
1817
- return [...paths].sort(compareCodeUnits4);
2233
+ return [...paths].sort(compareCodeUnits5);
1818
2234
  }
1819
2235
  function getMapIconRequests(project, mapNames) {
1820
- const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits4);
2236
+ const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits5);
1821
2237
  return selectedNames.map((mapName) => {
1822
2238
  const mapConfig = project.maps[mapName];
1823
2239
  if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
@@ -1827,7 +2243,7 @@ function getMapIconRequests(project, mapNames) {
1827
2243
  }
1828
2244
  function uniqueSequences(requests) {
1829
2245
  return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
1830
- (left, right) => compareCodeUnits4(left.sequenceKey, right.sequenceKey)
2246
+ (left, right) => compareCodeUnits5(left.sequenceKey, right.sequenceKey)
1831
2247
  );
1832
2248
  }
1833
2249
  function describeComposedContributor(contributor) {
@@ -1888,7 +2304,7 @@ async function inspectIconSource(directory, target, issues) {
1888
2304
  });
1889
2305
  return null;
1890
2306
  }
1891
- entries.sort((left, right) => compareCodeUnits4(left.name, right.name));
2307
+ entries.sort((left, right) => compareCodeUnits5(left.name, right.name));
1892
2308
  const candidates = [];
1893
2309
  const names = /* @__PURE__ */ new Set();
1894
2310
  let metadata = {
@@ -1976,7 +2392,7 @@ async function inspectIconSource(directory, target, issues) {
1976
2392
  size: entryStat.size
1977
2393
  });
1978
2394
  }
1979
- candidates.sort((left, right) => compareCodeUnits4(left.name, right.name));
2395
+ candidates.sort((left, right) => compareCodeUnits5(left.name, right.name));
1980
2396
  for (const id of Object.keys(metadata.icons)) {
1981
2397
  if (!names.has(id))
1982
2398
  issues.push({
@@ -2381,6 +2797,10 @@ export {
2381
2797
  readTileflowIconsLockfile,
2382
2798
  writeTileflowIconsLockfile,
2383
2799
  composeTileflowIconSources,
2800
+ prepareTileflowBuildingModel,
2801
+ packageTileflowLandmarkTile,
2802
+ encodeTileflowLandmarkRegion,
2803
+ prepareTileflowMapTreeModels,
2384
2804
  renderTileflowIconAtlasPreview,
2385
2805
  TileflowIconCompilationError,
2386
2806
  iconFileExtensions,