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