@tileflow/dev 0.1.0-alpha.30 → 0.1.0-alpha.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/THIRD_PARTY_NOTICES.md +225 -3
- package/dist/artifacts.d.ts +6 -2
- package/dist/artifacts.js +18 -3
- package/dist/{chunk-6MGFMKG3.js → chunk-AQUGMUZW.js} +43 -3
- package/dist/{chunk-L56FLV4R.js → chunk-CM6ZTYDJ.js} +48 -7
- package/dist/{chunk-A2VTI43Y.js → chunk-F4PLTZS4.js} +1007 -19
- package/dist/config.js +1 -1
- 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 -337
- package/dist/icons.js +3 -1
- package/dist/inspect.js +1 -1
- package/dist/{preview-assets-MVUJJ5XR.js → preview-assets-KGUICQHG.js} +6 -0
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +251 -1855
- package/dist/server.d.ts +11 -4
- package/dist/server.js +1595 -278
- 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 -193
- 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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-7C4F6GAX.js";
|
|
5
5
|
|
|
6
6
|
// src/icons.ts
|
|
7
|
-
import { readdir as
|
|
7
|
+
import { readdir as readdir3, readFile as readFile2, realpath as realpath3, stat } from "fs/promises";
|
|
8
8
|
import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath, sep as sep2 } from "path";
|
|
9
9
|
import { SaxesParser } from "saxes";
|
|
10
10
|
import {
|
|
@@ -17,14 +17,15 @@ import {
|
|
|
17
17
|
tileflowIconSpriteIndexSchema
|
|
18
18
|
} from "@tileflow/core";
|
|
19
19
|
import {
|
|
20
|
-
compareCodeUnits as
|
|
20
|
+
compareCodeUnits as compareCodeUnits5,
|
|
21
21
|
hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels3,
|
|
22
22
|
parseResolvedTileflowMap,
|
|
23
23
|
parseTileflowIconJson as parseTileflowIconJson2,
|
|
24
24
|
serializeCanonicalJson as serializeCanonicalJson3,
|
|
25
25
|
sha256Hex as sha256Hex3,
|
|
26
26
|
tileflowIconIdPattern,
|
|
27
|
-
tileflowIconPackageLimits as tileflowIconPackageLimits4
|
|
27
|
+
tileflowIconPackageLimits as tileflowIconPackageLimits4,
|
|
28
|
+
tileflowModelContentType
|
|
28
29
|
} from "@tileflow/core";
|
|
29
30
|
|
|
30
31
|
// src/icon-composition.ts
|
|
@@ -849,7 +850,7 @@ async function composeTileflowIconSources(input, options) {
|
|
|
849
850
|
source: winner.source
|
|
850
851
|
}))
|
|
851
852
|
};
|
|
852
|
-
function addWinner(ordinal, icon,
|
|
853
|
+
function addWinner(ordinal, icon, identity2, source) {
|
|
853
854
|
pixelBytes += icon.oneX.rgba.byteLength + icon.twoX.rgba.byteLength;
|
|
854
855
|
const maximumPixelBytes = (tileflowIconPackageLimits3.maxAtlasDimension ** 2 + (tileflowIconPackageLimits3.maxAtlasDimension / 2) ** 2) * 4;
|
|
855
856
|
if (winners.size >= tileflowIconPackageLimits3.maxIconCount || pixelBytes > maximumPixelBytes)
|
|
@@ -857,7 +858,7 @@ async function composeTileflowIconSources(input, options) {
|
|
|
857
858
|
"ICON_COMPOSITION_INVALID",
|
|
858
859
|
"Composed rendered cells exceed the final sprite capacity"
|
|
859
860
|
);
|
|
860
|
-
winners.set(icon.id, { ordinal, icon, identity, source });
|
|
861
|
+
winners.set(icon.id, { ordinal, icon, identity: identity2, source });
|
|
861
862
|
}
|
|
862
863
|
}
|
|
863
864
|
function isPathInside(root, candidate) {
|
|
@@ -939,6 +940,964 @@ function transform(grid, width, height) {
|
|
|
939
940
|
}
|
|
940
941
|
}
|
|
941
942
|
|
|
943
|
+
// src/tree-models.ts
|
|
944
|
+
import { MeshoptEncoder } from "meshoptimizer/encoder";
|
|
945
|
+
import { createHash } from "crypto";
|
|
946
|
+
import { lstat as lstat3, readdir as readdir2, readFile } from "fs/promises";
|
|
947
|
+
import { join as join3 } from "path";
|
|
948
|
+
import {
|
|
949
|
+
compareCodeUnits as compareCodeUnits4,
|
|
950
|
+
encodeTileflowModels,
|
|
951
|
+
encodeTileflowTreeModel,
|
|
952
|
+
serializeTileflowLandmarkRegion,
|
|
953
|
+
tileflowLandmarkTileOf,
|
|
954
|
+
tileflowTreeModelLimits as tileflowTreeModelLimits2
|
|
955
|
+
} from "@tileflow/core";
|
|
956
|
+
|
|
957
|
+
// src/building-model-mesh.ts
|
|
958
|
+
import { tileflowModelLimits, tileflowModelParts } from "@tileflow/core";
|
|
959
|
+
|
|
960
|
+
// src/gltf-reader.ts
|
|
961
|
+
var TileflowGltfError = class extends Error {
|
|
962
|
+
name = "TileflowGltfError";
|
|
963
|
+
};
|
|
964
|
+
var glbMagic = 1179937895;
|
|
965
|
+
var jsonChunk = 1313821514;
|
|
966
|
+
var binChunk = 5130562;
|
|
967
|
+
var triangles = 4;
|
|
968
|
+
var compressions = /* @__PURE__ */ new Set([
|
|
969
|
+
"KHR_draco_mesh_compression",
|
|
970
|
+
"EXT_meshopt_compression",
|
|
971
|
+
"KHR_meshopt_compression",
|
|
972
|
+
"KHR_mesh_quantization"
|
|
973
|
+
]);
|
|
974
|
+
var componentBytes = {
|
|
975
|
+
5120: 1,
|
|
976
|
+
5121: 1,
|
|
977
|
+
5122: 2,
|
|
978
|
+
5123: 2,
|
|
979
|
+
5125: 4,
|
|
980
|
+
5126: 4
|
|
981
|
+
};
|
|
982
|
+
var typeSizes = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
|
|
983
|
+
function readTileflowGltfTriangles(bytes) {
|
|
984
|
+
const { json, bin } = chunks(bytes);
|
|
985
|
+
const required = array(json.extensionsRequired).filter((name) => typeof name === "string");
|
|
986
|
+
const compressed = required.find((name) => compressions.has(name));
|
|
987
|
+
if (compressed)
|
|
988
|
+
throw new TileflowGltfError(
|
|
989
|
+
`uses compressed geometry (${String(compressed)}); export it without compression`
|
|
990
|
+
);
|
|
991
|
+
for (const buffer of array(json.buffers))
|
|
992
|
+
if (record(buffer).uri !== void 0)
|
|
993
|
+
throw new TileflowGltfError("keeps its geometry in another file; export a single .glb");
|
|
994
|
+
const nodes = array(json.nodes).map(record);
|
|
995
|
+
const scenes = array(json.scenes).map(record);
|
|
996
|
+
const scene = scenes[typeof json.scene === "number" ? json.scene : 0];
|
|
997
|
+
const roots = scene ? array(scene.nodes) : nodes.map((_, index) => index).filter((index) => !nodes.some((node) => array(node.children).includes(index)));
|
|
998
|
+
const out = [];
|
|
999
|
+
const visit = (index, parent, depth) => {
|
|
1000
|
+
if (typeof index !== "number" || !nodes[index])
|
|
1001
|
+
throw new TileflowGltfError("names a node it does not hold");
|
|
1002
|
+
if (depth > 64) throw new TileflowGltfError("nests its nodes more than 64 deep");
|
|
1003
|
+
const node = nodes[index];
|
|
1004
|
+
const world = multiply(parent, local(node));
|
|
1005
|
+
if (typeof node.mesh === "number") out.push(...meshOf(json, bin, node.mesh, world));
|
|
1006
|
+
for (const child of array(node.children)) visit(child, world, depth + 1);
|
|
1007
|
+
};
|
|
1008
|
+
for (const root of roots) visit(root, identity(), 0);
|
|
1009
|
+
if (out.length === 0) throw new TileflowGltfError("holds no triangles in its scene");
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
function chunks(bytes) {
|
|
1013
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1014
|
+
if (bytes.byteLength < 20 || view.getUint32(0, true) !== glbMagic)
|
|
1015
|
+
throw new TileflowGltfError("is not a glTF binary; export the model as .glb");
|
|
1016
|
+
if (view.getUint32(4, true) !== 2) throw new TileflowGltfError("is not glTF 2.0");
|
|
1017
|
+
if (view.getUint32(8, true) !== bytes.byteLength)
|
|
1018
|
+
throw new TileflowGltfError("is cut short or has trailing bytes");
|
|
1019
|
+
let at = 12;
|
|
1020
|
+
let json;
|
|
1021
|
+
let bin;
|
|
1022
|
+
while (at + 8 <= bytes.byteLength) {
|
|
1023
|
+
const length = view.getUint32(at, true);
|
|
1024
|
+
const type = view.getUint32(at + 4, true);
|
|
1025
|
+
const start = at + 8;
|
|
1026
|
+
if (start + length > bytes.byteLength) throw new TileflowGltfError("has a chunk past its end");
|
|
1027
|
+
const body = bytes.subarray(start, start + length);
|
|
1028
|
+
if (type === jsonChunk && json === void 0) {
|
|
1029
|
+
try {
|
|
1030
|
+
json = record(JSON.parse(new TextDecoder().decode(body)));
|
|
1031
|
+
} catch {
|
|
1032
|
+
throw new TileflowGltfError("has an unreadable JSON chunk");
|
|
1033
|
+
}
|
|
1034
|
+
} else if (type === binChunk && bin === void 0) bin = body;
|
|
1035
|
+
at = start + length + (4 - length % 4) % 4;
|
|
1036
|
+
}
|
|
1037
|
+
if (!json) throw new TileflowGltfError("has no JSON chunk");
|
|
1038
|
+
return { json, bin };
|
|
1039
|
+
}
|
|
1040
|
+
function meshOf(json, bin, index, world) {
|
|
1041
|
+
const mesh = record(array(json.meshes)[index]);
|
|
1042
|
+
const normalMatrix = inverseTranspose(world);
|
|
1043
|
+
return array(mesh.primitives).map((value) => {
|
|
1044
|
+
const primitive = record(value);
|
|
1045
|
+
if ((primitive.mode ?? triangles) !== triangles)
|
|
1046
|
+
throw new TileflowGltfError("draws points or lines; a tree model draws triangles");
|
|
1047
|
+
const attributes = record(primitive.attributes);
|
|
1048
|
+
if (typeof attributes.POSITION !== "number")
|
|
1049
|
+
throw new TileflowGltfError("has a primitive without positions");
|
|
1050
|
+
const raw = accessor(json, bin, attributes.POSITION, 3);
|
|
1051
|
+
const count = raw.length / 3;
|
|
1052
|
+
const positions = [];
|
|
1053
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1054
|
+
const [x, y, z] = transform2(
|
|
1055
|
+
world,
|
|
1056
|
+
raw[vertex * 3],
|
|
1057
|
+
raw[vertex * 3 + 1],
|
|
1058
|
+
raw[vertex * 3 + 2],
|
|
1059
|
+
1
|
|
1060
|
+
);
|
|
1061
|
+
positions.push(x, -z, y);
|
|
1062
|
+
}
|
|
1063
|
+
let normals;
|
|
1064
|
+
if (typeof attributes.NORMAL === "number") {
|
|
1065
|
+
const rawNormals = accessor(json, bin, attributes.NORMAL, 3);
|
|
1066
|
+
normals = [];
|
|
1067
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1068
|
+
const [x, y, z] = transform2(
|
|
1069
|
+
normalMatrix,
|
|
1070
|
+
rawNormals[vertex * 3],
|
|
1071
|
+
rawNormals[vertex * 3 + 1],
|
|
1072
|
+
rawNormals[vertex * 3 + 2],
|
|
1073
|
+
0
|
|
1074
|
+
);
|
|
1075
|
+
const size = Math.hypot(x, y, z) || 1;
|
|
1076
|
+
normals.push(x / size, -z / size, y / size);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const material = typeof primitive.material === "number" ? record(array(json.materials)[primitive.material]) : void 0;
|
|
1080
|
+
const base = array(record(material?.pbrMetallicRoughness).baseColorFactor).map(Number);
|
|
1081
|
+
const factor = base.length >= 3 && base.every(Number.isFinite) ? base : [1, 1, 1];
|
|
1082
|
+
const colours = [];
|
|
1083
|
+
const vertexColours = typeof attributes.COLOR_0 === "number" ? accessorWithSize(json, bin, attributes.COLOR_0) : void 0;
|
|
1084
|
+
for (let vertex = 0; vertex < count; vertex += 1)
|
|
1085
|
+
for (let channel = 0; channel < 3; channel += 1)
|
|
1086
|
+
colours.push(
|
|
1087
|
+
factor[channel] * (vertexColours ? vertexColours.values[vertex * vertexColours.size + channel] : 1)
|
|
1088
|
+
);
|
|
1089
|
+
const indices = typeof primitive.indices === "number" ? accessor(json, bin, primitive.indices, 1).map((value2) => Math.round(value2)) : Array.from({ length: count }, (_, vertex) => vertex);
|
|
1090
|
+
if (indices.length % 3 !== 0 || indices.some((vertex) => vertex < 0 || vertex >= count))
|
|
1091
|
+
throw new TileflowGltfError("has triangles that name missing vertices");
|
|
1092
|
+
if (determinant(world) < 0)
|
|
1093
|
+
for (let at = 0; at + 2 < indices.length; at += 3)
|
|
1094
|
+
[indices[at + 1], indices[at + 2]] = [indices[at + 2], indices[at + 1]];
|
|
1095
|
+
return {
|
|
1096
|
+
material: typeof material?.name === "string" ? material.name : void 0,
|
|
1097
|
+
positions,
|
|
1098
|
+
normals,
|
|
1099
|
+
colours,
|
|
1100
|
+
indices
|
|
1101
|
+
};
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
function accessor(json, bin, index, size) {
|
|
1105
|
+
const read = accessorWithSize(json, bin, index);
|
|
1106
|
+
if (read.size !== size)
|
|
1107
|
+
throw new TileflowGltfError(`has an accessor of ${read.size} components where ${size} belong`);
|
|
1108
|
+
return read.values;
|
|
1109
|
+
}
|
|
1110
|
+
function accessorWithSize(json, bin, index) {
|
|
1111
|
+
const accessor2 = record(array(json.accessors)[index]);
|
|
1112
|
+
if (accessor2.sparse !== void 0)
|
|
1113
|
+
throw new TileflowGltfError("uses sparse accessors; export without them");
|
|
1114
|
+
const size = typeSizes[String(accessor2.type)];
|
|
1115
|
+
const component = Number(accessor2.componentType);
|
|
1116
|
+
const bytesEach = componentBytes[component];
|
|
1117
|
+
const count = Number(accessor2.count);
|
|
1118
|
+
if (!size || !bytesEach || !Number.isInteger(count) || count < 0 || count > 1e6)
|
|
1119
|
+
throw new TileflowGltfError("has an accessor Tileflow cannot read");
|
|
1120
|
+
if (typeof accessor2.bufferView !== "number")
|
|
1121
|
+
return { values: new Array(count * size).fill(0), size };
|
|
1122
|
+
const view = record(array(json.bufferViews)[accessor2.bufferView]);
|
|
1123
|
+
if (!bin || Number(view.buffer ?? 0) !== 0)
|
|
1124
|
+
throw new TileflowGltfError("has geometry outside its binary chunk");
|
|
1125
|
+
const stride = Number(view.byteStride ?? 0) || size * bytesEach;
|
|
1126
|
+
const start = Number(view.byteOffset ?? 0) + Number(accessor2.byteOffset ?? 0);
|
|
1127
|
+
const end = start + (count - 1) * stride + size * bytesEach;
|
|
1128
|
+
if (count > 0 && (start < 0 || end > Number(view.byteOffset ?? 0) + Number(view.byteLength) || end > bin.byteLength))
|
|
1129
|
+
throw new TileflowGltfError("has an accessor past the end of its buffer");
|
|
1130
|
+
const data = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
|
|
1131
|
+
const normalized = accessor2.normalized === true;
|
|
1132
|
+
const values = [];
|
|
1133
|
+
for (let item = 0; item < count; item += 1)
|
|
1134
|
+
for (let part = 0; part < size; part += 1) {
|
|
1135
|
+
const at = start + item * stride + part * bytesEach;
|
|
1136
|
+
let value;
|
|
1137
|
+
switch (component) {
|
|
1138
|
+
case 5126:
|
|
1139
|
+
value = data.getFloat32(at, true);
|
|
1140
|
+
break;
|
|
1141
|
+
case 5125:
|
|
1142
|
+
value = data.getUint32(at, true);
|
|
1143
|
+
break;
|
|
1144
|
+
case 5123:
|
|
1145
|
+
value = data.getUint16(at, true);
|
|
1146
|
+
if (normalized) value /= 65535;
|
|
1147
|
+
break;
|
|
1148
|
+
case 5122:
|
|
1149
|
+
value = data.getInt16(at, true);
|
|
1150
|
+
if (normalized) value = Math.max(value / 32767, -1);
|
|
1151
|
+
break;
|
|
1152
|
+
case 5121:
|
|
1153
|
+
value = data.getUint8(at);
|
|
1154
|
+
if (normalized) value /= 255;
|
|
1155
|
+
break;
|
|
1156
|
+
default:
|
|
1157
|
+
value = data.getInt8(at);
|
|
1158
|
+
if (normalized) value = Math.max(value / 127, -1);
|
|
1159
|
+
}
|
|
1160
|
+
if (!Number.isFinite(value)) throw new TileflowGltfError("has a value that is not a number");
|
|
1161
|
+
values.push(value);
|
|
1162
|
+
}
|
|
1163
|
+
return { values, size };
|
|
1164
|
+
}
|
|
1165
|
+
function identity() {
|
|
1166
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
1167
|
+
}
|
|
1168
|
+
function local(node) {
|
|
1169
|
+
const matrix = array(node.matrix).map(Number);
|
|
1170
|
+
if (matrix.length === 16 && matrix.every(Number.isFinite)) return matrix;
|
|
1171
|
+
const [tx, ty, tz] = vector(node.translation, [0, 0, 0]);
|
|
1172
|
+
const [qx, qy, qz, qw] = vector(node.rotation, [0, 0, 0, 1]);
|
|
1173
|
+
const [sx, sy, sz] = vector(node.scale, [1, 1, 1]);
|
|
1174
|
+
const [xx, yy, zz] = [qx * qx, qy * qy, qz * qz];
|
|
1175
|
+
const [xy, xz, yz, wx, wy, wz] = [qx * qy, qx * qz, qy * qz, qw * qx, qw * qy, qw * qz];
|
|
1176
|
+
return [
|
|
1177
|
+
(1 - 2 * (yy + zz)) * sx,
|
|
1178
|
+
2 * (xy + wz) * sx,
|
|
1179
|
+
2 * (xz - wy) * sx,
|
|
1180
|
+
0,
|
|
1181
|
+
2 * (xy - wz) * sy,
|
|
1182
|
+
(1 - 2 * (xx + zz)) * sy,
|
|
1183
|
+
2 * (yz + wx) * sy,
|
|
1184
|
+
0,
|
|
1185
|
+
2 * (xz + wy) * sz,
|
|
1186
|
+
2 * (yz - wx) * sz,
|
|
1187
|
+
(1 - 2 * (xx + yy)) * sz,
|
|
1188
|
+
0,
|
|
1189
|
+
tx,
|
|
1190
|
+
ty,
|
|
1191
|
+
tz,
|
|
1192
|
+
1
|
|
1193
|
+
];
|
|
1194
|
+
}
|
|
1195
|
+
function multiply(a, b) {
|
|
1196
|
+
const out = new Array(16).fill(0);
|
|
1197
|
+
for (let column = 0; column < 4; column += 1)
|
|
1198
|
+
for (let row = 0; row < 4; row += 1)
|
|
1199
|
+
for (let k = 0; k < 4; k += 1) out[column * 4 + row] += a[k * 4 + row] * b[column * 4 + k];
|
|
1200
|
+
return out;
|
|
1201
|
+
}
|
|
1202
|
+
function transform2(m, x, y, z, w) {
|
|
1203
|
+
return [
|
|
1204
|
+
m[0] * x + m[4] * y + m[8] * z + m[12] * w,
|
|
1205
|
+
m[1] * x + m[5] * y + m[9] * z + m[13] * w,
|
|
1206
|
+
m[2] * x + m[6] * y + m[10] * z + m[14] * w
|
|
1207
|
+
];
|
|
1208
|
+
}
|
|
1209
|
+
function determinant(m) {
|
|
1210
|
+
return m[0] * (m[5] * m[10] - m[9] * m[6]) - m[4] * (m[1] * m[10] - m[9] * m[2]) + m[8] * (m[1] * m[6] - m[5] * m[2]);
|
|
1211
|
+
}
|
|
1212
|
+
function inverseTranspose(m) {
|
|
1213
|
+
const [a, b, c, d, e, f, g, h, i] = [
|
|
1214
|
+
m[0],
|
|
1215
|
+
m[1],
|
|
1216
|
+
m[2],
|
|
1217
|
+
m[4],
|
|
1218
|
+
m[5],
|
|
1219
|
+
m[6],
|
|
1220
|
+
m[8],
|
|
1221
|
+
m[9],
|
|
1222
|
+
m[10]
|
|
1223
|
+
];
|
|
1224
|
+
const det = determinant(m) || 1;
|
|
1225
|
+
return [
|
|
1226
|
+
(e * i - h * f) / det,
|
|
1227
|
+
(g * f - d * i) / det,
|
|
1228
|
+
(d * h - g * e) / det,
|
|
1229
|
+
0,
|
|
1230
|
+
(h * c - b * i) / det,
|
|
1231
|
+
(a * i - g * c) / det,
|
|
1232
|
+
(g * b - a * h) / det,
|
|
1233
|
+
0,
|
|
1234
|
+
(b * f - e * c) / det,
|
|
1235
|
+
(d * c - a * f) / det,
|
|
1236
|
+
(a * e - d * b) / det,
|
|
1237
|
+
0,
|
|
1238
|
+
0,
|
|
1239
|
+
0,
|
|
1240
|
+
0,
|
|
1241
|
+
1
|
|
1242
|
+
];
|
|
1243
|
+
}
|
|
1244
|
+
function record(value) {
|
|
1245
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1246
|
+
}
|
|
1247
|
+
function array(value) {
|
|
1248
|
+
return Array.isArray(value) ? value : [];
|
|
1249
|
+
}
|
|
1250
|
+
function vector(value, fallback) {
|
|
1251
|
+
const list = array(value).map(Number);
|
|
1252
|
+
return list.length === fallback.length && list.every(Number.isFinite) ? list : fallback;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/building-model-mesh.ts
|
|
1256
|
+
var tileflowBuildingModelPartNames = Object.keys(tileflowModelParts);
|
|
1257
|
+
function isTileflowBuildingModel(primitives) {
|
|
1258
|
+
const drawn = primitives.filter((primitive) => !isFootprint(primitive));
|
|
1259
|
+
return drawn.length > 0 && drawn.every((primitive) => partOf(primitive.material) !== void 0);
|
|
1260
|
+
}
|
|
1261
|
+
function tileflowBuildingModelDeclaredFootprint(primitives) {
|
|
1262
|
+
const positions = [];
|
|
1263
|
+
const indices = [];
|
|
1264
|
+
for (const primitive of primitives) {
|
|
1265
|
+
if (!isFootprint(primitive)) continue;
|
|
1266
|
+
const offset = positions.length / 3;
|
|
1267
|
+
positions.push(...primitive.positions);
|
|
1268
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
1269
|
+
}
|
|
1270
|
+
return indices.length > 0 ? { positions, indices } : void 0;
|
|
1271
|
+
}
|
|
1272
|
+
function prepareTileflowBuildingModelMesh(bytes) {
|
|
1273
|
+
const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes) : bytes;
|
|
1274
|
+
const positions = [];
|
|
1275
|
+
const normals = [];
|
|
1276
|
+
const parts = [];
|
|
1277
|
+
const luminance = [];
|
|
1278
|
+
const indices = [];
|
|
1279
|
+
for (const primitive of primitives) {
|
|
1280
|
+
if (isFootprint(primitive)) continue;
|
|
1281
|
+
const part = partOf(primitive.material);
|
|
1282
|
+
if (part === void 0)
|
|
1283
|
+
throw new TileflowGltfError(
|
|
1284
|
+
primitive.material === void 0 ? "has a mesh without a material; name each material surface, secondary, glass, accent or roof" : `has a material "${primitive.material}" that names no part of a building; name each material surface, secondary, glass, accent or roof`
|
|
1285
|
+
);
|
|
1286
|
+
const offset = positions.length / 3;
|
|
1287
|
+
const count2 = primitive.positions.length / 3;
|
|
1288
|
+
positions.push(...primitive.positions);
|
|
1289
|
+
for (let vertex = 0; vertex < count2; vertex += 1) {
|
|
1290
|
+
parts.push(part);
|
|
1291
|
+
normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
|
|
1292
|
+
const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
|
|
1293
|
+
luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
1294
|
+
}
|
|
1295
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
1296
|
+
}
|
|
1297
|
+
const count = positions.length / 3;
|
|
1298
|
+
const triangles2 = indices.length / 3;
|
|
1299
|
+
if (triangles2 > tileflowModelLimits.triangles)
|
|
1300
|
+
throw new TileflowGltfError(
|
|
1301
|
+
`has ${triangles2.toLocaleString("en")} triangles; a building model takes at most ${tileflowModelLimits.triangles.toLocaleString("en")} a level. Simplify it and export it again`
|
|
1302
|
+
);
|
|
1303
|
+
if (count > tileflowModelLimits.vertices)
|
|
1304
|
+
throw new TileflowGltfError(
|
|
1305
|
+
`has ${count.toLocaleString("en")} vertices; a building model takes at most ${tileflowModelLimits.vertices.toLocaleString("en")} a level. Merge vertices by distance and export it again`
|
|
1306
|
+
);
|
|
1307
|
+
let low = Infinity;
|
|
1308
|
+
let high = -Infinity;
|
|
1309
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1310
|
+
low = Math.min(low, positions[vertex * 3 + 2]);
|
|
1311
|
+
high = Math.max(high, positions[vertex * 3 + 2]);
|
|
1312
|
+
}
|
|
1313
|
+
if (low < -0.5)
|
|
1314
|
+
throw new TileflowGltfError(
|
|
1315
|
+
`reaches ${(-low).toFixed(1)} m below its origin; put the origin on the ground where the building stands, with the building above it`
|
|
1316
|
+
);
|
|
1317
|
+
if (!(high > 0)) throw new TileflowGltfError("has no height above its origin");
|
|
1318
|
+
const sums = new Array(count * 3).fill(0);
|
|
1319
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1320
|
+
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
1321
|
+
const n = cross(
|
|
1322
|
+
sub(point(positions, b), point(positions, a)),
|
|
1323
|
+
sub(point(positions, c), point(positions, a))
|
|
1324
|
+
);
|
|
1325
|
+
for (const vertex of [a, b, c])
|
|
1326
|
+
for (let axis = 0; axis < 3; axis += 1) sums[vertex * 3 + axis] += n[axis];
|
|
1327
|
+
}
|
|
1328
|
+
const outNormals = [];
|
|
1329
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1330
|
+
const n = normals[vertex] ?? sums.slice(vertex * 3, vertex * 3 + 3);
|
|
1331
|
+
const size = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
1332
|
+
outNormals.push(n[0] / size, n[1] / size, n[2] / size);
|
|
1333
|
+
}
|
|
1334
|
+
const shades = new Array(count).fill(0);
|
|
1335
|
+
for (const part of new Set(parts)) {
|
|
1336
|
+
const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
|
|
1337
|
+
const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
|
|
1338
|
+
const spread = Math.max(0.06, mean * 0.6);
|
|
1339
|
+
for (const vertex of members)
|
|
1340
|
+
shades[vertex] = clamp((luminance[vertex] - mean) / spread, -1, 1);
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
positions,
|
|
1344
|
+
normals: outNormals,
|
|
1345
|
+
parts,
|
|
1346
|
+
shades,
|
|
1347
|
+
occlusion: new Array(count).fill(1),
|
|
1348
|
+
indices
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function tileflowBuildingModelFootprint(mesh) {
|
|
1352
|
+
const { positions, indices } = mesh;
|
|
1353
|
+
let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1354
|
+
for (let at = 0; at < positions.length; at += 3) {
|
|
1355
|
+
minX = Math.min(minX, positions[at]);
|
|
1356
|
+
maxX = Math.max(maxX, positions[at]);
|
|
1357
|
+
minY = Math.min(minY, positions[at + 1]);
|
|
1358
|
+
maxY = Math.max(maxY, positions[at + 1]);
|
|
1359
|
+
}
|
|
1360
|
+
const cell = Math.max(1, Math.max(maxX - minX, maxY - minY) / 1024);
|
|
1361
|
+
const columns = Math.ceil((maxX - minX) / cell) + 5;
|
|
1362
|
+
const rows = Math.ceil((maxY - minY) / cell) + 5;
|
|
1363
|
+
const x0 = minX - 2 * cell;
|
|
1364
|
+
const y0 = minY - 2 * cell;
|
|
1365
|
+
const filled = new Uint8Array(columns * rows);
|
|
1366
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1367
|
+
const corners = [indices[at], indices[at + 1], indices[at + 2]].map((vertex) => [
|
|
1368
|
+
(positions[vertex * 3] - x0) / cell,
|
|
1369
|
+
(positions[vertex * 3 + 1] - y0) / cell
|
|
1370
|
+
]);
|
|
1371
|
+
for (const [cx, cy] of corners) filled[Math.floor(cy) * columns + Math.floor(cx)] = 1;
|
|
1372
|
+
const left = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[0]))));
|
|
1373
|
+
const right = Math.min(columns - 1, Math.ceil(Math.max(...corners.map((c2) => c2[0]))));
|
|
1374
|
+
const top = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[1]))));
|
|
1375
|
+
const bottom = Math.min(rows - 1, Math.ceil(Math.max(...corners.map((c2) => c2[1]))));
|
|
1376
|
+
const [a, b, c] = corners;
|
|
1377
|
+
const area = edge(a, b, c);
|
|
1378
|
+
if (Math.abs(area) < 1e-9) continue;
|
|
1379
|
+
for (let y = top; y <= bottom; y += 1)
|
|
1380
|
+
for (let x = left; x <= right; x += 1) {
|
|
1381
|
+
const p = [x + 0.5, y + 0.5];
|
|
1382
|
+
const w0 = edge(b, c, p) / area;
|
|
1383
|
+
const w1 = edge(c, a, p) / area;
|
|
1384
|
+
const w2 = edge(a, b, p) / area;
|
|
1385
|
+
if (w0 >= 0 && w1 >= 0 && w2 >= 0) filled[y * columns + x] = 1;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const grown = new Uint8Array(filled);
|
|
1389
|
+
for (let y = 1; y < rows - 1; y += 1)
|
|
1390
|
+
for (let x = 1; x < columns - 1; x += 1) {
|
|
1391
|
+
if (filled[y * columns + x]) continue;
|
|
1392
|
+
for (let dy = -1; dy <= 1 && !grown[y * columns + x]; dy += 1)
|
|
1393
|
+
for (let dx = -1; dx <= 1; dx += 1)
|
|
1394
|
+
if (filled[(y + dy) * columns + x + dx]) {
|
|
1395
|
+
grown[y * columns + x] = 1;
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const outside = new Uint8Array(columns * rows);
|
|
1400
|
+
const stack = [0];
|
|
1401
|
+
outside[0] = 1;
|
|
1402
|
+
while (stack.length) {
|
|
1403
|
+
const at = stack.pop();
|
|
1404
|
+
const x = at % columns;
|
|
1405
|
+
const y = (at - x) / columns;
|
|
1406
|
+
for (const [dx, dy] of [
|
|
1407
|
+
[1, 0],
|
|
1408
|
+
[-1, 0],
|
|
1409
|
+
[0, 1],
|
|
1410
|
+
[0, -1]
|
|
1411
|
+
]) {
|
|
1412
|
+
const nx = x + dx;
|
|
1413
|
+
const ny = y + dy;
|
|
1414
|
+
if (nx < 0 || ny < 0 || nx >= columns || ny >= rows) continue;
|
|
1415
|
+
const next2 = ny * columns + nx;
|
|
1416
|
+
if (outside[next2] || grown[next2]) continue;
|
|
1417
|
+
outside[next2] = 1;
|
|
1418
|
+
stack.push(next2);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const covered = (x, y) => x >= 0 && y >= 0 && x < columns && y < rows && !outside[y * columns + x];
|
|
1422
|
+
const next = /* @__PURE__ */ new Map();
|
|
1423
|
+
const add = (from, to) => {
|
|
1424
|
+
const key = `${from[0]},${from[1]}`;
|
|
1425
|
+
const list = next.get(key) ?? [];
|
|
1426
|
+
list.push(to);
|
|
1427
|
+
next.set(key, list);
|
|
1428
|
+
};
|
|
1429
|
+
for (let y = 0; y < rows; y += 1)
|
|
1430
|
+
for (let x = 0; x < columns; x += 1) {
|
|
1431
|
+
if (!covered(x, y)) continue;
|
|
1432
|
+
if (!covered(x, y - 1)) add([x, y], [x + 1, y]);
|
|
1433
|
+
if (!covered(x + 1, y)) add([x + 1, y], [x + 1, y + 1]);
|
|
1434
|
+
if (!covered(x, y + 1)) add([x + 1, y + 1], [x, y + 1]);
|
|
1435
|
+
if (!covered(x - 1, y)) add([x, y + 1], [x, y]);
|
|
1436
|
+
}
|
|
1437
|
+
const rings = [];
|
|
1438
|
+
for (const [startKey, targets] of next) {
|
|
1439
|
+
while (targets.length) {
|
|
1440
|
+
const start = startKey.split(",").map(Number);
|
|
1441
|
+
const ring = [start];
|
|
1442
|
+
let current = targets.pop();
|
|
1443
|
+
while (`${current[0]},${current[1]}` !== startKey) {
|
|
1444
|
+
ring.push(current);
|
|
1445
|
+
const outs = next.get(`${current[0]},${current[1]}`);
|
|
1446
|
+
if (!outs?.length) break;
|
|
1447
|
+
current = outs.pop();
|
|
1448
|
+
}
|
|
1449
|
+
const simplified = simplify(ring, 0.75);
|
|
1450
|
+
if (simplified.length >= 3)
|
|
1451
|
+
rings.push(simplified.flatMap(([x, y]) => [x0 + x * cell, y0 + y * cell]));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
rings.sort((a, b) => Math.abs(ringArea(b)) - Math.abs(ringArea(a)));
|
|
1455
|
+
const kept = [];
|
|
1456
|
+
let points = 0;
|
|
1457
|
+
for (const ring of rings) {
|
|
1458
|
+
if (points + ring.length / 2 > tileflowModelLimits.footprintPoints) break;
|
|
1459
|
+
kept.push(ring);
|
|
1460
|
+
points += ring.length / 2;
|
|
1461
|
+
}
|
|
1462
|
+
return kept;
|
|
1463
|
+
}
|
|
1464
|
+
function isFootprint(primitive) {
|
|
1465
|
+
return primitive.material?.trim().toLowerCase().replace(/\.\d+$/u, "") === "footprint";
|
|
1466
|
+
}
|
|
1467
|
+
function partOf(material) {
|
|
1468
|
+
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
1469
|
+
const part = tileflowBuildingModelPartNames.find((candidate) => candidate === name);
|
|
1470
|
+
return part === void 0 ? void 0 : tileflowModelParts[part];
|
|
1471
|
+
}
|
|
1472
|
+
function simplify(ring, tolerance) {
|
|
1473
|
+
if (ring.length <= 4) return [...ring];
|
|
1474
|
+
let far = 0;
|
|
1475
|
+
let distance = -1;
|
|
1476
|
+
for (const [index, [x, y]] of ring.entries()) {
|
|
1477
|
+
const d = Math.hypot(x - ring[0][0], y - ring[0][1]);
|
|
1478
|
+
if (d > distance) {
|
|
1479
|
+
distance = d;
|
|
1480
|
+
far = index;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const half = (points) => {
|
|
1484
|
+
if (points.length <= 2) return [...points];
|
|
1485
|
+
const [a, b] = [points[0], points[points.length - 1]];
|
|
1486
|
+
let index = 0;
|
|
1487
|
+
let most = -1;
|
|
1488
|
+
for (let at = 1; at < points.length - 1; at += 1) {
|
|
1489
|
+
const d = segmentDistance(points[at], a, b);
|
|
1490
|
+
if (d > most) {
|
|
1491
|
+
most = d;
|
|
1492
|
+
index = at;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (most <= tolerance) return [a, b];
|
|
1496
|
+
const left = half(points.slice(0, index + 1));
|
|
1497
|
+
return [...left.slice(0, -1), ...half(points.slice(index))];
|
|
1498
|
+
};
|
|
1499
|
+
const first = half(ring.slice(0, far + 1));
|
|
1500
|
+
const second = half([...ring.slice(far), ring[0]]);
|
|
1501
|
+
return [...first.slice(0, -1), ...second.slice(0, -1)];
|
|
1502
|
+
}
|
|
1503
|
+
function segmentDistance(p, a, b) {
|
|
1504
|
+
const dx = b[0] - a[0];
|
|
1505
|
+
const dy = b[1] - a[1];
|
|
1506
|
+
const length = dx * dx + dy * dy;
|
|
1507
|
+
const t = length > 0 ? clamp(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
|
|
1508
|
+
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
|
1509
|
+
}
|
|
1510
|
+
function ringArea(ring) {
|
|
1511
|
+
let area = 0;
|
|
1512
|
+
for (let a = 0, b = ring.length - 2; a < ring.length; b = a, a += 2)
|
|
1513
|
+
area += ring[b] * ring[a + 1] - ring[a] * ring[b + 1];
|
|
1514
|
+
return area / 2;
|
|
1515
|
+
}
|
|
1516
|
+
function edge(a, b, p) {
|
|
1517
|
+
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
1518
|
+
}
|
|
1519
|
+
function point(list, vertex) {
|
|
1520
|
+
return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
|
|
1521
|
+
}
|
|
1522
|
+
function sub(a, b) {
|
|
1523
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1524
|
+
}
|
|
1525
|
+
function cross(a, b) {
|
|
1526
|
+
return [
|
|
1527
|
+
a[1] * b[2] - a[2] * b[1],
|
|
1528
|
+
a[2] * b[0] - a[0] * b[2],
|
|
1529
|
+
a[0] * b[1] - a[1] * b[0]
|
|
1530
|
+
];
|
|
1531
|
+
}
|
|
1532
|
+
function clamp(value, low, high) {
|
|
1533
|
+
return Math.min(high, Math.max(low, value));
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/tree-model-mesh.ts
|
|
1537
|
+
import {
|
|
1538
|
+
tileflowTreeModelLimits,
|
|
1539
|
+
tileflowTreeParts
|
|
1540
|
+
} from "@tileflow/core";
|
|
1541
|
+
var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
|
|
1542
|
+
function prepareTileflowTreeModelMesh(bytes, level) {
|
|
1543
|
+
const primitives = readTileflowGltfTriangles(bytes);
|
|
1544
|
+
const positions = [];
|
|
1545
|
+
const normals = [];
|
|
1546
|
+
const parts = [];
|
|
1547
|
+
const luminance = [];
|
|
1548
|
+
const indices = [];
|
|
1549
|
+
for (const primitive of primitives) {
|
|
1550
|
+
const part = partOf2(primitive.material);
|
|
1551
|
+
const offset = positions.length / 3;
|
|
1552
|
+
const count2 = primitive.positions.length / 3;
|
|
1553
|
+
positions.push(...primitive.positions);
|
|
1554
|
+
for (let vertex = 0; vertex < count2; vertex += 1) {
|
|
1555
|
+
parts.push(part);
|
|
1556
|
+
normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
|
|
1557
|
+
const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
|
|
1558
|
+
luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
1559
|
+
}
|
|
1560
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
1561
|
+
}
|
|
1562
|
+
const count = positions.length / 3;
|
|
1563
|
+
const triangleCount = indices.length / 3;
|
|
1564
|
+
const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
|
|
1565
|
+
if (triangleCount > limit)
|
|
1566
|
+
throw new TileflowGltfError(
|
|
1567
|
+
`has ${triangleCount.toLocaleString("en")} triangles; a ${level} tree model takes at most ${limit.toLocaleString("en")}. Simplify it (for example with Blender's Decimate modifier) and export it again`
|
|
1568
|
+
);
|
|
1569
|
+
if (count > tileflowTreeModelLimits.vertices)
|
|
1570
|
+
throw new TileflowGltfError(
|
|
1571
|
+
`has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
|
|
1572
|
+
);
|
|
1573
|
+
let height = 0;
|
|
1574
|
+
let reach = 0;
|
|
1575
|
+
const crowned = parts.some((part) => isCrown(part));
|
|
1576
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1577
|
+
height = Math.max(height, positions[vertex * 3 + 2]);
|
|
1578
|
+
if (!crowned || isCrown(parts[vertex]))
|
|
1579
|
+
reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
|
|
1580
|
+
}
|
|
1581
|
+
if (!(height > 0))
|
|
1582
|
+
throw new TileflowGltfError(
|
|
1583
|
+
"stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
|
|
1584
|
+
);
|
|
1585
|
+
if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
|
|
1586
|
+
const across = 2 * reach;
|
|
1587
|
+
const unit = [];
|
|
1588
|
+
for (let vertex = 0; vertex < count; vertex += 1)
|
|
1589
|
+
unit.push(
|
|
1590
|
+
positions[vertex * 3] / across,
|
|
1591
|
+
positions[vertex * 3 + 1] / across,
|
|
1592
|
+
positions[vertex * 3 + 2] / height
|
|
1593
|
+
);
|
|
1594
|
+
const faceSums = new Array(count * 3).fill(0);
|
|
1595
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1596
|
+
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
1597
|
+
const n = cross2(sub2(point2(unit, b), point2(unit, a)), sub2(point2(unit, c), point2(unit, a)));
|
|
1598
|
+
for (const vertex of [a, b, c])
|
|
1599
|
+
for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
|
|
1600
|
+
}
|
|
1601
|
+
const outNormals = [];
|
|
1602
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1603
|
+
const own = normals[vertex];
|
|
1604
|
+
const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
|
|
1605
|
+
const size = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
1606
|
+
outNormals.push(n[0] / size, n[1] / size, n[2] / size);
|
|
1607
|
+
}
|
|
1608
|
+
const shades = new Array(count).fill(0);
|
|
1609
|
+
for (const part of new Set(parts)) {
|
|
1610
|
+
const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
|
|
1611
|
+
const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
|
|
1612
|
+
const spread = Math.max(0.06, mean * 0.6);
|
|
1613
|
+
for (const vertex of members)
|
|
1614
|
+
shades[vertex] = clamp2((luminance[vertex] - mean) / spread, -1, 1);
|
|
1615
|
+
}
|
|
1616
|
+
let crownLow = 1;
|
|
1617
|
+
let crownHigh = 0;
|
|
1618
|
+
for (let vertex = 0; vertex < count; vertex += 1)
|
|
1619
|
+
if (isCrown(parts[vertex])) {
|
|
1620
|
+
crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
|
|
1621
|
+
crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
|
|
1622
|
+
}
|
|
1623
|
+
const occlusion = [];
|
|
1624
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
1625
|
+
const z = unit[vertex * 3 + 2];
|
|
1626
|
+
if (!isCrown(parts[vertex])) {
|
|
1627
|
+
occlusion.push(z > crownLow ? 0.55 : 0.8);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
|
|
1631
|
+
const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
|
|
1632
|
+
occlusion.push(clamp2(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
|
|
1633
|
+
}
|
|
1634
|
+
return {
|
|
1635
|
+
// The author's proportions: the trees it stands keep them around the sizes the data gives.
|
|
1636
|
+
proportion: Math.min(3, Math.max(0.1, across / height)),
|
|
1637
|
+
positions: unit,
|
|
1638
|
+
normals: outNormals,
|
|
1639
|
+
parts,
|
|
1640
|
+
shades,
|
|
1641
|
+
occlusion,
|
|
1642
|
+
indices
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function partOf2(material) {
|
|
1646
|
+
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
1647
|
+
const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
|
|
1648
|
+
if (part) return tileflowTreeParts[part];
|
|
1649
|
+
throw new TileflowGltfError(
|
|
1650
|
+
material === void 0 ? "has a mesh without a material; name each material bark, leaves, fronds or stem" : `has a material "${material}" that names no part of a tree; name each material bark, leaves, fronds or stem`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
function isCrown(part) {
|
|
1654
|
+
return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
|
|
1655
|
+
}
|
|
1656
|
+
function point2(list, vertex) {
|
|
1657
|
+
return [list[vertex * 3], list[vertex * 3 + 1], list[vertex * 3 + 2]];
|
|
1658
|
+
}
|
|
1659
|
+
function sub2(a, b) {
|
|
1660
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1661
|
+
}
|
|
1662
|
+
function cross2(a, b) {
|
|
1663
|
+
return [
|
|
1664
|
+
a[1] * b[2] - a[2] * b[1],
|
|
1665
|
+
a[2] * b[0] - a[0] * b[2],
|
|
1666
|
+
a[0] * b[1] - a[1] * b[0]
|
|
1667
|
+
];
|
|
1668
|
+
}
|
|
1669
|
+
function clamp2(value, low, high) {
|
|
1670
|
+
return Math.min(high, Math.max(low, value));
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/tree-models.ts
|
|
1674
|
+
var TileflowTreeModelError = class extends Error {
|
|
1675
|
+
code = "TREE_MODEL_INVALID";
|
|
1676
|
+
issues;
|
|
1677
|
+
phase = "tree-model-preparation";
|
|
1678
|
+
constructor(issues) {
|
|
1679
|
+
super(
|
|
1680
|
+
[
|
|
1681
|
+
"Invalid Tileflow tree models",
|
|
1682
|
+
...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
1683
|
+
].join("\n")
|
|
1684
|
+
);
|
|
1685
|
+
this.name = "TileflowTreeModelError";
|
|
1686
|
+
this.issues = issues;
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
var tileflowBuildingModelLimit = 64;
|
|
1690
|
+
async function prepareTileflowBuildingModel(id, levels) {
|
|
1691
|
+
const model = buildingModelEntry(id, levels);
|
|
1692
|
+
await MeshoptEncoder.ready;
|
|
1693
|
+
const bytes = encodeTileflowModels([model], MeshoptEncoder);
|
|
1694
|
+
return { bytes, sha256: sha256Of(bytes) };
|
|
1695
|
+
}
|
|
1696
|
+
async function packageTileflowLandmarkTile(landmarks) {
|
|
1697
|
+
if (landmarks.length === 0) throw new RangeError("A landmark tile holds at least one landmark.");
|
|
1698
|
+
if (new Set(landmarks.map((landmark) => landmark.id)).size !== landmarks.length)
|
|
1699
|
+
throw new RangeError("Every landmark has an id of its own.");
|
|
1700
|
+
let tile;
|
|
1701
|
+
const models = [];
|
|
1702
|
+
for (const landmark of [...landmarks].sort((a, b) => compareCodeUnits4(a.id, b.id))) {
|
|
1703
|
+
if (!(landmark.heading >= 0 && landmark.heading < 360))
|
|
1704
|
+
throw new RangeError(`Landmark ${landmark.id} is turned 0 to 360 degrees.`);
|
|
1705
|
+
let own;
|
|
1706
|
+
try {
|
|
1707
|
+
own = tileflowLandmarkTileOf(landmark.at);
|
|
1708
|
+
} catch {
|
|
1709
|
+
throw new RangeError(`Landmark ${landmark.id} stands at no place on the map.`);
|
|
1710
|
+
}
|
|
1711
|
+
tile ??= own;
|
|
1712
|
+
if (own[0] !== tile[0] || own[1] !== tile[1])
|
|
1713
|
+
throw new RangeError(
|
|
1714
|
+
`Landmark ${landmark.id} stands in tile ${own.join("/")}, not ${tile.join("/")}.`
|
|
1715
|
+
);
|
|
1716
|
+
let model;
|
|
1717
|
+
try {
|
|
1718
|
+
model = buildingModelEntry(landmark.id, landmark.levels);
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
throw new RangeError(`Landmark ${landmark.id}: ${error.message}`);
|
|
1721
|
+
}
|
|
1722
|
+
models.push({ ...model, at: [landmark.at[0], landmark.at[1]], heading: landmark.heading });
|
|
1723
|
+
}
|
|
1724
|
+
await MeshoptEncoder.ready;
|
|
1725
|
+
const bytes = encodeTileflowModels(models, MeshoptEncoder);
|
|
1726
|
+
return { x: tile[0], y: tile[1], bytes, sha256: sha256Of(bytes) };
|
|
1727
|
+
}
|
|
1728
|
+
function encodeTileflowLandmarkRegion(region) {
|
|
1729
|
+
const bytes = serializeTileflowLandmarkRegion(region);
|
|
1730
|
+
return { bytes, sha256: sha256Of(bytes) };
|
|
1731
|
+
}
|
|
1732
|
+
function buildingModelEntry(id, levels) {
|
|
1733
|
+
const primitives = readTileflowGltfTriangles(levels.near);
|
|
1734
|
+
if (!isTileflowBuildingModel(primitives))
|
|
1735
|
+
throw new TileflowGltfError(
|
|
1736
|
+
"is not a building model; name each material surface, secondary, glass, accent or roof"
|
|
1737
|
+
);
|
|
1738
|
+
const meshes = [prepareTileflowBuildingModelMesh(primitives)];
|
|
1739
|
+
for (const level of [levels.mid, levels.far])
|
|
1740
|
+
if (level) meshes.push(prepareTileflowBuildingModelMesh(level));
|
|
1741
|
+
return {
|
|
1742
|
+
id,
|
|
1743
|
+
levels: meshes,
|
|
1744
|
+
footprint: tileflowBuildingModelFootprint(
|
|
1745
|
+
tileflowBuildingModelDeclaredFootprint(primitives) ?? meshes[0]
|
|
1746
|
+
)
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function sha256Of(bytes) {
|
|
1750
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1751
|
+
}
|
|
1752
|
+
var fileLimit = 8 * 1024 * 1024;
|
|
1753
|
+
var modelId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1754
|
+
async function prepareTileflowMapTreeModels(directories, options) {
|
|
1755
|
+
const resolved = await resolveTileflowAssetDirectories(directories, {
|
|
1756
|
+
baseDirectory: options.baseDirectory,
|
|
1757
|
+
configPath: options.configPath,
|
|
1758
|
+
cwd: options.cwd,
|
|
1759
|
+
kind: "models",
|
|
1760
|
+
target: options.target
|
|
1761
|
+
});
|
|
1762
|
+
const issues = [];
|
|
1763
|
+
const files = /* @__PURE__ */ new Map();
|
|
1764
|
+
for (const directory of resolved) {
|
|
1765
|
+
let names;
|
|
1766
|
+
try {
|
|
1767
|
+
names = (await readdir2(directory.realPath)).sort();
|
|
1768
|
+
} catch {
|
|
1769
|
+
issues.push({ message: "is not a readable directory", path: directory.configPath });
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
for (const name of names) {
|
|
1773
|
+
const display = `${directory.configPath}/${name}`;
|
|
1774
|
+
const lower = name.toLowerCase();
|
|
1775
|
+
if (lower.endsWith(".gltf")) {
|
|
1776
|
+
issues.push({
|
|
1777
|
+
message: "is a text glTF; export the model as a glTF binary (.glb)",
|
|
1778
|
+
path: display
|
|
1779
|
+
});
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
if (!lower.endsWith(".glb")) continue;
|
|
1783
|
+
const level = name.endsWith(".far.glb") ? "far" : name.endsWith(".mid.glb") ? "mid" : "near";
|
|
1784
|
+
const id = name.slice(0, name.length - (level === "near" ? 4 : 8));
|
|
1785
|
+
if (!modelId.test(id) || id.length > 64) {
|
|
1786
|
+
issues.push({
|
|
1787
|
+
message: "a model's file name is its id: lowercase letters, digits and hyphens, up to 64, such as royal-palm.glb",
|
|
1788
|
+
path: display
|
|
1789
|
+
});
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const path = join3(directory.realPath, name);
|
|
1793
|
+
const info = await lstat3(path).catch(() => void 0);
|
|
1794
|
+
if (!info?.isFile()) {
|
|
1795
|
+
issues.push({ message: "is not a regular file", path: display });
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
if (info.size > fileLimit) {
|
|
1799
|
+
issues.push({ message: "is larger than 8 MiB", path: display });
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
const entry = files.get(id) ?? {};
|
|
1803
|
+
entry[level] = { path, display };
|
|
1804
|
+
files.set(id, entry);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const models = {};
|
|
1808
|
+
const buildings = {};
|
|
1809
|
+
const identities = [];
|
|
1810
|
+
for (const [id, entry] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
1811
|
+
if (!entry.near) {
|
|
1812
|
+
issues.push({ message: `has no ${id}.glb beside it`, path: (entry.mid ?? entry.far).display });
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
let nearBytes;
|
|
1816
|
+
let primitives;
|
|
1817
|
+
try {
|
|
1818
|
+
nearBytes = new Uint8Array(await readFile(entry.near.path));
|
|
1819
|
+
primitives = readTileflowGltfTriangles(nearBytes);
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
issues.push({
|
|
1822
|
+
message: error instanceof TileflowGltfError ? error.message : "cannot be read",
|
|
1823
|
+
path: entry.near.display
|
|
1824
|
+
});
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
if (isTileflowBuildingModel(primitives)) {
|
|
1828
|
+
const files2 = { near: nearBytes };
|
|
1829
|
+
let failing = entry.near;
|
|
1830
|
+
try {
|
|
1831
|
+
for (const level of ["near", "mid", "far"]) {
|
|
1832
|
+
const file = entry[level];
|
|
1833
|
+
if (!file) continue;
|
|
1834
|
+
failing = file;
|
|
1835
|
+
const bytes = level === "near" ? nearBytes : new Uint8Array(await readFile(file.path));
|
|
1836
|
+
files2[level] = bytes;
|
|
1837
|
+
if (level !== "near") prepareTileflowBuildingModelMesh(bytes);
|
|
1838
|
+
identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
|
|
1839
|
+
}
|
|
1840
|
+
failing = entry.near;
|
|
1841
|
+
const prepared2 = await prepareTileflowBuildingModel(id, files2);
|
|
1842
|
+
buildings[id] = {
|
|
1843
|
+
fileName: `${id}-${prepared2.sha256.slice(0, 16)}.glb`,
|
|
1844
|
+
bytes: prepared2.bytes
|
|
1845
|
+
};
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
issues.push({
|
|
1848
|
+
message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
|
|
1849
|
+
path: failing.display
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (entry.mid) {
|
|
1855
|
+
issues.push({
|
|
1856
|
+
message: "is a tree model; trees have a near model and a far one (<id>.far.glb), not a mid one",
|
|
1857
|
+
path: entry.mid.display
|
|
1858
|
+
});
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
const prepared = {};
|
|
1862
|
+
for (const level of ["near", "far"]) {
|
|
1863
|
+
const file = entry[level];
|
|
1864
|
+
if (!file) continue;
|
|
1865
|
+
try {
|
|
1866
|
+
const bytes = new Uint8Array(await readFile(file.path));
|
|
1867
|
+
prepared[level] = encodeTileflowTreeModel(
|
|
1868
|
+
prepareTileflowTreeModelMesh(bytes, level),
|
|
1869
|
+
level
|
|
1870
|
+
);
|
|
1871
|
+
identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
issues.push({
|
|
1874
|
+
message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
|
|
1875
|
+
path: file.display
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (prepared.near)
|
|
1880
|
+
models[id] = { near: prepared.near, ...prepared.far ? { far: prepared.far } : {} };
|
|
1881
|
+
}
|
|
1882
|
+
if (Object.keys(models).length > tileflowTreeModelLimits2.models)
|
|
1883
|
+
issues.push({
|
|
1884
|
+
message: `holds ${Object.keys(models).length} tree models; a map takes at most ${tileflowTreeModelLimits2.models}`,
|
|
1885
|
+
path: options.configPath
|
|
1886
|
+
});
|
|
1887
|
+
if (Object.keys(buildings).length > tileflowBuildingModelLimit)
|
|
1888
|
+
issues.push({
|
|
1889
|
+
message: `holds ${Object.keys(buildings).length} building models; a map takes at most ${tileflowBuildingModelLimit}`,
|
|
1890
|
+
path: options.configPath
|
|
1891
|
+
});
|
|
1892
|
+
if (issues.length > 0) throw new TileflowTreeModelError(issues);
|
|
1893
|
+
return {
|
|
1894
|
+
models,
|
|
1895
|
+
buildings,
|
|
1896
|
+
identities,
|
|
1897
|
+
watchPaths: resolved.filter((directory) => directory.watch).map((directory) => directory.realPath)
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
942
1901
|
// src/icon-preview.ts
|
|
943
1902
|
import { renderTileflowIconAtlasPixels } from "@tileflow/core";
|
|
944
1903
|
async function renderTileflowIconAtlasPreview(source, index) {
|
|
@@ -1005,19 +1964,19 @@ async function compileTileflowIconPackages(project, options) {
|
|
|
1005
1964
|
bindings,
|
|
1006
1965
|
compositions,
|
|
1007
1966
|
packages: [...packagesByHash.values()].sort(
|
|
1008
|
-
(left, right) =>
|
|
1967
|
+
(left, right) => compareCodeUnits5(left.contentHash, right.contentHash)
|
|
1009
1968
|
),
|
|
1010
1969
|
sourceIdentities,
|
|
1011
1970
|
watchFiles: uniqueStrings(
|
|
1012
1971
|
result.mapRequests.flatMap(
|
|
1013
1972
|
(request) => result.composedBySequence.get(request.sequenceKey)?.watchFiles ?? []
|
|
1014
1973
|
)
|
|
1015
|
-
).sort(
|
|
1974
|
+
).sort(compareCodeUnits5),
|
|
1016
1975
|
watchPaths: uniqueStrings(
|
|
1017
1976
|
result.mapRequests.flatMap(
|
|
1018
1977
|
(request) => result.composedBySequence.get(request.sequenceKey)?.watchPaths ?? []
|
|
1019
1978
|
)
|
|
1020
|
-
).sort(
|
|
1979
|
+
).sort(compareCodeUnits5)
|
|
1021
1980
|
};
|
|
1022
1981
|
}
|
|
1023
1982
|
async function inspectTileflowIconCatalogs(project, options) {
|
|
@@ -1110,7 +2069,7 @@ async function inspectTileflowIconCatalogs(project, options) {
|
|
|
1110
2069
|
)
|
|
1111
2070
|
);
|
|
1112
2071
|
catalogs.sort(
|
|
1113
|
-
(left, right) =>
|
|
2072
|
+
(left, right) => compareCodeUnits5(
|
|
1114
2073
|
left.contributors.map((contributor) => contributor.label).join("\0"),
|
|
1115
2074
|
right.contributors.map((contributor) => contributor.label).join("\0")
|
|
1116
2075
|
)
|
|
@@ -1162,7 +2121,7 @@ async function readWorkspaceIconSetLock(requests, baseDirectory) {
|
|
|
1162
2121
|
references.add(reference);
|
|
1163
2122
|
}
|
|
1164
2123
|
if (references.size === 0) return void 0;
|
|
1165
|
-
const sources = [...references].sort(
|
|
2124
|
+
const sources = [...references].sort(compareCodeUnits5).map((reference) => ({ kind: "icon-set", reference }));
|
|
1166
2125
|
return readTileflowIconsLockfile(baseDirectory, sources);
|
|
1167
2126
|
}
|
|
1168
2127
|
function selectMapIconSetLock(lock, sources) {
|
|
@@ -1210,6 +2169,30 @@ async function prepareTileflowCatalogIcons(project, options) {
|
|
|
1210
2169
|
});
|
|
1211
2170
|
}
|
|
1212
2171
|
}
|
|
2172
|
+
const mapModelSources = {};
|
|
2173
|
+
const modelWatchPaths = [];
|
|
2174
|
+
for (const [mapName, map] of Object.entries(project.maps)) {
|
|
2175
|
+
if (!map.models?.length) continue;
|
|
2176
|
+
const models = await prepareTileflowMapTreeModels(map.models, {
|
|
2177
|
+
baseDirectory: options.baseDirectory ?? options.cwd,
|
|
2178
|
+
configPath: `maps.${mapName}.models`,
|
|
2179
|
+
cwd: options.cwd,
|
|
2180
|
+
target: "local"
|
|
2181
|
+
});
|
|
2182
|
+
const buildingModels = {};
|
|
2183
|
+
for (const [id, model] of Object.entries(models.buildings)) {
|
|
2184
|
+
const fileName = `models/${mapName}/${model.fileName}`;
|
|
2185
|
+
assets.push({ contentType: tileflowModelContentType, fileName, source: model.bytes });
|
|
2186
|
+
buildingModels[id] = joinUrl(options.assetBaseUrl, fileName);
|
|
2187
|
+
}
|
|
2188
|
+
mapAssets[mapName] = {
|
|
2189
|
+
...mapAssets[mapName],
|
|
2190
|
+
models: models.models,
|
|
2191
|
+
...Object.keys(buildingModels).length > 0 ? { buildingModels } : {}
|
|
2192
|
+
};
|
|
2193
|
+
mapModelSources[mapName] = models.identities;
|
|
2194
|
+
modelWatchPaths.push(...models.watchPaths);
|
|
2195
|
+
}
|
|
1213
2196
|
return {
|
|
1214
2197
|
assets,
|
|
1215
2198
|
baseDirectory: await realpath3(options.baseDirectory ?? options.cwd),
|
|
@@ -1217,10 +2200,11 @@ async function prepareTileflowCatalogIcons(project, options) {
|
|
|
1217
2200
|
mapAssets,
|
|
1218
2201
|
mapIconCompositions,
|
|
1219
2202
|
mapIconSources,
|
|
2203
|
+
...Object.keys(mapModelSources).length > 0 ? { mapModelSources } : {},
|
|
1220
2204
|
project,
|
|
1221
2205
|
sourceProject: project,
|
|
1222
2206
|
watchFiles: compiled.watchFiles,
|
|
1223
|
-
watchPaths: compiled.watchPaths
|
|
2207
|
+
watchPaths: [...compiled.watchPaths, ...modelWatchPaths]
|
|
1224
2208
|
};
|
|
1225
2209
|
}
|
|
1226
2210
|
async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
|
|
@@ -1246,10 +2230,10 @@ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
|
|
|
1246
2230
|
}
|
|
1247
2231
|
}
|
|
1248
2232
|
}
|
|
1249
|
-
return [...paths].sort(
|
|
2233
|
+
return [...paths].sort(compareCodeUnits5);
|
|
1250
2234
|
}
|
|
1251
2235
|
function getMapIconRequests(project, mapNames) {
|
|
1252
|
-
const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(
|
|
2236
|
+
const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits5);
|
|
1253
2237
|
return selectedNames.map((mapName) => {
|
|
1254
2238
|
const mapConfig = project.maps[mapName];
|
|
1255
2239
|
if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
|
|
@@ -1259,7 +2243,7 @@ function getMapIconRequests(project, mapNames) {
|
|
|
1259
2243
|
}
|
|
1260
2244
|
function uniqueSequences(requests) {
|
|
1261
2245
|
return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
|
|
1262
|
-
(left, right) =>
|
|
2246
|
+
(left, right) => compareCodeUnits5(left.sequenceKey, right.sequenceKey)
|
|
1263
2247
|
);
|
|
1264
2248
|
}
|
|
1265
2249
|
function describeComposedContributor(contributor) {
|
|
@@ -1312,7 +2296,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1312
2296
|
let entries;
|
|
1313
2297
|
const issueCount = issues.length;
|
|
1314
2298
|
try {
|
|
1315
|
-
entries = await
|
|
2299
|
+
entries = await readdir3(directory.realPath, { withFileTypes: true });
|
|
1316
2300
|
} catch (error) {
|
|
1317
2301
|
issues.push({
|
|
1318
2302
|
message: describeFileSystemError(error, "Icon source could not be read"),
|
|
@@ -1320,7 +2304,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1320
2304
|
});
|
|
1321
2305
|
return null;
|
|
1322
2306
|
}
|
|
1323
|
-
entries.sort((left, right) =>
|
|
2307
|
+
entries.sort((left, right) => compareCodeUnits5(left.name, right.name));
|
|
1324
2308
|
const candidates = [];
|
|
1325
2309
|
const names = /* @__PURE__ */ new Set();
|
|
1326
2310
|
let metadata = {
|
|
@@ -1360,7 +2344,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1360
2344
|
throw new Error("Icon metadata exceeds its byte limit");
|
|
1361
2345
|
metadata = tileflowIconAuthorMetadataSchema.parse(
|
|
1362
2346
|
parseTileflowIconJson2(
|
|
1363
|
-
await
|
|
2347
|
+
await readFile2(realEntryPath, "utf8"),
|
|
1364
2348
|
tileflowIconPackageLimits4.maxSourceFileBytes
|
|
1365
2349
|
)
|
|
1366
2350
|
);
|
|
@@ -1408,7 +2392,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1408
2392
|
size: entryStat.size
|
|
1409
2393
|
});
|
|
1410
2394
|
}
|
|
1411
|
-
candidates.sort((left, right) =>
|
|
2395
|
+
candidates.sort((left, right) => compareCodeUnits5(left.name, right.name));
|
|
1412
2396
|
for (const id of Object.keys(metadata.icons)) {
|
|
1413
2397
|
if (!names.has(id))
|
|
1414
2398
|
issues.push({
|
|
@@ -1429,7 +2413,7 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1429
2413
|
for (const candidate of candidates) {
|
|
1430
2414
|
const displayPath = candidate.displayPath;
|
|
1431
2415
|
try {
|
|
1432
|
-
const source = await
|
|
2416
|
+
const source = await readFile2(candidate.path);
|
|
1433
2417
|
if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits4.maxSourceFileBytes) {
|
|
1434
2418
|
throw new Error("Icon changed while it was being validated");
|
|
1435
2419
|
}
|
|
@@ -1813,6 +2797,10 @@ export {
|
|
|
1813
2797
|
readTileflowIconsLockfile,
|
|
1814
2798
|
writeTileflowIconsLockfile,
|
|
1815
2799
|
composeTileflowIconSources,
|
|
2800
|
+
prepareTileflowBuildingModel,
|
|
2801
|
+
packageTileflowLandmarkTile,
|
|
2802
|
+
encodeTileflowLandmarkRegion,
|
|
2803
|
+
prepareTileflowMapTreeModels,
|
|
1816
2804
|
renderTileflowIconAtlasPreview,
|
|
1817
2805
|
TileflowIconCompilationError,
|
|
1818
2806
|
iconFileExtensions,
|