@tileflow/dev 0.1.0-alpha.37 → 0.1.0-alpha.39

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  TileflowAssetDirectoryError,
3
3
  resolveTileflowAssetDirectories
4
- } from "./chunk-7C4F6GAX.js";
4
+ } from "./chunk-RTU7TD4M.js";
5
5
 
6
6
  // src/icons.ts
7
7
  import { readdir as readdir3, readFile as readFile2, realpath as realpath3, stat } from "fs/promises";
@@ -958,82 +958,38 @@ import {
958
958
 
959
959
  // src/building-model-mesh.ts
960
960
  import {
961
- tileflowModelDefaultLightingRoles,
961
+ isTileflowModelPartName,
962
+ tileflowModelDefaultLightingRole,
962
963
  tileflowModelLightingRoles,
963
964
  tileflowModelLimits,
964
- tileflowModelParts
965
+ tileflowModelSurfaceLimits as tileflowModelSurfaceLimits2,
966
+ tileflowModelTextureChannels as tileflowModelTextureChannels2
965
967
  } from "@tileflow/core";
966
968
 
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 });
969
+ // src/building-model-occlusion.ts
970
+ function buildingModelAmbientVisibility(mesh, options = {}) {
971
+ const sampler = buildingModelAmbientSampler(mesh, options);
972
+ const { positions, normals } = mesh;
973
+ const values = new Array(positions.length / 3);
974
+ const cache = /* @__PURE__ */ new Map();
975
+ for (let vertex = 0; vertex < values.length; vertex += 1) {
976
+ const at = vertex * 3;
977
+ const point3 = [positions[at], positions[at + 1], positions[at + 2]];
978
+ const normal = [normals[at], normals[at + 1], normals[at + 2]];
979
+ const key = [...point3, ...normal].map((v) => Math.round(v * 1e5)).join(",");
980
+ let value = cache.get(key);
981
+ if (value === void 0) {
982
+ value = sampler.sky(point3, normal);
983
+ cache.set(key, value);
1024
984
  }
1025
- indexCount += primitive.indices.length;
985
+ values[vertex] = value;
1026
986
  }
1027
- const result = { attributes, materials, textures, ranges, ...emitters.length ? { emitters } : {} };
1028
- checkTileflowModelSurface(result, attributes.length / 8, indexCount);
1029
- return result;
987
+ return values;
1030
988
  }
1031
-
1032
- // src/building-model-occlusion.ts
1033
- function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {}) {
989
+ function buildingModelAmbientSampler(mesh, { radius = 2, samples = 48 } = {}) {
1034
990
  if (!(radius > 0 && Number.isFinite(radius)) || !Number.isInteger(samples) || samples < 1 || samples > 256)
1035
991
  throw new RangeError("Model ambient visibility needs a positive radius and 1 to 256 samples.");
1036
- const { positions, normals, indices } = mesh;
992
+ const { positions, indices } = mesh;
1037
993
  const count = indices.length / 3;
1038
994
  const triangles2 = new Float64Array(count * 9);
1039
995
  const bounds = new Float64Array(count * 6);
@@ -1077,17 +1033,26 @@ function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {})
1077
1033
  }
1078
1034
  return index;
1079
1035
  };
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
- });
1036
+ if (count > 0) build(0, count);
1037
+ const hemispheres = /* @__PURE__ */ new Map();
1038
+ const hemisphere = (count2) => {
1039
+ let pattern = hemispheres.get(count2);
1040
+ if (!pattern) {
1041
+ pattern = Array.from({ length: count2 }, (_, i) => {
1042
+ const r = Math.sqrt((i + 0.5) / count2), angle = i * Math.PI * (3 - Math.sqrt(5));
1043
+ return [r * Math.cos(angle), r * Math.sin(angle), Math.sqrt(1 - r * r)];
1044
+ });
1045
+ hemispheres.set(count2, pattern);
1046
+ }
1047
+ return pattern;
1048
+ };
1085
1049
  const bias = Math.min(2e-3, radius * 1e-3);
1086
1050
  const stack = new Int32Array(64);
1087
1051
  const origin = [0, 0, 0];
1088
1052
  const direction = [0, 0, 0];
1089
1053
  const nearest = () => {
1090
1054
  let best = radius, pending = 1;
1055
+ if (nodes.length === 0) return best;
1091
1056
  stack[0] = 0;
1092
1057
  while (pending > 0) {
1093
1058
  const node = nodes[stack[--pending]];
@@ -1120,43 +1085,40 @@ function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {})
1120
1085
  }
1121
1086
  return best;
1122
1087
  };
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;
1088
+ return {
1089
+ radius,
1090
+ sky(point3, normal, rays = samples, turn = 0) {
1091
+ const [nx, ny, nz] = normal;
1092
+ const flat = Math.abs(nz) < 0.9 ? [-ny, nx, 0] : [nz, 0, -nx];
1093
+ const length = Math.hypot(...flat);
1094
+ if (length === 0) return 1;
1095
+ const [c, s] = [Math.cos(turn), Math.sin(turn)];
1096
+ const base = flat.map((value) => value / length);
1097
+ const cross4 = [
1098
+ ny * base[2] - nz * base[1],
1099
+ nz * base[0] - nx * base[2],
1100
+ nx * base[1] - ny * base[0]
1101
+ ];
1102
+ const tangent = base.map((value, axis) => value * c + cross4[axis] * s);
1103
+ const bitangent = cross4.map((value, axis) => value * c - base[axis] * s);
1104
+ for (let axis = 0; axis < 3; axis += 1) origin[axis] = point3[axis] + normal[axis] * bias;
1105
+ let visibility = 0;
1106
+ const pattern = hemisphere(rays);
1107
+ for (const sample of pattern) {
1108
+ for (let axis = 0; axis < 3; axis += 1)
1109
+ direction[axis] = sample[0] * tangent[axis] + sample[1] * bitangent[axis] + sample[2] * normal[axis];
1110
+ visibility += nearest() / radius;
1111
+ }
1112
+ return visibility / pattern.length;
1113
+ },
1114
+ along(point3, towards) {
1115
+ for (let axis = 0; axis < 3; axis += 1) {
1116
+ origin[axis] = point3[axis] + towards[axis] * bias;
1117
+ direction[axis] = towards[axis];
1118
+ }
1119
+ return nearest();
1155
1120
  }
1156
- values[vertex] = visibility / samples;
1157
- cache.set(key, values[vertex]);
1158
- }
1159
- return values;
1121
+ };
1160
1122
  }
1161
1123
  function hitsBox(box, origin, direction, limit) {
1162
1124
  let low = 0, high = limit;
@@ -1174,6 +1136,460 @@ function hitsBox(box, origin, direction, limit) {
1174
1136
  return true;
1175
1137
  }
1176
1138
 
1139
+ // src/building-model-lightmap.ts
1140
+ var paletteSide = 16;
1141
+ var maximumSide = 4096;
1142
+ function bakeBuildingModelLightmap(mesh, { texels: budget, radius = 2, ...options }) {
1143
+ if (options.points !== void 0 && !(options.points >= 1 && options.points <= 4 && Number.isInteger(options.points)))
1144
+ throw new RangeError("A lightmap samples one to four points along each side of a texel.");
1145
+ const { positions, indices } = mesh;
1146
+ const point3 = (vertex) => [
1147
+ positions[vertex * 3],
1148
+ positions[vertex * 3 + 1],
1149
+ positions[vertex * 3 + 2]
1150
+ ];
1151
+ const charts = chartsOf(indices, point3);
1152
+ const palette = {
1153
+ triangles: [],
1154
+ normal: [0, 0, 1],
1155
+ u: [1, 0, 0],
1156
+ v: [0, 1, 0],
1157
+ min: [0, 0],
1158
+ extent: [0, 0],
1159
+ scale: [0, 0],
1160
+ w: 0,
1161
+ h: 0,
1162
+ x: 0,
1163
+ y: 0,
1164
+ palette: true
1165
+ };
1166
+ charts.push(palette);
1167
+ let density = densityFor(charts, budget);
1168
+ if (density === void 0) return void 0;
1169
+ let layout = pack(charts, density);
1170
+ for (let attempt = 0; attempt < 12 && !(layout && layout.width * layout.height <= budget); attempt++) {
1171
+ density *= 0.9;
1172
+ layout = pack(charts, density);
1173
+ }
1174
+ if (!layout || layout.width * layout.height > budget) return void 0;
1175
+ const { width, height } = layout;
1176
+ const points = options.points ?? Math.min(4, Math.max(2, Math.ceil(1 / (density * 0.35))));
1177
+ const rays = options.rays ?? Math.max(4, Math.round(48 / (points * points)));
1178
+ const sampler = buildingModelAmbientSampler(mesh, { radius });
1179
+ const sums = new Float64Array(width * height);
1180
+ const counts = new Uint16Array(width * height);
1181
+ const taken = new Uint16Array(width * height);
1182
+ const hidden = Math.min(0.05, radius * 0.025);
1183
+ for (const chart of charts) {
1184
+ const local2 = (vertex) => {
1185
+ const p = point3(vertex);
1186
+ return [
1187
+ (dot(chart.u, p) - chart.min[0]) * chart.scale[0] + 0.5,
1188
+ (dot(chart.v, p) - chart.min[1]) * chart.scale[1] + 0.5
1189
+ ];
1190
+ };
1191
+ if (chart.tiny || chart.palette) continue;
1192
+ for (const triangle of chart.triangles) {
1193
+ const corners = [0, 1, 2].map((k) => local2(indices[triangle * 3 + k]));
1194
+ const [a, b, c] = corners;
1195
+ const area = cross2(a, b, c);
1196
+ if (Math.abs(area) < 1e-12) continue;
1197
+ const [A, B, C] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1198
+ const rawNormal = crossOf(sub(B, A), sub(C, A));
1199
+ const facing = rawNormal.map((value) => value / (Math.hypot(...rawNormal) || 1));
1200
+ const x0 = Math.max(0, Math.floor(Math.min(a[0], b[0], c[0])));
1201
+ const x1 = Math.min(chart.w - 1, Math.floor(Math.max(a[0], b[0], c[0])));
1202
+ const y0 = Math.max(0, Math.floor(Math.min(a[1], b[1], c[1])));
1203
+ const y1 = Math.min(chart.h - 1, Math.floor(Math.max(a[1], b[1], c[1])));
1204
+ for (let y = y0; y <= y1; y++)
1205
+ for (let x = x0; x <= x1; x++) {
1206
+ const texel = (chart.y + y) * width + chart.x + x;
1207
+ for (let j = 0; j < points; j++)
1208
+ for (let i = 0; i < points; i++) {
1209
+ const bit = 1 << j * points + i;
1210
+ if (taken[texel] & bit) continue;
1211
+ const p = [x + (i + 0.5) / points, y + (j + 0.5) / points];
1212
+ const w0 = cross2(b, c, p) / area, w1 = cross2(c, a, p) / area, w2 = 1 - w0 - w1;
1213
+ if (w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9) continue;
1214
+ taken[texel] |= bit;
1215
+ const world = [0, 1, 2].map((axis) => w0 * A[axis] + w1 * B[axis] + w2 * C[axis]);
1216
+ if (sampler.along(world, facing) < hidden) continue;
1217
+ const turn = ((j * points + i) * 2.399963 + (x * 0.618034 + y * 0.381966) * 6.283185) % 6.283185;
1218
+ sums[texel] += sampler.sky(world, facing, rays, turn);
1219
+ counts[texel] += 1;
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ const values = new Float32Array(width * height).fill(-1);
1225
+ for (let texel = 0; texel < values.length; texel++)
1226
+ if (counts[texel]) values[texel] = sums[texel] / counts[texel];
1227
+ for (let level = 0; level < paletteSide * paletteSide; level++) {
1228
+ const x = palette.x + level % paletteSide, y = palette.y + Math.floor(level / paletteSide);
1229
+ values[y * width + x] = level / 255;
1230
+ }
1231
+ const levels = /* @__PURE__ */ new Map();
1232
+ for (const chart of charts) {
1233
+ if (!chart.tiny) continue;
1234
+ let sum = 0, weight = 0;
1235
+ for (const triangle of chart.triangles) {
1236
+ const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1237
+ const raw = crossOf(sub(b, a), sub(c, a));
1238
+ const area = Math.hypot(...raw) / 2;
1239
+ if (!(area > 0)) continue;
1240
+ const facing = raw.map((value) => value / (2 * area));
1241
+ const centre = [0, 1, 2].map((axis) => (a[axis] + b[axis] + c[axis]) / 3);
1242
+ if (sampler.along(centre, facing) < hidden) continue;
1243
+ sum += area * sampler.sky(centre, facing, rays * points * points);
1244
+ weight += area;
1245
+ }
1246
+ levels.set(chart, Math.round((weight > 0 ? sum / weight : 1) * 255));
1247
+ }
1248
+ for (let pass = 0; pass < 3; pass++) {
1249
+ const previous = values.slice();
1250
+ for (let y = 0; y < height; y++)
1251
+ for (let x = 0; x < width; x++) {
1252
+ if (previous[y * width + x] >= 0) continue;
1253
+ let sum = 0, count = 0;
1254
+ for (const [dx, dy] of [
1255
+ [1, 0],
1256
+ [-1, 0],
1257
+ [0, 1],
1258
+ [0, -1]
1259
+ ]) {
1260
+ const xx = x + dx, yy = y + dy;
1261
+ if (xx < 0 || yy < 0 || xx >= width || yy >= height) continue;
1262
+ const value = previous[yy * width + xx];
1263
+ if (value >= 0) {
1264
+ sum += value;
1265
+ count++;
1266
+ }
1267
+ }
1268
+ if (count) values[y * width + x] = sum / count;
1269
+ }
1270
+ }
1271
+ const bytes = new Uint8Array(width * height);
1272
+ for (let texel = 0; texel < bytes.length; texel++)
1273
+ bytes[texel] = Math.round(
1274
+ Math.min(1, Math.max(0, values[texel] < 0 ? 1 : values[texel])) * 255
1275
+ );
1276
+ const sources = [];
1277
+ const uvs = [];
1278
+ const out = new Array(indices.length);
1279
+ for (const chart of charts) {
1280
+ const own = /* @__PURE__ */ new Map();
1281
+ for (const triangle of chart.triangles)
1282
+ for (let k = 0; k < 3; k++) {
1283
+ const vertex = indices[triangle * 3 + k];
1284
+ let index = own.get(vertex);
1285
+ if (index === void 0) {
1286
+ index = sources.length;
1287
+ own.set(vertex, index);
1288
+ sources.push(vertex);
1289
+ const level = levels.get(chart);
1290
+ if (level !== void 0)
1291
+ uvs.push(
1292
+ (palette.x + level % paletteSide + 0.5) / width,
1293
+ (palette.y + Math.floor(level / paletteSide) + 0.5) / height
1294
+ );
1295
+ else {
1296
+ const p = point3(vertex);
1297
+ uvs.push(
1298
+ (chart.x + 0.5 + (dot(chart.u, p) - chart.min[0]) * chart.scale[0]) / width,
1299
+ (chart.y + 0.5 + (dot(chart.v, p) - chart.min[1]) * chart.scale[1]) / height
1300
+ );
1301
+ }
1302
+ }
1303
+ out[triangle * 3 + k] = index;
1304
+ }
1305
+ }
1306
+ for (let at = 0; at < uvs.length; at++) uvs[at] = Math.round(uvs[at] * 65536) / 65536;
1307
+ return {
1308
+ sources,
1309
+ indices: out,
1310
+ uvs,
1311
+ width,
1312
+ height,
1313
+ density,
1314
+ texels: bytes
1315
+ };
1316
+ }
1317
+ var chartCone = Math.cos(20 * Math.PI / 180);
1318
+ function chartsOf(indices, point3) {
1319
+ const key = (p) => p.map((value) => Math.round(value * 1e4)).join(",");
1320
+ const count = indices.length / 3;
1321
+ const normals = [];
1322
+ const areas = [];
1323
+ const edges = /* @__PURE__ */ new Map();
1324
+ const edgeKeys = [];
1325
+ for (let triangle = 0; triangle < count; triangle++) {
1326
+ const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1327
+ const normal = crossOf(sub(b, a), sub(c, a));
1328
+ const length = Math.hypot(...normal);
1329
+ areas.push(length / 2);
1330
+ normals.push(length > 1e-12 ? normal.map((value) => value / length) : [0, 0, 1]);
1331
+ for (const [from, to] of [
1332
+ [a, b],
1333
+ [b, c],
1334
+ [c, a]
1335
+ ]) {
1336
+ const [u, v] = [key(from), key(to)];
1337
+ const edge2 = u < v ? `${u}|${v}` : `${v}|${u}`;
1338
+ edgeKeys.push(edge2);
1339
+ const users = edges.get(edge2);
1340
+ if (users) users.push(triangle);
1341
+ else edges.set(edge2, [triangle]);
1342
+ }
1343
+ }
1344
+ const owner = new Int32Array(count).fill(-1);
1345
+ const order = Array.from({ length: count }, (_, i) => i).sort(
1346
+ (a, b) => areas[b] - areas[a] || a - b
1347
+ );
1348
+ const charts = [];
1349
+ for (const seed of order) {
1350
+ if (owner[seed] !== -1) continue;
1351
+ const normal = normals[seed];
1352
+ const group = [seed];
1353
+ owner[seed] = seed;
1354
+ for (let at = 0; at < group.length; at++)
1355
+ for (let e = 0; e < 3; e++)
1356
+ for (const next of edges.get(edgeKeys[group[at] * 3 + e]))
1357
+ if (owner[next] === -1 && dot(normals[next], normal) >= chartCone) {
1358
+ owner[next] = seed;
1359
+ group.push(next);
1360
+ }
1361
+ charts.push(...flatCharts(group, normal, indices, point3));
1362
+ }
1363
+ return charts;
1364
+ }
1365
+ function flatCharts(group, normal, indices, point3, depth = 0) {
1366
+ let longest = 0;
1367
+ let u = Math.abs(normal[2]) < 0.9 ? [-normal[1], normal[0], 0] : [normal[2], 0, -normal[0]];
1368
+ let area = 0;
1369
+ for (const triangle of group) {
1370
+ const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1371
+ area += Math.hypot(...crossOf(sub(b, a), sub(c, a))) / 2;
1372
+ for (const [from, to] of [
1373
+ [a, b],
1374
+ [b, c],
1375
+ [c, a]
1376
+ ]) {
1377
+ const d = sub(to, from);
1378
+ const along = d.map((value, axis) => value - normal[axis] * dot(d, normal));
1379
+ const length = Math.hypot(...along);
1380
+ if (length > longest) {
1381
+ longest = length;
1382
+ u = along;
1383
+ }
1384
+ }
1385
+ }
1386
+ const size = Math.hypot(...u) || 1;
1387
+ u = u.map((value) => value / size);
1388
+ const v = crossOf(normal, u);
1389
+ let [minU, minV, maxU, maxV] = [Infinity, Infinity, -Infinity, -Infinity];
1390
+ for (const triangle of group)
1391
+ for (let k = 0; k < 3; k++) {
1392
+ const p = point3(indices[triangle * 3 + k]);
1393
+ minU = Math.min(minU, dot(u, p));
1394
+ maxU = Math.max(maxU, dot(u, p));
1395
+ minV = Math.min(minV, dot(v, p));
1396
+ maxV = Math.max(maxV, dot(v, p));
1397
+ }
1398
+ const flip = maxV - minV > maxU - minU;
1399
+ if (group.length > 1 && depth < 16 && area < 0.5 * (maxU - minU) * (maxV - minV)) {
1400
+ const axis = flip ? v : u;
1401
+ const centre = (triangle) => [0, 1, 2].reduce((sum, k) => sum + dot(axis, point3(indices[triangle * 3 + k])), 0) / 3;
1402
+ const sorted = [...group].sort((a, b) => centre(a) - centre(b) || a - b);
1403
+ const half = sorted.length >> 1;
1404
+ return [
1405
+ ...flatCharts(sorted.slice(0, half), normal, indices, point3, depth + 1),
1406
+ ...flatCharts(sorted.slice(half), normal, indices, point3, depth + 1)
1407
+ ];
1408
+ }
1409
+ return [
1410
+ {
1411
+ triangles: group,
1412
+ normal,
1413
+ u: flip ? v : u,
1414
+ v: flip ? u : v,
1415
+ min: flip ? [minV, minU] : [minU, minV],
1416
+ extent: flip ? [maxV - minV, maxU - minU] : [maxU - minU, maxV - minV],
1417
+ scale: [0, 0],
1418
+ w: 0,
1419
+ h: 0,
1420
+ x: 0,
1421
+ y: 0
1422
+ }
1423
+ ];
1424
+ }
1425
+ function sizeAt(chart, density) {
1426
+ if (chart.palette) return [paletteSide, paletteSide];
1427
+ chart.tiny = chart.extent[0] * density < 2 && chart.extent[1] * density < 2;
1428
+ if (chart.tiny) return [0, 0];
1429
+ const size = chart.extent.map((extent) => Math.max(2, Math.round(extent * density) + 1));
1430
+ chart.scale = [0, 1].map(
1431
+ (axis) => chart.extent[axis] > 0 ? (size[axis] - 1) / chart.extent[axis] : 0
1432
+ );
1433
+ return size;
1434
+ }
1435
+ function densityFor(charts, budget) {
1436
+ const area = (density) => charts.reduce((sum, chart) => {
1437
+ const [w, h] = sizeAt(chart, density);
1438
+ return sum + w * h;
1439
+ }, 0);
1440
+ let low = 1 / 16, high = 64;
1441
+ if (area(low) / 0.8 > budget) return void 0;
1442
+ for (let step = 0; step < 24; step++) {
1443
+ const middle = Math.sqrt(low * high);
1444
+ if (area(middle) / 0.8 <= budget) low = middle;
1445
+ else high = middle;
1446
+ }
1447
+ return low;
1448
+ }
1449
+ function pack(charts, density) {
1450
+ for (const chart of charts) [chart.w, chart.h] = sizeAt(chart, density);
1451
+ const order = charts.map((_, i) => i).sort((a, b) => charts[b].h - charts[a].h || charts[b].w - charts[a].w || a - b);
1452
+ let best;
1453
+ for (let width = 64; width <= maximumSide; width *= 2) {
1454
+ let line = [{ x: 0, y: 0, w: width }];
1455
+ const places = new Array(charts.length);
1456
+ let fits = true;
1457
+ for (const index of order) {
1458
+ const chart = charts[index];
1459
+ if (chart.w === 0) {
1460
+ places[index] = [0, 0];
1461
+ continue;
1462
+ }
1463
+ if (chart.w > width) {
1464
+ fits = false;
1465
+ break;
1466
+ }
1467
+ let place;
1468
+ for (let i = 0; i < line.length; i++) {
1469
+ const x = line[i].x;
1470
+ if (x + chart.w > width) break;
1471
+ let y = 0, covered = 0;
1472
+ for (let j = i; j < line.length && covered < chart.w; j++) {
1473
+ y = Math.max(y, line[j].y);
1474
+ covered += line[j].w;
1475
+ }
1476
+ if (covered < chart.w) continue;
1477
+ if (!place || y < place.y) place = { x, y };
1478
+ }
1479
+ if (!place || place.y + chart.h > maximumSide) {
1480
+ fits = false;
1481
+ break;
1482
+ }
1483
+ places[index] = [place.x, place.y];
1484
+ const next = [];
1485
+ for (const segment of line) {
1486
+ const end = segment.x + segment.w;
1487
+ if (end <= place.x || segment.x >= place.x + chart.w) next.push(segment);
1488
+ else {
1489
+ if (segment.x < place.x) next.push({ x: segment.x, y: segment.y, w: place.x - segment.x });
1490
+ if (end > place.x + chart.w)
1491
+ next.push({ x: place.x + chart.w, y: segment.y, w: end - place.x - chart.w });
1492
+ }
1493
+ }
1494
+ next.push({ x: place.x, y: place.y + chart.h, w: chart.w });
1495
+ next.sort((a, b) => a.x - b.x);
1496
+ line = [];
1497
+ for (const segment of next) {
1498
+ const last = line.at(-1);
1499
+ if (last && last.y === segment.y && last.x + last.w === segment.x) last.w += segment.w;
1500
+ else line.push({ ...segment });
1501
+ }
1502
+ }
1503
+ if (!fits) continue;
1504
+ const height = Math.max(...line.map((segment) => segment.y));
1505
+ if (!best || width * height < best.width * best.height) best = { width, height, places };
1506
+ }
1507
+ if (!best) return void 0;
1508
+ charts.forEach((chart, i) => [chart.x, chart.y] = best.places[i]);
1509
+ return { width: best.width, height: best.height };
1510
+ }
1511
+ function dot(a, b) {
1512
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1513
+ }
1514
+ function sub(a, b) {
1515
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1516
+ }
1517
+ function crossOf(a, b) {
1518
+ return [
1519
+ a[1] * b[2] - a[2] * b[1],
1520
+ a[2] * b[0] - a[0] * b[2],
1521
+ a[0] * b[1] - a[1] * b[0]
1522
+ ];
1523
+ }
1524
+ function cross2(a, b, p) {
1525
+ return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1526
+ }
1527
+
1528
+ // src/building-model-materials.ts
1529
+ import {
1530
+ checkTileflowModelSurface,
1531
+ tileflowModelTextureChannels
1532
+ } from "@tileflow/core";
1533
+ function buildingModelSurface(primitives) {
1534
+ if (!primitives.some((primitive) => primitive.surface)) return void 0;
1535
+ const attributes = [];
1536
+ const materials = [];
1537
+ const materialIds = /* @__PURE__ */ new Map();
1538
+ const textures = [];
1539
+ const textureIds = /* @__PURE__ */ new Map();
1540
+ const ranges = [];
1541
+ const emitters = [];
1542
+ let indexCount = 0;
1543
+ for (const primitive of primitives) {
1544
+ const count = primitive.positions.length / 3;
1545
+ const surface = primitive.surface;
1546
+ if (surface) {
1547
+ checkTileflowModelSurface(surface, count, primitive.indices.length);
1548
+ for (const value of surface.attributes) attributes.push(value);
1549
+ emitters.push(...surface.emitters ?? []);
1550
+ } else {
1551
+ for (let vertex = 0; vertex < count; vertex++) attributes.push(0, 0, 0, 0, 1, 1, 1, 1);
1552
+ }
1553
+ const remap = (surface?.materials ?? [{}]).map((material) => {
1554
+ const copy = { ...material };
1555
+ for (const channel of tileflowModelTextureChannels) {
1556
+ const ref = copy[channel];
1557
+ if (!ref) continue;
1558
+ const texture = surface.textures[ref.texture];
1559
+ let id2 = textureIds.get(texture);
1560
+ if (id2 === void 0) {
1561
+ id2 = textures.length;
1562
+ textures.push(texture);
1563
+ textureIds.set(texture, id2);
1564
+ }
1565
+ copy[channel] = { ...ref, texture: id2 };
1566
+ }
1567
+ const key = JSON.stringify(copy);
1568
+ let id = materialIds.get(key);
1569
+ if (id === void 0) {
1570
+ id = materials.length;
1571
+ materials.push(copy);
1572
+ materialIds.set(key, id);
1573
+ }
1574
+ return id;
1575
+ });
1576
+ const sourceRanges = surface?.ranges ?? [
1577
+ { from: 0, count: primitive.indices.length, material: 0 }
1578
+ ];
1579
+ for (const range of sourceRanges) {
1580
+ const material = remap[range.material];
1581
+ const previous = ranges.at(-1);
1582
+ if (previous?.material === material && JSON.stringify(previous.emissiveBounds) === JSON.stringify(range.emissiveBounds))
1583
+ ranges[ranges.length - 1] = { ...previous, count: previous.count + range.count };
1584
+ else ranges.push({ ...range, from: indexCount + range.from, material });
1585
+ }
1586
+ indexCount += primitive.indices.length;
1587
+ }
1588
+ const result = { attributes, materials, textures, ranges, ...emitters.length ? { emitters } : {} };
1589
+ checkTileflowModelSurface(result, attributes.length / 8, indexCount);
1590
+ return result;
1591
+ }
1592
+
1177
1593
  // src/gltf-materials.ts
1178
1594
  import {
1179
1595
  checkTileflowModelSurface as checkTileflowModelSurface2,
@@ -1282,6 +1698,8 @@ function createGltfMaterialReader(json, bin, accessor2) {
1282
1698
  return value;
1283
1699
  };
1284
1700
  const uv0 = read("TEXCOORD_0", [2]), uv1 = read("TEXCOORD_1", [2]);
1701
+ if (uv1?.values.some((value) => !(value >= 0 && value <= 1)))
1702
+ throw new RangeError("TEXCOORD_1 must lie between zero and one.");
1285
1703
  const colour = read("COLOR_0", [3, 4]);
1286
1704
  for (const key of [
1287
1705
  "baseColorTexture",
@@ -1681,41 +2099,486 @@ function vector(value, fallback) {
1681
2099
  return list2.length === fallback.length && list2.every(Number.isFinite) ? list2 : fallback;
1682
2100
  }
1683
2101
 
1684
- // src/building-model-mesh.ts
1685
- var tileflowBuildingModelPartNames = Object.keys(tileflowModelParts);
1686
- function isTileflowBuildingModel(primitives) {
1687
- const drawn = primitives.filter((primitive) => !isFootprint(primitive));
1688
- return drawn.length > 0 && drawn.every((primitive) => partOf(primitive.material) !== void 0);
1689
- }
1690
- function tileflowBuildingModelDeclaredFootprint(primitives) {
1691
- const positions = [];
1692
- const indices = [];
1693
- for (const primitive of primitives) {
1694
- if (!isFootprint(primitive)) continue;
1695
- const offset = positions.length / 3;
1696
- positions.push(...primitive.positions);
1697
- for (const index of primitive.indices) indices.push(offset + index);
2102
+ // src/grey-jpeg.ts
2103
+ var zigzag = [
2104
+ 0,
2105
+ 1,
2106
+ 8,
2107
+ 16,
2108
+ 9,
2109
+ 2,
2110
+ 3,
2111
+ 10,
2112
+ 17,
2113
+ 24,
2114
+ 32,
2115
+ 25,
2116
+ 18,
2117
+ 11,
2118
+ 4,
2119
+ 5,
2120
+ 12,
2121
+ 19,
2122
+ 26,
2123
+ 33,
2124
+ 40,
2125
+ 48,
2126
+ 41,
2127
+ 34,
2128
+ 27,
2129
+ 20,
2130
+ 13,
2131
+ 6,
2132
+ 7,
2133
+ 14,
2134
+ 21,
2135
+ 28,
2136
+ 35,
2137
+ 42,
2138
+ 49,
2139
+ 56,
2140
+ 57,
2141
+ 50,
2142
+ 43,
2143
+ 36,
2144
+ 29,
2145
+ 22,
2146
+ 15,
2147
+ 23,
2148
+ 30,
2149
+ 37,
2150
+ 44,
2151
+ 51,
2152
+ 58,
2153
+ 59,
2154
+ 52,
2155
+ 45,
2156
+ 38,
2157
+ 31,
2158
+ 39,
2159
+ 46,
2160
+ 53,
2161
+ 60,
2162
+ 61,
2163
+ 54,
2164
+ 47,
2165
+ 55,
2166
+ 62,
2167
+ 63
2168
+ ];
2169
+ var luminance = [
2170
+ 16,
2171
+ 11,
2172
+ 10,
2173
+ 16,
2174
+ 24,
2175
+ 40,
2176
+ 51,
2177
+ 61,
2178
+ 12,
2179
+ 12,
2180
+ 14,
2181
+ 19,
2182
+ 26,
2183
+ 58,
2184
+ 60,
2185
+ 55,
2186
+ 14,
2187
+ 13,
2188
+ 16,
2189
+ 24,
2190
+ 40,
2191
+ 57,
2192
+ 69,
2193
+ 56,
2194
+ 14,
2195
+ 17,
2196
+ 22,
2197
+ 29,
2198
+ 51,
2199
+ 87,
2200
+ 80,
2201
+ 62,
2202
+ 18,
2203
+ 22,
2204
+ 37,
2205
+ 56,
2206
+ 68,
2207
+ 109,
2208
+ 103,
2209
+ 77,
2210
+ 24,
2211
+ 35,
2212
+ 55,
2213
+ 64,
2214
+ 81,
2215
+ 104,
2216
+ 113,
2217
+ 92,
2218
+ 49,
2219
+ 64,
2220
+ 78,
2221
+ 87,
2222
+ 103,
2223
+ 121,
2224
+ 120,
2225
+ 101,
2226
+ 72,
2227
+ 92,
2228
+ 95,
2229
+ 98,
2230
+ 112,
2231
+ 100,
2232
+ 103,
2233
+ 99
2234
+ ];
2235
+ function huffman(counts, symbols) {
2236
+ const out = { code: [], size: [] };
2237
+ let code = 0, k = 0;
2238
+ for (let length = 1; length <= 16; length++) {
2239
+ for (let i = 0; i < counts[length - 1]; i++) {
2240
+ out.code[symbols[k]] = code;
2241
+ out.size[symbols[k]] = length;
2242
+ code++;
2243
+ k++;
2244
+ }
2245
+ code <<= 1;
1698
2246
  }
1699
- return indices.length > 0 ? { positions, indices } : void 0;
2247
+ return out;
1700
2248
  }
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);
1705
- const positions = [];
1706
- const normals = [];
1707
- const parts = [];
1708
- const lightingRoles = [];
1709
- const luminance = [];
1710
- const indices = [];
1711
- for (const primitive of drawn) {
1712
- const part = partOf(primitive.material);
1713
- if (part === void 0)
1714
- throw new TileflowGltfError(
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`
1716
- );
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;
2249
+ var basis = Array.from({ length: 64 }, (_, i) => {
2250
+ const u = i >> 3, x = i & 7;
2251
+ return (u === 0 ? Math.SQRT1_2 : 1) * 0.5 * Math.cos((2 * x + 1) * u * Math.PI / 16);
2252
+ });
2253
+ function encodeGreyJpeg(pixels, width, height, quality) {
2254
+ if (!(width > 0 && height > 0 && width <= 65535 && height <= 65535))
2255
+ throw new RangeError("A JPEG is 1 to 65,535 pixels on each side.");
2256
+ if (pixels.length !== width * height) throw new RangeError("One byte a pixel is required.");
2257
+ const q = Math.min(100, Math.max(1, Math.round(quality)));
2258
+ const scale = q < 50 ? 5e3 / q : 200 - 2 * q;
2259
+ const table = luminance.map(
2260
+ (value) => Math.min(255, Math.max(1, Math.floor((value * scale + 50) / 100)))
2261
+ );
2262
+ const symbols = [];
2263
+ const extra = [];
2264
+ const frequencies = [new Array(257).fill(0), new Array(257).fill(0)];
2265
+ const emit = (table2, symbol, value = 0, size = 0) => {
2266
+ symbols.push(table2 << 8 | symbol);
2267
+ extra.push(size ? (value < 0 ? value + (1 << size) - 1 : value) << 5 | size : 0);
2268
+ frequencies[table2][symbol] += 1;
2269
+ };
2270
+ const magnitude = (value) => {
2271
+ let size = 0;
2272
+ for (let v = Math.abs(value); v > 0; v >>= 1) size++;
2273
+ return size;
2274
+ };
2275
+ const block = new Float64Array(64);
2276
+ const row = new Float64Array(64);
2277
+ const coefficients = new Int32Array(64);
2278
+ let previous = 0;
2279
+ for (let by = 0; by < height; by += 8)
2280
+ for (let bx = 0; bx < width; bx += 8) {
2281
+ for (let y = 0; y < 8; y++)
2282
+ for (let x = 0; x < 8; x++)
2283
+ block[y * 8 + x] = pixels[Math.min(height - 1, by + y) * width + Math.min(width - 1, bx + x)] - 128;
2284
+ for (let y = 0; y < 8; y++)
2285
+ for (let u = 0; u < 8; u++) {
2286
+ let sum = 0;
2287
+ for (let x = 0; x < 8; x++) sum += block[y * 8 + x] * basis[u * 8 + x];
2288
+ row[y * 8 + u] = sum;
2289
+ }
2290
+ for (let v = 0; v < 8; v++)
2291
+ for (let u = 0; u < 8; u++) {
2292
+ let sum = 0;
2293
+ for (let y = 0; y < 8; y++) sum += row[y * 8 + u] * basis[v * 8 + y];
2294
+ coefficients[v * 8 + u] = Math.round(sum / table[v * 8 + u]);
2295
+ }
2296
+ const dc = coefficients[0] - previous;
2297
+ previous = coefficients[0];
2298
+ const dcSize = magnitude(dc);
2299
+ emit(0, dcSize, dc, dcSize);
2300
+ let zeros = 0;
2301
+ for (let k = 1; k < 64; k++) {
2302
+ const value = coefficients[zigzag[k]];
2303
+ if (value === 0) {
2304
+ zeros++;
2305
+ continue;
2306
+ }
2307
+ for (; zeros > 15; zeros -= 16) emit(1, 240);
2308
+ const size = magnitude(value);
2309
+ emit(1, zeros << 4 | size, value, size);
2310
+ zeros = 0;
2311
+ }
2312
+ if (zeros > 0) emit(1, 0);
2313
+ }
2314
+ const tables = frequencies.map(optimalTable);
2315
+ const codes = tables.map(({ counts, values }) => huffman(counts, values));
2316
+ const out = [];
2317
+ const word = (value) => out.push(value >> 8 & 255, value & 255);
2318
+ word(65496);
2319
+ word(65499);
2320
+ word(67);
2321
+ out.push(0, ...zigzag.map((at) => table[at]));
2322
+ word(65472);
2323
+ word(11);
2324
+ out.push(8);
2325
+ word(height);
2326
+ word(width);
2327
+ out.push(1, 1, 17, 0);
2328
+ for (const [index, { counts, values }] of tables.entries()) {
2329
+ word(65476);
2330
+ word(3 + 16 + values.length);
2331
+ out.push(index << 4, ...counts, ...values);
2332
+ }
2333
+ word(65498);
2334
+ word(8);
2335
+ out.push(1, 1, 0, 0, 63, 0);
2336
+ let bits = 0, count = 0;
2337
+ const put = (code, size) => {
2338
+ bits = bits << size | code & (1 << size) - 1;
2339
+ count += size;
2340
+ while (count >= 8) {
2341
+ const byte = bits >> count - 8 & 255;
2342
+ out.push(byte);
2343
+ if (byte === 255) out.push(0);
2344
+ count -= 8;
2345
+ }
2346
+ bits &= (1 << count) - 1;
2347
+ };
2348
+ for (let i = 0; i < symbols.length; i++) {
2349
+ const own = codes[symbols[i] >> 8];
2350
+ const symbol = symbols[i] & 255;
2351
+ put(own.code[symbol], own.size[symbol]);
2352
+ const size = extra[i] & 31;
2353
+ if (size) put(extra[i] >> 5, size);
2354
+ }
2355
+ if (count > 0) put(127, 8 - count);
2356
+ word(65497);
2357
+ return Uint8Array.from(out);
2358
+ }
2359
+ function optimalTable(frequencies) {
2360
+ const freq = [...frequencies];
2361
+ freq[256] = 1;
2362
+ const codesize = new Array(257).fill(0);
2363
+ const others = new Array(257).fill(-1);
2364
+ for (; ; ) {
2365
+ let v1 = -1, v2 = -1;
2366
+ for (let i = 0; i <= 256; i++) if (freq[i] > 0 && (v1 < 0 || freq[i] <= freq[v1])) v1 = i;
2367
+ for (let i = 0; i <= 256; i++)
2368
+ if (freq[i] > 0 && i !== v1 && (v2 < 0 || freq[i] <= freq[v2])) v2 = i;
2369
+ if (v2 < 0) break;
2370
+ freq[v1] += freq[v2];
2371
+ freq[v2] = 0;
2372
+ for (codesize[v1]++; others[v1] >= 0; codesize[v1]++) v1 = others[v1];
2373
+ others[v1] = v2;
2374
+ for (codesize[v2]++; others[v2] >= 0; codesize[v2]++) v2 = others[v2];
2375
+ }
2376
+ const bits = new Array(33).fill(0);
2377
+ for (let i = 0; i <= 256; i++) if (codesize[i]) bits[codesize[i]]++;
2378
+ for (let i = 32; i > 16; i--)
2379
+ while (bits[i] > 0) {
2380
+ let j = i - 2;
2381
+ while (bits[j] === 0) j--;
2382
+ bits[i] -= 2;
2383
+ bits[i - 1]++;
2384
+ bits[j + 1] += 2;
2385
+ bits[j]--;
2386
+ }
2387
+ let last = 16;
2388
+ while (bits[last] === 0) last--;
2389
+ bits[last]--;
2390
+ const values = [];
2391
+ for (let length = 1; length <= 32; length++)
2392
+ for (let symbol = 0; symbol < 256; symbol++)
2393
+ if (codesize[symbol] === length) values.push(symbol);
2394
+ return { counts: bits.slice(1, 17), values };
2395
+ }
2396
+
2397
+ // src/tree-model-mesh.ts
2398
+ import {
2399
+ tileflowTreeModelLimits,
2400
+ tileflowTreeParts
2401
+ } from "@tileflow/core";
2402
+ var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
2403
+ function prepareTileflowTreeModelMesh(bytes, level) {
2404
+ const primitives = readTileflowGltfTriangles(bytes);
2405
+ const positions = [];
2406
+ const normals = [];
2407
+ const parts = [];
2408
+ const luminance2 = [];
2409
+ const indices = [];
2410
+ for (const primitive of primitives) {
2411
+ const part = partOf(primitive.material);
2412
+ const offset = positions.length / 3;
2413
+ const count2 = primitive.positions.length / 3;
2414
+ positions.push(...primitive.positions);
2415
+ for (let vertex = 0; vertex < count2; vertex += 1) {
2416
+ parts.push(part);
2417
+ normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
2418
+ const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
2419
+ luminance2.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
2420
+ }
2421
+ for (const index of primitive.indices) indices.push(offset + index);
2422
+ }
2423
+ const count = positions.length / 3;
2424
+ const triangleCount = indices.length / 3;
2425
+ const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
2426
+ if (triangleCount > limit)
2427
+ throw new TileflowGltfError(
2428
+ `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`
2429
+ );
2430
+ if (count > tileflowTreeModelLimits.vertices)
2431
+ throw new TileflowGltfError(
2432
+ `has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
2433
+ );
2434
+ let height = 0;
2435
+ let reach = 0;
2436
+ const crowned = parts.some((part) => isCrown(part));
2437
+ for (let vertex = 0; vertex < count; vertex += 1) {
2438
+ height = Math.max(height, positions[vertex * 3 + 2]);
2439
+ if (!crowned || isCrown(parts[vertex]))
2440
+ reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
2441
+ }
2442
+ if (!(height > 0))
2443
+ throw new TileflowGltfError(
2444
+ "stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
2445
+ );
2446
+ if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
2447
+ const across = 2 * reach;
2448
+ const unit = [];
2449
+ for (let vertex = 0; vertex < count; vertex += 1)
2450
+ unit.push(
2451
+ positions[vertex * 3] / across,
2452
+ positions[vertex * 3 + 1] / across,
2453
+ positions[vertex * 3 + 2] / height
2454
+ );
2455
+ const faceSums = new Array(count * 3).fill(0);
2456
+ for (let at = 0; at + 2 < indices.length; at += 3) {
2457
+ const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
2458
+ const n = cross(sub2(point(unit, b), point(unit, a)), sub2(point(unit, c), point(unit, a)));
2459
+ for (const vertex of [a, b, c])
2460
+ for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
2461
+ }
2462
+ const outNormals = [];
2463
+ for (let vertex = 0; vertex < count; vertex += 1) {
2464
+ const own = normals[vertex];
2465
+ const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
2466
+ const size = Math.hypot(n[0], n[1], n[2]) || 1;
2467
+ outNormals.push(n[0] / size, n[1] / size, n[2] / size);
2468
+ }
2469
+ const shades = new Array(count).fill(0);
2470
+ for (const part of new Set(parts)) {
2471
+ const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
2472
+ const mean = members.reduce((sum, vertex) => sum + luminance2[vertex], 0) / members.length;
2473
+ const spread = Math.max(0.06, mean * 0.6);
2474
+ for (const vertex of members)
2475
+ shades[vertex] = clamp((luminance2[vertex] - mean) / spread, -1, 1);
2476
+ }
2477
+ let crownLow = 1;
2478
+ let crownHigh = 0;
2479
+ for (let vertex = 0; vertex < count; vertex += 1)
2480
+ if (isCrown(parts[vertex])) {
2481
+ crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
2482
+ crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
2483
+ }
2484
+ const occlusion = [];
2485
+ for (let vertex = 0; vertex < count; vertex += 1) {
2486
+ const z = unit[vertex * 3 + 2];
2487
+ if (!isCrown(parts[vertex])) {
2488
+ occlusion.push(z > crownLow ? 0.55 : 0.8);
2489
+ continue;
2490
+ }
2491
+ const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
2492
+ const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
2493
+ occlusion.push(clamp(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
2494
+ }
2495
+ return {
2496
+ // The author's proportions: the trees it stands keep them around the sizes the data gives.
2497
+ proportion: Math.min(3, Math.max(0.1, across / height)),
2498
+ positions: unit,
2499
+ normals: outNormals,
2500
+ parts,
2501
+ shades,
2502
+ occlusion,
2503
+ indices
2504
+ };
2505
+ }
2506
+ function partOf(material) {
2507
+ const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
2508
+ const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
2509
+ if (part) return tileflowTreeParts[part];
2510
+ throw new TileflowGltfError(
2511
+ 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`
2512
+ );
2513
+ }
2514
+ function isCrown(part) {
2515
+ return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
2516
+ }
2517
+ function point(list2, vertex) {
2518
+ return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
2519
+ }
2520
+ function sub2(a, b) {
2521
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2522
+ }
2523
+ function cross(a, b) {
2524
+ return [
2525
+ a[1] * b[2] - a[2] * b[1],
2526
+ a[2] * b[0] - a[0] * b[2],
2527
+ a[0] * b[1] - a[1] * b[0]
2528
+ ];
2529
+ }
2530
+ function clamp(value, low, high) {
2531
+ return Math.min(high, Math.max(low, value));
2532
+ }
2533
+
2534
+ // src/building-model-mesh.ts
2535
+ function isTileflowTreeModel(primitives) {
2536
+ const drawn = primitives.filter((primitive) => !isFootprint(primitive));
2537
+ return drawn.length > 0 && drawn.every(
2538
+ (primitive) => tileflowTreeModelPartNames.includes(
2539
+ partOf2(primitive.material)
2540
+ )
2541
+ );
2542
+ }
2543
+ function tileflowBuildingModelDeclaredFootprint(primitives) {
2544
+ const positions = [];
2545
+ const indices = [];
2546
+ for (const primitive of primitives) {
2547
+ if (!isFootprint(primitive)) continue;
2548
+ const offset = positions.length / 3;
2549
+ positions.push(...primitive.positions);
2550
+ for (const index of primitive.indices) indices.push(offset + index);
2551
+ }
2552
+ return indices.length > 0 ? { positions, indices } : void 0;
2553
+ }
2554
+ function prepareTileflowBuildingModelMesh(bytes, {
2555
+ ambientOcclusion = true,
2556
+ lightmapTexels = 0
2557
+ } = {}) {
2558
+ const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes, { materials: true }) : bytes;
2559
+ const drawn = primitives.filter((primitive) => !isFootprint(primitive));
2560
+ const surface = buildingModelSurface(drawn);
2561
+ const positions = [];
2562
+ const normals = [];
2563
+ const partNames = [];
2564
+ const parts = [];
2565
+ const lightingRoles = [];
2566
+ const luminance2 = [];
2567
+ const indices = [];
2568
+ for (const primitive of drawn) {
2569
+ const name = partOf2(primitive.material);
2570
+ if (name === void 0)
2571
+ throw new TileflowGltfError(
2572
+ primitive.material === void 0 ? "has a mesh without a material; name each material after its part, such as surface, glass, roof or water" : `has a material "${primitive.material}" that names no part; name each material after its part in lower-case words joined by hyphens, such as surface, glass, roof or water`
2573
+ );
2574
+ if (!partNames.includes(name)) partNames.push(name);
2575
+ if (partNames.length > tileflowModelLimits.parts)
2576
+ throw new TileflowGltfError(
2577
+ `names ${partNames.length} parts in its materials; a building model takes at most ${tileflowModelLimits.parts} a level. Give materials of the same part the same name`
2578
+ );
2579
+ const part = partNames.indexOf(name);
2580
+ const offset = positions.length / 3;
2581
+ const role = primitive.lightingRole === void 0 ? tileflowModelDefaultLightingRole(name) : typeof primitive.lightingRole === "string" && Object.hasOwn(tileflowModelLightingRoles, primitive.lightingRole) ? tileflowModelLightingRoles[primitive.lightingRole] : void 0;
1719
2582
  if (role === void 0)
1720
2583
  throw new TileflowGltfError(
1721
2584
  "has an unknown lighting role; use unclassified, wall, door, roof, window, lamp or logo"
@@ -1727,7 +2590,7 @@ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {
1727
2590
  lightingRoles.push(role);
1728
2591
  normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
1729
2592
  const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
1730
- luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
2593
+ luminance2.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
1731
2594
  }
1732
2595
  for (const index of primitive.indices) indices.push(offset + index);
1733
2596
  }
@@ -1755,9 +2618,9 @@ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {
1755
2618
  const sums = new Array(count * 3).fill(0);
1756
2619
  for (let at = 0; at + 2 < indices.length; at += 3) {
1757
2620
  const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
1758
- const n = cross(
1759
- sub(point(positions, b), point(positions, a)),
1760
- sub(point(positions, c), point(positions, a))
2621
+ const n = cross3(
2622
+ sub3(point2(positions, b), point2(positions, a)),
2623
+ sub3(point2(positions, c), point2(positions, a))
1761
2624
  );
1762
2625
  for (const vertex of [a, b, c])
1763
2626
  for (let axis = 0; axis < 3; axis += 1) sums[vertex * 3 + axis] += n[axis];
@@ -1771,22 +2634,84 @@ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {
1771
2634
  const shades = new Array(count).fill(0);
1772
2635
  for (const part of new Set(parts)) {
1773
2636
  const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
1774
- const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
2637
+ const mean = members.reduce((sum, vertex) => sum + luminance2[vertex], 0) / members.length;
1775
2638
  const spread = Math.max(0.06, mean * 0.6);
1776
2639
  for (const vertex of members)
1777
- shades[vertex] = clamp((luminance[vertex] - mean) / spread, -1, 1);
2640
+ shades[vertex] = clamp2((luminance2[vertex] - mean) / spread, -1, 1);
1778
2641
  }
2642
+ const texels = typeof lightmapTexels === "function" ? lightmapTexels(surfaceArea(positions, indices)) : lightmapTexels;
2643
+ const lightmap = ambientOcclusion && texels > 0 && surface && lightmapFits(surface) ? bakeBuildingModelLightmap({ positions, normals: outNormals, indices }, { texels }) : void 0;
2644
+ if (!lightmap)
2645
+ return {
2646
+ positions,
2647
+ normals: outNormals,
2648
+ parts,
2649
+ partNames,
2650
+ lightingRoles,
2651
+ shades,
2652
+ ...surface ? { surface } : {},
2653
+ occlusion: ambientOcclusion ? buildingModelAmbientVisibility({ positions, normals: outNormals, indices }) : new Array(count).fill(1),
2654
+ indices
2655
+ };
2656
+ const { sources } = lightmap;
2657
+ const from = (values, width = 1) => sources.flatMap((source) => values.slice(source * width, source * width + width));
2658
+ const attributes = from([...surface.attributes], 8);
2659
+ for (let vertex = 0; vertex < sources.length; vertex++) {
2660
+ attributes[vertex * 8 + 2] = lightmap.uvs[vertex * 2];
2661
+ attributes[vertex * 8 + 3] = lightmap.uvs[vertex * 2 + 1];
2662
+ }
2663
+ const texture = surface.textures.length;
1779
2664
  return {
1780
- positions,
1781
- normals: outNormals,
1782
- parts,
1783
- lightingRoles,
1784
- shades,
1785
- ...surface ? { surface } : {},
1786
- occlusion: ambientOcclusion ? buildingModelAmbientVisibility({ positions, normals: outNormals, indices }) : new Array(count).fill(1),
1787
- indices
2665
+ positions: from(positions, 3),
2666
+ normals: from(outNormals, 3),
2667
+ parts: from(parts),
2668
+ partNames,
2669
+ lightingRoles: from(lightingRoles),
2670
+ shades: from(shades),
2671
+ surface: {
2672
+ ...surface,
2673
+ attributes,
2674
+ materials: surface.materials.map((material) => ({
2675
+ ...material,
2676
+ occlusionTexture: { texture, texCoord: 1 }
2677
+ })),
2678
+ textures: [
2679
+ ...surface.textures,
2680
+ {
2681
+ mimeType: "image/jpeg",
2682
+ bytes: encodeGreyJpeg(lightmap.texels, lightmap.width, lightmap.height, lightmapQuality),
2683
+ width: lightmap.width,
2684
+ height: lightmap.height,
2685
+ wrapS: 33071,
2686
+ wrapT: 33071,
2687
+ minFilter: 9987,
2688
+ magFilter: 9729
2689
+ }
2690
+ ]
2691
+ },
2692
+ occlusion: new Array(sources.length).fill(1),
2693
+ indices: lightmap.indices
1788
2694
  };
1789
2695
  }
2696
+ var lightmapQuality = 35;
2697
+ function surfaceArea(positions, indices) {
2698
+ let area = 0;
2699
+ for (let at = 0; at + 2 < indices.length; at += 3) {
2700
+ const a = point2(positions, indices[at]);
2701
+ area += Math.hypot(
2702
+ ...cross3(
2703
+ sub3(point2(positions, indices[at + 1]), a),
2704
+ sub3(point2(positions, indices[at + 2]), a)
2705
+ )
2706
+ ) / 2;
2707
+ }
2708
+ return area;
2709
+ }
2710
+ function lightmapFits(surface) {
2711
+ return surface.textures.length < tileflowModelSurfaceLimits2.textures && surface.materials.every(
2712
+ (material) => !material.occlusionTexture && tileflowModelTextureChannels2.every((channel) => material[channel]?.texCoord !== 1)
2713
+ );
2714
+ }
1790
2715
  function tileflowBuildingModelFootprint(mesh) {
1791
2716
  const { positions, indices } = mesh;
1792
2717
  let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity];
@@ -1903,10 +2828,9 @@ function tileflowBuildingModelFootprint(mesh) {
1903
2828
  function isFootprint(primitive) {
1904
2829
  return primitive.material?.trim().toLowerCase().replace(/\.\d+$/u, "") === "footprint";
1905
2830
  }
1906
- function partOf(material) {
2831
+ function partOf2(material) {
1907
2832
  const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
1908
- const part = tileflowBuildingModelPartNames.find((candidate) => candidate === name);
1909
- return part === void 0 ? void 0 : tileflowModelParts[part];
2833
+ return isTileflowModelPartName(name) ? name : void 0;
1910
2834
  }
1911
2835
  function simplify(ring, tolerance) {
1912
2836
  if (ring.length <= 4) return [...ring];
@@ -1943,7 +2867,7 @@ function segmentDistance(p, a, b) {
1943
2867
  const dx = b[0] - a[0];
1944
2868
  const dy = b[1] - a[1];
1945
2869
  const length = dx * dx + dy * dy;
1946
- const t = length > 0 ? clamp(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
2870
+ const t = length > 0 ? clamp2(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
1947
2871
  return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
1948
2872
  }
1949
2873
  function ringArea(ring) {
@@ -1955,150 +2879,13 @@ function ringArea(ring) {
1955
2879
  function edge(a, b, p) {
1956
2880
  return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1957
2881
  }
1958
- function point(list2, vertex) {
1959
- return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
1960
- }
1961
- function sub(a, b) {
1962
- return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1963
- }
1964
- function cross(a, b) {
1965
- return [
1966
- a[1] * b[2] - a[2] * b[1],
1967
- a[2] * b[0] - a[0] * b[2],
1968
- a[0] * b[1] - a[1] * b[0]
1969
- ];
1970
- }
1971
- function clamp(value, low, high) {
1972
- return Math.min(high, Math.max(low, value));
1973
- }
1974
-
1975
- // src/tree-model-mesh.ts
1976
- import {
1977
- tileflowTreeModelLimits,
1978
- tileflowTreeParts
1979
- } from "@tileflow/core";
1980
- var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
1981
- function prepareTileflowTreeModelMesh(bytes, level) {
1982
- const primitives = readTileflowGltfTriangles(bytes);
1983
- const positions = [];
1984
- const normals = [];
1985
- const parts = [];
1986
- const luminance = [];
1987
- const indices = [];
1988
- for (const primitive of primitives) {
1989
- const part = partOf2(primitive.material);
1990
- const offset = positions.length / 3;
1991
- const count2 = primitive.positions.length / 3;
1992
- positions.push(...primitive.positions);
1993
- for (let vertex = 0; vertex < count2; vertex += 1) {
1994
- parts.push(part);
1995
- normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
1996
- const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
1997
- luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
1998
- }
1999
- for (const index of primitive.indices) indices.push(offset + index);
2000
- }
2001
- const count = positions.length / 3;
2002
- const triangleCount = indices.length / 3;
2003
- const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
2004
- if (triangleCount > limit)
2005
- throw new TileflowGltfError(
2006
- `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`
2007
- );
2008
- if (count > tileflowTreeModelLimits.vertices)
2009
- throw new TileflowGltfError(
2010
- `has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
2011
- );
2012
- let height = 0;
2013
- let reach = 0;
2014
- const crowned = parts.some((part) => isCrown(part));
2015
- for (let vertex = 0; vertex < count; vertex += 1) {
2016
- height = Math.max(height, positions[vertex * 3 + 2]);
2017
- if (!crowned || isCrown(parts[vertex]))
2018
- reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
2019
- }
2020
- if (!(height > 0))
2021
- throw new TileflowGltfError(
2022
- "stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
2023
- );
2024
- if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
2025
- const across = 2 * reach;
2026
- const unit = [];
2027
- for (let vertex = 0; vertex < count; vertex += 1)
2028
- unit.push(
2029
- positions[vertex * 3] / across,
2030
- positions[vertex * 3 + 1] / across,
2031
- positions[vertex * 3 + 2] / height
2032
- );
2033
- const faceSums = new Array(count * 3).fill(0);
2034
- for (let at = 0; at + 2 < indices.length; at += 3) {
2035
- const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
2036
- const n = cross2(sub2(point2(unit, b), point2(unit, a)), sub2(point2(unit, c), point2(unit, a)));
2037
- for (const vertex of [a, b, c])
2038
- for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
2039
- }
2040
- const outNormals = [];
2041
- for (let vertex = 0; vertex < count; vertex += 1) {
2042
- const own = normals[vertex];
2043
- const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
2044
- const size = Math.hypot(n[0], n[1], n[2]) || 1;
2045
- outNormals.push(n[0] / size, n[1] / size, n[2] / size);
2046
- }
2047
- const shades = new Array(count).fill(0);
2048
- for (const part of new Set(parts)) {
2049
- const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
2050
- const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
2051
- const spread = Math.max(0.06, mean * 0.6);
2052
- for (const vertex of members)
2053
- shades[vertex] = clamp2((luminance[vertex] - mean) / spread, -1, 1);
2054
- }
2055
- let crownLow = 1;
2056
- let crownHigh = 0;
2057
- for (let vertex = 0; vertex < count; vertex += 1)
2058
- if (isCrown(parts[vertex])) {
2059
- crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
2060
- crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
2061
- }
2062
- const occlusion = [];
2063
- for (let vertex = 0; vertex < count; vertex += 1) {
2064
- const z = unit[vertex * 3 + 2];
2065
- if (!isCrown(parts[vertex])) {
2066
- occlusion.push(z > crownLow ? 0.55 : 0.8);
2067
- continue;
2068
- }
2069
- const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
2070
- const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
2071
- occlusion.push(clamp2(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
2072
- }
2073
- return {
2074
- // The author's proportions: the trees it stands keep them around the sizes the data gives.
2075
- proportion: Math.min(3, Math.max(0.1, across / height)),
2076
- positions: unit,
2077
- normals: outNormals,
2078
- parts,
2079
- shades,
2080
- occlusion,
2081
- indices
2082
- };
2083
- }
2084
- function partOf2(material) {
2085
- const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
2086
- const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
2087
- if (part) return tileflowTreeParts[part];
2088
- throw new TileflowGltfError(
2089
- 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`
2090
- );
2091
- }
2092
- function isCrown(part) {
2093
- return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
2094
- }
2095
2882
  function point2(list2, vertex) {
2096
2883
  return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
2097
2884
  }
2098
- function sub2(a, b) {
2885
+ function sub3(a, b) {
2099
2886
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2100
2887
  }
2101
- function cross2(a, b) {
2888
+ function cross3(a, b) {
2102
2889
  return [
2103
2890
  a[1] * b[2] - a[2] * b[1],
2104
2891
  a[2] * b[0] - a[0] * b[2],
@@ -2170,15 +2957,32 @@ function encodeTileflowLandmarkRegion(region) {
2170
2957
  const bytes = serializeTileflowLandmarkRegion(region);
2171
2958
  return { bytes, sha256: sha256Of(bytes) };
2172
2959
  }
2960
+ var nearLightmapTexels = (area) => Math.min(
2961
+ 524288,
2962
+ Math.max(65536, Math.round(89 / 0.6 * area ** 0.54), Math.round(1.3 ** 2 * area / 0.6))
2963
+ );
2964
+ var buildingModelLightmapTexels = {
2965
+ near: nearLightmapTexels,
2966
+ mid: (area) => nearLightmapTexels(area) / 4
2967
+ };
2173
2968
  function buildingModelEntry(id, levels) {
2174
2969
  const primitives = readTileflowGltfTriangles(levels.near, { materials: true });
2175
- if (!isTileflowBuildingModel(primitives))
2970
+ if (isTileflowTreeModel(primitives))
2176
2971
  throw new TileflowGltfError(
2177
- "is not a building model; name each material surface, secondary, glass, accent or roof"
2972
+ "is not a building model; its materials name only the parts of a tree (bark, leaves, fronds and stem)"
2973
+ );
2974
+ const meshes = [
2975
+ prepareTileflowBuildingModelMesh(primitives, {
2976
+ lightmapTexels: buildingModelLightmapTexels.near
2977
+ })
2978
+ ];
2979
+ if (levels.mid)
2980
+ meshes.push(
2981
+ prepareTileflowBuildingModelMesh(levels.mid, {
2982
+ lightmapTexels: buildingModelLightmapTexels.mid
2983
+ })
2178
2984
  );
2179
- const meshes = [prepareTileflowBuildingModelMesh(primitives)];
2180
- for (const level of [levels.mid, levels.far])
2181
- if (level) meshes.push(prepareTileflowBuildingModelMesh(level));
2985
+ if (levels.far) meshes.push(prepareTileflowBuildingModelMesh(levels.far));
2182
2986
  return {
2183
2987
  id,
2184
2988
  levels: meshes,
@@ -2265,7 +3069,7 @@ async function prepareTileflowMapTreeModels(directories, options) {
2265
3069
  });
2266
3070
  continue;
2267
3071
  }
2268
- if (isTileflowBuildingModel(primitives)) {
3072
+ if (!isTileflowTreeModel(primitives)) {
2269
3073
  const files2 = { near: nearBytes };
2270
3074
  const sources = [];
2271
3075
  let failing = entry.near;