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