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

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 {
@@ -849,7 +849,7 @@ async function composeTileflowIconSources(input, options) {
849
849
  source: winner.source
850
850
  }))
851
851
  };
852
- function addWinner(ordinal, icon, identity, source) {
852
+ function addWinner(ordinal, icon, identity2, source) {
853
853
  pixelBytes += icon.oneX.rgba.byteLength + icon.twoX.rgba.byteLength;
854
854
  const maximumPixelBytes = (tileflowIconPackageLimits3.maxAtlasDimension ** 2 + (tileflowIconPackageLimits3.maxAtlasDimension / 2) ** 2) * 4;
855
855
  if (winners.size >= tileflowIconPackageLimits3.maxIconCount || pixelBytes > maximumPixelBytes)
@@ -857,7 +857,7 @@ async function composeTileflowIconSources(input, options) {
857
857
  "ICON_COMPOSITION_INVALID",
858
858
  "Composed rendered cells exceed the final sprite capacity"
859
859
  );
860
- winners.set(icon.id, { ordinal, icon, identity, source });
860
+ winners.set(icon.id, { ordinal, icon, identity: identity2, source });
861
861
  }
862
862
  }
863
863
  function isPathInside(root, candidate) {
@@ -939,6 +939,559 @@ function transform(grid, width, height) {
939
939
  }
940
940
  }
941
941
 
942
+ // src/tree-models.ts
943
+ import { createHash } from "crypto";
944
+ import { lstat as lstat3, readdir as readdir2, readFile } from "fs/promises";
945
+ import { join as join3 } from "path";
946
+ import {
947
+ encodeTileflowTreeModel,
948
+ tileflowTreeModelLimits as tileflowTreeModelLimits2
949
+ } from "@tileflow/core";
950
+
951
+ // src/gltf-reader.ts
952
+ var TileflowGltfError = class extends Error {
953
+ name = "TileflowGltfError";
954
+ };
955
+ var glbMagic = 1179937895;
956
+ var jsonChunk = 1313821514;
957
+ var binChunk = 5130562;
958
+ var triangles = 4;
959
+ var compressions = /* @__PURE__ */ new Set([
960
+ "KHR_draco_mesh_compression",
961
+ "EXT_meshopt_compression",
962
+ "KHR_meshopt_compression",
963
+ "KHR_mesh_quantization"
964
+ ]);
965
+ var componentBytes = {
966
+ 5120: 1,
967
+ 5121: 1,
968
+ 5122: 2,
969
+ 5123: 2,
970
+ 5125: 4,
971
+ 5126: 4
972
+ };
973
+ var typeSizes = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
974
+ function readTileflowGltfTriangles(bytes) {
975
+ const { json, bin } = chunks(bytes);
976
+ const required = array(json.extensionsRequired).filter((name) => typeof name === "string");
977
+ const compressed = required.find((name) => compressions.has(name));
978
+ if (compressed)
979
+ throw new TileflowGltfError(
980
+ `uses compressed geometry (${String(compressed)}); export it without compression`
981
+ );
982
+ for (const buffer of array(json.buffers))
983
+ if (record(buffer).uri !== void 0)
984
+ throw new TileflowGltfError("keeps its geometry in another file; export a single .glb");
985
+ const nodes = array(json.nodes).map(record);
986
+ const scenes = array(json.scenes).map(record);
987
+ const scene = scenes[typeof json.scene === "number" ? json.scene : 0];
988
+ const roots = scene ? array(scene.nodes) : nodes.map((_, index) => index).filter((index) => !nodes.some((node) => array(node.children).includes(index)));
989
+ const out = [];
990
+ const visit = (index, parent, depth) => {
991
+ if (typeof index !== "number" || !nodes[index])
992
+ throw new TileflowGltfError("names a node it does not hold");
993
+ if (depth > 64) throw new TileflowGltfError("nests its nodes more than 64 deep");
994
+ const node = nodes[index];
995
+ const world = multiply(parent, local(node));
996
+ if (typeof node.mesh === "number") out.push(...meshOf(json, bin, node.mesh, world));
997
+ for (const child of array(node.children)) visit(child, world, depth + 1);
998
+ };
999
+ for (const root of roots) visit(root, identity(), 0);
1000
+ if (out.length === 0) throw new TileflowGltfError("holds no triangles in its scene");
1001
+ return out;
1002
+ }
1003
+ function chunks(bytes) {
1004
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1005
+ if (bytes.byteLength < 20 || view.getUint32(0, true) !== glbMagic)
1006
+ throw new TileflowGltfError("is not a glTF binary; export the model as .glb");
1007
+ if (view.getUint32(4, true) !== 2) throw new TileflowGltfError("is not glTF 2.0");
1008
+ if (view.getUint32(8, true) !== bytes.byteLength)
1009
+ throw new TileflowGltfError("is cut short or has trailing bytes");
1010
+ let at = 12;
1011
+ let json;
1012
+ let bin;
1013
+ while (at + 8 <= bytes.byteLength) {
1014
+ const length = view.getUint32(at, true);
1015
+ const type = view.getUint32(at + 4, true);
1016
+ const start = at + 8;
1017
+ if (start + length > bytes.byteLength) throw new TileflowGltfError("has a chunk past its end");
1018
+ const body = bytes.subarray(start, start + length);
1019
+ if (type === jsonChunk && json === void 0) {
1020
+ try {
1021
+ json = record(JSON.parse(new TextDecoder().decode(body)));
1022
+ } catch {
1023
+ throw new TileflowGltfError("has an unreadable JSON chunk");
1024
+ }
1025
+ } else if (type === binChunk && bin === void 0) bin = body;
1026
+ at = start + length + (4 - length % 4) % 4;
1027
+ }
1028
+ if (!json) throw new TileflowGltfError("has no JSON chunk");
1029
+ return { json, bin };
1030
+ }
1031
+ function meshOf(json, bin, index, world) {
1032
+ const mesh = record(array(json.meshes)[index]);
1033
+ const normalMatrix = inverseTranspose(world);
1034
+ return array(mesh.primitives).map((value) => {
1035
+ const primitive = record(value);
1036
+ if ((primitive.mode ?? triangles) !== triangles)
1037
+ throw new TileflowGltfError("draws points or lines; a tree model draws triangles");
1038
+ const attributes = record(primitive.attributes);
1039
+ if (typeof attributes.POSITION !== "number")
1040
+ throw new TileflowGltfError("has a primitive without positions");
1041
+ const raw = accessor(json, bin, attributes.POSITION, 3);
1042
+ const count = raw.length / 3;
1043
+ const positions = [];
1044
+ for (let vertex = 0; vertex < count; vertex += 1) {
1045
+ const [x, y, z] = transform2(
1046
+ world,
1047
+ raw[vertex * 3],
1048
+ raw[vertex * 3 + 1],
1049
+ raw[vertex * 3 + 2],
1050
+ 1
1051
+ );
1052
+ positions.push(x, -z, y);
1053
+ }
1054
+ let normals;
1055
+ if (typeof attributes.NORMAL === "number") {
1056
+ const rawNormals = accessor(json, bin, attributes.NORMAL, 3);
1057
+ normals = [];
1058
+ for (let vertex = 0; vertex < count; vertex += 1) {
1059
+ const [x, y, z] = transform2(
1060
+ normalMatrix,
1061
+ rawNormals[vertex * 3],
1062
+ rawNormals[vertex * 3 + 1],
1063
+ rawNormals[vertex * 3 + 2],
1064
+ 0
1065
+ );
1066
+ const size = Math.hypot(x, y, z) || 1;
1067
+ normals.push(x / size, -z / size, y / size);
1068
+ }
1069
+ }
1070
+ const material = typeof primitive.material === "number" ? record(array(json.materials)[primitive.material]) : void 0;
1071
+ const base = array(record(material?.pbrMetallicRoughness).baseColorFactor).map(Number);
1072
+ const factor = base.length >= 3 && base.every(Number.isFinite) ? base : [1, 1, 1];
1073
+ const colours = [];
1074
+ const vertexColours = typeof attributes.COLOR_0 === "number" ? accessorWithSize(json, bin, attributes.COLOR_0) : void 0;
1075
+ for (let vertex = 0; vertex < count; vertex += 1)
1076
+ for (let channel = 0; channel < 3; channel += 1)
1077
+ colours.push(
1078
+ factor[channel] * (vertexColours ? vertexColours.values[vertex * vertexColours.size + channel] : 1)
1079
+ );
1080
+ const indices = typeof primitive.indices === "number" ? accessor(json, bin, primitive.indices, 1).map((value2) => Math.round(value2)) : Array.from({ length: count }, (_, vertex) => vertex);
1081
+ if (indices.length % 3 !== 0 || indices.some((vertex) => vertex < 0 || vertex >= count))
1082
+ throw new TileflowGltfError("has triangles that name missing vertices");
1083
+ if (determinant(world) < 0)
1084
+ for (let at = 0; at + 2 < indices.length; at += 3)
1085
+ [indices[at + 1], indices[at + 2]] = [indices[at + 2], indices[at + 1]];
1086
+ return {
1087
+ material: typeof material?.name === "string" ? material.name : void 0,
1088
+ positions,
1089
+ normals,
1090
+ colours,
1091
+ indices
1092
+ };
1093
+ });
1094
+ }
1095
+ function accessor(json, bin, index, size) {
1096
+ const read = accessorWithSize(json, bin, index);
1097
+ if (read.size !== size)
1098
+ throw new TileflowGltfError(`has an accessor of ${read.size} components where ${size} belong`);
1099
+ return read.values;
1100
+ }
1101
+ function accessorWithSize(json, bin, index) {
1102
+ const accessor2 = record(array(json.accessors)[index]);
1103
+ if (accessor2.sparse !== void 0)
1104
+ throw new TileflowGltfError("uses sparse accessors; export without them");
1105
+ const size = typeSizes[String(accessor2.type)];
1106
+ const component = Number(accessor2.componentType);
1107
+ const bytesEach = componentBytes[component];
1108
+ const count = Number(accessor2.count);
1109
+ if (!size || !bytesEach || !Number.isInteger(count) || count < 0 || count > 1e6)
1110
+ throw new TileflowGltfError("has an accessor Tileflow cannot read");
1111
+ if (typeof accessor2.bufferView !== "number")
1112
+ return { values: new Array(count * size).fill(0), size };
1113
+ const view = record(array(json.bufferViews)[accessor2.bufferView]);
1114
+ if (!bin || Number(view.buffer ?? 0) !== 0)
1115
+ throw new TileflowGltfError("has geometry outside its binary chunk");
1116
+ const stride = Number(view.byteStride ?? 0) || size * bytesEach;
1117
+ const start = Number(view.byteOffset ?? 0) + Number(accessor2.byteOffset ?? 0);
1118
+ const end = start + (count - 1) * stride + size * bytesEach;
1119
+ if (count > 0 && (start < 0 || end > Number(view.byteOffset ?? 0) + Number(view.byteLength) || end > bin.byteLength))
1120
+ throw new TileflowGltfError("has an accessor past the end of its buffer");
1121
+ const data = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
1122
+ const normalized = accessor2.normalized === true;
1123
+ const values = [];
1124
+ for (let item = 0; item < count; item += 1)
1125
+ for (let part = 0; part < size; part += 1) {
1126
+ const at = start + item * stride + part * bytesEach;
1127
+ let value;
1128
+ switch (component) {
1129
+ case 5126:
1130
+ value = data.getFloat32(at, true);
1131
+ break;
1132
+ case 5125:
1133
+ value = data.getUint32(at, true);
1134
+ break;
1135
+ case 5123:
1136
+ value = data.getUint16(at, true);
1137
+ if (normalized) value /= 65535;
1138
+ break;
1139
+ case 5122:
1140
+ value = data.getInt16(at, true);
1141
+ if (normalized) value = Math.max(value / 32767, -1);
1142
+ break;
1143
+ case 5121:
1144
+ value = data.getUint8(at);
1145
+ if (normalized) value /= 255;
1146
+ break;
1147
+ default:
1148
+ value = data.getInt8(at);
1149
+ if (normalized) value = Math.max(value / 127, -1);
1150
+ }
1151
+ if (!Number.isFinite(value)) throw new TileflowGltfError("has a value that is not a number");
1152
+ values.push(value);
1153
+ }
1154
+ return { values, size };
1155
+ }
1156
+ function identity() {
1157
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
1158
+ }
1159
+ function local(node) {
1160
+ const matrix = array(node.matrix).map(Number);
1161
+ if (matrix.length === 16 && matrix.every(Number.isFinite)) return matrix;
1162
+ const [tx, ty, tz] = vector(node.translation, [0, 0, 0]);
1163
+ const [qx, qy, qz, qw] = vector(node.rotation, [0, 0, 0, 1]);
1164
+ const [sx, sy, sz] = vector(node.scale, [1, 1, 1]);
1165
+ const [xx, yy, zz] = [qx * qx, qy * qy, qz * qz];
1166
+ const [xy, xz, yz, wx, wy, wz] = [qx * qy, qx * qz, qy * qz, qw * qx, qw * qy, qw * qz];
1167
+ return [
1168
+ (1 - 2 * (yy + zz)) * sx,
1169
+ 2 * (xy + wz) * sx,
1170
+ 2 * (xz - wy) * sx,
1171
+ 0,
1172
+ 2 * (xy - wz) * sy,
1173
+ (1 - 2 * (xx + zz)) * sy,
1174
+ 2 * (yz + wx) * sy,
1175
+ 0,
1176
+ 2 * (xz + wy) * sz,
1177
+ 2 * (yz - wx) * sz,
1178
+ (1 - 2 * (xx + yy)) * sz,
1179
+ 0,
1180
+ tx,
1181
+ ty,
1182
+ tz,
1183
+ 1
1184
+ ];
1185
+ }
1186
+ function multiply(a, b) {
1187
+ const out = new Array(16).fill(0);
1188
+ for (let column = 0; column < 4; column += 1)
1189
+ for (let row = 0; row < 4; row += 1)
1190
+ for (let k = 0; k < 4; k += 1) out[column * 4 + row] += a[k * 4 + row] * b[column * 4 + k];
1191
+ return out;
1192
+ }
1193
+ function transform2(m, x, y, z, w) {
1194
+ return [
1195
+ m[0] * x + m[4] * y + m[8] * z + m[12] * w,
1196
+ m[1] * x + m[5] * y + m[9] * z + m[13] * w,
1197
+ m[2] * x + m[6] * y + m[10] * z + m[14] * w
1198
+ ];
1199
+ }
1200
+ function determinant(m) {
1201
+ 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]);
1202
+ }
1203
+ function inverseTranspose(m) {
1204
+ const [a, b, c, d, e, f, g, h, i] = [
1205
+ m[0],
1206
+ m[1],
1207
+ m[2],
1208
+ m[4],
1209
+ m[5],
1210
+ m[6],
1211
+ m[8],
1212
+ m[9],
1213
+ m[10]
1214
+ ];
1215
+ const det = determinant(m) || 1;
1216
+ return [
1217
+ (e * i - h * f) / det,
1218
+ (g * f - d * i) / det,
1219
+ (d * h - g * e) / det,
1220
+ 0,
1221
+ (h * c - b * i) / det,
1222
+ (a * i - g * c) / det,
1223
+ (g * b - a * h) / det,
1224
+ 0,
1225
+ (b * f - e * c) / det,
1226
+ (d * c - a * f) / det,
1227
+ (a * e - d * b) / det,
1228
+ 0,
1229
+ 0,
1230
+ 0,
1231
+ 0,
1232
+ 1
1233
+ ];
1234
+ }
1235
+ function record(value) {
1236
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1237
+ }
1238
+ function array(value) {
1239
+ return Array.isArray(value) ? value : [];
1240
+ }
1241
+ function vector(value, fallback) {
1242
+ const list = array(value).map(Number);
1243
+ return list.length === fallback.length && list.every(Number.isFinite) ? list : fallback;
1244
+ }
1245
+
1246
+ // src/tree-model-mesh.ts
1247
+ import {
1248
+ tileflowTreeModelLimits,
1249
+ tileflowTreeParts
1250
+ } from "@tileflow/core";
1251
+ var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
1252
+ function prepareTileflowTreeModelMesh(bytes, level) {
1253
+ const primitives = readTileflowGltfTriangles(bytes);
1254
+ const positions = [];
1255
+ const normals = [];
1256
+ const parts = [];
1257
+ const luminance = [];
1258
+ const indices = [];
1259
+ for (const primitive of primitives) {
1260
+ const part = partOf(primitive.material);
1261
+ const offset = positions.length / 3;
1262
+ const count2 = primitive.positions.length / 3;
1263
+ positions.push(...primitive.positions);
1264
+ for (let vertex = 0; vertex < count2; vertex += 1) {
1265
+ parts.push(part);
1266
+ normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
1267
+ const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
1268
+ luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
1269
+ }
1270
+ for (const index of primitive.indices) indices.push(offset + index);
1271
+ }
1272
+ const count = positions.length / 3;
1273
+ const triangleCount = indices.length / 3;
1274
+ const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
1275
+ if (triangleCount > limit)
1276
+ throw new TileflowGltfError(
1277
+ `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`
1278
+ );
1279
+ if (count > tileflowTreeModelLimits.vertices)
1280
+ throw new TileflowGltfError(
1281
+ `has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
1282
+ );
1283
+ let height = 0;
1284
+ let reach = 0;
1285
+ const crowned = parts.some((part) => isCrown(part));
1286
+ for (let vertex = 0; vertex < count; vertex += 1) {
1287
+ height = Math.max(height, positions[vertex * 3 + 2]);
1288
+ if (!crowned || isCrown(parts[vertex]))
1289
+ reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
1290
+ }
1291
+ if (!(height > 0))
1292
+ throw new TileflowGltfError(
1293
+ "stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
1294
+ );
1295
+ if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
1296
+ const across = 2 * reach;
1297
+ const unit = [];
1298
+ for (let vertex = 0; vertex < count; vertex += 1)
1299
+ unit.push(
1300
+ positions[vertex * 3] / across,
1301
+ positions[vertex * 3 + 1] / across,
1302
+ positions[vertex * 3 + 2] / height
1303
+ );
1304
+ const faceSums = new Array(count * 3).fill(0);
1305
+ for (let at = 0; at + 2 < indices.length; at += 3) {
1306
+ 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)));
1308
+ for (const vertex of [a, b, c])
1309
+ for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
1310
+ }
1311
+ const outNormals = [];
1312
+ for (let vertex = 0; vertex < count; vertex += 1) {
1313
+ const own = normals[vertex];
1314
+ const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
1315
+ const size = Math.hypot(n[0], n[1], n[2]) || 1;
1316
+ outNormals.push(n[0] / size, n[1] / size, n[2] / size);
1317
+ }
1318
+ const shades = new Array(count).fill(0);
1319
+ for (const part of new Set(parts)) {
1320
+ const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
1321
+ const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
1322
+ const spread = Math.max(0.06, mean * 0.6);
1323
+ for (const vertex of members)
1324
+ shades[vertex] = clamp((luminance[vertex] - mean) / spread, -1, 1);
1325
+ }
1326
+ let crownLow = 1;
1327
+ let crownHigh = 0;
1328
+ for (let vertex = 0; vertex < count; vertex += 1)
1329
+ if (isCrown(parts[vertex])) {
1330
+ crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
1331
+ crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
1332
+ }
1333
+ const occlusion = [];
1334
+ for (let vertex = 0; vertex < count; vertex += 1) {
1335
+ const z = unit[vertex * 3 + 2];
1336
+ if (!isCrown(parts[vertex])) {
1337
+ occlusion.push(z > crownLow ? 0.55 : 0.8);
1338
+ continue;
1339
+ }
1340
+ const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
1341
+ 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));
1343
+ }
1344
+ return {
1345
+ // The author's proportions: the trees it stands keep them around the sizes the data gives.
1346
+ proportion: Math.min(3, Math.max(0.1, across / height)),
1347
+ positions: unit,
1348
+ normals: outNormals,
1349
+ parts,
1350
+ shades,
1351
+ occlusion,
1352
+ indices
1353
+ };
1354
+ }
1355
+ function partOf(material) {
1356
+ const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
1357
+ const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
1358
+ if (part) return tileflowTreeParts[part];
1359
+ throw new TileflowGltfError(
1360
+ 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`
1361
+ );
1362
+ }
1363
+ function isCrown(part) {
1364
+ return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
1365
+ }
1366
+ function point(list, vertex) {
1367
+ return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
1368
+ }
1369
+ function sub(a, b) {
1370
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1371
+ }
1372
+ function cross(a, b) {
1373
+ return [
1374
+ a[1] * b[2] - a[2] * b[1],
1375
+ a[2] * b[0] - a[0] * b[2],
1376
+ a[0] * b[1] - a[1] * b[0]
1377
+ ];
1378
+ }
1379
+ function clamp(value, low, high) {
1380
+ return Math.min(high, Math.max(low, value));
1381
+ }
1382
+
1383
+ // src/tree-models.ts
1384
+ var TileflowTreeModelError = class extends Error {
1385
+ code = "TREE_MODEL_INVALID";
1386
+ issues;
1387
+ phase = "tree-model-preparation";
1388
+ constructor(issues) {
1389
+ super(
1390
+ [
1391
+ "Invalid Tileflow tree models",
1392
+ ...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
1393
+ ].join("\n")
1394
+ );
1395
+ this.name = "TileflowTreeModelError";
1396
+ this.issues = issues;
1397
+ }
1398
+ };
1399
+ var fileLimit = 8 * 1024 * 1024;
1400
+ var modelId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1401
+ async function prepareTileflowMapTreeModels(directories, options) {
1402
+ const resolved = await resolveTileflowAssetDirectories(directories, {
1403
+ baseDirectory: options.baseDirectory,
1404
+ configPath: options.configPath,
1405
+ cwd: options.cwd,
1406
+ kind: "models",
1407
+ target: options.target
1408
+ });
1409
+ const issues = [];
1410
+ const files = /* @__PURE__ */ new Map();
1411
+ for (const directory of resolved) {
1412
+ let names;
1413
+ try {
1414
+ names = (await readdir2(directory.realPath)).sort();
1415
+ } catch {
1416
+ issues.push({ message: "is not a readable directory", path: directory.configPath });
1417
+ continue;
1418
+ }
1419
+ for (const name of names) {
1420
+ const display = `${directory.configPath}/${name}`;
1421
+ const lower = name.toLowerCase();
1422
+ if (lower.endsWith(".gltf")) {
1423
+ issues.push({
1424
+ message: "is a text glTF; export the model as a glTF binary (.glb)",
1425
+ path: display
1426
+ });
1427
+ continue;
1428
+ }
1429
+ if (!lower.endsWith(".glb")) continue;
1430
+ const far = name.endsWith(".far.glb");
1431
+ const id = name.slice(0, name.length - (far ? 8 : 4));
1432
+ if (!modelId.test(id) || id.length > 64) {
1433
+ issues.push({
1434
+ message: "a model's file name is its id: lowercase letters, digits and hyphens, up to 64, such as royal-palm.glb",
1435
+ path: display
1436
+ });
1437
+ continue;
1438
+ }
1439
+ const path = join3(directory.realPath, name);
1440
+ const info = await lstat3(path).catch(() => void 0);
1441
+ if (!info?.isFile()) {
1442
+ issues.push({ message: "is not a regular file", path: display });
1443
+ continue;
1444
+ }
1445
+ if (info.size > fileLimit) {
1446
+ issues.push({ message: "is larger than 8 MiB", path: display });
1447
+ continue;
1448
+ }
1449
+ const entry = files.get(id) ?? {};
1450
+ entry[far ? "far" : "near"] = { path, display };
1451
+ files.set(id, entry);
1452
+ }
1453
+ }
1454
+ const models = {};
1455
+ const identities = [];
1456
+ for (const [id, entry] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1457
+ if (!entry.near) {
1458
+ issues.push({ message: `has no ${id}.glb beside it`, path: entry.far.display });
1459
+ continue;
1460
+ }
1461
+ const prepared = {};
1462
+ for (const level of ["near", "far"]) {
1463
+ const file = entry[level];
1464
+ if (!file) continue;
1465
+ try {
1466
+ const bytes = new Uint8Array(await readFile(file.path));
1467
+ prepared[level] = encodeTileflowTreeModel(
1468
+ prepareTileflowTreeModelMesh(bytes, level),
1469
+ level
1470
+ );
1471
+ identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
1472
+ } catch (error) {
1473
+ issues.push({
1474
+ message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
1475
+ path: file.display
1476
+ });
1477
+ }
1478
+ }
1479
+ if (prepared.near)
1480
+ models[id] = { near: prepared.near, ...prepared.far ? { far: prepared.far } : {} };
1481
+ }
1482
+ if (Object.keys(models).length > tileflowTreeModelLimits2.models)
1483
+ issues.push({
1484
+ message: `holds ${Object.keys(models).length} models; a map takes at most ${tileflowTreeModelLimits2.models}`,
1485
+ path: options.configPath
1486
+ });
1487
+ if (issues.length > 0) throw new TileflowTreeModelError(issues);
1488
+ return {
1489
+ models,
1490
+ identities,
1491
+ watchPaths: resolved.filter((directory) => directory.watch).map((directory) => directory.realPath)
1492
+ };
1493
+ }
1494
+
942
1495
  // src/icon-preview.ts
943
1496
  import { renderTileflowIconAtlasPixels } from "@tileflow/core";
944
1497
  async function renderTileflowIconAtlasPreview(source, index) {
@@ -1210,6 +1763,20 @@ async function prepareTileflowCatalogIcons(project, options) {
1210
1763
  });
1211
1764
  }
1212
1765
  }
1766
+ const mapModelSources = {};
1767
+ const modelWatchPaths = [];
1768
+ for (const [mapName, map] of Object.entries(project.maps)) {
1769
+ if (!map.models?.length) continue;
1770
+ const models = await prepareTileflowMapTreeModels(map.models, {
1771
+ baseDirectory: options.baseDirectory ?? options.cwd,
1772
+ configPath: `maps.${mapName}.models`,
1773
+ cwd: options.cwd,
1774
+ target: "local"
1775
+ });
1776
+ mapAssets[mapName] = { ...mapAssets[mapName], models: models.models };
1777
+ mapModelSources[mapName] = models.identities;
1778
+ modelWatchPaths.push(...models.watchPaths);
1779
+ }
1213
1780
  return {
1214
1781
  assets,
1215
1782
  baseDirectory: await realpath3(options.baseDirectory ?? options.cwd),
@@ -1217,10 +1784,11 @@ async function prepareTileflowCatalogIcons(project, options) {
1217
1784
  mapAssets,
1218
1785
  mapIconCompositions,
1219
1786
  mapIconSources,
1787
+ ...Object.keys(mapModelSources).length > 0 ? { mapModelSources } : {},
1220
1788
  project,
1221
1789
  sourceProject: project,
1222
1790
  watchFiles: compiled.watchFiles,
1223
- watchPaths: compiled.watchPaths
1791
+ watchPaths: [...compiled.watchPaths, ...modelWatchPaths]
1224
1792
  };
1225
1793
  }
1226
1794
  async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
@@ -1312,7 +1880,7 @@ async function inspectIconSource(directory, target, issues) {
1312
1880
  let entries;
1313
1881
  const issueCount = issues.length;
1314
1882
  try {
1315
- entries = await readdir2(directory.realPath, { withFileTypes: true });
1883
+ entries = await readdir3(directory.realPath, { withFileTypes: true });
1316
1884
  } catch (error) {
1317
1885
  issues.push({
1318
1886
  message: describeFileSystemError(error, "Icon source could not be read"),
@@ -1360,7 +1928,7 @@ async function inspectIconSource(directory, target, issues) {
1360
1928
  throw new Error("Icon metadata exceeds its byte limit");
1361
1929
  metadata = tileflowIconAuthorMetadataSchema.parse(
1362
1930
  parseTileflowIconJson2(
1363
- await readFile(realEntryPath, "utf8"),
1931
+ await readFile2(realEntryPath, "utf8"),
1364
1932
  tileflowIconPackageLimits4.maxSourceFileBytes
1365
1933
  )
1366
1934
  );
@@ -1429,7 +1997,7 @@ async function inspectIconSource(directory, target, issues) {
1429
1997
  for (const candidate of candidates) {
1430
1998
  const displayPath = candidate.displayPath;
1431
1999
  try {
1432
- const source = await readFile(candidate.path);
2000
+ const source = await readFile2(candidate.path);
1433
2001
  if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits4.maxSourceFileBytes) {
1434
2002
  throw new Error("Icon changed while it was being validated");
1435
2003
  }
package/dist/config.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  loadTileflowConfigWithInputs,
11
11
  loadValidTileflowConfig,
12
12
  loadValidTileflowConfigWithInputs
13
- } from "./chunk-6MGFMKG3.js";
13
+ } from "./chunk-AQUGMUZW.js";
14
14
  import "./chunk-D4OE4ITL.js";
15
15
  export {
16
16
  TileflowValidationError,
package/dist/icons.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TileflowIconAppearance, TileflowIconLayoutMetadata, TileflowIconSetPin, TileflowIconSpriteIndex, TileflowIconSource, TileflowIconsLockfileV1, TileflowIconCompositionV1, TileflowIconPackageFileName, TileflowIconPackageManifest, TileflowIconDirectory } from '@tileflow/core';
2
- import { TileflowEffectiveIconSourceIdentity, TileflowBuildCatalog, TileflowPreparedMapAssets } from '@tileflow/core/build';
2
+ import { TileflowEffectiveIconSourceIdentity, TileflowBuildCatalog, TileflowPreparedMapAssets, TileflowEffectiveModelSourceIdentity } from '@tileflow/core/build';
3
3
 
4
4
  type TileflowRenderedIconCell = {
5
5
  height: number;
@@ -126,6 +126,8 @@ type PreparedTileflowCatalog = {
126
126
  /** Ordered shared dependency receipts, present only for maps that declare an Icon Set. */
127
127
  mapIconCompositions: Record<string, TileflowIconCompositionV1>;
128
128
  mapIconSources: Record<string, readonly TileflowEffectiveIconSourceIdentity[]>;
129
+ /** The files of each map's own 3D models, for its revision. */
130
+ mapModelSources?: Record<string, readonly TileflowEffectiveModelSourceIdentity[]>;
129
131
  project: PreparedTileflowBuildCatalog;
130
132
  sourceProject: TileflowSourceCatalog;
131
133
  /** Exact files whose contents select the composition, such as the icon lock. */
package/dist/icons.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  storeTileflowIconSetArtifact,
17
17
  verifyTileflowIconArtifact,
18
18
  writeTileflowIconsLockfile
19
- } from "./chunk-A2VTI43Y.js";
19
+ } from "./chunk-IR22ZTQW.js";
20
20
  import "./chunk-7C4F6GAX.js";
21
21
  export {
22
22
  TileflowIconCompilationError,
package/dist/inspect.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  defaultTileflowConfigPath,
10
10
  loadValidTileflowConfigWithInputs
11
- } from "./chunk-6MGFMKG3.js";
11
+ } from "./chunk-AQUGMUZW.js";
12
12
  import "./chunk-D4OE4ITL.js";
13
13
 
14
14
  // src/inspect.ts