@tileflow/dev 0.1.0-alpha.30 → 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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-7C4F6GAX.js";
5
5
 
6
6
  // src/icons.ts
7
- import { readdir as readdir2, readFile, realpath as realpath3, stat } from "fs/promises";
7
+ import { readdir as readdir3, readFile as readFile2, realpath as realpath3, stat } from "fs/promises";
8
8
  import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath, sep as sep2 } from "path";
9
9
  import { SaxesParser } from "saxes";
10
10
  import {
@@ -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
@@ -849,7 +850,7 @@ async function composeTileflowIconSources(input, options) {
849
850
  source: winner.source
850
851
  }))
851
852
  };
852
- function addWinner(ordinal, icon, identity, source) {
853
+ function addWinner(ordinal, icon, identity2, source) {
853
854
  pixelBytes += icon.oneX.rgba.byteLength + icon.twoX.rgba.byteLength;
854
855
  const maximumPixelBytes = (tileflowIconPackageLimits3.maxAtlasDimension ** 2 + (tileflowIconPackageLimits3.maxAtlasDimension / 2) ** 2) * 4;
855
856
  if (winners.size >= tileflowIconPackageLimits3.maxIconCount || pixelBytes > maximumPixelBytes)
@@ -857,7 +858,7 @@ async function composeTileflowIconSources(input, options) {
857
858
  "ICON_COMPOSITION_INVALID",
858
859
  "Composed rendered cells exceed the final sprite capacity"
859
860
  );
860
- winners.set(icon.id, { ordinal, icon, identity, source });
861
+ winners.set(icon.id, { ordinal, icon, identity: identity2, source });
861
862
  }
862
863
  }
863
864
  function isPathInside(root, candidate) {
@@ -939,6 +940,964 @@ function transform(grid, width, height) {
939
940
  }
940
941
  }
941
942
 
943
+ // src/tree-models.ts
944
+ import { MeshoptEncoder } from "meshoptimizer/encoder";
945
+ import { createHash } from "crypto";
946
+ import { lstat as lstat3, readdir as readdir2, readFile } from "fs/promises";
947
+ import { join as join3 } from "path";
948
+ import {
949
+ compareCodeUnits as compareCodeUnits4,
950
+ encodeTileflowModels,
951
+ encodeTileflowTreeModel,
952
+ serializeTileflowLandmarkRegion,
953
+ tileflowLandmarkTileOf,
954
+ tileflowTreeModelLimits as tileflowTreeModelLimits2
955
+ } from "@tileflow/core";
956
+
957
+ // src/building-model-mesh.ts
958
+ import { tileflowModelLimits, tileflowModelParts } from "@tileflow/core";
959
+
960
+ // src/gltf-reader.ts
961
+ var TileflowGltfError = class extends Error {
962
+ name = "TileflowGltfError";
963
+ };
964
+ var glbMagic = 1179937895;
965
+ var jsonChunk = 1313821514;
966
+ var binChunk = 5130562;
967
+ var triangles = 4;
968
+ var compressions = /* @__PURE__ */ new Set([
969
+ "KHR_draco_mesh_compression",
970
+ "EXT_meshopt_compression",
971
+ "KHR_meshopt_compression",
972
+ "KHR_mesh_quantization"
973
+ ]);
974
+ var componentBytes = {
975
+ 5120: 1,
976
+ 5121: 1,
977
+ 5122: 2,
978
+ 5123: 2,
979
+ 5125: 4,
980
+ 5126: 4
981
+ };
982
+ var typeSizes = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
983
+ function readTileflowGltfTriangles(bytes) {
984
+ const { json, bin } = chunks(bytes);
985
+ const required = array(json.extensionsRequired).filter((name) => typeof name === "string");
986
+ const compressed = required.find((name) => compressions.has(name));
987
+ if (compressed)
988
+ throw new TileflowGltfError(
989
+ `uses compressed geometry (${String(compressed)}); export it without compression`
990
+ );
991
+ for (const buffer of array(json.buffers))
992
+ if (record(buffer).uri !== void 0)
993
+ throw new TileflowGltfError("keeps its geometry in another file; export a single .glb");
994
+ const nodes = array(json.nodes).map(record);
995
+ const scenes = array(json.scenes).map(record);
996
+ const scene = scenes[typeof json.scene === "number" ? json.scene : 0];
997
+ const roots = scene ? array(scene.nodes) : nodes.map((_, index) => index).filter((index) => !nodes.some((node) => array(node.children).includes(index)));
998
+ const out = [];
999
+ const visit = (index, parent, depth) => {
1000
+ if (typeof index !== "number" || !nodes[index])
1001
+ throw new TileflowGltfError("names a node it does not hold");
1002
+ if (depth > 64) throw new TileflowGltfError("nests its nodes more than 64 deep");
1003
+ const node = nodes[index];
1004
+ const world = multiply(parent, local(node));
1005
+ if (typeof node.mesh === "number") out.push(...meshOf(json, bin, node.mesh, world));
1006
+ for (const child of array(node.children)) visit(child, world, depth + 1);
1007
+ };
1008
+ for (const root of roots) visit(root, identity(), 0);
1009
+ if (out.length === 0) throw new TileflowGltfError("holds no triangles in its scene");
1010
+ return out;
1011
+ }
1012
+ function chunks(bytes) {
1013
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1014
+ if (bytes.byteLength < 20 || view.getUint32(0, true) !== glbMagic)
1015
+ throw new TileflowGltfError("is not a glTF binary; export the model as .glb");
1016
+ if (view.getUint32(4, true) !== 2) throw new TileflowGltfError("is not glTF 2.0");
1017
+ if (view.getUint32(8, true) !== bytes.byteLength)
1018
+ throw new TileflowGltfError("is cut short or has trailing bytes");
1019
+ let at = 12;
1020
+ let json;
1021
+ let bin;
1022
+ while (at + 8 <= bytes.byteLength) {
1023
+ const length = view.getUint32(at, true);
1024
+ const type = view.getUint32(at + 4, true);
1025
+ const start = at + 8;
1026
+ if (start + length > bytes.byteLength) throw new TileflowGltfError("has a chunk past its end");
1027
+ const body = bytes.subarray(start, start + length);
1028
+ if (type === jsonChunk && json === void 0) {
1029
+ try {
1030
+ json = record(JSON.parse(new TextDecoder().decode(body)));
1031
+ } catch {
1032
+ throw new TileflowGltfError("has an unreadable JSON chunk");
1033
+ }
1034
+ } else if (type === binChunk && bin === void 0) bin = body;
1035
+ at = start + length + (4 - length % 4) % 4;
1036
+ }
1037
+ if (!json) throw new TileflowGltfError("has no JSON chunk");
1038
+ return { json, bin };
1039
+ }
1040
+ function meshOf(json, bin, index, world) {
1041
+ const mesh = record(array(json.meshes)[index]);
1042
+ const normalMatrix = inverseTranspose(world);
1043
+ return array(mesh.primitives).map((value) => {
1044
+ const primitive = record(value);
1045
+ if ((primitive.mode ?? triangles) !== triangles)
1046
+ throw new TileflowGltfError("draws points or lines; a tree model draws triangles");
1047
+ const attributes = record(primitive.attributes);
1048
+ if (typeof attributes.POSITION !== "number")
1049
+ throw new TileflowGltfError("has a primitive without positions");
1050
+ const raw = accessor(json, bin, attributes.POSITION, 3);
1051
+ const count = raw.length / 3;
1052
+ const positions = [];
1053
+ for (let vertex = 0; vertex < count; vertex += 1) {
1054
+ const [x, y, z] = transform2(
1055
+ world,
1056
+ raw[vertex * 3],
1057
+ raw[vertex * 3 + 1],
1058
+ raw[vertex * 3 + 2],
1059
+ 1
1060
+ );
1061
+ positions.push(x, -z, y);
1062
+ }
1063
+ let normals;
1064
+ if (typeof attributes.NORMAL === "number") {
1065
+ const rawNormals = accessor(json, bin, attributes.NORMAL, 3);
1066
+ normals = [];
1067
+ for (let vertex = 0; vertex < count; vertex += 1) {
1068
+ const [x, y, z] = transform2(
1069
+ normalMatrix,
1070
+ rawNormals[vertex * 3],
1071
+ rawNormals[vertex * 3 + 1],
1072
+ rawNormals[vertex * 3 + 2],
1073
+ 0
1074
+ );
1075
+ const size = Math.hypot(x, y, z) || 1;
1076
+ normals.push(x / size, -z / size, y / size);
1077
+ }
1078
+ }
1079
+ const material = typeof primitive.material === "number" ? record(array(json.materials)[primitive.material]) : void 0;
1080
+ const base = array(record(material?.pbrMetallicRoughness).baseColorFactor).map(Number);
1081
+ const factor = base.length >= 3 && base.every(Number.isFinite) ? base : [1, 1, 1];
1082
+ const colours = [];
1083
+ const vertexColours = typeof attributes.COLOR_0 === "number" ? accessorWithSize(json, bin, attributes.COLOR_0) : void 0;
1084
+ for (let vertex = 0; vertex < count; vertex += 1)
1085
+ for (let channel = 0; channel < 3; channel += 1)
1086
+ colours.push(
1087
+ factor[channel] * (vertexColours ? vertexColours.values[vertex * vertexColours.size + channel] : 1)
1088
+ );
1089
+ const indices = typeof primitive.indices === "number" ? accessor(json, bin, primitive.indices, 1).map((value2) => Math.round(value2)) : Array.from({ length: count }, (_, vertex) => vertex);
1090
+ if (indices.length % 3 !== 0 || indices.some((vertex) => vertex < 0 || vertex >= count))
1091
+ throw new TileflowGltfError("has triangles that name missing vertices");
1092
+ if (determinant(world) < 0)
1093
+ for (let at = 0; at + 2 < indices.length; at += 3)
1094
+ [indices[at + 1], indices[at + 2]] = [indices[at + 2], indices[at + 1]];
1095
+ return {
1096
+ material: typeof material?.name === "string" ? material.name : void 0,
1097
+ positions,
1098
+ normals,
1099
+ colours,
1100
+ indices
1101
+ };
1102
+ });
1103
+ }
1104
+ function accessor(json, bin, index, size) {
1105
+ const read = accessorWithSize(json, bin, index);
1106
+ if (read.size !== size)
1107
+ throw new TileflowGltfError(`has an accessor of ${read.size} components where ${size} belong`);
1108
+ return read.values;
1109
+ }
1110
+ function accessorWithSize(json, bin, index) {
1111
+ const accessor2 = record(array(json.accessors)[index]);
1112
+ if (accessor2.sparse !== void 0)
1113
+ throw new TileflowGltfError("uses sparse accessors; export without them");
1114
+ const size = typeSizes[String(accessor2.type)];
1115
+ const component = Number(accessor2.componentType);
1116
+ const bytesEach = componentBytes[component];
1117
+ const count = Number(accessor2.count);
1118
+ if (!size || !bytesEach || !Number.isInteger(count) || count < 0 || count > 1e6)
1119
+ throw new TileflowGltfError("has an accessor Tileflow cannot read");
1120
+ if (typeof accessor2.bufferView !== "number")
1121
+ return { values: new Array(count * size).fill(0), size };
1122
+ const view = record(array(json.bufferViews)[accessor2.bufferView]);
1123
+ if (!bin || Number(view.buffer ?? 0) !== 0)
1124
+ throw new TileflowGltfError("has geometry outside its binary chunk");
1125
+ const stride = Number(view.byteStride ?? 0) || size * bytesEach;
1126
+ const start = Number(view.byteOffset ?? 0) + Number(accessor2.byteOffset ?? 0);
1127
+ const end = start + (count - 1) * stride + size * bytesEach;
1128
+ if (count > 0 && (start < 0 || end > Number(view.byteOffset ?? 0) + Number(view.byteLength) || end > bin.byteLength))
1129
+ throw new TileflowGltfError("has an accessor past the end of its buffer");
1130
+ const data = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
1131
+ const normalized = accessor2.normalized === true;
1132
+ const values = [];
1133
+ for (let item = 0; item < count; item += 1)
1134
+ for (let part = 0; part < size; part += 1) {
1135
+ const at = start + item * stride + part * bytesEach;
1136
+ let value;
1137
+ switch (component) {
1138
+ case 5126:
1139
+ value = data.getFloat32(at, true);
1140
+ break;
1141
+ case 5125:
1142
+ value = data.getUint32(at, true);
1143
+ break;
1144
+ case 5123:
1145
+ value = data.getUint16(at, true);
1146
+ if (normalized) value /= 65535;
1147
+ break;
1148
+ case 5122:
1149
+ value = data.getInt16(at, true);
1150
+ if (normalized) value = Math.max(value / 32767, -1);
1151
+ break;
1152
+ case 5121:
1153
+ value = data.getUint8(at);
1154
+ if (normalized) value /= 255;
1155
+ break;
1156
+ default:
1157
+ value = data.getInt8(at);
1158
+ if (normalized) value = Math.max(value / 127, -1);
1159
+ }
1160
+ if (!Number.isFinite(value)) throw new TileflowGltfError("has a value that is not a number");
1161
+ values.push(value);
1162
+ }
1163
+ return { values, size };
1164
+ }
1165
+ function identity() {
1166
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
1167
+ }
1168
+ function local(node) {
1169
+ const matrix = array(node.matrix).map(Number);
1170
+ if (matrix.length === 16 && matrix.every(Number.isFinite)) return matrix;
1171
+ const [tx, ty, tz] = vector(node.translation, [0, 0, 0]);
1172
+ const [qx, qy, qz, qw] = vector(node.rotation, [0, 0, 0, 1]);
1173
+ const [sx, sy, sz] = vector(node.scale, [1, 1, 1]);
1174
+ const [xx, yy, zz] = [qx * qx, qy * qy, qz * qz];
1175
+ const [xy, xz, yz, wx, wy, wz] = [qx * qy, qx * qz, qy * qz, qw * qx, qw * qy, qw * qz];
1176
+ return [
1177
+ (1 - 2 * (yy + zz)) * sx,
1178
+ 2 * (xy + wz) * sx,
1179
+ 2 * (xz - wy) * sx,
1180
+ 0,
1181
+ 2 * (xy - wz) * sy,
1182
+ (1 - 2 * (xx + zz)) * sy,
1183
+ 2 * (yz + wx) * sy,
1184
+ 0,
1185
+ 2 * (xz + wy) * sz,
1186
+ 2 * (yz - wx) * sz,
1187
+ (1 - 2 * (xx + yy)) * sz,
1188
+ 0,
1189
+ tx,
1190
+ ty,
1191
+ tz,
1192
+ 1
1193
+ ];
1194
+ }
1195
+ function multiply(a, b) {
1196
+ const out = new Array(16).fill(0);
1197
+ for (let column = 0; column < 4; column += 1)
1198
+ for (let row = 0; row < 4; row += 1)
1199
+ for (let k = 0; k < 4; k += 1) out[column * 4 + row] += a[k * 4 + row] * b[column * 4 + k];
1200
+ return out;
1201
+ }
1202
+ function transform2(m, x, y, z, w) {
1203
+ return [
1204
+ m[0] * x + m[4] * y + m[8] * z + m[12] * w,
1205
+ m[1] * x + m[5] * y + m[9] * z + m[13] * w,
1206
+ m[2] * x + m[6] * y + m[10] * z + m[14] * w
1207
+ ];
1208
+ }
1209
+ function determinant(m) {
1210
+ return m[0] * (m[5] * m[10] - m[9] * m[6]) - m[4] * (m[1] * m[10] - m[9] * m[2]) + m[8] * (m[1] * m[6] - m[5] * m[2]);
1211
+ }
1212
+ function inverseTranspose(m) {
1213
+ const [a, b, c, d, e, f, g, h, i] = [
1214
+ m[0],
1215
+ m[1],
1216
+ m[2],
1217
+ m[4],
1218
+ m[5],
1219
+ m[6],
1220
+ m[8],
1221
+ m[9],
1222
+ m[10]
1223
+ ];
1224
+ const det = determinant(m) || 1;
1225
+ return [
1226
+ (e * i - h * f) / det,
1227
+ (g * f - d * i) / det,
1228
+ (d * h - g * e) / det,
1229
+ 0,
1230
+ (h * c - b * i) / det,
1231
+ (a * i - g * c) / det,
1232
+ (g * b - a * h) / det,
1233
+ 0,
1234
+ (b * f - e * c) / det,
1235
+ (d * c - a * f) / det,
1236
+ (a * e - d * b) / det,
1237
+ 0,
1238
+ 0,
1239
+ 0,
1240
+ 0,
1241
+ 1
1242
+ ];
1243
+ }
1244
+ function record(value) {
1245
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1246
+ }
1247
+ function array(value) {
1248
+ return Array.isArray(value) ? value : [];
1249
+ }
1250
+ function vector(value, fallback) {
1251
+ const list = array(value).map(Number);
1252
+ return list.length === fallback.length && list.every(Number.isFinite) ? list : fallback;
1253
+ }
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
+
1536
+ // src/tree-model-mesh.ts
1537
+ import {
1538
+ tileflowTreeModelLimits,
1539
+ tileflowTreeParts
1540
+ } from "@tileflow/core";
1541
+ var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
1542
+ function prepareTileflowTreeModelMesh(bytes, level) {
1543
+ const primitives = readTileflowGltfTriangles(bytes);
1544
+ const positions = [];
1545
+ const normals = [];
1546
+ const parts = [];
1547
+ const luminance = [];
1548
+ const indices = [];
1549
+ for (const primitive of primitives) {
1550
+ const part = partOf2(primitive.material);
1551
+ const offset = positions.length / 3;
1552
+ const count2 = primitive.positions.length / 3;
1553
+ positions.push(...primitive.positions);
1554
+ for (let vertex = 0; vertex < count2; vertex += 1) {
1555
+ parts.push(part);
1556
+ normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
1557
+ const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
1558
+ luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
1559
+ }
1560
+ for (const index of primitive.indices) indices.push(offset + index);
1561
+ }
1562
+ const count = positions.length / 3;
1563
+ const triangleCount = indices.length / 3;
1564
+ const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
1565
+ if (triangleCount > limit)
1566
+ throw new TileflowGltfError(
1567
+ `has ${triangleCount.toLocaleString("en")} triangles; a ${level} tree model takes at most ${limit.toLocaleString("en")}. Simplify it (for example with Blender's Decimate modifier) and export it again`
1568
+ );
1569
+ if (count > tileflowTreeModelLimits.vertices)
1570
+ throw new TileflowGltfError(
1571
+ `has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
1572
+ );
1573
+ let height = 0;
1574
+ let reach = 0;
1575
+ const crowned = parts.some((part) => isCrown(part));
1576
+ for (let vertex = 0; vertex < count; vertex += 1) {
1577
+ height = Math.max(height, positions[vertex * 3 + 2]);
1578
+ if (!crowned || isCrown(parts[vertex]))
1579
+ reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
1580
+ }
1581
+ if (!(height > 0))
1582
+ throw new TileflowGltfError(
1583
+ "stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
1584
+ );
1585
+ if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
1586
+ const across = 2 * reach;
1587
+ const unit = [];
1588
+ for (let vertex = 0; vertex < count; vertex += 1)
1589
+ unit.push(
1590
+ positions[vertex * 3] / across,
1591
+ positions[vertex * 3 + 1] / across,
1592
+ positions[vertex * 3 + 2] / height
1593
+ );
1594
+ const faceSums = new Array(count * 3).fill(0);
1595
+ for (let at = 0; at + 2 < indices.length; at += 3) {
1596
+ const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
1597
+ const n = cross2(sub2(point2(unit, b), point2(unit, a)), sub2(point2(unit, c), point2(unit, a)));
1598
+ for (const vertex of [a, b, c])
1599
+ for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
1600
+ }
1601
+ const outNormals = [];
1602
+ for (let vertex = 0; vertex < count; vertex += 1) {
1603
+ const own = normals[vertex];
1604
+ const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
1605
+ const size = Math.hypot(n[0], n[1], n[2]) || 1;
1606
+ outNormals.push(n[0] / size, n[1] / size, n[2] / size);
1607
+ }
1608
+ const shades = new Array(count).fill(0);
1609
+ for (const part of new Set(parts)) {
1610
+ const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
1611
+ const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
1612
+ const spread = Math.max(0.06, mean * 0.6);
1613
+ for (const vertex of members)
1614
+ shades[vertex] = clamp2((luminance[vertex] - mean) / spread, -1, 1);
1615
+ }
1616
+ let crownLow = 1;
1617
+ let crownHigh = 0;
1618
+ for (let vertex = 0; vertex < count; vertex += 1)
1619
+ if (isCrown(parts[vertex])) {
1620
+ crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
1621
+ crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
1622
+ }
1623
+ const occlusion = [];
1624
+ for (let vertex = 0; vertex < count; vertex += 1) {
1625
+ const z = unit[vertex * 3 + 2];
1626
+ if (!isCrown(parts[vertex])) {
1627
+ occlusion.push(z > crownLow ? 0.55 : 0.8);
1628
+ continue;
1629
+ }
1630
+ const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
1631
+ const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
1632
+ occlusion.push(clamp2(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
1633
+ }
1634
+ return {
1635
+ // The author's proportions: the trees it stands keep them around the sizes the data gives.
1636
+ proportion: Math.min(3, Math.max(0.1, across / height)),
1637
+ positions: unit,
1638
+ normals: outNormals,
1639
+ parts,
1640
+ shades,
1641
+ occlusion,
1642
+ indices
1643
+ };
1644
+ }
1645
+ function partOf2(material) {
1646
+ const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
1647
+ const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
1648
+ if (part) return tileflowTreeParts[part];
1649
+ throw new TileflowGltfError(
1650
+ material === void 0 ? "has a mesh without a material; name each material bark, leaves, fronds or stem" : `has a material "${material}" that names no part of a tree; name each material bark, leaves, fronds or stem`
1651
+ );
1652
+ }
1653
+ function isCrown(part) {
1654
+ return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
1655
+ }
1656
+ function point2(list, vertex) {
1657
+ return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
1658
+ }
1659
+ function sub2(a, b) {
1660
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1661
+ }
1662
+ function cross2(a, b) {
1663
+ return [
1664
+ a[1] * b[2] - a[2] * b[1],
1665
+ a[2] * b[0] - a[0] * b[2],
1666
+ a[0] * b[1] - a[1] * b[0]
1667
+ ];
1668
+ }
1669
+ function clamp2(value, low, high) {
1670
+ return Math.min(high, Math.max(low, value));
1671
+ }
1672
+
1673
+ // src/tree-models.ts
1674
+ var TileflowTreeModelError = class extends Error {
1675
+ code = "TREE_MODEL_INVALID";
1676
+ issues;
1677
+ phase = "tree-model-preparation";
1678
+ constructor(issues) {
1679
+ super(
1680
+ [
1681
+ "Invalid Tileflow tree models",
1682
+ ...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
1683
+ ].join("\n")
1684
+ );
1685
+ this.name = "TileflowTreeModelError";
1686
+ this.issues = issues;
1687
+ }
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
+ }
1752
+ var fileLimit = 8 * 1024 * 1024;
1753
+ var modelId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1754
+ async function prepareTileflowMapTreeModels(directories, options) {
1755
+ const resolved = await resolveTileflowAssetDirectories(directories, {
1756
+ baseDirectory: options.baseDirectory,
1757
+ configPath: options.configPath,
1758
+ cwd: options.cwd,
1759
+ kind: "models",
1760
+ target: options.target
1761
+ });
1762
+ const issues = [];
1763
+ const files = /* @__PURE__ */ new Map();
1764
+ for (const directory of resolved) {
1765
+ let names;
1766
+ try {
1767
+ names = (await readdir2(directory.realPath)).sort();
1768
+ } catch {
1769
+ issues.push({ message: "is not a readable directory", path: directory.configPath });
1770
+ continue;
1771
+ }
1772
+ for (const name of names) {
1773
+ const display = `${directory.configPath}/${name}`;
1774
+ const lower = name.toLowerCase();
1775
+ if (lower.endsWith(".gltf")) {
1776
+ issues.push({
1777
+ message: "is a text glTF; export the model as a glTF binary (.glb)",
1778
+ path: display
1779
+ });
1780
+ continue;
1781
+ }
1782
+ if (!lower.endsWith(".glb")) continue;
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));
1785
+ if (!modelId.test(id) || id.length > 64) {
1786
+ issues.push({
1787
+ message: "a model's file name is its id: lowercase letters, digits and hyphens, up to 64, such as royal-palm.glb",
1788
+ path: display
1789
+ });
1790
+ continue;
1791
+ }
1792
+ const path = join3(directory.realPath, name);
1793
+ const info = await lstat3(path).catch(() => void 0);
1794
+ if (!info?.isFile()) {
1795
+ issues.push({ message: "is not a regular file", path: display });
1796
+ continue;
1797
+ }
1798
+ if (info.size > fileLimit) {
1799
+ issues.push({ message: "is larger than 8 MiB", path: display });
1800
+ continue;
1801
+ }
1802
+ const entry = files.get(id) ?? {};
1803
+ entry[level] = { path, display };
1804
+ files.set(id, entry);
1805
+ }
1806
+ }
1807
+ const models = {};
1808
+ const buildings = {};
1809
+ const identities = [];
1810
+ for (const [id, entry] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1811
+ if (!entry.near) {
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
+ });
1859
+ continue;
1860
+ }
1861
+ const prepared = {};
1862
+ for (const level of ["near", "far"]) {
1863
+ const file = entry[level];
1864
+ if (!file) continue;
1865
+ try {
1866
+ const bytes = new Uint8Array(await readFile(file.path));
1867
+ prepared[level] = encodeTileflowTreeModel(
1868
+ prepareTileflowTreeModelMesh(bytes, level),
1869
+ level
1870
+ );
1871
+ identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
1872
+ } catch (error) {
1873
+ issues.push({
1874
+ message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
1875
+ path: file.display
1876
+ });
1877
+ }
1878
+ }
1879
+ if (prepared.near)
1880
+ models[id] = { near: prepared.near, ...prepared.far ? { far: prepared.far } : {} };
1881
+ }
1882
+ if (Object.keys(models).length > tileflowTreeModelLimits2.models)
1883
+ issues.push({
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}`,
1890
+ path: options.configPath
1891
+ });
1892
+ if (issues.length > 0) throw new TileflowTreeModelError(issues);
1893
+ return {
1894
+ models,
1895
+ buildings,
1896
+ identities,
1897
+ watchPaths: resolved.filter((directory) => directory.watch).map((directory) => directory.realPath)
1898
+ };
1899
+ }
1900
+
942
1901
  // src/icon-preview.ts
943
1902
  import { renderTileflowIconAtlasPixels } from "@tileflow/core";
944
1903
  async function renderTileflowIconAtlasPreview(source, index) {
@@ -1005,19 +1964,19 @@ async function compileTileflowIconPackages(project, options) {
1005
1964
  bindings,
1006
1965
  compositions,
1007
1966
  packages: [...packagesByHash.values()].sort(
1008
- (left, right) => compareCodeUnits4(left.contentHash, right.contentHash)
1967
+ (left, right) => compareCodeUnits5(left.contentHash, right.contentHash)
1009
1968
  ),
1010
1969
  sourceIdentities,
1011
1970
  watchFiles: uniqueStrings(
1012
1971
  result.mapRequests.flatMap(
1013
1972
  (request) => result.composedBySequence.get(request.sequenceKey)?.watchFiles ?? []
1014
1973
  )
1015
- ).sort(compareCodeUnits4),
1974
+ ).sort(compareCodeUnits5),
1016
1975
  watchPaths: uniqueStrings(
1017
1976
  result.mapRequests.flatMap(
1018
1977
  (request) => result.composedBySequence.get(request.sequenceKey)?.watchPaths ?? []
1019
1978
  )
1020
- ).sort(compareCodeUnits4)
1979
+ ).sort(compareCodeUnits5)
1021
1980
  };
1022
1981
  }
1023
1982
  async function inspectTileflowIconCatalogs(project, options) {
@@ -1110,7 +2069,7 @@ async function inspectTileflowIconCatalogs(project, options) {
1110
2069
  )
1111
2070
  );
1112
2071
  catalogs.sort(
1113
- (left, right) => compareCodeUnits4(
2072
+ (left, right) => compareCodeUnits5(
1114
2073
  left.contributors.map((contributor) => contributor.label).join("\0"),
1115
2074
  right.contributors.map((contributor) => contributor.label).join("\0")
1116
2075
  )
@@ -1162,7 +2121,7 @@ async function readWorkspaceIconSetLock(requests, baseDirectory) {
1162
2121
  references.add(reference);
1163
2122
  }
1164
2123
  if (references.size === 0) return void 0;
1165
- const sources = [...references].sort(compareCodeUnits4).map((reference) => ({ kind: "icon-set", reference }));
2124
+ const sources = [...references].sort(compareCodeUnits5).map((reference) => ({ kind: "icon-set", reference }));
1166
2125
  return readTileflowIconsLockfile(baseDirectory, sources);
1167
2126
  }
1168
2127
  function selectMapIconSetLock(lock, sources) {
@@ -1210,6 +2169,30 @@ async function prepareTileflowCatalogIcons(project, options) {
1210
2169
  });
1211
2170
  }
1212
2171
  }
2172
+ const mapModelSources = {};
2173
+ const modelWatchPaths = [];
2174
+ for (const [mapName, map] of Object.entries(project.maps)) {
2175
+ if (!map.models?.length) continue;
2176
+ const models = await prepareTileflowMapTreeModels(map.models, {
2177
+ baseDirectory: options.baseDirectory ?? options.cwd,
2178
+ configPath: `maps.${mapName}.models`,
2179
+ cwd: options.cwd,
2180
+ target: "local"
2181
+ });
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
+ };
2193
+ mapModelSources[mapName] = models.identities;
2194
+ modelWatchPaths.push(...models.watchPaths);
2195
+ }
1213
2196
  return {
1214
2197
  assets,
1215
2198
  baseDirectory: await realpath3(options.baseDirectory ?? options.cwd),
@@ -1217,10 +2200,11 @@ async function prepareTileflowCatalogIcons(project, options) {
1217
2200
  mapAssets,
1218
2201
  mapIconCompositions,
1219
2202
  mapIconSources,
2203
+ ...Object.keys(mapModelSources).length > 0 ? { mapModelSources } : {},
1220
2204
  project,
1221
2205
  sourceProject: project,
1222
2206
  watchFiles: compiled.watchFiles,
1223
- watchPaths: compiled.watchPaths
2207
+ watchPaths: [...compiled.watchPaths, ...modelWatchPaths]
1224
2208
  };
1225
2209
  }
1226
2210
  async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
@@ -1246,10 +2230,10 @@ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
1246
2230
  }
1247
2231
  }
1248
2232
  }
1249
- return [...paths].sort(compareCodeUnits4);
2233
+ return [...paths].sort(compareCodeUnits5);
1250
2234
  }
1251
2235
  function getMapIconRequests(project, mapNames) {
1252
- 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);
1253
2237
  return selectedNames.map((mapName) => {
1254
2238
  const mapConfig = project.maps[mapName];
1255
2239
  if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
@@ -1259,7 +2243,7 @@ function getMapIconRequests(project, mapNames) {
1259
2243
  }
1260
2244
  function uniqueSequences(requests) {
1261
2245
  return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
1262
- (left, right) => compareCodeUnits4(left.sequenceKey, right.sequenceKey)
2246
+ (left, right) => compareCodeUnits5(left.sequenceKey, right.sequenceKey)
1263
2247
  );
1264
2248
  }
1265
2249
  function describeComposedContributor(contributor) {
@@ -1312,7 +2296,7 @@ async function inspectIconSource(directory, target, issues) {
1312
2296
  let entries;
1313
2297
  const issueCount = issues.length;
1314
2298
  try {
1315
- entries = await readdir2(directory.realPath, { withFileTypes: true });
2299
+ entries = await readdir3(directory.realPath, { withFileTypes: true });
1316
2300
  } catch (error) {
1317
2301
  issues.push({
1318
2302
  message: describeFileSystemError(error, "Icon source could not be read"),
@@ -1320,7 +2304,7 @@ async function inspectIconSource(directory, target, issues) {
1320
2304
  });
1321
2305
  return null;
1322
2306
  }
1323
- entries.sort((left, right) => compareCodeUnits4(left.name, right.name));
2307
+ entries.sort((left, right) => compareCodeUnits5(left.name, right.name));
1324
2308
  const candidates = [];
1325
2309
  const names = /* @__PURE__ */ new Set();
1326
2310
  let metadata = {
@@ -1360,7 +2344,7 @@ async function inspectIconSource(directory, target, issues) {
1360
2344
  throw new Error("Icon metadata exceeds its byte limit");
1361
2345
  metadata = tileflowIconAuthorMetadataSchema.parse(
1362
2346
  parseTileflowIconJson2(
1363
- await readFile(realEntryPath, "utf8"),
2347
+ await readFile2(realEntryPath, "utf8"),
1364
2348
  tileflowIconPackageLimits4.maxSourceFileBytes
1365
2349
  )
1366
2350
  );
@@ -1408,7 +2392,7 @@ async function inspectIconSource(directory, target, issues) {
1408
2392
  size: entryStat.size
1409
2393
  });
1410
2394
  }
1411
- candidates.sort((left, right) => compareCodeUnits4(left.name, right.name));
2395
+ candidates.sort((left, right) => compareCodeUnits5(left.name, right.name));
1412
2396
  for (const id of Object.keys(metadata.icons)) {
1413
2397
  if (!names.has(id))
1414
2398
  issues.push({
@@ -1429,7 +2413,7 @@ async function inspectIconSource(directory, target, issues) {
1429
2413
  for (const candidate of candidates) {
1430
2414
  const displayPath = candidate.displayPath;
1431
2415
  try {
1432
- const source = await readFile(candidate.path);
2416
+ const source = await readFile2(candidate.path);
1433
2417
  if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits4.maxSourceFileBytes) {
1434
2418
  throw new Error("Icon changed while it was being validated");
1435
2419
  }
@@ -1813,6 +2797,10 @@ export {
1813
2797
  readTileflowIconsLockfile,
1814
2798
  writeTileflowIconsLockfile,
1815
2799
  composeTileflowIconSources,
2800
+ prepareTileflowBuildingModel,
2801
+ packageTileflowLandmarkTile,
2802
+ encodeTileflowLandmarkRegion,
2803
+ prepareTileflowMapTreeModels,
1816
2804
  renderTileflowIconAtlasPreview,
1817
2805
  TileflowIconCompilationError,
1818
2806
  iconFileExtensions,