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