@tileflow/dev 0.1.0-alpha.33 → 0.1.0-alpha.35

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.
@@ -25,7 +25,7 @@ import {
25
25
  sha256Hex as sha256Hex3,
26
26
  tileflowIconIdPattern,
27
27
  tileflowIconPackageLimits as tileflowIconPackageLimits4,
28
- tileflowModelContentType as tileflowModelContentType2
28
+ tileflowModelContentType
29
29
  } from "@tileflow/core";
30
30
 
31
31
  // src/icon-composition.ts
@@ -949,17 +949,384 @@ import {
949
949
  compareCodeUnits as compareCodeUnits4,
950
950
  encodeTileflowModels,
951
951
  encodeTileflowTreeModel,
952
- hashTileflowModelBundleManifest,
952
+ readTileflowLandmarkTileFootprints,
953
953
  serializeTileflowLandmarkRegion,
954
954
  tileflowLandmarkTileOf,
955
- tileflowModelBundleFileName,
956
- tileflowModelBundleLimits,
957
- tileflowModelContentType,
955
+ tileflowModelFileName,
958
956
  tileflowTreeModelLimits as tileflowTreeModelLimits2
959
957
  } from "@tileflow/core";
960
958
 
961
959
  // src/building-model-mesh.ts
962
- import { tileflowModelLimits, tileflowModelParts } from "@tileflow/core";
960
+ import {
961
+ tileflowModelDefaultLightingRoles,
962
+ tileflowModelLightingRoles,
963
+ tileflowModelLimits,
964
+ tileflowModelParts
965
+ } from "@tileflow/core";
966
+
967
+ // src/building-model-materials.ts
968
+ import {
969
+ checkTileflowModelSurface,
970
+ tileflowModelTextureChannels
971
+ } from "@tileflow/core";
972
+ function buildingModelSurface(primitives) {
973
+ if (!primitives.some((primitive) => primitive.surface)) return void 0;
974
+ const attributes = [];
975
+ const materials = [];
976
+ const materialIds = /* @__PURE__ */ new Map();
977
+ const textures = [];
978
+ const textureIds = /* @__PURE__ */ new Map();
979
+ const ranges = [];
980
+ const emitters = [];
981
+ let indexCount = 0;
982
+ for (const primitive of primitives) {
983
+ const count = primitive.positions.length / 3;
984
+ const surface = primitive.surface;
985
+ if (surface) {
986
+ checkTileflowModelSurface(surface, count, primitive.indices.length);
987
+ for (const value of surface.attributes) attributes.push(value);
988
+ emitters.push(...surface.emitters ?? []);
989
+ } else {
990
+ for (let vertex = 0; vertex < count; vertex++) attributes.push(0, 0, 0, 0, 1, 1, 1, 1);
991
+ }
992
+ const remap = (surface?.materials ?? [{}]).map((material) => {
993
+ const copy = { ...material };
994
+ for (const channel of tileflowModelTextureChannels) {
995
+ const ref = copy[channel];
996
+ if (!ref) continue;
997
+ const texture = surface.textures[ref.texture];
998
+ let id2 = textureIds.get(texture);
999
+ if (id2 === void 0) {
1000
+ id2 = textures.length;
1001
+ textures.push(texture);
1002
+ textureIds.set(texture, id2);
1003
+ }
1004
+ copy[channel] = { ...ref, texture: id2 };
1005
+ }
1006
+ const key = JSON.stringify(copy);
1007
+ let id = materialIds.get(key);
1008
+ if (id === void 0) {
1009
+ id = materials.length;
1010
+ materials.push(copy);
1011
+ materialIds.set(key, id);
1012
+ }
1013
+ return id;
1014
+ });
1015
+ const sourceRanges = surface?.ranges ?? [
1016
+ { from: 0, count: primitive.indices.length, material: 0 }
1017
+ ];
1018
+ for (const range of sourceRanges) {
1019
+ const material = remap[range.material];
1020
+ const previous = ranges.at(-1);
1021
+ if (previous?.material === material && JSON.stringify(previous.emissiveBounds) === JSON.stringify(range.emissiveBounds))
1022
+ ranges[ranges.length - 1] = { ...previous, count: previous.count + range.count };
1023
+ else ranges.push({ ...range, from: indexCount + range.from, material });
1024
+ }
1025
+ indexCount += primitive.indices.length;
1026
+ }
1027
+ const result = { attributes, materials, textures, ranges, ...emitters.length ? { emitters } : {} };
1028
+ checkTileflowModelSurface(result, attributes.length / 8, indexCount);
1029
+ return result;
1030
+ }
1031
+
1032
+ // src/building-model-occlusion.ts
1033
+ function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {}) {
1034
+ if (!(radius > 0 && Number.isFinite(radius)) || !Number.isInteger(samples) || samples < 1 || samples > 256)
1035
+ throw new RangeError("Model ambient visibility needs a positive radius and 1 to 256 samples.");
1036
+ const { positions, normals, indices } = mesh;
1037
+ const count = indices.length / 3;
1038
+ const triangles2 = new Float64Array(count * 9);
1039
+ const bounds = new Float64Array(count * 6);
1040
+ const centres = new Float64Array(count * 3);
1041
+ const order = Uint32Array.from({ length: count }, (_, i) => i);
1042
+ for (let i = 0; i < count; i += 1) {
1043
+ const a = indices[i * 3] * 3;
1044
+ const b = indices[i * 3 + 1] * 3;
1045
+ const c = indices[i * 3 + 2] * 3;
1046
+ for (let axis = 0; axis < 3; axis += 1) {
1047
+ const x = positions[a + axis], y = positions[b + axis], z = positions[c + axis];
1048
+ triangles2[i * 9 + axis] = x;
1049
+ triangles2[i * 9 + 3 + axis] = y - x;
1050
+ triangles2[i * 9 + 6 + axis] = z - x;
1051
+ bounds[i * 6 + axis] = Math.min(x, y, z);
1052
+ bounds[i * 6 + 3 + axis] = Math.max(x, y, z);
1053
+ centres[i * 3 + axis] = (x + y + z) / 3;
1054
+ }
1055
+ }
1056
+ const nodes = [];
1057
+ const build = (from, to) => {
1058
+ const box = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
1059
+ for (let at = from; at < to; at += 1) {
1060
+ const i = order[at] * 6;
1061
+ for (let axis2 = 0; axis2 < 3; axis2 += 1) {
1062
+ box[axis2] = Math.min(box[axis2], bounds[i + axis2]);
1063
+ box[axis2 + 3] = Math.max(box[axis2 + 3], bounds[i + axis2 + 3]);
1064
+ }
1065
+ }
1066
+ let axis = 0;
1067
+ for (let next = 1; next < 3; next += 1)
1068
+ if (box[next + 3] - box[next] > box[axis + 3] - box[axis]) axis = next;
1069
+ const index = nodes.length;
1070
+ const node = { bounds: box, from, to, left: -1, right: -1, axis };
1071
+ nodes.push(node);
1072
+ if (to - from > 8) {
1073
+ order.subarray(from, to).sort((a, b) => centres[a * 3 + axis] - centres[b * 3 + axis] || a - b);
1074
+ const middle = from + to >>> 1;
1075
+ node.left = build(from, middle);
1076
+ node.right = build(middle, to);
1077
+ }
1078
+ return index;
1079
+ };
1080
+ build(0, count);
1081
+ const hemisphere = Array.from({ length: samples }, (_, i) => {
1082
+ const r = Math.sqrt((i + 0.5) / samples), angle = i * Math.PI * (3 - Math.sqrt(5));
1083
+ return [r * Math.cos(angle), r * Math.sin(angle), Math.sqrt(1 - r * r)];
1084
+ });
1085
+ const bias = Math.min(2e-3, radius * 1e-3);
1086
+ const stack = new Int32Array(64);
1087
+ const origin = [0, 0, 0];
1088
+ const direction = [0, 0, 0];
1089
+ const nearest = () => {
1090
+ let best = radius, pending = 1;
1091
+ stack[0] = 0;
1092
+ while (pending > 0) {
1093
+ const node = nodes[stack[--pending]];
1094
+ if (!hitsBox(node.bounds, origin, direction, best)) continue;
1095
+ if (node.left >= 0) {
1096
+ const forward = direction[node.axis] >= 0;
1097
+ stack[pending++] = forward ? node.right : node.left;
1098
+ stack[pending++] = forward ? node.left : node.right;
1099
+ continue;
1100
+ }
1101
+ for (let at = node.from; at < node.to; at += 1) {
1102
+ const i = order[at] * 9;
1103
+ const e1x = triangles2[i + 3], e1y = triangles2[i + 4], e1z = triangles2[i + 5];
1104
+ const e2x = triangles2[i + 6], e2y = triangles2[i + 7], e2z = triangles2[i + 8];
1105
+ const px = direction[1] * e2z - direction[2] * e2y;
1106
+ const py = direction[2] * e2x - direction[0] * e2z;
1107
+ const pz = direction[0] * e2y - direction[1] * e2x;
1108
+ const determinant2 = e1x * px + e1y * py + e1z * pz;
1109
+ if (Math.abs(determinant2) < 1e-12) continue;
1110
+ const tx = origin[0] - triangles2[i], ty = origin[1] - triangles2[i + 1], tz = origin[2] - triangles2[i + 2];
1111
+ const inverse = 1 / determinant2;
1112
+ const u = (tx * px + ty * py + tz * pz) * inverse;
1113
+ if (u < 0 || u > 1) continue;
1114
+ const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x;
1115
+ const v = (direction[0] * qx + direction[1] * qy + direction[2] * qz) * inverse;
1116
+ if (v < 0 || u + v > 1) continue;
1117
+ const distance = (e2x * qx + e2y * qy + e2z * qz) * inverse;
1118
+ if (distance > bias && distance < best) best = distance;
1119
+ }
1120
+ }
1121
+ return best;
1122
+ };
1123
+ const values = new Array(positions.length / 3);
1124
+ const cache = /* @__PURE__ */ new Map();
1125
+ for (let vertex = 0; vertex < values.length; vertex += 1) {
1126
+ const at = vertex * 3;
1127
+ const nx = normals[at], ny = normals[at + 1], nz = normals[at + 2];
1128
+ const point3 = [positions[at], positions[at + 1], positions[at + 2]];
1129
+ const key = [...point3, nx, ny, nz].map((v) => Math.round(v * 1e5)).join(",");
1130
+ const cached = cache.get(key);
1131
+ if (cached !== void 0) {
1132
+ values[vertex] = cached;
1133
+ continue;
1134
+ }
1135
+ const tangent = Math.abs(nz) < 0.9 ? [-ny, nx, 0] : [nz, 0, -nx];
1136
+ const length = Math.hypot(...tangent);
1137
+ if (length === 0) {
1138
+ values[vertex] = 1;
1139
+ continue;
1140
+ }
1141
+ for (let axis = 0; axis < 3; axis += 1) {
1142
+ tangent[axis] /= length;
1143
+ origin[axis] = point3[axis] + normals[at + axis] * bias;
1144
+ }
1145
+ const bitangent = [
1146
+ ny * tangent[2] - nz * tangent[1],
1147
+ nz * tangent[0] - nx * tangent[2],
1148
+ nx * tangent[1] - ny * tangent[0]
1149
+ ];
1150
+ let visibility = 0;
1151
+ for (const sample of hemisphere) {
1152
+ for (let axis = 0; axis < 3; axis += 1)
1153
+ direction[axis] = sample[0] * tangent[axis] + sample[1] * bitangent[axis] + sample[2] * normals[at + axis];
1154
+ visibility += nearest() / radius;
1155
+ }
1156
+ values[vertex] = visibility / samples;
1157
+ cache.set(key, values[vertex]);
1158
+ }
1159
+ return values;
1160
+ }
1161
+ function hitsBox(box, origin, direction, limit) {
1162
+ let low = 0, high = limit;
1163
+ for (let axis = 0; axis < 3; axis += 1) {
1164
+ const o = origin[axis], d = direction[axis];
1165
+ if (Math.abs(d) < 1e-12) {
1166
+ if (o < box[axis] || o > box[axis + 3]) return false;
1167
+ continue;
1168
+ }
1169
+ const a = (box[axis] - o) / d, b = (box[axis + 3] - o) / d;
1170
+ low = Math.max(low, Math.min(a, b));
1171
+ high = Math.min(high, Math.max(a, b));
1172
+ if (low > high) return false;
1173
+ }
1174
+ return true;
1175
+ }
1176
+
1177
+ // src/gltf-materials.ts
1178
+ import {
1179
+ checkTileflowModelSurface as checkTileflowModelSurface2,
1180
+ tileflowModelImageSize,
1181
+ tileflowModelSurfaceLimits
1182
+ } from "@tileflow/core";
1183
+ function createGltfMaterialReader(json, bin, accessor2) {
1184
+ const textures = [];
1185
+ const textureIds = /* @__PURE__ */ new Map();
1186
+ const imageCache = /* @__PURE__ */ new Map();
1187
+ function imageAt(index) {
1188
+ if (!Number.isInteger(index)) throw new RangeError("A material names a missing image.");
1189
+ const cached = imageCache.get(index);
1190
+ if (cached) return cached;
1191
+ const image = record(list(json.images)[index]);
1192
+ let bytes;
1193
+ let mimeType = image.mimeType;
1194
+ if (typeof image.uri === "string") {
1195
+ const match = /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image.uri);
1196
+ if (!match || image.uri.length > 24 * 1024 * 1024)
1197
+ throw new RangeError("Embed PNG/JPEG images in the GLB; external images are not read.");
1198
+ if (mimeType !== void 0 && mimeType !== match[1])
1199
+ throw new RangeError("An image has conflicting media types.");
1200
+ mimeType = match[1];
1201
+ bytes = new Uint8Array(Buffer.from(match[2], "base64"));
1202
+ } else {
1203
+ if (!Number.isInteger(image.bufferView)) throw new RangeError("Embed each image in the GLB.");
1204
+ const view = record(list(json.bufferViews)[image.bufferView]);
1205
+ const start = view.byteOffset ?? 0;
1206
+ const length = view.byteLength;
1207
+ if (!bin || (view.buffer ?? 0) !== 0 || !Number.isInteger(start) || !Number.isInteger(length) || start < 0 || length <= 0 || start + length > bin.length)
1208
+ throw new RangeError("An embedded image extends outside the GLB.");
1209
+ bytes = bin.subarray(start, start + length);
1210
+ }
1211
+ if (mimeType !== "image/png" && mimeType !== "image/jpeg")
1212
+ throw new RangeError("Use an embedded PNG or JPEG image.");
1213
+ const size = tileflowModelImageSize(bytes, mimeType);
1214
+ if (!size || Math.max(...size) > tileflowModelSurfaceLimits.textureDimension || size[0] * size[1] > tileflowModelSurfaceLimits.texturePixels)
1215
+ throw new RangeError("The model image is invalid or exceeds its pixel budget.");
1216
+ const result = {
1217
+ bytes,
1218
+ width: size[0],
1219
+ height: size[1],
1220
+ mimeType
1221
+ };
1222
+ imageCache.set(index, result);
1223
+ return result;
1224
+ }
1225
+ function textureAt(index) {
1226
+ if (!Number.isInteger(index)) throw new RangeError("A material names a missing texture.");
1227
+ const cached = textureIds.get(index);
1228
+ if (cached !== void 0) return cached;
1229
+ const source = record(list(json.textures)[index]);
1230
+ const image = imageAt(source.source);
1231
+ const sampler = source.sampler === void 0 ? {} : record(list(json.samplers)[source.sampler]);
1232
+ if (source.sampler !== void 0 && (!Number.isInteger(source.sampler) || !list(json.samplers)[source.sampler]))
1233
+ throw new RangeError("A texture names a missing sampler.");
1234
+ const id = textures.length;
1235
+ if (id >= tileflowModelSurfaceLimits.textures)
1236
+ throw new RangeError("The model has too many textures.");
1237
+ textures.push({
1238
+ ...image,
1239
+ ...pick(sampler, ["wrapS", "wrapT", "minFilter", "magFilter"])
1240
+ });
1241
+ textureIds.set(index, id);
1242
+ return id;
1243
+ }
1244
+ function textureRef(value, normal = false) {
1245
+ if (value === void 0) return void 0;
1246
+ const ref = record(value);
1247
+ const transform3 = record(record(ref.extensions).KHR_texture_transform);
1248
+ const texCoord = transform3.texCoord ?? ref.texCoord;
1249
+ return {
1250
+ texture: textureAt(ref.index),
1251
+ ...texCoord === void 0 ? {} : { texCoord },
1252
+ ...pick(transform3, ["offset", "scale", "rotation"]),
1253
+ ...normal && ref.scale !== void 0 ? { strength: ref.scale } : {},
1254
+ ...!normal && ref.strength !== void 0 ? { strength: ref.strength } : {}
1255
+ };
1256
+ }
1257
+ return (primitive, count, indexCount) => {
1258
+ const source = primitive.material === void 0 ? {} : record(list(json.materials)[primitive.material]);
1259
+ if (primitive.material !== void 0 && (!Number.isInteger(primitive.material) || !list(json.materials)[primitive.material]))
1260
+ throw new RangeError("A primitive names a missing material.");
1261
+ const pbr = record(source.pbrMetallicRoughness);
1262
+ const extensions = record(source.extensions);
1263
+ const material = {
1264
+ ...pick(pbr, ["baseColorFactor", "metallicFactor", "roughnessFactor"]),
1265
+ ...pick(source, ["emissiveFactor", "alphaMode", "alphaCutoff", "doubleSided"]),
1266
+ ...extensions.KHR_materials_unlit === void 0 ? {} : { unlit: true },
1267
+ ...pick(record(extensions.KHR_materials_emissive_strength), ["emissiveStrength"]),
1268
+ ...optional("baseColorTexture", textureRef(pbr.baseColorTexture)),
1269
+ ...optional("metallicRoughnessTexture", textureRef(pbr.metallicRoughnessTexture)),
1270
+ ...optional("normalTexture", textureRef(source.normalTexture, true)),
1271
+ ...optional("occlusionTexture", textureRef(source.occlusionTexture)),
1272
+ ...optional("emissiveTexture", textureRef(source.emissiveTexture))
1273
+ };
1274
+ const attributes = record(primitive.attributes);
1275
+ const read = (name, size) => {
1276
+ const index = attributes[name];
1277
+ if (index === void 0) return void 0;
1278
+ if (!Number.isInteger(index)) throw new RangeError(`Invalid ${name} accessor.`);
1279
+ const value = accessor2(index);
1280
+ if (!size.includes(value.size) || value.values.length !== count * value.size)
1281
+ throw new RangeError(`${name} must have one value per vertex.`);
1282
+ return value;
1283
+ };
1284
+ const uv0 = read("TEXCOORD_0", [2]), uv1 = read("TEXCOORD_1", [2]);
1285
+ const colour = read("COLOR_0", [3, 4]);
1286
+ for (const key of [
1287
+ "baseColorTexture",
1288
+ "metallicRoughnessTexture",
1289
+ "normalTexture",
1290
+ "occlusionTexture",
1291
+ "emissiveTexture"
1292
+ ]) {
1293
+ const ref = material[key];
1294
+ if (ref && !(ref.texCoord === 1 ? uv1 : ref.texCoord === void 0 || ref.texCoord === 0 ? uv0 : void 0))
1295
+ throw new RangeError(`The ${key} needs its declared UV coordinates.`);
1296
+ }
1297
+ const data = new Float32Array(count * 8);
1298
+ for (let vertex = 0; vertex < count; vertex++) {
1299
+ for (let axis = 0; axis < 2; axis++) {
1300
+ data[vertex * 8 + axis] = uv0?.values[vertex * 2 + axis] ?? 0;
1301
+ data[vertex * 8 + 2 + axis] = uv1?.values[vertex * 2 + axis] ?? 0;
1302
+ }
1303
+ for (let channel = 0; channel < 4; channel++)
1304
+ data[vertex * 8 + 4 + channel] = colour && channel < colour.size ? colour.values[vertex * colour.size + channel] : 1;
1305
+ }
1306
+ const surface = {
1307
+ attributes: data,
1308
+ materials: [material],
1309
+ textures,
1310
+ ranges: [{ from: 0, count: indexCount, material: 0 }]
1311
+ };
1312
+ checkTileflowModelSurface2(surface, count, indexCount);
1313
+ return surface;
1314
+ };
1315
+ }
1316
+ function record(value) {
1317
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1318
+ }
1319
+ function list(value) {
1320
+ return Array.isArray(value) ? value : [];
1321
+ }
1322
+ function pick(value, keys) {
1323
+ return Object.fromEntries(
1324
+ keys.filter((key) => value[key] !== void 0).map((key) => [key, value[key]])
1325
+ );
1326
+ }
1327
+ function optional(key, value) {
1328
+ return value === void 0 ? {} : { [key]: value };
1329
+ }
963
1330
 
964
1331
  // src/gltf-reader.ts
965
1332
  var TileflowGltfError = class extends Error {
@@ -984,7 +1351,7 @@ var componentBytes = {
984
1351
  5126: 4
985
1352
  };
986
1353
  var typeSizes = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
987
- function readTileflowGltfTriangles(bytes) {
1354
+ function readTileflowGltfTriangles(bytes, { materials = false } = {}) {
988
1355
  const { json, bin } = chunks(bytes);
989
1356
  const required = array(json.extensionsRequired).filter((name) => typeof name === "string");
990
1357
  const compressed = required.find((name) => compressions.has(name));
@@ -993,24 +1360,57 @@ function readTileflowGltfTriangles(bytes) {
993
1360
  `uses compressed geometry (${String(compressed)}); export it without compression`
994
1361
  );
995
1362
  for (const buffer of array(json.buffers))
996
- if (record(buffer).uri !== void 0)
1363
+ if (record2(buffer).uri !== void 0)
997
1364
  throw new TileflowGltfError("keeps its geometry in another file; export a single .glb");
998
- const nodes = array(json.nodes).map(record);
999
- const scenes = array(json.scenes).map(record);
1365
+ const nodes = array(json.nodes).map(record2);
1366
+ const scenes = array(json.scenes).map(record2);
1000
1367
  const scene = scenes[typeof json.scene === "number" ? json.scene : 0];
1001
1368
  const roots = scene ? array(scene.nodes) : nodes.map((_, index) => index).filter((index) => !nodes.some((node) => array(node.children).includes(index)));
1002
1369
  const out = [];
1370
+ const physical = materials ? createGltfMaterialReader(json, bin, (index) => accessorWithSize(json, bin, index)) : void 0;
1371
+ const emitters = [];
1003
1372
  const visit = (index, parent, depth) => {
1004
1373
  if (typeof index !== "number" || !nodes[index])
1005
1374
  throw new TileflowGltfError("names a node it does not hold");
1006
1375
  if (depth > 64) throw new TileflowGltfError("nests its nodes more than 64 deep");
1007
1376
  const node = nodes[index];
1008
1377
  const world = multiply(parent, local(node));
1009
- if (typeof node.mesh === "number") out.push(...meshOf(json, bin, node.mesh, world));
1378
+ if (physical) {
1379
+ const lights = record2(record2(node.extras).tileflow).emitters;
1380
+ if (lights !== void 0 && !Array.isArray(lights))
1381
+ throw new TileflowGltfError("area emitters must be an array");
1382
+ for (const value of array(lights)) {
1383
+ const own = record2(value);
1384
+ const point3 = (key) => {
1385
+ const input = array(own[key]);
1386
+ if (input.length !== 3 || !input.every((value2) => typeof value2 === "number" && Number.isFinite(value2)))
1387
+ throw new TileflowGltfError("an area emitter needs finite three-dimensional endpoints");
1388
+ const p = transform2(world, input[0], input[1], input[2], 1);
1389
+ return [p[0], -p[2], p[1]];
1390
+ };
1391
+ const heightScale = Math.hypot(...transform2(world, 0, 1, 0, 0));
1392
+ const depthScale = Math.hypot(...transform2(world, 0, 0, 1, 0));
1393
+ if (typeof own.height !== "number")
1394
+ throw new TileflowGltfError("an area emitter needs its height");
1395
+ emitters.push({
1396
+ start: point3("start"),
1397
+ end: point3("end"),
1398
+ height: own.height * heightScale,
1399
+ ...own.depth === void 0 ? {} : { depth: Number(own.depth) * depthScale },
1400
+ role: own.role,
1401
+ part: own.part
1402
+ });
1403
+ }
1404
+ }
1405
+ if (typeof node.mesh === "number") out.push(...meshOf(json, bin, node.mesh, world, physical));
1010
1406
  for (const child of array(node.children)) visit(child, world, depth + 1);
1011
1407
  };
1012
1408
  for (const root of roots) visit(root, identity(), 0);
1013
1409
  if (out.length === 0) throw new TileflowGltfError("holds no triangles in its scene");
1410
+ if (emitters.length) {
1411
+ const first = out.find((part) => !/^footprint(?:\.\d+)?$/i.test(part.material ?? ""));
1412
+ if (first?.surface) first.surface = { ...first.surface, emitters };
1413
+ }
1014
1414
  return out;
1015
1415
  }
1016
1416
  function chunks(bytes) {
@@ -1031,7 +1431,7 @@ function chunks(bytes) {
1031
1431
  const body = bytes.subarray(start, start + length);
1032
1432
  if (type === jsonChunk && json === void 0) {
1033
1433
  try {
1034
- json = record(JSON.parse(new TextDecoder().decode(body)));
1434
+ json = record2(JSON.parse(new TextDecoder().decode(body)));
1035
1435
  } catch {
1036
1436
  throw new TileflowGltfError("has an unreadable JSON chunk");
1037
1437
  }
@@ -1041,14 +1441,14 @@ function chunks(bytes) {
1041
1441
  if (!json) throw new TileflowGltfError("has no JSON chunk");
1042
1442
  return { json, bin };
1043
1443
  }
1044
- function meshOf(json, bin, index, world) {
1045
- const mesh = record(array(json.meshes)[index]);
1444
+ function meshOf(json, bin, index, world, physical) {
1445
+ const mesh = record2(array(json.meshes)[index]);
1046
1446
  const normalMatrix = inverseTranspose(world);
1047
- return array(mesh.primitives).map((value) => {
1048
- const primitive = record(value);
1447
+ const result = array(mesh.primitives).map((value) => {
1448
+ const primitive = record2(value);
1049
1449
  if ((primitive.mode ?? triangles) !== triangles)
1050
1450
  throw new TileflowGltfError("draws points or lines; a tree model draws triangles");
1051
- const attributes = record(primitive.attributes);
1451
+ const attributes = record2(primitive.attributes);
1052
1452
  if (typeof attributes.POSITION !== "number")
1053
1453
  throw new TileflowGltfError("has a primitive without positions");
1054
1454
  const raw = accessor(json, bin, attributes.POSITION, 3);
@@ -1080,8 +1480,8 @@ function meshOf(json, bin, index, world) {
1080
1480
  normals.push(x / size, -z / size, y / size);
1081
1481
  }
1082
1482
  }
1083
- const material = typeof primitive.material === "number" ? record(array(json.materials)[primitive.material]) : void 0;
1084
- const base = array(record(material?.pbrMetallicRoughness).baseColorFactor).map(Number);
1483
+ const material = typeof primitive.material === "number" ? record2(array(json.materials)[primitive.material]) : void 0;
1484
+ const base = array(record2(material?.pbrMetallicRoughness).baseColorFactor).map(Number);
1085
1485
  const factor = base.length >= 3 && base.every(Number.isFinite) ? base : [1, 1, 1];
1086
1486
  const colours = [];
1087
1487
  const vertexColours = typeof attributes.COLOR_0 === "number" ? accessorWithSize(json, bin, attributes.COLOR_0) : void 0;
@@ -1098,12 +1498,37 @@ function meshOf(json, bin, index, world) {
1098
1498
  [indices[at + 1], indices[at + 2]] = [indices[at + 2], indices[at + 1]];
1099
1499
  return {
1100
1500
  material: typeof material?.name === "string" ? material.name : void 0,
1501
+ ...record2(record2(material?.extras).tileflow).lightingRole === void 0 ? {} : {
1502
+ lightingRole: record2(record2(material?.extras).tileflow).lightingRole
1503
+ },
1101
1504
  positions,
1102
1505
  normals,
1103
1506
  colours,
1104
- indices
1507
+ indices,
1508
+ ...physical ? { surface: physical(primitive, count, indices.length) } : {}
1105
1509
  };
1106
1510
  });
1511
+ if (physical) {
1512
+ let low = Infinity, high = -Infinity;
1513
+ for (const part of result)
1514
+ if (!/^footprint(?:\.\d+)?$/i.test(part.material ?? ""))
1515
+ for (let at = 2; at < part.positions.length; at += 3) {
1516
+ low = Math.min(low, part.positions[at]);
1517
+ high = Math.max(high, part.positions[at]);
1518
+ }
1519
+ if (Number.isFinite(low)) {
1520
+ for (const part of result)
1521
+ if (part.surface)
1522
+ part.surface = {
1523
+ ...part.surface,
1524
+ ranges: part.surface.ranges.map((range) => ({
1525
+ ...range,
1526
+ emissiveBounds: [low, high]
1527
+ }))
1528
+ };
1529
+ }
1530
+ }
1531
+ return result;
1107
1532
  }
1108
1533
  function accessor(json, bin, index, size) {
1109
1534
  const read = accessorWithSize(json, bin, index);
@@ -1112,7 +1537,7 @@ function accessor(json, bin, index, size) {
1112
1537
  return read.values;
1113
1538
  }
1114
1539
  function accessorWithSize(json, bin, index) {
1115
- const accessor2 = record(array(json.accessors)[index]);
1540
+ const accessor2 = record2(array(json.accessors)[index]);
1116
1541
  if (accessor2.sparse !== void 0)
1117
1542
  throw new TileflowGltfError("uses sparse accessors; export without them");
1118
1543
  const size = typeSizes[String(accessor2.type)];
@@ -1123,7 +1548,7 @@ function accessorWithSize(json, bin, index) {
1123
1548
  throw new TileflowGltfError("has an accessor Tileflow cannot read");
1124
1549
  if (typeof accessor2.bufferView !== "number")
1125
1550
  return { values: new Array(count * size).fill(0), size };
1126
- const view = record(array(json.bufferViews)[accessor2.bufferView]);
1551
+ const view = record2(array(json.bufferViews)[accessor2.bufferView]);
1127
1552
  if (!bin || Number(view.buffer ?? 0) !== 0)
1128
1553
  throw new TileflowGltfError("has geometry outside its binary chunk");
1129
1554
  const stride = Number(view.byteStride ?? 0) || size * bytesEach;
@@ -1245,15 +1670,15 @@ function inverseTranspose(m) {
1245
1670
  1
1246
1671
  ];
1247
1672
  }
1248
- function record(value) {
1673
+ function record2(value) {
1249
1674
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1250
1675
  }
1251
1676
  function array(value) {
1252
1677
  return Array.isArray(value) ? value : [];
1253
1678
  }
1254
1679
  function vector(value, fallback) {
1255
- const list = array(value).map(Number);
1256
- return list.length === fallback.length && list.every(Number.isFinite) ? list : fallback;
1680
+ const list2 = array(value).map(Number);
1681
+ return list2.length === fallback.length && list2.every(Number.isFinite) ? list2 : fallback;
1257
1682
  }
1258
1683
 
1259
1684
  // src/building-model-mesh.ts
@@ -1273,25 +1698,33 @@ function tileflowBuildingModelDeclaredFootprint(primitives) {
1273
1698
  }
1274
1699
  return indices.length > 0 ? { positions, indices } : void 0;
1275
1700
  }
1276
- function prepareTileflowBuildingModelMesh(bytes) {
1277
- const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes) : bytes;
1701
+ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {}) {
1702
+ const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes, { materials: true }) : bytes;
1703
+ const drawn = primitives.filter((primitive) => !isFootprint(primitive));
1704
+ const surface = buildingModelSurface(drawn);
1278
1705
  const positions = [];
1279
1706
  const normals = [];
1280
1707
  const parts = [];
1708
+ const lightingRoles = [];
1281
1709
  const luminance = [];
1282
1710
  const indices = [];
1283
- for (const primitive of primitives) {
1284
- if (isFootprint(primitive)) continue;
1711
+ for (const primitive of drawn) {
1285
1712
  const part = partOf(primitive.material);
1286
1713
  if (part === void 0)
1287
1714
  throw new TileflowGltfError(
1288
1715
  primitive.material === void 0 ? "has a mesh without a material; name each material surface, secondary, glass, accent or roof" : `has a material "${primitive.material}" that names no part of a building; name each material surface, secondary, glass, accent or roof`
1289
1716
  );
1290
1717
  const offset = positions.length / 3;
1718
+ const role = primitive.lightingRole === void 0 ? tileflowModelDefaultLightingRoles[part] : typeof primitive.lightingRole === "string" && Object.hasOwn(tileflowModelLightingRoles, primitive.lightingRole) ? tileflowModelLightingRoles[primitive.lightingRole] : void 0;
1719
+ if (role === void 0)
1720
+ throw new TileflowGltfError(
1721
+ "has an unknown lighting role; use unclassified, wall, door, roof, window, lamp or logo"
1722
+ );
1291
1723
  const count2 = primitive.positions.length / 3;
1292
1724
  positions.push(...primitive.positions);
1293
1725
  for (let vertex = 0; vertex < count2; vertex += 1) {
1294
1726
  parts.push(part);
1727
+ lightingRoles.push(role);
1295
1728
  normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
1296
1729
  const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
1297
1730
  luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
@@ -1347,8 +1780,10 @@ function prepareTileflowBuildingModelMesh(bytes) {
1347
1780
  positions,
1348
1781
  normals: outNormals,
1349
1782
  parts,
1783
+ lightingRoles,
1350
1784
  shades,
1351
- occlusion: new Array(count).fill(1),
1785
+ ...surface ? { surface } : {},
1786
+ occlusion: ambientOcclusion ? buildingModelAmbientVisibility({ positions, normals: outNormals, indices }) : new Array(count).fill(1),
1352
1787
  indices
1353
1788
  };
1354
1789
  }
@@ -1426,9 +1861,9 @@ function tileflowBuildingModelFootprint(mesh) {
1426
1861
  const next = /* @__PURE__ */ new Map();
1427
1862
  const add = (from, to) => {
1428
1863
  const key = `${from[0]},${from[1]}`;
1429
- const list = next.get(key) ?? [];
1430
- list.push(to);
1431
- next.set(key, list);
1864
+ const list2 = next.get(key) ?? [];
1865
+ list2.push(to);
1866
+ next.set(key, list2);
1432
1867
  };
1433
1868
  for (let y = 0; y < rows; y += 1)
1434
1869
  for (let x = 0; x < columns; x += 1) {
@@ -1520,8 +1955,8 @@ function ringArea(ring) {
1520
1955
  function edge(a, b, p) {
1521
1956
  return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1522
1957
  }
1523
- function point(list, vertex) {
1524
- return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
1958
+ function point(list2, vertex) {
1959
+ return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
1525
1960
  }
1526
1961
  function sub(a, b) {
1527
1962
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
@@ -1657,8 +2092,8 @@ function partOf2(material) {
1657
2092
  function isCrown(part) {
1658
2093
  return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
1659
2094
  }
1660
- function point2(list, vertex) {
1661
- return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
2095
+ function point2(list2, vertex) {
2096
+ return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
1662
2097
  }
1663
2098
  function sub2(a, b) {
1664
2099
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
@@ -1727,14 +2162,16 @@ async function packageTileflowLandmarkTile(landmarks) {
1727
2162
  }
1728
2163
  await MeshoptEncoder.ready;
1729
2164
  const bytes = encodeTileflowModels(models, MeshoptEncoder);
1730
- return { x: tile[0], y: tile[1], bytes, sha256: sha256Of(bytes) };
2165
+ const footprints = readTileflowLandmarkTileFootprints(bytes);
2166
+ if (!footprints) throw new Error("The packaged landmark tile cannot be read back.");
2167
+ return { x: tile[0], y: tile[1], bytes, sha256: sha256Of(bytes), landmarks: footprints };
1731
2168
  }
1732
2169
  function encodeTileflowLandmarkRegion(region) {
1733
2170
  const bytes = serializeTileflowLandmarkRegion(region);
1734
2171
  return { bytes, sha256: sha256Of(bytes) };
1735
2172
  }
1736
2173
  function buildingModelEntry(id, levels) {
1737
- const primitives = readTileflowGltfTriangles(levels.near);
2174
+ const primitives = readTileflowGltfTriangles(levels.near, { materials: true });
1738
2175
  if (!isTileflowBuildingModel(primitives))
1739
2176
  throw new TileflowGltfError(
1740
2177
  "is not a building model; name each material surface, secondary, glass, accent or roof"
@@ -1750,34 +2187,6 @@ function buildingModelEntry(id, levels) {
1750
2187
  )
1751
2188
  };
1752
2189
  }
1753
- async function createTileflowModelBundle(buildings) {
1754
- const entries = Object.entries(buildings).sort(([a], [b]) => compareCodeUnits4(a, b));
1755
- if (entries.length === 0) return void 0;
1756
- const totalBytes = entries.reduce((total, [, model]) => total + model.bytes.byteLength, 0);
1757
- if (totalBytes > tileflowModelBundleLimits.maxPackageBytes)
1758
- throw new RangeError(
1759
- `The map's building models together are ${totalBytes} bytes once prepared; a hosted map takes at most ${tileflowModelBundleLimits.maxPackageBytes}. Simplify or remove models.`
1760
- );
1761
- const manifest = {
1762
- format: "tileflow-model-bundle-v1",
1763
- models: entries.map(([id, model]) => ({
1764
- byteLength: model.bytes.byteLength,
1765
- contentType: tileflowModelContentType,
1766
- file: model.fileName,
1767
- id,
1768
- sha256: model.sha256
1769
- }))
1770
- };
1771
- return {
1772
- contentHash: await hashTileflowModelBundleManifest(manifest),
1773
- manifest,
1774
- files: entries.map(([, model]) => ({
1775
- contentType: tileflowModelContentType,
1776
- fileName: model.fileName,
1777
- source: model.bytes
1778
- }))
1779
- };
1780
- }
1781
2190
  function sha256Of(bytes) {
1782
2191
  return createHash("sha256").update(bytes).digest("hex");
1783
2192
  }
@@ -1858,6 +2267,7 @@ async function prepareTileflowMapTreeModels(directories, options) {
1858
2267
  }
1859
2268
  if (isTileflowBuildingModel(primitives)) {
1860
2269
  const files2 = { near: nearBytes };
2270
+ const sources = [];
1861
2271
  let failing = entry.near;
1862
2272
  try {
1863
2273
  for (const level of ["near", "mid", "far"]) {
@@ -1866,15 +2276,18 @@ async function prepareTileflowMapTreeModels(directories, options) {
1866
2276
  failing = file;
1867
2277
  const bytes = level === "near" ? nearBytes : new Uint8Array(await readFile(file.path));
1868
2278
  files2[level] = bytes;
1869
- if (level !== "near") prepareTileflowBuildingModelMesh(bytes);
1870
- identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
2279
+ if (level !== "near") prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion: false });
2280
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
2281
+ identities.push({ id, level, sha256 });
2282
+ sources.push({ level, sha256, byteLength: bytes.byteLength });
1871
2283
  }
1872
2284
  failing = entry.near;
1873
2285
  const prepared2 = await prepareTileflowBuildingModel(id, files2);
1874
2286
  buildings[id] = {
1875
- fileName: tileflowModelBundleFileName(id, prepared2.sha256),
2287
+ fileName: tileflowModelFileName(id, prepared2.sha256),
1876
2288
  bytes: prepared2.bytes,
1877
- sha256: prepared2.sha256
2289
+ sha256: prepared2.sha256,
2290
+ sources
1878
2291
  };
1879
2292
  } catch (error) {
1880
2293
  issues.push({
@@ -2215,7 +2628,7 @@ async function prepareTileflowCatalogIcons(project, options) {
2215
2628
  const buildingModels = {};
2216
2629
  for (const [id, model] of Object.entries(models.buildings)) {
2217
2630
  const fileName = `models/${mapName}/${model.fileName}`;
2218
- assets.push({ contentType: tileflowModelContentType2, fileName, source: model.bytes });
2631
+ assets.push({ contentType: tileflowModelContentType, fileName, source: model.bytes });
2219
2632
  buildingModels[id] = joinUrl(options.assetBaseUrl, fileName);
2220
2633
  }
2221
2634
  mapAssets[mapName] = {
@@ -2833,7 +3246,6 @@ export {
2833
3246
  prepareTileflowBuildingModel,
2834
3247
  packageTileflowLandmarkTile,
2835
3248
  encodeTileflowLandmarkRegion,
2836
- createTileflowModelBundle,
2837
3249
  prepareTileflowMapTreeModels,
2838
3250
  renderTileflowIconAtlasPreview,
2839
3251
  TileflowIconCompilationError,