@tileflow/dev 0.1.0-alpha.4 → 0.1.0-alpha.41

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.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +264 -5
  3. package/THIRD_PARTY_NOTICES.md +309 -0
  4. package/dist/artifacts.d.ts +269 -0
  5. package/dist/artifacts.js +98 -0
  6. package/dist/chunk-AQUGMUZW.js +324 -0
  7. package/dist/chunk-C4VPHZ3F.js +4062 -0
  8. package/dist/chunk-D4OE4ITL.js +13 -0
  9. package/dist/chunk-FOMTSCOP.js +671 -0
  10. package/dist/chunk-GXLY73PD.js +3199 -0
  11. package/dist/chunk-KDFTE4Q5.js +156 -0
  12. package/dist/chunk-ONYQNLNA.js +1108 -0
  13. package/dist/chunk-RTU7TD4M.js +172 -0
  14. package/dist/chunk-XHJBCVBB.js +241 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.js +27 -0
  17. package/dist/fonts.d.ts +47 -0
  18. package/dist/fonts.js +16 -0
  19. package/dist/icons-BIHpw5Zy.d.ts +417 -0
  20. package/dist/icons.d.ts +3 -0
  21. package/dist/icons.js +41 -0
  22. package/dist/inspect.d.ts +149 -0
  23. package/dist/inspect.js +805 -0
  24. package/dist/inspector-browser.js +16955 -0
  25. package/dist/inspector-observer.js +95 -0
  26. package/dist/preview-assets-ATLLYY72.js +103 -0
  27. package/dist/preview.d.ts +31 -0
  28. package/dist/preview.js +1974 -0
  29. package/dist/server.d.ts +89 -0
  30. package/dist/server.js +3662 -0
  31. package/dist/tilesets.d.ts +89 -0
  32. package/dist/tilesets.js +6 -0
  33. package/dist/validation.d.ts +65 -0
  34. package/dist/validation.js +24 -0
  35. package/package.json +77 -13
  36. package/ui-fonts/hubot-sans-OFL.txt +93 -0
  37. package/ui-fonts/hubot-sans.woff2 +0 -0
  38. package/ui-fonts/inter-OFL.txt +92 -0
  39. package/ui-fonts/inter.woff2 +0 -0
  40. package/ui-fonts/jetbrains-mono-OFL.txt +93 -0
  41. package/ui-fonts/jetbrains-mono.woff2 +0 -0
  42. package/ui-fonts/mona-sans-OFL.txt +93 -0
  43. package/ui-fonts/mona-sans.woff2 +0 -0
  44. package/dist/index.d.ts +0 -54
  45. package/dist/index.js +0 -314
@@ -0,0 +1,4062 @@
1
+ import {
2
+ TileflowAssetDirectoryError,
3
+ resolveTileflowAssetDirectories
4
+ } from "./chunk-RTU7TD4M.js";
5
+
6
+ // src/icons.ts
7
+ import { readdir as readdir3, readFile as readFile2, realpath as realpath3, stat } from "fs/promises";
8
+ import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath, sep as sep2 } from "path";
9
+ import { SaxesParser } from "saxes";
10
+ import {
11
+ assertTileflowIconLayoutBounds as assertTileflowIconLayoutBounds2,
12
+ collectTileflowIconSetReferences as collectTileflowIconSetReferences2,
13
+ tileflowIconAuthorMetadataSchema,
14
+ tileflowIconMetadataFileName,
15
+ TileflowIconSetError as TileflowIconSetError5,
16
+ tileflowIconsLockfileName as tileflowIconsLockfileName3,
17
+ tileflowIconSpriteIndexSchema
18
+ } from "@tileflow/core";
19
+ import {
20
+ compareCodeUnits as compareCodeUnits5,
21
+ hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels3,
22
+ parseResolvedTileflowMap,
23
+ parseTileflowIconJson as parseTileflowIconJson2,
24
+ serializeCanonicalJson as serializeCanonicalJson3,
25
+ sha256Hex as sha256Hex3,
26
+ tileflowIconIdPattern,
27
+ tileflowIconPackageLimits as tileflowIconPackageLimits4,
28
+ tileflowModelContentType
29
+ } from "@tileflow/core";
30
+
31
+ // src/icon-composition.ts
32
+ import { realpath as realpath2 } from "fs/promises";
33
+ import { isAbsolute, relative, resolve as resolve3, sep } from "path";
34
+ import { tileflowIconsLockfileName as tileflowIconsLockfileName2 } from "@tileflow/core";
35
+ import {
36
+ collectTileflowIconSetReferences,
37
+ compareCodeUnits as compareCodeUnits3,
38
+ parseTileflowIconComposition,
39
+ parseTileflowIconsLockfile as parseTileflowIconsLockfile2,
40
+ tileflowIconPackageLimits as tileflowIconPackageLimits3,
41
+ TileflowIconSetError as TileflowIconSetError4
42
+ } from "@tileflow/core";
43
+
44
+ // src/icon-cache.ts
45
+ import { hashTileflowIconPackageManifest as hashTileflowIconPackageManifest3 } from "@tileflow/core";
46
+ import { constants } from "fs";
47
+ import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm } from "fs/promises";
48
+ import { homedir, platform } from "os";
49
+ import { join, resolve } from "path";
50
+ import {
51
+ TileflowIconSetError as TileflowIconSetError2,
52
+ tileflowIconSetPinSchema
53
+ } from "@tileflow/core";
54
+
55
+ // src/icon-artifact.ts
56
+ import {
57
+ tileflowIconSpriteIndexSchema as indexSchema
58
+ } from "@tileflow/core";
59
+ import {
60
+ assertTileflowSdfPixels as assertTileflowSdfPixels2,
61
+ compareCodeUnits as compareCodeUnits2,
62
+ hashTileflowIconPackageManifest as hashTileflowIconPackageManifest2,
63
+ hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels2,
64
+ parseTileflowIconJson,
65
+ readTileflowIconLayout,
66
+ scaleTileflowIconLayout as scaleTileflowIconLayout2,
67
+ serializeCanonicalJson as serializeCanonicalJson2,
68
+ sha256Hex as sha256Hex2,
69
+ tileflowIconPackageLimits as tileflowIconPackageLimits2,
70
+ tileflowIconPackageManifestSchema as tileflowIconPackageManifestSchema2,
71
+ TileflowIconSetError
72
+ } from "@tileflow/core";
73
+
74
+ // src/icon-sprite.ts
75
+ import {
76
+ assertTileflowIconLayoutBounds,
77
+ assertTileflowSdfPixels,
78
+ compareCodeUnits,
79
+ hashTileflowIconPackageManifest,
80
+ hashTileflowRenderedIconPixels,
81
+ scaleTileflowIconLayout,
82
+ serializeCanonicalJson,
83
+ sha256Hex,
84
+ tileflowIconIdSchema,
85
+ tileflowIconPackageLimits,
86
+ tileflowIconPackageManifestSchema
87
+ } from "@tileflow/core";
88
+ async function packTileflowRenderedIcons(input) {
89
+ const icons = [...input].sort((left, right) => compareCodeUnits(left.id, right.id));
90
+ if (icons.length < 1 || icons.length > tileflowIconPackageLimits.maxIconCount || new Set(icons.map((icon) => icon.id)).size !== icons.length)
91
+ throw new Error(
92
+ `Expected 1 through ${tileflowIconPackageLimits.maxIconCount} unique rendered icons`
93
+ );
94
+ const renderedIcons = [];
95
+ for (const icon of icons) {
96
+ tileflowIconIdSchema.parse(icon.id);
97
+ if (icon.layout) assertTileflowIconLayoutBounds(icon.layout, icon.oneX.width, icon.oneX.height);
98
+ if (icon.appearance) {
99
+ assertTileflowSdfPixels({ ...icon.oneX, pixelRatio: 1 });
100
+ assertTileflowSdfPixels({ ...icon.twoX, pixelRatio: 2 });
101
+ }
102
+ if (icon.twoX.width !== icon.oneX.width * 2 || icon.twoX.height !== icon.oneX.height * 2)
103
+ throw new Error("Rendered density geometry must double exactly");
104
+ renderedIcons.push({
105
+ name: icon.id,
106
+ ...icon.appearance ? { appearance: icon.appearance } : {},
107
+ ...icon.layout ? { layout: icon.layout } : {},
108
+ pixelSha256: {
109
+ oneX: await hashTileflowRenderedIconPixels({ ...icon.oneX, pixelRatio: 1 }),
110
+ twoX: await hashTileflowRenderedIconPixels({ ...icon.twoX, pixelRatio: 2 })
111
+ }
112
+ });
113
+ }
114
+ const oneX = icons.map((icon) => ({
115
+ ...icon.oneX,
116
+ name: icon.id,
117
+ appearance: icon.appearance,
118
+ layout: icon.layout
119
+ }));
120
+ const twoX = icons.map((icon) => ({
121
+ ...icon.twoX,
122
+ name: icon.id,
123
+ appearance: icon.appearance,
124
+ layout: icon.layout
125
+ }));
126
+ const layoutOneX = createSpriteLayout(oneX, 1);
127
+ const layoutTwoX = createSpriteLayout(twoX, 2);
128
+ if (layoutTwoX.width !== layoutOneX.width * 2 || layoutTwoX.height !== layoutOneX.height * 2)
129
+ throw new Error("Composed atlas exceeds the paired density geometry limit");
130
+ const files = [
131
+ {
132
+ contentType: "application/json",
133
+ fileName: "sprite.json",
134
+ source: new TextEncoder().encode(`${serializeCanonicalJson(layoutOneX.index)}
135
+ `)
136
+ },
137
+ {
138
+ contentType: "image/png",
139
+ fileName: "sprite.png",
140
+ source: await createSpriteImage(oneX, layoutOneX)
141
+ },
142
+ {
143
+ contentType: "application/json",
144
+ fileName: "sprite@2x.json",
145
+ source: new TextEncoder().encode(`${serializeCanonicalJson(layoutTwoX.index)}
146
+ `)
147
+ },
148
+ {
149
+ contentType: "image/png",
150
+ fileName: "sprite@2x.png",
151
+ source: await createSpriteImage(twoX, layoutTwoX)
152
+ }
153
+ ];
154
+ assertGeneratedFileLimits(files);
155
+ const manifest = tileflowIconPackageManifestSchema.parse({
156
+ format: icons.some((icon) => icon.appearance || icon.layout) ? "tileflow-icon-package-v2" : "tileflow-icon-package-v1",
157
+ files: await Promise.all(
158
+ files.map(async (file) => ({
159
+ name: file.fileName,
160
+ byteLength: file.source.byteLength,
161
+ contentType: file.contentType,
162
+ sha256: await sha256Hex(file.source)
163
+ }))
164
+ ),
165
+ iconNames: icons.map((icon) => icon.id),
166
+ renderedIcons,
167
+ sprites: {
168
+ oneX: { width: layoutOneX.width, height: layoutOneX.height, pixelRatio: 1 },
169
+ twoX: { width: layoutTwoX.width, height: layoutTwoX.height, pixelRatio: 2 }
170
+ }
171
+ });
172
+ return { contentHash: await hashTileflowIconPackageManifest(manifest), manifest, files };
173
+ }
174
+ function createSpriteLayout(icons, pixelRatio) {
175
+ const columns = Math.ceil(Math.sqrt(icons.length));
176
+ const widest = Math.max(...icons.map((icon) => icon.width));
177
+ const maxDimension = tileflowIconPackageLimits.maxAtlasDimension / 2 * pixelRatio;
178
+ const targetWidth = Math.min(maxDimension, columns * widest);
179
+ const placements = [];
180
+ let left = 0;
181
+ let rowHeight = 0;
182
+ let top = 0;
183
+ for (const icon of icons) {
184
+ if (icon.width > maxDimension)
185
+ throw new Error(
186
+ `Generated sprite exceeds ${maxDimension} pixels per dimension at ${pixelRatio}x`
187
+ );
188
+ if (left > 0 && left + icon.width > targetWidth) {
189
+ top += rowHeight;
190
+ left = 0;
191
+ rowHeight = 0;
192
+ }
193
+ placements.push({ left, top });
194
+ left += icon.width;
195
+ rowHeight = Math.max(rowHeight, icon.height);
196
+ }
197
+ const width = targetWidth;
198
+ const height = top + rowHeight;
199
+ if (width > maxDimension || height > maxDimension)
200
+ throw new Error(
201
+ `Generated sprite exceeds ${maxDimension} pixels per dimension at ${pixelRatio}x`
202
+ );
203
+ const index = Object.fromEntries(
204
+ icons.map((icon, ordinal) => {
205
+ const placement = placements[ordinal];
206
+ return [
207
+ icon.name,
208
+ {
209
+ height: icon.height,
210
+ pixelRatio,
211
+ width: icon.width,
212
+ x: placement.left,
213
+ y: placement.top,
214
+ ...icon.appearance ? { sdf: true, tileflow: icon.appearance } : {},
215
+ ...icon.layout ? scaleTileflowIconLayout(icon.layout, pixelRatio) : {}
216
+ }
217
+ ];
218
+ })
219
+ );
220
+ return { height, index, width };
221
+ }
222
+ async function createSpriteImage(icons, layout) {
223
+ const sharp = await loadSharp();
224
+ const atlas = new Uint8Array(layout.width * layout.height * 4);
225
+ for (const icon of icons) {
226
+ const placement = layout.index[icon.name];
227
+ if (!placement || icon.width !== placement.width || icon.height !== placement.height || icon.rgba.byteLength !== icon.width * icon.height * 4)
228
+ throw new Error("Rendered icon dimensions do not match the sprite layout");
229
+ const rowBytes = icon.width * 4;
230
+ for (let row = 0; row < icon.height; row += 1) {
231
+ const sourceStart = row * rowBytes;
232
+ const targetStart = ((placement.y + row) * layout.width + placement.x) * 4;
233
+ atlas.set(icon.rgba.subarray(sourceStart, sourceStart + rowBytes), targetStart);
234
+ }
235
+ }
236
+ return sharp(atlas, { raw: { channels: 4, height: layout.height, width: layout.width } }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
237
+ }
238
+ function assertGeneratedFileLimits(files) {
239
+ let totalBytes = 0;
240
+ for (const file of files) {
241
+ if (file.source.byteLength > tileflowIconPackageLimits.maxGeneratedFileBytes)
242
+ throw new Error(
243
+ `${file.fileName} exceeds ${tileflowIconPackageLimits.maxGeneratedFileBytes} generated bytes`
244
+ );
245
+ totalBytes += file.source.byteLength;
246
+ }
247
+ if (totalBytes > tileflowIconPackageLimits.maxGeneratedPackageBytes)
248
+ throw new Error(
249
+ `Generated package exceeds ${tileflowIconPackageLimits.maxGeneratedPackageBytes} bytes`
250
+ );
251
+ }
252
+ async function loadSharp() {
253
+ try {
254
+ return (await import("sharp")).default;
255
+ } catch (cause) {
256
+ throw new Error(
257
+ 'Local icon sprites require the optional "sharp" package. Install sharp or disable local icons.',
258
+ { cause }
259
+ );
260
+ }
261
+ }
262
+
263
+ // src/icon-artifact.ts
264
+ async function verifyTileflowIconArtifact(input) {
265
+ try {
266
+ const manifest = tileflowIconPackageManifestSchema2.parse(input.manifest);
267
+ const contentHash = input.contentHash;
268
+ if (input.files.length !== 4 || new Set(input.files.map((file) => file.fileName)).size !== 4)
269
+ throw new Error("An artifact must contain exactly the four generated files");
270
+ const files = manifest.files.map((expected) => {
271
+ const source = input.files.find((file) => file.fileName === expected.name);
272
+ if (!source || source.contentType !== expected.contentType || source.source.byteLength !== expected.byteLength)
273
+ throw new Error("Generated file identity does not match the manifest");
274
+ return {
275
+ contentType: source.contentType,
276
+ fileName: source.fileName,
277
+ source: new Uint8Array(source.source)
278
+ };
279
+ });
280
+ if (await hashTileflowIconPackageManifest2(manifest) !== contentHash)
281
+ throw new Error("Artifact manifest hash mismatch");
282
+ for (const [index, file] of files.entries()) {
283
+ if (await sha256Hex2(file.source) !== manifest.files[index].sha256)
284
+ throw new Error(`Generated ${file.fileName} checksum mismatch`);
285
+ }
286
+ const decoder = new TextDecoder("utf-8", { fatal: true });
287
+ const oneIndex = indexSchema.parse(
288
+ parseTileflowIconJson(
289
+ decoder.decode(files[0].source),
290
+ tileflowIconPackageLimits2.maxGeneratedFileBytes
291
+ )
292
+ );
293
+ const twoIndex = indexSchema.parse(
294
+ parseTileflowIconJson(
295
+ decoder.decode(files[2].source),
296
+ tileflowIconPackageLimits2.maxGeneratedFileBytes
297
+ )
298
+ );
299
+ for (const index of [oneIndex, twoIndex]) {
300
+ const names = Object.keys(index).sort(compareCodeUnits2);
301
+ if (names.length !== manifest.iconNames.length || names.some((name, ordinal) => name !== manifest.iconNames[ordinal]))
302
+ throw new Error("Sprite index does not exactly match the manifest icon names");
303
+ }
304
+ for (const [index, dimensions, ratio] of [
305
+ [oneIndex, manifest.sprites.oneX, 1],
306
+ [twoIndex, manifest.sprites.twoX, 2]
307
+ ]) {
308
+ const rectangles = Object.values(index);
309
+ for (const [ordinal, rect] of rectangles.entries()) {
310
+ if (rect.pixelRatio !== ratio || rect.x + rect.width > dimensions.width || rect.y + rect.height > dimensions.height)
311
+ throw new Error("Sprite rectangle is outside the atlas or has the wrong density");
312
+ for (const other of rectangles.slice(0, ordinal)) {
313
+ if (rect.x < other.x + other.width && other.x < rect.x + rect.width && rect.y < other.y + other.height && other.y < rect.y + rect.height)
314
+ throw new Error("Sprite rectangles must not overlap");
315
+ }
316
+ }
317
+ }
318
+ for (const name of manifest.iconNames) {
319
+ const one = oneIndex[name];
320
+ const two = twoIndex[name];
321
+ const declared = manifest.renderedIcons.find((icon) => icon.name === name).appearance;
322
+ const declaredLayout = manifest.renderedIcons.find((icon) => icon.name === name).layout;
323
+ const oneLayout = readTileflowIconLayout(one);
324
+ const twoLayout = readTileflowIconLayout(two);
325
+ if (serializeCanonicalJson2(oneLayout ?? null) !== serializeCanonicalJson2(declaredLayout ?? null) || serializeCanonicalJson2(twoLayout ?? null) !== serializeCanonicalJson2(oneLayout ? scaleTileflowIconLayout2(oneLayout, 2) : null))
326
+ throw new Error(
327
+ "Sprite layout must match the manifest and scale exactly between densities"
328
+ );
329
+ if (serializeCanonicalJson2(one.tileflow ?? null) !== serializeCanonicalJson2(two.tileflow ?? null) || serializeCanonicalJson2(one.tileflow ?? null) !== serializeCanonicalJson2(declared ?? null))
330
+ throw new Error("Sprite appearance must match at both densities and in the manifest");
331
+ if (two.x !== one.x * 2 || two.y !== one.y * 2 || two.width !== one.width * 2 || two.height !== one.height * 2)
332
+ throw new Error("Sprite densities must have exactly doubled geometry");
333
+ }
334
+ const oneAtlas = await decodeAtlas(files[1].source, manifest.sprites.oneX);
335
+ const twoAtlas = await decodeAtlas(files[3].source, manifest.sprites.twoX);
336
+ const icons = [];
337
+ for (const [ordinal, id] of manifest.iconNames.entries()) {
338
+ const oneX = extractCell(oneAtlas, oneIndex[id]);
339
+ const twoX = extractCell(twoAtlas, twoIndex[id]);
340
+ const expected = manifest.renderedIcons[ordinal].pixelSha256;
341
+ if (await hashTileflowRenderedIconPixels2({ ...oneX, pixelRatio: 1 }) !== expected.oneX || await hashTileflowRenderedIconPixels2({ ...twoX, pixelRatio: 2 }) !== expected.twoX)
342
+ throw new Error(`Rendered pixels do not match the manifest for ${id}`);
343
+ const appearance = oneIndex[id].tileflow;
344
+ if (appearance) {
345
+ assertTileflowSdfPixels2({ ...oneX, pixelRatio: 1 });
346
+ assertTileflowSdfPixels2({ ...twoX, pixelRatio: 2 });
347
+ }
348
+ const layout = readTileflowIconLayout(oneIndex[id]);
349
+ icons.push({
350
+ id,
351
+ oneX,
352
+ twoX,
353
+ ...appearance ? { appearance } : {},
354
+ ...layout ? { layout } : {}
355
+ });
356
+ }
357
+ return { package: { contentHash, manifest, files }, icons };
358
+ } catch (cause) {
359
+ throw new TileflowIconSetError(
360
+ "ICON_PACKAGE_INTEGRITY",
361
+ "Generated icon artifact verification failed",
362
+ { cause }
363
+ );
364
+ }
365
+ }
366
+ async function decodeAtlas(bytes, dimensions) {
367
+ if ([137, 80, 78, 71, 13, 10, 26, 10].some((byte, index) => bytes[index] !== byte))
368
+ throw new Error("Expected PNG bytes");
369
+ const sharp = await loadSharp();
370
+ const image = sharp(bytes, {
371
+ failOn: "error",
372
+ limitInputPixels: tileflowIconPackageLimits2.maxAtlasDimension ** 2
373
+ });
374
+ const metadata = await image.metadata();
375
+ if (metadata.format !== "png" || (metadata.pages ?? 1) !== 1 || metadata.width !== dimensions.width || metadata.height !== dimensions.height)
376
+ throw new Error("PNG geometry does not match the manifest");
377
+ const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
378
+ if (info.channels !== 4 || info.width !== dimensions.width || info.height !== dimensions.height || data.length !== dimensions.width * dimensions.height * 4)
379
+ throw new Error("Expected bounded RGBA atlas pixels");
380
+ return { ...dimensions, rgba: new Uint8Array(data) };
381
+ }
382
+ function extractCell(atlas, rect) {
383
+ const rgba = new Uint8Array(rect.width * rect.height * 4);
384
+ const rowBytes = rect.width * 4;
385
+ for (let row = 0; row < rect.height; row += 1) {
386
+ const start = ((rect.y + row) * atlas.width + rect.x) * 4;
387
+ rgba.set(atlas.rgba.subarray(start, start + rowBytes), row * rowBytes);
388
+ }
389
+ return { height: rect.height, width: rect.width, rgba };
390
+ }
391
+
392
+ // src/icon-cache.ts
393
+ function getTileflowIconCacheDirectory(options = {}) {
394
+ const defaultRoot = platform() === "darwin" ? join(homedir(), "Library", "Caches") : platform() === "win32" ? process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
395
+ return join(
396
+ resolve(options.cacheRoot ?? process.env.TILEFLOW_ICON_CACHE_DIR ?? defaultRoot),
397
+ "tileflow",
398
+ "icons",
399
+ "v1"
400
+ );
401
+ }
402
+ async function loadTileflowIconSetArtifact(input, options = {}) {
403
+ const pin = tileflowIconSetPinSchema.parse(input);
404
+ if (await hashTileflowIconPackageManifest3(pin.manifest) !== pin.contentHash)
405
+ throw new TileflowIconSetError2("ICON_LOCK_INVALID", "Locked manifest hash mismatch");
406
+ let cacheFailure;
407
+ try {
408
+ return await readCached(pin, options);
409
+ } catch (cause) {
410
+ if (cause instanceof TileflowIconSetError2 && cause.code === "ICON_CACHE_UNSAFE") throw cause;
411
+ cacheFailure = cause;
412
+ }
413
+ if (options.offline) {
414
+ throw new TileflowIconSetError2(
415
+ isMissing(cacheFailure) ? "ICON_CACHE_MISS" : "ICON_PACKAGE_INTEGRITY",
416
+ "The exact verified icon artifact is unavailable offline; hydrate this pin explicitly",
417
+ { cause: cacheFailure }
418
+ );
419
+ }
420
+ const url = new URL(pin.spriteUrl);
421
+ const allowed = options.deliveryOrigins ?? ["https://api.tileflow.dev"];
422
+ if (!allowed.some((origin) => origin === url.origin && new URL(origin).href === `${origin}/`)) {
423
+ throw new TileflowIconSetError2(
424
+ "ICON_DOWNLOAD_FAILED",
425
+ "Icon artifact delivery origin is not trusted"
426
+ );
427
+ }
428
+ const files = [];
429
+ const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4);
430
+ try {
431
+ for (const expected of pin.manifest.files) {
432
+ signal.throwIfAborted();
433
+ const fileUrl = `${pin.spriteUrl}${expected.name.slice("sprite".length)}`;
434
+ const response = await (options.fetch ?? globalThis.fetch)(fileUrl, {
435
+ method: "GET",
436
+ credentials: "omit",
437
+ redirect: "error",
438
+ headers: { Accept: expected.contentType, "Accept-Encoding": "identity" },
439
+ signal
440
+ });
441
+ if (response.status !== 200 || response.redirected || response.url && response.url !== fileUrl || !response.body)
442
+ throw new Error("Unexpected artifact response");
443
+ const type = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
444
+ const length = response.headers.get("content-length");
445
+ const encoding = response.headers.get("content-encoding")?.trim().toLowerCase();
446
+ const isIdentityEncoding = encoding === void 0 || encoding === "" || encoding === "identity";
447
+ if (type !== expected.contentType || length !== null && (!/^\d+$/u.test(length) || isIdentityEncoding && Number(length) !== expected.byteLength)) {
448
+ await response.body.cancel();
449
+ throw new Error("Artifact response headers do not match the manifest");
450
+ }
451
+ const reader = response.body.getReader();
452
+ const source = new Uint8Array(expected.byteLength);
453
+ let offset = 0;
454
+ try {
455
+ while (true) {
456
+ const chunk = await reader.read();
457
+ if (chunk.done) break;
458
+ if (offset + chunk.value.byteLength > source.byteLength)
459
+ throw new Error("Artifact response exceeds its exact byte length");
460
+ source.set(chunk.value, offset);
461
+ offset += chunk.value.byteLength;
462
+ }
463
+ if (offset !== source.byteLength) throw new Error("Artifact response was truncated");
464
+ } catch (cause) {
465
+ await reader.cancel().catch(() => void 0);
466
+ throw cause;
467
+ } finally {
468
+ reader.releaseLock();
469
+ }
470
+ files.push({ fileName: expected.name, contentType: expected.contentType, source });
471
+ }
472
+ } catch (cause) {
473
+ throw new TileflowIconSetError2("ICON_DOWNLOAD_FAILED", "Exact icon artifact download failed", {
474
+ cause
475
+ });
476
+ }
477
+ return storeTileflowIconSetArtifact(
478
+ pin,
479
+ { contentHash: pin.contentHash, manifest: pin.manifest, files },
480
+ options
481
+ );
482
+ }
483
+ async function storeTileflowIconSetArtifact(input, artifact, options = {}) {
484
+ const pin = tileflowIconSetPinSchema.parse(input);
485
+ if (await hashTileflowIconPackageManifest3(pin.manifest) !== pin.contentHash)
486
+ throw new TileflowIconSetError2("ICON_LOCK_INVALID", "Locked manifest hash mismatch");
487
+ if (artifact.contentHash !== pin.contentHash)
488
+ throw new TileflowIconSetError2(
489
+ "ICON_PACKAGE_INTEGRITY",
490
+ "Artifact does not match the selected pin"
491
+ );
492
+ const verified = await verifyTileflowIconArtifact({
493
+ contentHash: pin.contentHash,
494
+ manifest: pin.manifest,
495
+ files: artifact.files
496
+ });
497
+ const root = await safeCacheRoot(options, true);
498
+ const target = join(root, pin.contentHash);
499
+ const temporary = await mkdtemp(join(root, `.incoming-${pin.contentHash}-`));
500
+ try {
501
+ for (const file of verified.package.files) {
502
+ const handle = await open(join(temporary, file.fileName), "wx", 384);
503
+ try {
504
+ await handle.writeFile(file.source);
505
+ await handle.sync();
506
+ } finally {
507
+ await handle.close();
508
+ }
509
+ }
510
+ for (let attempt = 0; attempt < 8; attempt += 1) {
511
+ try {
512
+ await rename(temporary, target);
513
+ return verified;
514
+ } catch (cause) {
515
+ if (!hasCode(cause, "EEXIST") && !hasCode(cause, "ENOTEMPTY") && !hasCode(cause, "EPERM"))
516
+ throw cause;
517
+ }
518
+ try {
519
+ return await readCached(pin, options);
520
+ } catch (cause) {
521
+ if (cause instanceof TileflowIconSetError2 && cause.code === "ICON_CACHE_UNSAFE")
522
+ throw cause;
523
+ }
524
+ try {
525
+ await assertDirectory(target);
526
+ const retired = await mkdtemp(join(root, `.retired-${pin.contentHash}-`));
527
+ try {
528
+ await rename(target, join(retired, "entry"));
529
+ } catch (cause) {
530
+ if (!isMissing(cause)) throw cause;
531
+ } finally {
532
+ await rm(retired, { recursive: true, force: true });
533
+ }
534
+ } catch (cause) {
535
+ if (!isMissing(cause)) throw cause;
536
+ }
537
+ }
538
+ throw new TileflowIconSetError2(
539
+ "ICON_CACHE_UNSAFE",
540
+ "Concurrent cache repairs did not converge; retry this exact pin"
541
+ );
542
+ } finally {
543
+ await rm(temporary, { recursive: true, force: true });
544
+ }
545
+ }
546
+ async function readCached(pin, options) {
547
+ const root = await safeCacheRoot(options, false);
548
+ const directory = join(root, pin.contentHash);
549
+ await assertDirectory(directory);
550
+ const entries = (await readdir(directory)).sort();
551
+ const expectedNames = pin.manifest.files.map((file) => file.name).sort();
552
+ if (entries.length !== 4 || entries.some((name, index) => name !== expectedNames[index]))
553
+ throw new TileflowIconSetError2(
554
+ "ICON_PACKAGE_INTEGRITY",
555
+ "Cache entry does not contain exactly four generated files"
556
+ );
557
+ const files = [];
558
+ for (const expected of pin.manifest.files) {
559
+ files.push({
560
+ fileName: expected.name,
561
+ contentType: expected.contentType,
562
+ source: await readSafeFile(join(directory, expected.name), expected.byteLength)
563
+ });
564
+ }
565
+ return verifyTileflowIconArtifact({ contentHash: pin.contentHash, manifest: pin.manifest, files });
566
+ }
567
+ async function safeCacheRoot(options, create) {
568
+ const root = getTileflowIconCacheDirectory(options);
569
+ const base = resolve(root, "..", "..", "..");
570
+ if (create) await mkdir(base, { recursive: true, mode: 448 });
571
+ await assertDirectory(base);
572
+ let current = await realpath(base);
573
+ for (const segment of ["tileflow", "icons", "v1"]) {
574
+ current = join(current, segment);
575
+ if (create)
576
+ await mkdir(current, { mode: 448 }).catch((cause) => {
577
+ if (!hasCode(cause, "EEXIST")) throw cause;
578
+ });
579
+ await assertDirectory(current);
580
+ }
581
+ return current;
582
+ }
583
+ async function assertDirectory(path) {
584
+ const entry = await lstat(path);
585
+ if (!entry.isDirectory() || entry.isSymbolicLink())
586
+ throw new TileflowIconSetError2(
587
+ "ICON_CACHE_UNSAFE",
588
+ "Icon cache paths must be directories, not symlinks"
589
+ );
590
+ }
591
+ async function readSafeFile(path, maximumBytes, exact = true) {
592
+ const entry = await lstat(path);
593
+ if (!entry.isFile() || entry.isSymbolicLink())
594
+ throw new TileflowIconSetError2(
595
+ "ICON_CACHE_UNSAFE",
596
+ "Icon files must be regular files, not symlinks"
597
+ );
598
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
599
+ try {
600
+ const opened = await handle.stat();
601
+ if (!opened.isFile() || opened.ino !== entry.ino || opened.dev !== entry.dev || opened.size > maximumBytes || exact && opened.size !== maximumBytes)
602
+ throw new TileflowIconSetError2(
603
+ "ICON_PACKAGE_INTEGRITY",
604
+ "Icon file size or identity changed"
605
+ );
606
+ const buffer = new Uint8Array(maximumBytes + 1);
607
+ let offset = 0;
608
+ while (offset < buffer.length) {
609
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
610
+ if (bytesRead === 0) break;
611
+ offset += bytesRead;
612
+ }
613
+ if (offset > maximumBytes || exact && offset !== maximumBytes)
614
+ throw new TileflowIconSetError2("ICON_PACKAGE_INTEGRITY", "Icon file byte length changed");
615
+ return buffer.slice(0, offset);
616
+ } finally {
617
+ await handle.close();
618
+ }
619
+ }
620
+ function hasCode(error, code) {
621
+ return error !== null && typeof error === "object" && "code" in error && error.code === code;
622
+ }
623
+ function isMissing(error) {
624
+ return hasCode(error, "ENOENT");
625
+ }
626
+
627
+ // src/icon-lockfile.ts
628
+ import { lstat as lstat2, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, rename as rename2, rm as rm2, rmdir } from "fs/promises";
629
+ import { join as join2, resolve as resolve2 } from "path";
630
+ import {
631
+ parseTileflowIconsLockfile,
632
+ serializeTileflowIconsLockfile,
633
+ TileflowIconSetError as TileflowIconSetError3,
634
+ tileflowIconsLockfileMaximumBytes,
635
+ tileflowIconsLockfileName
636
+ } from "@tileflow/core";
637
+ async function readTileflowIconsLockfileText(baseDirectory) {
638
+ return readLockText(join2(resolve2(baseDirectory), tileflowIconsLockfileName));
639
+ }
640
+ async function readTileflowIconsLockfile(baseDirectory, sources) {
641
+ const text = await readLockText(join2(resolve2(baseDirectory), tileflowIconsLockfileName));
642
+ if (text === null)
643
+ throw new TileflowIconSetError3(
644
+ "ICON_LOCK_MISSING",
645
+ "Missing tileflow.icons.lock.json; install exact icon set revisions explicitly"
646
+ );
647
+ return parseTileflowIconsLockfile(text, sources);
648
+ }
649
+ async function writeTileflowIconsLockfile(baseDirectory, input, expectedContents) {
650
+ const content = await serializeTileflowIconsLockfile(input);
651
+ const directory = resolve2(baseDirectory);
652
+ const destination = join2(directory, tileflowIconsLockfileName);
653
+ const guard = join2(directory, `.${tileflowIconsLockfileName}.writing`);
654
+ try {
655
+ await mkdir2(guard, { mode: 448 });
656
+ } catch (cause) {
657
+ throw new TileflowIconSetError3(
658
+ "ICON_LOCK_CONFLICT",
659
+ "Another icon lock writer is active; a stale .writing guard may be removed only after confirming its writer has exited",
660
+ { cause }
661
+ );
662
+ }
663
+ let temporary;
664
+ try {
665
+ const actual = await readLockText(destination);
666
+ if (actual !== expectedContents)
667
+ throw new TileflowIconSetError3(
668
+ "ICON_LOCK_CONFLICT",
669
+ "Icon lock changed since it was read; reload and retry without discarding other pins"
670
+ );
671
+ if (actual === content) return;
672
+ temporary = await mkdtemp2(join2(directory, ".tileflow-icons-lock-"));
673
+ const file = join2(temporary, "lock");
674
+ const handle = await open2(file, "wx", 384);
675
+ try {
676
+ await handle.writeFile(content, "utf8");
677
+ await handle.sync();
678
+ } finally {
679
+ await handle.close();
680
+ }
681
+ if (await readLockText(destination) !== actual)
682
+ throw new TileflowIconSetError3(
683
+ "ICON_LOCK_CONFLICT",
684
+ "Icon lock changed while preparing an update"
685
+ );
686
+ await rename2(file, destination);
687
+ } finally {
688
+ if (temporary) await rm2(temporary, { recursive: true, force: true });
689
+ await rmdir(guard);
690
+ }
691
+ }
692
+ async function readLockText(path) {
693
+ try {
694
+ const entry = await lstat2(path);
695
+ if (!entry.isFile() || entry.isSymbolicLink())
696
+ throw new TileflowIconSetError3(
697
+ "ICON_LOCK_INVALID",
698
+ "The icon lock must be a regular file, not a symlink"
699
+ );
700
+ return new TextDecoder("utf-8", { fatal: true }).decode(
701
+ await readSafeFile(path, tileflowIconsLockfileMaximumBytes, false)
702
+ );
703
+ } catch (cause) {
704
+ if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT")
705
+ return null;
706
+ throw cause;
707
+ }
708
+ }
709
+
710
+ // src/icon-composition.ts
711
+ async function composeTileflowIconSources(input, options) {
712
+ const sources = structuredClone(input);
713
+ const references = collectTileflowIconSetReferences(sources);
714
+ let realCwd;
715
+ const lock = references.length > 0 ? options.lock === void 0 ? await readTileflowIconsLockfile(options.baseDirectory ?? options.cwd, sources) : await parseTileflowIconsLockfile2(options.lock, sources) : await parseTileflowIconsLockfile2(options.lock ?? { lockfileVersion: 1, sets: {} }, sources);
716
+ const contributors = new Array(sources.length);
717
+ const details = new Array(sources.length);
718
+ const winners = /* @__PURE__ */ new Map();
719
+ const watchPaths = /* @__PURE__ */ new Set();
720
+ const watchFiles = /* @__PURE__ */ new Set();
721
+ if (references.length > 0 && options.lock === void 0)
722
+ watchFiles.add(resolve3(options.baseDirectory ?? options.cwd, tileflowIconsLockfileName2));
723
+ let reused;
724
+ let pixelBytes = 0;
725
+ let localSourceBytes = 0;
726
+ for (let ordinal = sources.length - 1; ordinal >= 0; ordinal -= 1) {
727
+ const source = sources[ordinal];
728
+ if (typeof source === "object" && source.kind === "icon-set") {
729
+ const pin = lock.sets[source.reference];
730
+ const loaded = await loadTileflowIconSetArtifact(pin, options);
731
+ contributors[ordinal] = {
732
+ kind: "icon-set",
733
+ reference: source.reference,
734
+ teamId: pin.teamId,
735
+ setId: pin.setId,
736
+ versionId: pin.versionId,
737
+ version: pin.version,
738
+ packageId: pin.packageId,
739
+ contentHash: pin.contentHash,
740
+ iconIds: [...pin.manifest.iconNames]
741
+ };
742
+ details[ordinal] = {
743
+ iconIds: [...pin.manifest.iconNames],
744
+ kind: "icon-set",
745
+ label: source.reference,
746
+ packageId: pin.packageId,
747
+ reference: source.reference,
748
+ setId: pin.setId,
749
+ version: pin.version,
750
+ versionId: pin.versionId
751
+ };
752
+ if (sources.length === 1) reused = loaded.package;
753
+ for (const icon of loaded.icons) {
754
+ if (winners.has(icon.id)) continue;
755
+ const pixels = pin.manifest.renderedIcons.find(
756
+ (entry) => entry.name === icon.id
757
+ ).pixelSha256;
758
+ addWinner(
759
+ ordinal,
760
+ icon,
761
+ {
762
+ kind: "rendered-icon",
763
+ id: icon.id,
764
+ width: icon.oneX.width,
765
+ height: icon.oneX.height,
766
+ pixelSha256: { ...pixels },
767
+ ...icon.appearance ? { appearance: icon.appearance } : {},
768
+ ...icon.layout ? { layout: icon.layout } : {}
769
+ },
770
+ null
771
+ );
772
+ }
773
+ } else {
774
+ const directory = await readTileflowIconDirectory(source, {
775
+ cwd: options.cwd,
776
+ baseDirectory: options.baseDirectory ?? options.cwd,
777
+ ...options.configPath === void 0 ? {} : { configPath: options.configPath },
778
+ ordinal,
779
+ target: options.target ?? "local"
780
+ });
781
+ if (directory.watchPath) watchPaths.add(directory.watchPath);
782
+ contributors[ordinal] = typeof source === "string" ? { kind: "local", iconIds: directory.iconIds } : { kind: "package", package: source.package, iconIds: directory.iconIds };
783
+ details[ordinal] = {
784
+ entries: directory.entries,
785
+ iconIds: directory.iconIds,
786
+ insideWorkingTree: isPathInside(
787
+ realCwd ??= await realpath2(options.cwd),
788
+ directory.realPath
789
+ ),
790
+ kind: typeof source === "string" ? "local" : "package",
791
+ label: directory.label
792
+ };
793
+ const selected = directory.iconIds.filter((id) => !winners.has(id));
794
+ if (winners.size + selected.length > tileflowIconPackageLimits3.maxIconCount)
795
+ throw new TileflowIconSetError4(
796
+ "ICON_COMPOSITION_INVALID",
797
+ `Composed icon set exceeds ${tileflowIconPackageLimits3.maxIconCount} effective icons`
798
+ );
799
+ for (const item of await directory.render(selected)) {
800
+ localSourceBytes += item.sourceBytes;
801
+ if (localSourceBytes > tileflowIconPackageLimits3.maxSourceBytes)
802
+ throw new TileflowIconSetError4(
803
+ "ICON_COMPOSITION_INVALID",
804
+ "Effective local icon originals exceed the source-byte limit"
805
+ );
806
+ addWinner(ordinal, item.icon, item.identity, item.source);
807
+ }
808
+ }
809
+ }
810
+ const sorted = [...winners.values()].sort(
811
+ (left, right) => compareCodeUnits3(left.icon.id, right.icon.id)
812
+ );
813
+ const iconPackage = sorted.length ? reused ?? await packTileflowRenderedIcons(sorted.map((winner) => winner.icon)) : null;
814
+ const composition = references.length && iconPackage ? parseTileflowIconComposition({
815
+ format: sorted.some((winner) => winner.icon.appearance || winner.icon.layout) ? "tileflow-icon-composition-v2" : "tileflow-icon-composition-v1",
816
+ compositionVersion: sorted.some((winner) => winner.icon.appearance || winner.icon.layout) ? 2 : 1,
817
+ packageHash: iconPackage.contentHash,
818
+ contributors,
819
+ winners: sorted.map(({ ordinal, icon }, index) => ({
820
+ id: icon.id,
821
+ contributor: ordinal,
822
+ width: icon.oneX.width,
823
+ height: icon.oneX.height,
824
+ pixelSha256: iconPackage.manifest.renderedIcons[index].pixelSha256,
825
+ ...icon.appearance ? { appearance: icon.appearance } : {},
826
+ ...icon.layout ? { layout: icon.layout } : {}
827
+ }))
828
+ }) : null;
829
+ const previous = /* @__PURE__ */ new Map();
830
+ const replacements = [];
831
+ for (const [ordinal, contributor] of contributors.entries()) {
832
+ for (const id of contributor.iconIds) {
833
+ const replaced = previous.get(id);
834
+ if (replaced !== void 0) replacements.push({ id, replaced, winner: ordinal });
835
+ previous.set(id, ordinal);
836
+ }
837
+ }
838
+ return {
839
+ package: iconPackage,
840
+ composition,
841
+ contributors: details,
842
+ sourceIdentities: sorted.map((winner) => winner.identity),
843
+ replacements,
844
+ watchFiles: [...watchFiles].sort(compareCodeUnits3),
845
+ watchPaths: [...watchPaths].sort(compareCodeUnits3),
846
+ winners: sorted.map((winner) => ({
847
+ contributor: winner.ordinal,
848
+ id: winner.icon.id,
849
+ identity: winner.identity,
850
+ source: winner.source
851
+ }))
852
+ };
853
+ function addWinner(ordinal, icon, identity2, source) {
854
+ pixelBytes += icon.oneX.rgba.byteLength + icon.twoX.rgba.byteLength;
855
+ const maximumPixelBytes = (tileflowIconPackageLimits3.maxAtlasDimension ** 2 + (tileflowIconPackageLimits3.maxAtlasDimension / 2) ** 2) * 4;
856
+ if (winners.size >= tileflowIconPackageLimits3.maxIconCount || pixelBytes > maximumPixelBytes)
857
+ throw new TileflowIconSetError4(
858
+ "ICON_COMPOSITION_INVALID",
859
+ "Composed rendered cells exceed the final sprite capacity"
860
+ );
861
+ winners.set(icon.id, { ordinal, icon, identity: identity2, source });
862
+ }
863
+ }
864
+ function isPathInside(root, candidate) {
865
+ const pathFromRoot = relative(root, candidate);
866
+ return pathFromRoot === "" || !isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`);
867
+ }
868
+
869
+ // src/icon-distance.ts
870
+ var infinity = 1e20;
871
+ function createIconDistanceField(rgba, width, height, ratio) {
872
+ if (width !== 24 * ratio || height !== 24 * ratio || rgba.length !== width * height * 4)
873
+ throw new Error("SDF icons require a 24px logical cell at each density.");
874
+ const outer = new Float64Array(width * height);
875
+ const inner = new Float64Array(width * height);
876
+ let occupied = false;
877
+ for (let y = 0; y < height; y++) {
878
+ for (let x = 0; x < width; x++) {
879
+ const index = y * width + x;
880
+ const alpha = rgba[index * 4 + 3] / 255;
881
+ if (alpha && (x < 6 * ratio || y < 6 * ratio || x >= width - 6 * ratio || y >= height - 6 * ratio))
882
+ throw new Error(
883
+ "SDF icons require six transparent pixels of padding inside the 24px cell. Add a transparent border to the source."
884
+ );
885
+ occupied ||= alpha > 0;
886
+ outer[index] = alpha === 1 ? 0 : alpha === 0 ? infinity : Math.max(0, 0.5 - alpha) ** 2;
887
+ inner[index] = alpha === 1 ? infinity : alpha === 0 ? 0 : Math.max(0, alpha - 0.5) ** 2;
888
+ }
889
+ }
890
+ if (!occupied) throw new Error("An SDF silhouette must contain visible pixels.");
891
+ transform(outer, width, height);
892
+ transform(inner, width, height);
893
+ const result = new Uint8Array(rgba.length);
894
+ for (let i = 0; i < outer.length; i++) {
895
+ const distance = Math.sqrt(outer[i]) - Math.sqrt(inner[i]);
896
+ const alpha = Math.round(255 * Math.max(0, Math.min(1, 0.75 - distance / (8 * ratio))));
897
+ result.set([255, 255, 255, alpha], i * 4);
898
+ }
899
+ return result;
900
+ }
901
+ function transform(grid, width, height) {
902
+ const size = Math.max(width, height);
903
+ const samples = new Float64Array(size);
904
+ const result = new Float64Array(size);
905
+ const sites = new Uint32Array(size);
906
+ const bounds = new Float64Array(size + 1);
907
+ const line = (length) => {
908
+ let count = 0;
909
+ sites[0] = 0;
910
+ bounds[0] = -infinity;
911
+ bounds[1] = infinity;
912
+ for (let position = 1; position < length; position++) {
913
+ let intersection;
914
+ do {
915
+ const site = sites[count];
916
+ intersection = (samples[position] + position ** 2 - samples[site] - site ** 2) / (2 * (position - site));
917
+ if (intersection > bounds[count]) break;
918
+ count--;
919
+ } while (count >= 0);
920
+ count++;
921
+ sites[count] = position;
922
+ bounds[count] = intersection;
923
+ bounds[count + 1] = infinity;
924
+ }
925
+ count = 0;
926
+ for (let position = 0; position < length; position++) {
927
+ while (bounds[count + 1] < position) count++;
928
+ result[position] = (position - sites[count]) ** 2 + samples[sites[count]];
929
+ }
930
+ };
931
+ for (let x = 0; x < width; x++) {
932
+ for (let y = 0; y < height; y++) samples[y] = grid[y * width + x];
933
+ line(height);
934
+ for (let y = 0; y < height; y++) grid[y * width + x] = result[y];
935
+ }
936
+ for (let y = 0; y < height; y++) {
937
+ samples.set(grid.subarray(y * width, (y + 1) * width));
938
+ line(width);
939
+ grid.set(result.subarray(0, width), y * width);
940
+ }
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
+ readTileflowLandmarkTileFootprints,
953
+ serializeTileflowLandmarkRegion,
954
+ tileflowLandmarkTileOf,
955
+ tileflowModelFileName,
956
+ tileflowTreeModelLimits as tileflowTreeModelLimits2
957
+ } from "@tileflow/core";
958
+
959
+ // src/building-model-mesh.ts
960
+ import {
961
+ isTileflowModelPartName,
962
+ tileflowModelDefaultLightingRole,
963
+ tileflowModelLightingRoles,
964
+ tileflowModelLimits,
965
+ tileflowModelSurfaceLimits as tileflowModelSurfaceLimits2,
966
+ tileflowModelTextureChannels as tileflowModelTextureChannels2
967
+ } from "@tileflow/core";
968
+
969
+ // src/building-model-occlusion.ts
970
+ function buildingModelAmbientVisibility(mesh, options = {}) {
971
+ const sampler = buildingModelAmbientSampler(mesh, options);
972
+ const { positions, normals } = mesh;
973
+ const values = new Array(positions.length / 3);
974
+ const cache = /* @__PURE__ */ new Map();
975
+ for (let vertex = 0; vertex < values.length; vertex += 1) {
976
+ const at = vertex * 3;
977
+ const point3 = [positions[at], positions[at + 1], positions[at + 2]];
978
+ const normal = [normals[at], normals[at + 1], normals[at + 2]];
979
+ const key = [...point3, ...normal].map((v) => Math.round(v * 1e5)).join(",");
980
+ let value = cache.get(key);
981
+ if (value === void 0) {
982
+ value = sampler.sky(point3, normal);
983
+ cache.set(key, value);
984
+ }
985
+ values[vertex] = value;
986
+ }
987
+ return values;
988
+ }
989
+ function buildingModelAmbientSampler(mesh, { radius = 2, samples = 48 } = {}) {
990
+ if (!(radius > 0 && Number.isFinite(radius)) || !Number.isInteger(samples) || samples < 1 || samples > 256)
991
+ throw new RangeError("Model ambient visibility needs a positive radius and 1 to 256 samples.");
992
+ const { positions, indices } = mesh;
993
+ const count = indices.length / 3;
994
+ const triangles2 = new Float64Array(count * 9);
995
+ const bounds = new Float64Array(count * 6);
996
+ const centres = new Float64Array(count * 3);
997
+ const order = Uint32Array.from({ length: count }, (_, i) => i);
998
+ for (let i = 0; i < count; i += 1) {
999
+ const a = indices[i * 3] * 3;
1000
+ const b = indices[i * 3 + 1] * 3;
1001
+ const c = indices[i * 3 + 2] * 3;
1002
+ for (let axis = 0; axis < 3; axis += 1) {
1003
+ const x = positions[a + axis], y = positions[b + axis], z = positions[c + axis];
1004
+ triangles2[i * 9 + axis] = x;
1005
+ triangles2[i * 9 + 3 + axis] = y - x;
1006
+ triangles2[i * 9 + 6 + axis] = z - x;
1007
+ bounds[i * 6 + axis] = Math.min(x, y, z);
1008
+ bounds[i * 6 + 3 + axis] = Math.max(x, y, z);
1009
+ centres[i * 3 + axis] = (x + y + z) / 3;
1010
+ }
1011
+ }
1012
+ const nodes = [];
1013
+ const build = (from, to) => {
1014
+ const box = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
1015
+ for (let at = from; at < to; at += 1) {
1016
+ const i = order[at] * 6;
1017
+ for (let axis2 = 0; axis2 < 3; axis2 += 1) {
1018
+ box[axis2] = Math.min(box[axis2], bounds[i + axis2]);
1019
+ box[axis2 + 3] = Math.max(box[axis2 + 3], bounds[i + axis2 + 3]);
1020
+ }
1021
+ }
1022
+ let axis = 0;
1023
+ for (let next = 1; next < 3; next += 1)
1024
+ if (box[next + 3] - box[next] > box[axis + 3] - box[axis]) axis = next;
1025
+ const index = nodes.length;
1026
+ const node = { bounds: box, from, to, left: -1, right: -1, axis };
1027
+ nodes.push(node);
1028
+ if (to - from > 8) {
1029
+ order.subarray(from, to).sort((a, b) => centres[a * 3 + axis] - centres[b * 3 + axis] || a - b);
1030
+ const middle = from + to >>> 1;
1031
+ node.left = build(from, middle);
1032
+ node.right = build(middle, to);
1033
+ }
1034
+ return index;
1035
+ };
1036
+ if (count > 0) build(0, count);
1037
+ const hemispheres = /* @__PURE__ */ new Map();
1038
+ const hemisphere = (count2) => {
1039
+ let pattern = hemispheres.get(count2);
1040
+ if (!pattern) {
1041
+ pattern = Array.from({ length: count2 }, (_, i) => {
1042
+ const r = Math.sqrt((i + 0.5) / count2), angle = i * Math.PI * (3 - Math.sqrt(5));
1043
+ return [r * Math.cos(angle), r * Math.sin(angle), Math.sqrt(1 - r * r)];
1044
+ });
1045
+ hemispheres.set(count2, pattern);
1046
+ }
1047
+ return pattern;
1048
+ };
1049
+ const bias = Math.min(2e-3, radius * 1e-3);
1050
+ const stack = new Int32Array(64);
1051
+ const origin = [0, 0, 0];
1052
+ const direction = [0, 0, 0];
1053
+ const nearest = () => {
1054
+ let best = radius, pending = 1;
1055
+ if (nodes.length === 0) return best;
1056
+ stack[0] = 0;
1057
+ while (pending > 0) {
1058
+ const node = nodes[stack[--pending]];
1059
+ if (!hitsBox(node.bounds, origin, direction, best)) continue;
1060
+ if (node.left >= 0) {
1061
+ const forward = direction[node.axis] >= 0;
1062
+ stack[pending++] = forward ? node.right : node.left;
1063
+ stack[pending++] = forward ? node.left : node.right;
1064
+ continue;
1065
+ }
1066
+ for (let at = node.from; at < node.to; at += 1) {
1067
+ const i = order[at] * 9;
1068
+ const e1x = triangles2[i + 3], e1y = triangles2[i + 4], e1z = triangles2[i + 5];
1069
+ const e2x = triangles2[i + 6], e2y = triangles2[i + 7], e2z = triangles2[i + 8];
1070
+ const px = direction[1] * e2z - direction[2] * e2y;
1071
+ const py = direction[2] * e2x - direction[0] * e2z;
1072
+ const pz = direction[0] * e2y - direction[1] * e2x;
1073
+ const determinant2 = e1x * px + e1y * py + e1z * pz;
1074
+ if (Math.abs(determinant2) < 1e-12) continue;
1075
+ const tx = origin[0] - triangles2[i], ty = origin[1] - triangles2[i + 1], tz = origin[2] - triangles2[i + 2];
1076
+ const inverse = 1 / determinant2;
1077
+ const u = (tx * px + ty * py + tz * pz) * inverse;
1078
+ if (u < 0 || u > 1) continue;
1079
+ const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x;
1080
+ const v = (direction[0] * qx + direction[1] * qy + direction[2] * qz) * inverse;
1081
+ if (v < 0 || u + v > 1) continue;
1082
+ const distance = (e2x * qx + e2y * qy + e2z * qz) * inverse;
1083
+ if (distance > bias && distance < best) best = distance;
1084
+ }
1085
+ }
1086
+ return best;
1087
+ };
1088
+ return {
1089
+ radius,
1090
+ sky(point3, normal, rays = samples, turn = 0) {
1091
+ const [nx, ny, nz] = normal;
1092
+ const flat = Math.abs(nz) < 0.9 ? [-ny, nx, 0] : [nz, 0, -nx];
1093
+ const length = Math.hypot(...flat);
1094
+ if (length === 0) return 1;
1095
+ const [c, s] = [Math.cos(turn), Math.sin(turn)];
1096
+ const base = flat.map((value) => value / length);
1097
+ const cross4 = [
1098
+ ny * base[2] - nz * base[1],
1099
+ nz * base[0] - nx * base[2],
1100
+ nx * base[1] - ny * base[0]
1101
+ ];
1102
+ const tangent = base.map((value, axis) => value * c + cross4[axis] * s);
1103
+ const bitangent = cross4.map((value, axis) => value * c - base[axis] * s);
1104
+ for (let axis = 0; axis < 3; axis += 1) origin[axis] = point3[axis] + normal[axis] * bias;
1105
+ let visibility = 0;
1106
+ const pattern = hemisphere(rays);
1107
+ for (const sample of pattern) {
1108
+ for (let axis = 0; axis < 3; axis += 1)
1109
+ direction[axis] = sample[0] * tangent[axis] + sample[1] * bitangent[axis] + sample[2] * normal[axis];
1110
+ visibility += nearest() / radius;
1111
+ }
1112
+ return visibility / pattern.length;
1113
+ },
1114
+ along(point3, towards) {
1115
+ for (let axis = 0; axis < 3; axis += 1) {
1116
+ origin[axis] = point3[axis] + towards[axis] * bias;
1117
+ direction[axis] = towards[axis];
1118
+ }
1119
+ return nearest();
1120
+ }
1121
+ };
1122
+ }
1123
+ function hitsBox(box, origin, direction, limit) {
1124
+ let low = 0, high = limit;
1125
+ for (let axis = 0; axis < 3; axis += 1) {
1126
+ const o = origin[axis], d = direction[axis];
1127
+ if (Math.abs(d) < 1e-12) {
1128
+ if (o < box[axis] || o > box[axis + 3]) return false;
1129
+ continue;
1130
+ }
1131
+ const a = (box[axis] - o) / d, b = (box[axis + 3] - o) / d;
1132
+ low = Math.max(low, Math.min(a, b));
1133
+ high = Math.min(high, Math.max(a, b));
1134
+ if (low > high) return false;
1135
+ }
1136
+ return true;
1137
+ }
1138
+
1139
+ // src/building-model-lightmap.ts
1140
+ var paletteSide = 16;
1141
+ var maximumSide = 4096;
1142
+ function bakeBuildingModelLightmap(mesh, { texels: budget, radius = 2, ...options }) {
1143
+ if (options.points !== void 0 && !(options.points >= 1 && options.points <= 4 && Number.isInteger(options.points)))
1144
+ throw new RangeError("A lightmap samples one to four points along each side of a texel.");
1145
+ const { positions, indices } = mesh;
1146
+ const point3 = (vertex) => [
1147
+ positions[vertex * 3],
1148
+ positions[vertex * 3 + 1],
1149
+ positions[vertex * 3 + 2]
1150
+ ];
1151
+ const charts = chartsOf(indices, point3);
1152
+ const palette = {
1153
+ triangles: [],
1154
+ normal: [0, 0, 1],
1155
+ u: [1, 0, 0],
1156
+ v: [0, 1, 0],
1157
+ min: [0, 0],
1158
+ extent: [0, 0],
1159
+ scale: [0, 0],
1160
+ w: 0,
1161
+ h: 0,
1162
+ x: 0,
1163
+ y: 0,
1164
+ palette: true
1165
+ };
1166
+ charts.push(palette);
1167
+ let density = densityFor(charts, budget);
1168
+ if (density === void 0) return void 0;
1169
+ let layout = pack(charts, density);
1170
+ for (let attempt = 0; attempt < 12 && !(layout && layout.width * layout.height <= budget); attempt++) {
1171
+ density *= 0.9;
1172
+ layout = pack(charts, density);
1173
+ }
1174
+ if (!layout || layout.width * layout.height > budget) return void 0;
1175
+ const { width, height } = layout;
1176
+ const points = options.points ?? Math.min(4, Math.max(2, Math.ceil(1 / (density * 0.35))));
1177
+ const rays = options.rays ?? Math.max(4, Math.round(48 / (points * points)));
1178
+ const sampler = buildingModelAmbientSampler(mesh, { radius });
1179
+ const sums = new Float64Array(width * height);
1180
+ const counts = new Uint16Array(width * height);
1181
+ const taken = new Uint16Array(width * height);
1182
+ const hidden = Math.min(0.05, radius * 0.025);
1183
+ for (const chart of charts) {
1184
+ const local2 = (vertex) => {
1185
+ const p = point3(vertex);
1186
+ return [
1187
+ (dot(chart.u, p) - chart.min[0]) * chart.scale[0] + 0.5,
1188
+ (dot(chart.v, p) - chart.min[1]) * chart.scale[1] + 0.5
1189
+ ];
1190
+ };
1191
+ if (chart.tiny || chart.palette) continue;
1192
+ for (const triangle of chart.triangles) {
1193
+ const corners = [0, 1, 2].map((k) => local2(indices[triangle * 3 + k]));
1194
+ const [a, b, c] = corners;
1195
+ const area = cross2(a, b, c);
1196
+ if (Math.abs(area) < 1e-12) continue;
1197
+ const [A, B, C] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1198
+ const rawNormal = crossOf(sub(B, A), sub(C, A));
1199
+ const facing = rawNormal.map((value) => value / (Math.hypot(...rawNormal) || 1));
1200
+ const x0 = Math.max(0, Math.floor(Math.min(a[0], b[0], c[0])));
1201
+ const x1 = Math.min(chart.w - 1, Math.floor(Math.max(a[0], b[0], c[0])));
1202
+ const y0 = Math.max(0, Math.floor(Math.min(a[1], b[1], c[1])));
1203
+ const y1 = Math.min(chart.h - 1, Math.floor(Math.max(a[1], b[1], c[1])));
1204
+ for (let y = y0; y <= y1; y++)
1205
+ for (let x = x0; x <= x1; x++) {
1206
+ const texel = (chart.y + y) * width + chart.x + x;
1207
+ for (let j = 0; j < points; j++)
1208
+ for (let i = 0; i < points; i++) {
1209
+ const bit = 1 << j * points + i;
1210
+ if (taken[texel] & bit) continue;
1211
+ const p = [x + (i + 0.5) / points, y + (j + 0.5) / points];
1212
+ const w0 = cross2(b, c, p) / area, w1 = cross2(c, a, p) / area, w2 = 1 - w0 - w1;
1213
+ if (w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9) continue;
1214
+ taken[texel] |= bit;
1215
+ const world = [0, 1, 2].map((axis) => w0 * A[axis] + w1 * B[axis] + w2 * C[axis]);
1216
+ if (sampler.along(world, facing) < hidden) continue;
1217
+ const turn = ((j * points + i) * 2.399963 + (x * 0.618034 + y * 0.381966) * 6.283185) % 6.283185;
1218
+ sums[texel] += sampler.sky(world, facing, rays, turn);
1219
+ counts[texel] += 1;
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ const values = new Float32Array(width * height).fill(-1);
1225
+ for (let texel = 0; texel < values.length; texel++)
1226
+ if (counts[texel]) values[texel] = sums[texel] / counts[texel];
1227
+ for (let level = 0; level < paletteSide * paletteSide; level++) {
1228
+ const x = palette.x + level % paletteSide, y = palette.y + Math.floor(level / paletteSide);
1229
+ values[y * width + x] = level / 255;
1230
+ }
1231
+ const levels = /* @__PURE__ */ new Map();
1232
+ for (const chart of charts) {
1233
+ if (!chart.tiny) continue;
1234
+ let sum = 0, weight = 0;
1235
+ for (const triangle of chart.triangles) {
1236
+ const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1237
+ const raw = crossOf(sub(b, a), sub(c, a));
1238
+ const area = Math.hypot(...raw) / 2;
1239
+ if (!(area > 0)) continue;
1240
+ const facing = raw.map((value) => value / (2 * area));
1241
+ const centre = [0, 1, 2].map((axis) => (a[axis] + b[axis] + c[axis]) / 3);
1242
+ if (sampler.along(centre, facing) < hidden) continue;
1243
+ sum += area * sampler.sky(centre, facing, rays * points * points);
1244
+ weight += area;
1245
+ }
1246
+ levels.set(chart, Math.round((weight > 0 ? sum / weight : 1) * 255));
1247
+ }
1248
+ for (let pass = 0; pass < 3; pass++) {
1249
+ const previous = values.slice();
1250
+ for (let y = 0; y < height; y++)
1251
+ for (let x = 0; x < width; x++) {
1252
+ if (previous[y * width + x] >= 0) continue;
1253
+ let sum = 0, count = 0;
1254
+ for (const [dx, dy] of [
1255
+ [1, 0],
1256
+ [-1, 0],
1257
+ [0, 1],
1258
+ [0, -1]
1259
+ ]) {
1260
+ const xx = x + dx, yy = y + dy;
1261
+ if (xx < 0 || yy < 0 || xx >= width || yy >= height) continue;
1262
+ const value = previous[yy * width + xx];
1263
+ if (value >= 0) {
1264
+ sum += value;
1265
+ count++;
1266
+ }
1267
+ }
1268
+ if (count) values[y * width + x] = sum / count;
1269
+ }
1270
+ }
1271
+ const bytes = new Uint8Array(width * height);
1272
+ for (let texel = 0; texel < bytes.length; texel++)
1273
+ bytes[texel] = Math.round(
1274
+ Math.min(1, Math.max(0, values[texel] < 0 ? 1 : values[texel])) * 255
1275
+ );
1276
+ const sources = [];
1277
+ const uvs = [];
1278
+ const out = new Array(indices.length);
1279
+ for (const chart of charts) {
1280
+ const own = /* @__PURE__ */ new Map();
1281
+ for (const triangle of chart.triangles)
1282
+ for (let k = 0; k < 3; k++) {
1283
+ const vertex = indices[triangle * 3 + k];
1284
+ let index = own.get(vertex);
1285
+ if (index === void 0) {
1286
+ index = sources.length;
1287
+ own.set(vertex, index);
1288
+ sources.push(vertex);
1289
+ const level = levels.get(chart);
1290
+ if (level !== void 0)
1291
+ uvs.push(
1292
+ (palette.x + level % paletteSide + 0.5) / width,
1293
+ (palette.y + Math.floor(level / paletteSide) + 0.5) / height
1294
+ );
1295
+ else {
1296
+ const p = point3(vertex);
1297
+ uvs.push(
1298
+ (chart.x + 0.5 + (dot(chart.u, p) - chart.min[0]) * chart.scale[0]) / width,
1299
+ (chart.y + 0.5 + (dot(chart.v, p) - chart.min[1]) * chart.scale[1]) / height
1300
+ );
1301
+ }
1302
+ }
1303
+ out[triangle * 3 + k] = index;
1304
+ }
1305
+ }
1306
+ for (let at = 0; at < uvs.length; at++) uvs[at] = Math.round(uvs[at] * 65536) / 65536;
1307
+ return {
1308
+ sources,
1309
+ indices: out,
1310
+ uvs,
1311
+ width,
1312
+ height,
1313
+ density,
1314
+ texels: bytes
1315
+ };
1316
+ }
1317
+ var chartCone = Math.cos(20 * Math.PI / 180);
1318
+ function chartsOf(indices, point3) {
1319
+ const key = (p) => p.map((value) => Math.round(value * 1e4)).join(",");
1320
+ const count = indices.length / 3;
1321
+ const normals = [];
1322
+ const areas = [];
1323
+ const edges = /* @__PURE__ */ new Map();
1324
+ const edgeKeys = [];
1325
+ for (let triangle = 0; triangle < count; triangle++) {
1326
+ const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1327
+ const normal = crossOf(sub(b, a), sub(c, a));
1328
+ const length = Math.hypot(...normal);
1329
+ areas.push(length / 2);
1330
+ normals.push(length > 1e-12 ? normal.map((value) => value / length) : [0, 0, 1]);
1331
+ for (const [from, to] of [
1332
+ [a, b],
1333
+ [b, c],
1334
+ [c, a]
1335
+ ]) {
1336
+ const [u, v] = [key(from), key(to)];
1337
+ const edge2 = u < v ? `${u}|${v}` : `${v}|${u}`;
1338
+ edgeKeys.push(edge2);
1339
+ const users = edges.get(edge2);
1340
+ if (users) users.push(triangle);
1341
+ else edges.set(edge2, [triangle]);
1342
+ }
1343
+ }
1344
+ const owner = new Int32Array(count).fill(-1);
1345
+ const order = Array.from({ length: count }, (_, i) => i).sort(
1346
+ (a, b) => areas[b] - areas[a] || a - b
1347
+ );
1348
+ const charts = [];
1349
+ for (const seed of order) {
1350
+ if (owner[seed] !== -1) continue;
1351
+ const normal = normals[seed];
1352
+ const group = [seed];
1353
+ owner[seed] = seed;
1354
+ for (let at = 0; at < group.length; at++)
1355
+ for (let e = 0; e < 3; e++)
1356
+ for (const next of edges.get(edgeKeys[group[at] * 3 + e]))
1357
+ if (owner[next] === -1 && dot(normals[next], normal) >= chartCone) {
1358
+ owner[next] = seed;
1359
+ group.push(next);
1360
+ }
1361
+ charts.push(...flatCharts(group, normal, indices, point3));
1362
+ }
1363
+ return charts;
1364
+ }
1365
+ function flatCharts(group, normal, indices, point3, depth = 0) {
1366
+ let longest = 0;
1367
+ let u = Math.abs(normal[2]) < 0.9 ? [-normal[1], normal[0], 0] : [normal[2], 0, -normal[0]];
1368
+ let area = 0;
1369
+ for (const triangle of group) {
1370
+ const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
1371
+ area += Math.hypot(...crossOf(sub(b, a), sub(c, a))) / 2;
1372
+ for (const [from, to] of [
1373
+ [a, b],
1374
+ [b, c],
1375
+ [c, a]
1376
+ ]) {
1377
+ const d = sub(to, from);
1378
+ const along = d.map((value, axis) => value - normal[axis] * dot(d, normal));
1379
+ const length = Math.hypot(...along);
1380
+ if (length > longest) {
1381
+ longest = length;
1382
+ u = along;
1383
+ }
1384
+ }
1385
+ }
1386
+ const size = Math.hypot(...u) || 1;
1387
+ u = u.map((value) => value / size);
1388
+ const v = crossOf(normal, u);
1389
+ let [minU, minV, maxU, maxV] = [Infinity, Infinity, -Infinity, -Infinity];
1390
+ for (const triangle of group)
1391
+ for (let k = 0; k < 3; k++) {
1392
+ const p = point3(indices[triangle * 3 + k]);
1393
+ minU = Math.min(minU, dot(u, p));
1394
+ maxU = Math.max(maxU, dot(u, p));
1395
+ minV = Math.min(minV, dot(v, p));
1396
+ maxV = Math.max(maxV, dot(v, p));
1397
+ }
1398
+ const flip = maxV - minV > maxU - minU;
1399
+ if (group.length > 1 && depth < 16 && area < 0.5 * (maxU - minU) * (maxV - minV)) {
1400
+ const axis = flip ? v : u;
1401
+ const centre = (triangle) => [0, 1, 2].reduce((sum, k) => sum + dot(axis, point3(indices[triangle * 3 + k])), 0) / 3;
1402
+ const sorted = [...group].sort((a, b) => centre(a) - centre(b) || a - b);
1403
+ const half = sorted.length >> 1;
1404
+ return [
1405
+ ...flatCharts(sorted.slice(0, half), normal, indices, point3, depth + 1),
1406
+ ...flatCharts(sorted.slice(half), normal, indices, point3, depth + 1)
1407
+ ];
1408
+ }
1409
+ return [
1410
+ {
1411
+ triangles: group,
1412
+ normal,
1413
+ u: flip ? v : u,
1414
+ v: flip ? u : v,
1415
+ min: flip ? [minV, minU] : [minU, minV],
1416
+ extent: flip ? [maxV - minV, maxU - minU] : [maxU - minU, maxV - minV],
1417
+ scale: [0, 0],
1418
+ w: 0,
1419
+ h: 0,
1420
+ x: 0,
1421
+ y: 0
1422
+ }
1423
+ ];
1424
+ }
1425
+ function sizeAt(chart, density) {
1426
+ if (chart.palette) return [paletteSide, paletteSide];
1427
+ chart.tiny = chart.extent[0] * density < 2 && chart.extent[1] * density < 2;
1428
+ if (chart.tiny) return [0, 0];
1429
+ const size = chart.extent.map((extent) => Math.max(2, Math.round(extent * density) + 1));
1430
+ chart.scale = [0, 1].map(
1431
+ (axis) => chart.extent[axis] > 0 ? (size[axis] - 1) / chart.extent[axis] : 0
1432
+ );
1433
+ return size;
1434
+ }
1435
+ function densityFor(charts, budget) {
1436
+ const area = (density) => charts.reduce((sum, chart) => {
1437
+ const [w, h] = sizeAt(chart, density);
1438
+ return sum + w * h;
1439
+ }, 0);
1440
+ let low = 1 / 16, high = 64;
1441
+ if (area(low) / 0.8 > budget) return void 0;
1442
+ for (let step = 0; step < 24; step++) {
1443
+ const middle = Math.sqrt(low * high);
1444
+ if (area(middle) / 0.8 <= budget) low = middle;
1445
+ else high = middle;
1446
+ }
1447
+ return low;
1448
+ }
1449
+ function pack(charts, density) {
1450
+ for (const chart of charts) [chart.w, chart.h] = sizeAt(chart, density);
1451
+ const order = charts.map((_, i) => i).sort((a, b) => charts[b].h - charts[a].h || charts[b].w - charts[a].w || a - b);
1452
+ let best;
1453
+ for (let width = 64; width <= maximumSide; width *= 2) {
1454
+ let line = [{ x: 0, y: 0, w: width }];
1455
+ const places = new Array(charts.length);
1456
+ let fits = true;
1457
+ for (const index of order) {
1458
+ const chart = charts[index];
1459
+ if (chart.w === 0) {
1460
+ places[index] = [0, 0];
1461
+ continue;
1462
+ }
1463
+ if (chart.w > width) {
1464
+ fits = false;
1465
+ break;
1466
+ }
1467
+ let place;
1468
+ for (let i = 0; i < line.length; i++) {
1469
+ const x = line[i].x;
1470
+ if (x + chart.w > width) break;
1471
+ let y = 0, covered = 0;
1472
+ for (let j = i; j < line.length && covered < chart.w; j++) {
1473
+ y = Math.max(y, line[j].y);
1474
+ covered += line[j].w;
1475
+ }
1476
+ if (covered < chart.w) continue;
1477
+ if (!place || y < place.y) place = { x, y };
1478
+ }
1479
+ if (!place || place.y + chart.h > maximumSide) {
1480
+ fits = false;
1481
+ break;
1482
+ }
1483
+ places[index] = [place.x, place.y];
1484
+ const next = [];
1485
+ for (const segment of line) {
1486
+ const end = segment.x + segment.w;
1487
+ if (end <= place.x || segment.x >= place.x + chart.w) next.push(segment);
1488
+ else {
1489
+ if (segment.x < place.x) next.push({ x: segment.x, y: segment.y, w: place.x - segment.x });
1490
+ if (end > place.x + chart.w)
1491
+ next.push({ x: place.x + chart.w, y: segment.y, w: end - place.x - chart.w });
1492
+ }
1493
+ }
1494
+ next.push({ x: place.x, y: place.y + chart.h, w: chart.w });
1495
+ next.sort((a, b) => a.x - b.x);
1496
+ line = [];
1497
+ for (const segment of next) {
1498
+ const last = line.at(-1);
1499
+ if (last && last.y === segment.y && last.x + last.w === segment.x) last.w += segment.w;
1500
+ else line.push({ ...segment });
1501
+ }
1502
+ }
1503
+ if (!fits) continue;
1504
+ const height = Math.max(...line.map((segment) => segment.y));
1505
+ if (!best || width * height < best.width * best.height) best = { width, height, places };
1506
+ }
1507
+ if (!best) return void 0;
1508
+ charts.forEach((chart, i) => [chart.x, chart.y] = best.places[i]);
1509
+ return { width: best.width, height: best.height };
1510
+ }
1511
+ function dot(a, b) {
1512
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1513
+ }
1514
+ function sub(a, b) {
1515
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1516
+ }
1517
+ function crossOf(a, b) {
1518
+ return [
1519
+ a[1] * b[2] - a[2] * b[1],
1520
+ a[2] * b[0] - a[0] * b[2],
1521
+ a[0] * b[1] - a[1] * b[0]
1522
+ ];
1523
+ }
1524
+ function cross2(a, b, p) {
1525
+ return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
1526
+ }
1527
+
1528
+ // src/building-model-materials.ts
1529
+ import {
1530
+ checkTileflowModelSurface,
1531
+ tileflowModelTextureChannels
1532
+ } from "@tileflow/core";
1533
+ function buildingModelSurface(primitives) {
1534
+ if (!primitives.some((primitive) => primitive.surface)) return void 0;
1535
+ const attributes = [];
1536
+ const materials = [];
1537
+ const materialIds = /* @__PURE__ */ new Map();
1538
+ const textures = [];
1539
+ const textureIds = /* @__PURE__ */ new Map();
1540
+ const ranges = [];
1541
+ const emitters = [];
1542
+ let indexCount = 0;
1543
+ for (const primitive of primitives) {
1544
+ const count = primitive.positions.length / 3;
1545
+ const surface = primitive.surface;
1546
+ if (surface) {
1547
+ checkTileflowModelSurface(surface, count, primitive.indices.length);
1548
+ for (const value of surface.attributes) attributes.push(value);
1549
+ emitters.push(...surface.emitters ?? []);
1550
+ } else {
1551
+ for (let vertex = 0; vertex < count; vertex++) attributes.push(0, 0, 0, 0, 1, 1, 1, 1);
1552
+ }
1553
+ const remap = (surface?.materials ?? [{}]).map((material) => {
1554
+ const copy = { ...material };
1555
+ for (const channel of tileflowModelTextureChannels) {
1556
+ const ref = copy[channel];
1557
+ if (!ref) continue;
1558
+ const texture = surface.textures[ref.texture];
1559
+ let id2 = textureIds.get(texture);
1560
+ if (id2 === void 0) {
1561
+ id2 = textures.length;
1562
+ textures.push(texture);
1563
+ textureIds.set(texture, id2);
1564
+ }
1565
+ copy[channel] = { ...ref, texture: id2 };
1566
+ }
1567
+ const key = JSON.stringify(copy);
1568
+ let id = materialIds.get(key);
1569
+ if (id === void 0) {
1570
+ id = materials.length;
1571
+ materials.push(copy);
1572
+ materialIds.set(key, id);
1573
+ }
1574
+ return id;
1575
+ });
1576
+ const sourceRanges = surface?.ranges ?? [
1577
+ { from: 0, count: primitive.indices.length, material: 0 }
1578
+ ];
1579
+ for (const range of sourceRanges) {
1580
+ const material = remap[range.material];
1581
+ const previous = ranges.at(-1);
1582
+ if (previous?.material === material && JSON.stringify(previous.emissiveBounds) === JSON.stringify(range.emissiveBounds))
1583
+ ranges[ranges.length - 1] = { ...previous, count: previous.count + range.count };
1584
+ else ranges.push({ ...range, from: indexCount + range.from, material });
1585
+ }
1586
+ indexCount += primitive.indices.length;
1587
+ }
1588
+ const result = { attributes, materials, textures, ranges, ...emitters.length ? { emitters } : {} };
1589
+ checkTileflowModelSurface(result, attributes.length / 8, indexCount);
1590
+ return result;
1591
+ }
1592
+
1593
+ // src/gltf-materials.ts
1594
+ import {
1595
+ checkTileflowModelSurface as checkTileflowModelSurface2,
1596
+ tileflowModelImageSize,
1597
+ tileflowModelSurfaceLimits
1598
+ } from "@tileflow/core";
1599
+ function createGltfMaterialReader(json, bin, accessor2) {
1600
+ const textures = [];
1601
+ const textureIds = /* @__PURE__ */ new Map();
1602
+ const imageCache = /* @__PURE__ */ new Map();
1603
+ function imageAt(index) {
1604
+ if (!Number.isInteger(index)) throw new RangeError("A material names a missing image.");
1605
+ const cached = imageCache.get(index);
1606
+ if (cached) return cached;
1607
+ const image = record(list(json.images)[index]);
1608
+ let bytes;
1609
+ let mimeType = image.mimeType;
1610
+ if (typeof image.uri === "string") {
1611
+ const match = /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image.uri);
1612
+ if (!match || image.uri.length > 24 * 1024 * 1024)
1613
+ throw new RangeError("Embed PNG/JPEG images in the GLB; external images are not read.");
1614
+ if (mimeType !== void 0 && mimeType !== match[1])
1615
+ throw new RangeError("An image has conflicting media types.");
1616
+ mimeType = match[1];
1617
+ bytes = new Uint8Array(Buffer.from(match[2], "base64"));
1618
+ } else {
1619
+ if (!Number.isInteger(image.bufferView)) throw new RangeError("Embed each image in the GLB.");
1620
+ const view = record(list(json.bufferViews)[image.bufferView]);
1621
+ const start = view.byteOffset ?? 0;
1622
+ const length = view.byteLength;
1623
+ if (!bin || (view.buffer ?? 0) !== 0 || !Number.isInteger(start) || !Number.isInteger(length) || start < 0 || length <= 0 || start + length > bin.length)
1624
+ throw new RangeError("An embedded image extends outside the GLB.");
1625
+ bytes = bin.subarray(start, start + length);
1626
+ }
1627
+ if (mimeType !== "image/png" && mimeType !== "image/jpeg")
1628
+ throw new RangeError("Use an embedded PNG or JPEG image.");
1629
+ const size = tileflowModelImageSize(bytes, mimeType);
1630
+ if (!size || Math.max(...size) > tileflowModelSurfaceLimits.textureDimension || size[0] * size[1] > tileflowModelSurfaceLimits.texturePixels)
1631
+ throw new RangeError("The model image is invalid or exceeds its pixel budget.");
1632
+ const result = {
1633
+ bytes,
1634
+ width: size[0],
1635
+ height: size[1],
1636
+ mimeType
1637
+ };
1638
+ imageCache.set(index, result);
1639
+ return result;
1640
+ }
1641
+ function textureAt(index) {
1642
+ if (!Number.isInteger(index)) throw new RangeError("A material names a missing texture.");
1643
+ const cached = textureIds.get(index);
1644
+ if (cached !== void 0) return cached;
1645
+ const source = record(list(json.textures)[index]);
1646
+ const image = imageAt(source.source);
1647
+ const sampler = source.sampler === void 0 ? {} : record(list(json.samplers)[source.sampler]);
1648
+ if (source.sampler !== void 0 && (!Number.isInteger(source.sampler) || !list(json.samplers)[source.sampler]))
1649
+ throw new RangeError("A texture names a missing sampler.");
1650
+ const id = textures.length;
1651
+ if (id >= tileflowModelSurfaceLimits.textures)
1652
+ throw new RangeError("The model has too many textures.");
1653
+ textures.push({
1654
+ ...image,
1655
+ ...pick(sampler, ["wrapS", "wrapT", "minFilter", "magFilter"])
1656
+ });
1657
+ textureIds.set(index, id);
1658
+ return id;
1659
+ }
1660
+ function textureRef(value, normal = false) {
1661
+ if (value === void 0) return void 0;
1662
+ const ref = record(value);
1663
+ const transform3 = record(record(ref.extensions).KHR_texture_transform);
1664
+ const texCoord = transform3.texCoord ?? ref.texCoord;
1665
+ return {
1666
+ texture: textureAt(ref.index),
1667
+ ...texCoord === void 0 ? {} : { texCoord },
1668
+ ...pick(transform3, ["offset", "scale", "rotation"]),
1669
+ ...normal && ref.scale !== void 0 ? { strength: ref.scale } : {},
1670
+ ...!normal && ref.strength !== void 0 ? { strength: ref.strength } : {}
1671
+ };
1672
+ }
1673
+ return (primitive, count, indexCount) => {
1674
+ const source = primitive.material === void 0 ? {} : record(list(json.materials)[primitive.material]);
1675
+ if (primitive.material !== void 0 && (!Number.isInteger(primitive.material) || !list(json.materials)[primitive.material]))
1676
+ throw new RangeError("A primitive names a missing material.");
1677
+ const pbr = record(source.pbrMetallicRoughness);
1678
+ const extensions = record(source.extensions);
1679
+ const material = {
1680
+ ...pick(pbr, ["baseColorFactor", "metallicFactor", "roughnessFactor"]),
1681
+ ...pick(source, ["emissiveFactor", "alphaMode", "alphaCutoff", "doubleSided"]),
1682
+ ...extensions.KHR_materials_unlit === void 0 ? {} : { unlit: true },
1683
+ ...pick(record(extensions.KHR_materials_emissive_strength), ["emissiveStrength"]),
1684
+ ...optional("baseColorTexture", textureRef(pbr.baseColorTexture)),
1685
+ ...optional("metallicRoughnessTexture", textureRef(pbr.metallicRoughnessTexture)),
1686
+ ...optional("normalTexture", textureRef(source.normalTexture, true)),
1687
+ ...optional("occlusionTexture", textureRef(source.occlusionTexture)),
1688
+ ...optional("emissiveTexture", textureRef(source.emissiveTexture))
1689
+ };
1690
+ const attributes = record(primitive.attributes);
1691
+ const read = (name, size) => {
1692
+ const index = attributes[name];
1693
+ if (index === void 0) return void 0;
1694
+ if (!Number.isInteger(index)) throw new RangeError(`Invalid ${name} accessor.`);
1695
+ const value = accessor2(index);
1696
+ if (!size.includes(value.size) || value.values.length !== count * value.size)
1697
+ throw new RangeError(`${name} must have one value per vertex.`);
1698
+ return value;
1699
+ };
1700
+ const uv0 = read("TEXCOORD_0", [2]), uv1 = read("TEXCOORD_1", [2]);
1701
+ if (uv1?.values.some((value) => !(value >= 0 && value <= 1)))
1702
+ throw new RangeError("TEXCOORD_1 must lie between zero and one.");
1703
+ const colour = read("COLOR_0", [3, 4]);
1704
+ for (const key of [
1705
+ "baseColorTexture",
1706
+ "metallicRoughnessTexture",
1707
+ "normalTexture",
1708
+ "occlusionTexture",
1709
+ "emissiveTexture"
1710
+ ]) {
1711
+ const ref = material[key];
1712
+ if (ref && !(ref.texCoord === 1 ? uv1 : ref.texCoord === void 0 || ref.texCoord === 0 ? uv0 : void 0))
1713
+ throw new RangeError(`The ${key} needs its declared UV coordinates.`);
1714
+ }
1715
+ const data = new Float32Array(count * 8);
1716
+ for (let vertex = 0; vertex < count; vertex++) {
1717
+ for (let axis = 0; axis < 2; axis++) {
1718
+ data[vertex * 8 + axis] = uv0?.values[vertex * 2 + axis] ?? 0;
1719
+ data[vertex * 8 + 2 + axis] = uv1?.values[vertex * 2 + axis] ?? 0;
1720
+ }
1721
+ for (let channel = 0; channel < 4; channel++)
1722
+ data[vertex * 8 + 4 + channel] = colour && channel < colour.size ? colour.values[vertex * colour.size + channel] : 1;
1723
+ }
1724
+ const surface = {
1725
+ attributes: data,
1726
+ materials: [material],
1727
+ textures,
1728
+ ranges: [{ from: 0, count: indexCount, material: 0 }]
1729
+ };
1730
+ checkTileflowModelSurface2(surface, count, indexCount);
1731
+ return surface;
1732
+ };
1733
+ }
1734
+ function record(value) {
1735
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1736
+ }
1737
+ function list(value) {
1738
+ return Array.isArray(value) ? value : [];
1739
+ }
1740
+ function pick(value, keys) {
1741
+ return Object.fromEntries(
1742
+ keys.filter((key) => value[key] !== void 0).map((key) => [key, value[key]])
1743
+ );
1744
+ }
1745
+ function optional(key, value) {
1746
+ return value === void 0 ? {} : { [key]: value };
1747
+ }
1748
+
1749
+ // src/gltf-reader.ts
1750
+ var TileflowGltfError = class extends Error {
1751
+ name = "TileflowGltfError";
1752
+ };
1753
+ var glbMagic = 1179937895;
1754
+ var jsonChunk = 1313821514;
1755
+ var binChunk = 5130562;
1756
+ var triangles = 4;
1757
+ var compressions = /* @__PURE__ */ new Set([
1758
+ "KHR_draco_mesh_compression",
1759
+ "EXT_meshopt_compression",
1760
+ "KHR_meshopt_compression",
1761
+ "KHR_mesh_quantization"
1762
+ ]);
1763
+ var componentBytes = {
1764
+ 5120: 1,
1765
+ 5121: 1,
1766
+ 5122: 2,
1767
+ 5123: 2,
1768
+ 5125: 4,
1769
+ 5126: 4
1770
+ };
1771
+ var typeSizes = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
1772
+ function readTileflowGltfTriangles(bytes, { materials = false } = {}) {
1773
+ const { json, bin } = chunks(bytes);
1774
+ const required = array(json.extensionsRequired).filter((name) => typeof name === "string");
1775
+ const compressed = required.find((name) => compressions.has(name));
1776
+ if (compressed)
1777
+ throw new TileflowGltfError(
1778
+ `uses compressed geometry (${String(compressed)}); export it without compression`
1779
+ );
1780
+ for (const buffer of array(json.buffers))
1781
+ if (record2(buffer).uri !== void 0)
1782
+ throw new TileflowGltfError("keeps its geometry in another file; export a single .glb");
1783
+ const nodes = array(json.nodes).map(record2);
1784
+ const scenes = array(json.scenes).map(record2);
1785
+ const scene = scenes[typeof json.scene === "number" ? json.scene : 0];
1786
+ const roots = scene ? array(scene.nodes) : nodes.map((_, index) => index).filter((index) => !nodes.some((node) => array(node.children).includes(index)));
1787
+ const out = [];
1788
+ const physical = materials ? createGltfMaterialReader(json, bin, (index) => accessorWithSize(json, bin, index)) : void 0;
1789
+ const emitters = [];
1790
+ const visit = (index, parent, depth) => {
1791
+ if (typeof index !== "number" || !nodes[index])
1792
+ throw new TileflowGltfError("names a node it does not hold");
1793
+ if (depth > 64) throw new TileflowGltfError("nests its nodes more than 64 deep");
1794
+ const node = nodes[index];
1795
+ const world = multiply(parent, local(node));
1796
+ if (physical) {
1797
+ const lights = record2(record2(node.extras).tileflow).emitters;
1798
+ if (lights !== void 0 && !Array.isArray(lights))
1799
+ throw new TileflowGltfError("area emitters must be an array");
1800
+ for (const value of array(lights)) {
1801
+ const own = record2(value);
1802
+ const point3 = (key) => {
1803
+ const input = array(own[key]);
1804
+ if (input.length !== 3 || !input.every((value2) => typeof value2 === "number" && Number.isFinite(value2)))
1805
+ throw new TileflowGltfError("an area emitter needs finite three-dimensional endpoints");
1806
+ const p = transform2(world, input[0], input[1], input[2], 1);
1807
+ return [p[0], -p[2], p[1]];
1808
+ };
1809
+ const heightScale = Math.hypot(...transform2(world, 0, 1, 0, 0));
1810
+ const depthScale = Math.hypot(...transform2(world, 0, 0, 1, 0));
1811
+ if (typeof own.height !== "number")
1812
+ throw new TileflowGltfError("an area emitter needs its height");
1813
+ emitters.push({
1814
+ start: point3("start"),
1815
+ end: point3("end"),
1816
+ height: own.height * heightScale,
1817
+ ...own.depth === void 0 ? {} : { depth: Number(own.depth) * depthScale },
1818
+ role: own.role,
1819
+ part: own.part
1820
+ });
1821
+ }
1822
+ }
1823
+ if (typeof node.mesh === "number") out.push(...meshOf(json, bin, node.mesh, world, physical));
1824
+ for (const child of array(node.children)) visit(child, world, depth + 1);
1825
+ };
1826
+ for (const root of roots) visit(root, identity(), 0);
1827
+ if (out.length === 0) throw new TileflowGltfError("holds no triangles in its scene");
1828
+ if (emitters.length) {
1829
+ const first = out.find((part) => !/^footprint(?:\.\d+)?$/i.test(part.material ?? ""));
1830
+ if (first?.surface) first.surface = { ...first.surface, emitters };
1831
+ }
1832
+ return out;
1833
+ }
1834
+ function chunks(bytes) {
1835
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1836
+ if (bytes.byteLength < 20 || view.getUint32(0, true) !== glbMagic)
1837
+ throw new TileflowGltfError("is not a glTF binary; export the model as .glb");
1838
+ if (view.getUint32(4, true) !== 2) throw new TileflowGltfError("is not glTF 2.0");
1839
+ if (view.getUint32(8, true) !== bytes.byteLength)
1840
+ throw new TileflowGltfError("is cut short or has trailing bytes");
1841
+ let at = 12;
1842
+ let json;
1843
+ let bin;
1844
+ while (at + 8 <= bytes.byteLength) {
1845
+ const length = view.getUint32(at, true);
1846
+ const type = view.getUint32(at + 4, true);
1847
+ const start = at + 8;
1848
+ if (start + length > bytes.byteLength) throw new TileflowGltfError("has a chunk past its end");
1849
+ const body = bytes.subarray(start, start + length);
1850
+ if (type === jsonChunk && json === void 0) {
1851
+ try {
1852
+ json = record2(JSON.parse(new TextDecoder().decode(body)));
1853
+ } catch {
1854
+ throw new TileflowGltfError("has an unreadable JSON chunk");
1855
+ }
1856
+ } else if (type === binChunk && bin === void 0) bin = body;
1857
+ at = start + length + (4 - length % 4) % 4;
1858
+ }
1859
+ if (!json) throw new TileflowGltfError("has no JSON chunk");
1860
+ return { json, bin };
1861
+ }
1862
+ function meshOf(json, bin, index, world, physical) {
1863
+ const mesh = record2(array(json.meshes)[index]);
1864
+ const normalMatrix = inverseTranspose(world);
1865
+ const result = array(mesh.primitives).map((value) => {
1866
+ const primitive = record2(value);
1867
+ if ((primitive.mode ?? triangles) !== triangles)
1868
+ throw new TileflowGltfError("draws points or lines; a tree model draws triangles");
1869
+ const attributes = record2(primitive.attributes);
1870
+ if (typeof attributes.POSITION !== "number")
1871
+ throw new TileflowGltfError("has a primitive without positions");
1872
+ const raw = accessor(json, bin, attributes.POSITION, 3);
1873
+ const count = raw.length / 3;
1874
+ const positions = [];
1875
+ for (let vertex = 0; vertex < count; vertex += 1) {
1876
+ const [x, y, z] = transform2(
1877
+ world,
1878
+ raw[vertex * 3],
1879
+ raw[vertex * 3 + 1],
1880
+ raw[vertex * 3 + 2],
1881
+ 1
1882
+ );
1883
+ positions.push(x, -z, y);
1884
+ }
1885
+ let normals;
1886
+ if (typeof attributes.NORMAL === "number") {
1887
+ const rawNormals = accessor(json, bin, attributes.NORMAL, 3);
1888
+ normals = [];
1889
+ for (let vertex = 0; vertex < count; vertex += 1) {
1890
+ const [x, y, z] = transform2(
1891
+ normalMatrix,
1892
+ rawNormals[vertex * 3],
1893
+ rawNormals[vertex * 3 + 1],
1894
+ rawNormals[vertex * 3 + 2],
1895
+ 0
1896
+ );
1897
+ const size = Math.hypot(x, y, z) || 1;
1898
+ normals.push(x / size, -z / size, y / size);
1899
+ }
1900
+ }
1901
+ const material = typeof primitive.material === "number" ? record2(array(json.materials)[primitive.material]) : void 0;
1902
+ const base = array(record2(material?.pbrMetallicRoughness).baseColorFactor).map(Number);
1903
+ const factor = base.length >= 3 && base.every(Number.isFinite) ? base : [1, 1, 1];
1904
+ const colours = [];
1905
+ const vertexColours = typeof attributes.COLOR_0 === "number" ? accessorWithSize(json, bin, attributes.COLOR_0) : void 0;
1906
+ for (let vertex = 0; vertex < count; vertex += 1)
1907
+ for (let channel = 0; channel < 3; channel += 1)
1908
+ colours.push(
1909
+ factor[channel] * (vertexColours ? vertexColours.values[vertex * vertexColours.size + channel] : 1)
1910
+ );
1911
+ const indices = typeof primitive.indices === "number" ? accessor(json, bin, primitive.indices, 1).map((value2) => Math.round(value2)) : Array.from({ length: count }, (_, vertex) => vertex);
1912
+ if (indices.length % 3 !== 0 || indices.some((vertex) => vertex < 0 || vertex >= count))
1913
+ throw new TileflowGltfError("has triangles that name missing vertices");
1914
+ if (determinant(world) < 0)
1915
+ for (let at = 0; at + 2 < indices.length; at += 3)
1916
+ [indices[at + 1], indices[at + 2]] = [indices[at + 2], indices[at + 1]];
1917
+ return {
1918
+ material: typeof material?.name === "string" ? material.name : void 0,
1919
+ ...record2(record2(material?.extras).tileflow).lightingRole === void 0 ? {} : {
1920
+ lightingRole: record2(record2(material?.extras).tileflow).lightingRole
1921
+ },
1922
+ positions,
1923
+ normals,
1924
+ colours,
1925
+ indices,
1926
+ ...physical ? { surface: physical(primitive, count, indices.length) } : {}
1927
+ };
1928
+ });
1929
+ if (physical) {
1930
+ let low = Infinity, high = -Infinity;
1931
+ for (const part of result)
1932
+ if (!/^footprint(?:\.\d+)?$/i.test(part.material ?? ""))
1933
+ for (let at = 2; at < part.positions.length; at += 3) {
1934
+ low = Math.min(low, part.positions[at]);
1935
+ high = Math.max(high, part.positions[at]);
1936
+ }
1937
+ if (Number.isFinite(low)) {
1938
+ for (const part of result)
1939
+ if (part.surface)
1940
+ part.surface = {
1941
+ ...part.surface,
1942
+ ranges: part.surface.ranges.map((range) => ({
1943
+ ...range,
1944
+ emissiveBounds: [low, high]
1945
+ }))
1946
+ };
1947
+ }
1948
+ }
1949
+ return result;
1950
+ }
1951
+ function accessor(json, bin, index, size) {
1952
+ const read = accessorWithSize(json, bin, index);
1953
+ if (read.size !== size)
1954
+ throw new TileflowGltfError(`has an accessor of ${read.size} components where ${size} belong`);
1955
+ return read.values;
1956
+ }
1957
+ function accessorWithSize(json, bin, index) {
1958
+ const accessor2 = record2(array(json.accessors)[index]);
1959
+ if (accessor2.sparse !== void 0)
1960
+ throw new TileflowGltfError("uses sparse accessors; export without them");
1961
+ const size = typeSizes[String(accessor2.type)];
1962
+ const component = Number(accessor2.componentType);
1963
+ const bytesEach = componentBytes[component];
1964
+ const count = Number(accessor2.count);
1965
+ if (!size || !bytesEach || !Number.isInteger(count) || count < 0 || count > 1e6)
1966
+ throw new TileflowGltfError("has an accessor Tileflow cannot read");
1967
+ if (typeof accessor2.bufferView !== "number")
1968
+ return { values: new Array(count * size).fill(0), size };
1969
+ const view = record2(array(json.bufferViews)[accessor2.bufferView]);
1970
+ if (!bin || Number(view.buffer ?? 0) !== 0)
1971
+ throw new TileflowGltfError("has geometry outside its binary chunk");
1972
+ const stride = Number(view.byteStride ?? 0) || size * bytesEach;
1973
+ const start = Number(view.byteOffset ?? 0) + Number(accessor2.byteOffset ?? 0);
1974
+ const end = start + (count - 1) * stride + size * bytesEach;
1975
+ if (count > 0 && (start < 0 || end > Number(view.byteOffset ?? 0) + Number(view.byteLength) || end > bin.byteLength))
1976
+ throw new TileflowGltfError("has an accessor past the end of its buffer");
1977
+ const data = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
1978
+ const normalized = accessor2.normalized === true;
1979
+ const values = [];
1980
+ for (let item = 0; item < count; item += 1)
1981
+ for (let part = 0; part < size; part += 1) {
1982
+ const at = start + item * stride + part * bytesEach;
1983
+ let value;
1984
+ switch (component) {
1985
+ case 5126:
1986
+ value = data.getFloat32(at, true);
1987
+ break;
1988
+ case 5125:
1989
+ value = data.getUint32(at, true);
1990
+ break;
1991
+ case 5123:
1992
+ value = data.getUint16(at, true);
1993
+ if (normalized) value /= 65535;
1994
+ break;
1995
+ case 5122:
1996
+ value = data.getInt16(at, true);
1997
+ if (normalized) value = Math.max(value / 32767, -1);
1998
+ break;
1999
+ case 5121:
2000
+ value = data.getUint8(at);
2001
+ if (normalized) value /= 255;
2002
+ break;
2003
+ default:
2004
+ value = data.getInt8(at);
2005
+ if (normalized) value = Math.max(value / 127, -1);
2006
+ }
2007
+ if (!Number.isFinite(value)) throw new TileflowGltfError("has a value that is not a number");
2008
+ values.push(value);
2009
+ }
2010
+ return { values, size };
2011
+ }
2012
+ function identity() {
2013
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
2014
+ }
2015
+ function local(node) {
2016
+ const matrix = array(node.matrix).map(Number);
2017
+ if (matrix.length === 16 && matrix.every(Number.isFinite)) return matrix;
2018
+ const [tx, ty, tz] = vector(node.translation, [0, 0, 0]);
2019
+ const [qx, qy, qz, qw] = vector(node.rotation, [0, 0, 0, 1]);
2020
+ const [sx, sy, sz] = vector(node.scale, [1, 1, 1]);
2021
+ const [xx, yy, zz] = [qx * qx, qy * qy, qz * qz];
2022
+ const [xy, xz, yz, wx, wy, wz] = [qx * qy, qx * qz, qy * qz, qw * qx, qw * qy, qw * qz];
2023
+ return [
2024
+ (1 - 2 * (yy + zz)) * sx,
2025
+ 2 * (xy + wz) * sx,
2026
+ 2 * (xz - wy) * sx,
2027
+ 0,
2028
+ 2 * (xy - wz) * sy,
2029
+ (1 - 2 * (xx + zz)) * sy,
2030
+ 2 * (yz + wx) * sy,
2031
+ 0,
2032
+ 2 * (xz + wy) * sz,
2033
+ 2 * (yz - wx) * sz,
2034
+ (1 - 2 * (xx + yy)) * sz,
2035
+ 0,
2036
+ tx,
2037
+ ty,
2038
+ tz,
2039
+ 1
2040
+ ];
2041
+ }
2042
+ function multiply(a, b) {
2043
+ const out = new Array(16).fill(0);
2044
+ for (let column = 0; column < 4; column += 1)
2045
+ for (let row = 0; row < 4; row += 1)
2046
+ for (let k = 0; k < 4; k += 1) out[column * 4 + row] += a[k * 4 + row] * b[column * 4 + k];
2047
+ return out;
2048
+ }
2049
+ function transform2(m, x, y, z, w) {
2050
+ return [
2051
+ m[0] * x + m[4] * y + m[8] * z + m[12] * w,
2052
+ m[1] * x + m[5] * y + m[9] * z + m[13] * w,
2053
+ m[2] * x + m[6] * y + m[10] * z + m[14] * w
2054
+ ];
2055
+ }
2056
+ function determinant(m) {
2057
+ 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]);
2058
+ }
2059
+ function inverseTranspose(m) {
2060
+ const [a, b, c, d, e, f, g, h, i] = [
2061
+ m[0],
2062
+ m[1],
2063
+ m[2],
2064
+ m[4],
2065
+ m[5],
2066
+ m[6],
2067
+ m[8],
2068
+ m[9],
2069
+ m[10]
2070
+ ];
2071
+ const det = determinant(m) || 1;
2072
+ return [
2073
+ (e * i - h * f) / det,
2074
+ (g * f - d * i) / det,
2075
+ (d * h - g * e) / det,
2076
+ 0,
2077
+ (h * c - b * i) / det,
2078
+ (a * i - g * c) / det,
2079
+ (g * b - a * h) / det,
2080
+ 0,
2081
+ (b * f - e * c) / det,
2082
+ (d * c - a * f) / det,
2083
+ (a * e - d * b) / det,
2084
+ 0,
2085
+ 0,
2086
+ 0,
2087
+ 0,
2088
+ 1
2089
+ ];
2090
+ }
2091
+ function record2(value) {
2092
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2093
+ }
2094
+ function array(value) {
2095
+ return Array.isArray(value) ? value : [];
2096
+ }
2097
+ function vector(value, fallback) {
2098
+ const list2 = array(value).map(Number);
2099
+ return list2.length === fallback.length && list2.every(Number.isFinite) ? list2 : fallback;
2100
+ }
2101
+
2102
+ // src/grey-jpeg.ts
2103
+ var zigzag = [
2104
+ 0,
2105
+ 1,
2106
+ 8,
2107
+ 16,
2108
+ 9,
2109
+ 2,
2110
+ 3,
2111
+ 10,
2112
+ 17,
2113
+ 24,
2114
+ 32,
2115
+ 25,
2116
+ 18,
2117
+ 11,
2118
+ 4,
2119
+ 5,
2120
+ 12,
2121
+ 19,
2122
+ 26,
2123
+ 33,
2124
+ 40,
2125
+ 48,
2126
+ 41,
2127
+ 34,
2128
+ 27,
2129
+ 20,
2130
+ 13,
2131
+ 6,
2132
+ 7,
2133
+ 14,
2134
+ 21,
2135
+ 28,
2136
+ 35,
2137
+ 42,
2138
+ 49,
2139
+ 56,
2140
+ 57,
2141
+ 50,
2142
+ 43,
2143
+ 36,
2144
+ 29,
2145
+ 22,
2146
+ 15,
2147
+ 23,
2148
+ 30,
2149
+ 37,
2150
+ 44,
2151
+ 51,
2152
+ 58,
2153
+ 59,
2154
+ 52,
2155
+ 45,
2156
+ 38,
2157
+ 31,
2158
+ 39,
2159
+ 46,
2160
+ 53,
2161
+ 60,
2162
+ 61,
2163
+ 54,
2164
+ 47,
2165
+ 55,
2166
+ 62,
2167
+ 63
2168
+ ];
2169
+ var luminance = [
2170
+ 16,
2171
+ 11,
2172
+ 10,
2173
+ 16,
2174
+ 24,
2175
+ 40,
2176
+ 51,
2177
+ 61,
2178
+ 12,
2179
+ 12,
2180
+ 14,
2181
+ 19,
2182
+ 26,
2183
+ 58,
2184
+ 60,
2185
+ 55,
2186
+ 14,
2187
+ 13,
2188
+ 16,
2189
+ 24,
2190
+ 40,
2191
+ 57,
2192
+ 69,
2193
+ 56,
2194
+ 14,
2195
+ 17,
2196
+ 22,
2197
+ 29,
2198
+ 51,
2199
+ 87,
2200
+ 80,
2201
+ 62,
2202
+ 18,
2203
+ 22,
2204
+ 37,
2205
+ 56,
2206
+ 68,
2207
+ 109,
2208
+ 103,
2209
+ 77,
2210
+ 24,
2211
+ 35,
2212
+ 55,
2213
+ 64,
2214
+ 81,
2215
+ 104,
2216
+ 113,
2217
+ 92,
2218
+ 49,
2219
+ 64,
2220
+ 78,
2221
+ 87,
2222
+ 103,
2223
+ 121,
2224
+ 120,
2225
+ 101,
2226
+ 72,
2227
+ 92,
2228
+ 95,
2229
+ 98,
2230
+ 112,
2231
+ 100,
2232
+ 103,
2233
+ 99
2234
+ ];
2235
+ function huffman(counts, symbols) {
2236
+ const out = { code: [], size: [] };
2237
+ let code = 0, k = 0;
2238
+ for (let length = 1; length <= 16; length++) {
2239
+ for (let i = 0; i < counts[length - 1]; i++) {
2240
+ out.code[symbols[k]] = code;
2241
+ out.size[symbols[k]] = length;
2242
+ code++;
2243
+ k++;
2244
+ }
2245
+ code <<= 1;
2246
+ }
2247
+ return out;
2248
+ }
2249
+ var basis = Array.from({ length: 64 }, (_, i) => {
2250
+ const u = i >> 3, x = i & 7;
2251
+ return (u === 0 ? Math.SQRT1_2 : 1) * 0.5 * Math.cos((2 * x + 1) * u * Math.PI / 16);
2252
+ });
2253
+ function encodeGreyJpeg(pixels, width, height, quality) {
2254
+ if (!(width > 0 && height > 0 && width <= 65535 && height <= 65535))
2255
+ throw new RangeError("A JPEG is 1 to 65,535 pixels on each side.");
2256
+ if (pixels.length !== width * height) throw new RangeError("One byte a pixel is required.");
2257
+ const q = Math.min(100, Math.max(1, Math.round(quality)));
2258
+ const scale = q < 50 ? 5e3 / q : 200 - 2 * q;
2259
+ const table = luminance.map(
2260
+ (value) => Math.min(255, Math.max(1, Math.floor((value * scale + 50) / 100)))
2261
+ );
2262
+ const symbols = [];
2263
+ const extra = [];
2264
+ const frequencies = [new Array(257).fill(0), new Array(257).fill(0)];
2265
+ const emit = (table2, symbol, value = 0, size = 0) => {
2266
+ symbols.push(table2 << 8 | symbol);
2267
+ extra.push(size ? (value < 0 ? value + (1 << size) - 1 : value) << 5 | size : 0);
2268
+ frequencies[table2][symbol] += 1;
2269
+ };
2270
+ const magnitude = (value) => {
2271
+ let size = 0;
2272
+ for (let v = Math.abs(value); v > 0; v >>= 1) size++;
2273
+ return size;
2274
+ };
2275
+ const block = new Float64Array(64);
2276
+ const row = new Float64Array(64);
2277
+ const coefficients = new Int32Array(64);
2278
+ let previous = 0;
2279
+ for (let by = 0; by < height; by += 8)
2280
+ for (let bx = 0; bx < width; bx += 8) {
2281
+ for (let y = 0; y < 8; y++)
2282
+ for (let x = 0; x < 8; x++)
2283
+ block[y * 8 + x] = pixels[Math.min(height - 1, by + y) * width + Math.min(width - 1, bx + x)] - 128;
2284
+ for (let y = 0; y < 8; y++)
2285
+ for (let u = 0; u < 8; u++) {
2286
+ let sum = 0;
2287
+ for (let x = 0; x < 8; x++) sum += block[y * 8 + x] * basis[u * 8 + x];
2288
+ row[y * 8 + u] = sum;
2289
+ }
2290
+ for (let v = 0; v < 8; v++)
2291
+ for (let u = 0; u < 8; u++) {
2292
+ let sum = 0;
2293
+ for (let y = 0; y < 8; y++) sum += row[y * 8 + u] * basis[v * 8 + y];
2294
+ coefficients[v * 8 + u] = Math.round(sum / table[v * 8 + u]);
2295
+ }
2296
+ const dc = coefficients[0] - previous;
2297
+ previous = coefficients[0];
2298
+ const dcSize = magnitude(dc);
2299
+ emit(0, dcSize, dc, dcSize);
2300
+ let zeros = 0;
2301
+ for (let k = 1; k < 64; k++) {
2302
+ const value = coefficients[zigzag[k]];
2303
+ if (value === 0) {
2304
+ zeros++;
2305
+ continue;
2306
+ }
2307
+ for (; zeros > 15; zeros -= 16) emit(1, 240);
2308
+ const size = magnitude(value);
2309
+ emit(1, zeros << 4 | size, value, size);
2310
+ zeros = 0;
2311
+ }
2312
+ if (zeros > 0) emit(1, 0);
2313
+ }
2314
+ const tables = frequencies.map(optimalTable);
2315
+ const codes = tables.map(({ counts, values }) => huffman(counts, values));
2316
+ const out = [];
2317
+ const word = (value) => out.push(value >> 8 & 255, value & 255);
2318
+ word(65496);
2319
+ word(65499);
2320
+ word(67);
2321
+ out.push(0, ...zigzag.map((at) => table[at]));
2322
+ word(65472);
2323
+ word(11);
2324
+ out.push(8);
2325
+ word(height);
2326
+ word(width);
2327
+ out.push(1, 1, 17, 0);
2328
+ for (const [index, { counts, values }] of tables.entries()) {
2329
+ word(65476);
2330
+ word(3 + 16 + values.length);
2331
+ out.push(index << 4, ...counts, ...values);
2332
+ }
2333
+ word(65498);
2334
+ word(8);
2335
+ out.push(1, 1, 0, 0, 63, 0);
2336
+ let bits = 0, count = 0;
2337
+ const put = (code, size) => {
2338
+ bits = bits << size | code & (1 << size) - 1;
2339
+ count += size;
2340
+ while (count >= 8) {
2341
+ const byte = bits >> count - 8 & 255;
2342
+ out.push(byte);
2343
+ if (byte === 255) out.push(0);
2344
+ count -= 8;
2345
+ }
2346
+ bits &= (1 << count) - 1;
2347
+ };
2348
+ for (let i = 0; i < symbols.length; i++) {
2349
+ const own = codes[symbols[i] >> 8];
2350
+ const symbol = symbols[i] & 255;
2351
+ put(own.code[symbol], own.size[symbol]);
2352
+ const size = extra[i] & 31;
2353
+ if (size) put(extra[i] >> 5, size);
2354
+ }
2355
+ if (count > 0) put(127, 8 - count);
2356
+ word(65497);
2357
+ return Uint8Array.from(out);
2358
+ }
2359
+ function optimalTable(frequencies) {
2360
+ const freq = [...frequencies];
2361
+ freq[256] = 1;
2362
+ const codesize = new Array(257).fill(0);
2363
+ const others = new Array(257).fill(-1);
2364
+ for (; ; ) {
2365
+ let v1 = -1, v2 = -1;
2366
+ for (let i = 0; i <= 256; i++) if (freq[i] > 0 && (v1 < 0 || freq[i] <= freq[v1])) v1 = i;
2367
+ for (let i = 0; i <= 256; i++)
2368
+ if (freq[i] > 0 && i !== v1 && (v2 < 0 || freq[i] <= freq[v2])) v2 = i;
2369
+ if (v2 < 0) break;
2370
+ freq[v1] += freq[v2];
2371
+ freq[v2] = 0;
2372
+ for (codesize[v1]++; others[v1] >= 0; codesize[v1]++) v1 = others[v1];
2373
+ others[v1] = v2;
2374
+ for (codesize[v2]++; others[v2] >= 0; codesize[v2]++) v2 = others[v2];
2375
+ }
2376
+ const bits = new Array(33).fill(0);
2377
+ for (let i = 0; i <= 256; i++) if (codesize[i]) bits[codesize[i]]++;
2378
+ for (let i = 32; i > 16; i--)
2379
+ while (bits[i] > 0) {
2380
+ let j = i - 2;
2381
+ while (bits[j] === 0) j--;
2382
+ bits[i] -= 2;
2383
+ bits[i - 1]++;
2384
+ bits[j + 1] += 2;
2385
+ bits[j]--;
2386
+ }
2387
+ let last = 16;
2388
+ while (bits[last] === 0) last--;
2389
+ bits[last]--;
2390
+ const values = [];
2391
+ for (let length = 1; length <= 32; length++)
2392
+ for (let symbol = 0; symbol < 256; symbol++)
2393
+ if (codesize[symbol] === length) values.push(symbol);
2394
+ return { counts: bits.slice(1, 17), values };
2395
+ }
2396
+
2397
+ // src/tree-model-mesh.ts
2398
+ import {
2399
+ tileflowTreeModelLimits,
2400
+ tileflowTreeParts
2401
+ } from "@tileflow/core";
2402
+ var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
2403
+ function prepareTileflowTreeModelMesh(bytes, level) {
2404
+ const primitives = readTileflowGltfTriangles(bytes);
2405
+ const positions = [];
2406
+ const normals = [];
2407
+ const parts = [];
2408
+ const luminance2 = [];
2409
+ const indices = [];
2410
+ for (const primitive of primitives) {
2411
+ const part = partOf(primitive.material);
2412
+ const offset = positions.length / 3;
2413
+ const count2 = primitive.positions.length / 3;
2414
+ positions.push(...primitive.positions);
2415
+ for (let vertex = 0; vertex < count2; vertex += 1) {
2416
+ parts.push(part);
2417
+ normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
2418
+ const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
2419
+ luminance2.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
2420
+ }
2421
+ for (const index of primitive.indices) indices.push(offset + index);
2422
+ }
2423
+ const count = positions.length / 3;
2424
+ const triangleCount = indices.length / 3;
2425
+ const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
2426
+ if (triangleCount > limit)
2427
+ throw new TileflowGltfError(
2428
+ `has ${triangleCount.toLocaleString("en")} triangles; a ${level} tree model takes at most ${limit.toLocaleString("en")}. Simplify it (for example with Blender's Decimate modifier) and export it again`
2429
+ );
2430
+ if (count > tileflowTreeModelLimits.vertices)
2431
+ throw new TileflowGltfError(
2432
+ `has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
2433
+ );
2434
+ let height = 0;
2435
+ let reach = 0;
2436
+ const crowned = parts.some((part) => isCrown(part));
2437
+ for (let vertex = 0; vertex < count; vertex += 1) {
2438
+ height = Math.max(height, positions[vertex * 3 + 2]);
2439
+ if (!crowned || isCrown(parts[vertex]))
2440
+ reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
2441
+ }
2442
+ if (!(height > 0))
2443
+ throw new TileflowGltfError(
2444
+ "stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
2445
+ );
2446
+ if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
2447
+ const across = 2 * reach;
2448
+ const unit = [];
2449
+ for (let vertex = 0; vertex < count; vertex += 1)
2450
+ unit.push(
2451
+ positions[vertex * 3] / across,
2452
+ positions[vertex * 3 + 1] / across,
2453
+ positions[vertex * 3 + 2] / height
2454
+ );
2455
+ const faceSums = new Array(count * 3).fill(0);
2456
+ for (let at = 0; at + 2 < indices.length; at += 3) {
2457
+ const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
2458
+ const n = cross(sub2(point(unit, b), point(unit, a)), sub2(point(unit, c), point(unit, a)));
2459
+ for (const vertex of [a, b, c])
2460
+ for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
2461
+ }
2462
+ const outNormals = [];
2463
+ for (let vertex = 0; vertex < count; vertex += 1) {
2464
+ const own = normals[vertex];
2465
+ const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
2466
+ const size = Math.hypot(n[0], n[1], n[2]) || 1;
2467
+ outNormals.push(n[0] / size, n[1] / size, n[2] / size);
2468
+ }
2469
+ const shades = new Array(count).fill(0);
2470
+ for (const part of new Set(parts)) {
2471
+ const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
2472
+ const mean = members.reduce((sum, vertex) => sum + luminance2[vertex], 0) / members.length;
2473
+ const spread = Math.max(0.06, mean * 0.6);
2474
+ for (const vertex of members)
2475
+ shades[vertex] = clamp((luminance2[vertex] - mean) / spread, -1, 1);
2476
+ }
2477
+ let crownLow = 1;
2478
+ let crownHigh = 0;
2479
+ for (let vertex = 0; vertex < count; vertex += 1)
2480
+ if (isCrown(parts[vertex])) {
2481
+ crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
2482
+ crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
2483
+ }
2484
+ const occlusion = [];
2485
+ for (let vertex = 0; vertex < count; vertex += 1) {
2486
+ const z = unit[vertex * 3 + 2];
2487
+ if (!isCrown(parts[vertex])) {
2488
+ occlusion.push(z > crownLow ? 0.55 : 0.8);
2489
+ continue;
2490
+ }
2491
+ const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
2492
+ const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
2493
+ occlusion.push(clamp(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
2494
+ }
2495
+ return {
2496
+ // The author's proportions: the trees it stands keep them around the sizes the data gives.
2497
+ proportion: Math.min(3, Math.max(0.1, across / height)),
2498
+ positions: unit,
2499
+ normals: outNormals,
2500
+ parts,
2501
+ shades,
2502
+ occlusion,
2503
+ indices
2504
+ };
2505
+ }
2506
+ function partOf(material) {
2507
+ const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
2508
+ const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
2509
+ if (part) return tileflowTreeParts[part];
2510
+ throw new TileflowGltfError(
2511
+ material === void 0 ? "has a mesh without a material; name each material bark, leaves, fronds or stem" : `has a material "${material}" that names no part of a tree; name each material bark, leaves, fronds or stem`
2512
+ );
2513
+ }
2514
+ function isCrown(part) {
2515
+ return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
2516
+ }
2517
+ function point(list2, vertex) {
2518
+ return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
2519
+ }
2520
+ function sub2(a, b) {
2521
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2522
+ }
2523
+ function cross(a, b) {
2524
+ return [
2525
+ a[1] * b[2] - a[2] * b[1],
2526
+ a[2] * b[0] - a[0] * b[2],
2527
+ a[0] * b[1] - a[1] * b[0]
2528
+ ];
2529
+ }
2530
+ function clamp(value, low, high) {
2531
+ return Math.min(high, Math.max(low, value));
2532
+ }
2533
+
2534
+ // src/building-model-mesh.ts
2535
+ function isTileflowTreeModel(primitives) {
2536
+ const drawn = primitives.filter((primitive) => !isFootprint(primitive));
2537
+ return drawn.length > 0 && drawn.every(
2538
+ (primitive) => tileflowTreeModelPartNames.includes(
2539
+ partOf2(primitive.material)
2540
+ )
2541
+ );
2542
+ }
2543
+ function tileflowBuildingModelDeclaredFootprint(primitives) {
2544
+ const positions = [];
2545
+ const indices = [];
2546
+ for (const primitive of primitives) {
2547
+ if (!isFootprint(primitive)) continue;
2548
+ const offset = positions.length / 3;
2549
+ positions.push(...primitive.positions);
2550
+ for (const index of primitive.indices) indices.push(offset + index);
2551
+ }
2552
+ return indices.length > 0 ? { positions, indices } : void 0;
2553
+ }
2554
+ function prepareTileflowBuildingModelMesh(bytes, {
2555
+ ambientOcclusion = true,
2556
+ lightmapTexels = 0
2557
+ } = {}) {
2558
+ const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes, { materials: true }) : bytes;
2559
+ const drawn = primitives.filter((primitive) => !isFootprint(primitive));
2560
+ const surface = buildingModelSurface(drawn);
2561
+ const positions = [];
2562
+ const normals = [];
2563
+ const partNames = [];
2564
+ const parts = [];
2565
+ const lightingRoles = [];
2566
+ const luminance2 = [];
2567
+ const indices = [];
2568
+ for (const primitive of drawn) {
2569
+ const name = partOf2(primitive.material);
2570
+ if (name === void 0)
2571
+ throw new TileflowGltfError(
2572
+ primitive.material === void 0 ? "has a mesh without a material; name each material after its part, such as surface, glass, roof or water" : `has a material "${primitive.material}" that names no part; name each material after its part in lower-case words joined by hyphens, such as surface, glass, roof or water`
2573
+ );
2574
+ if (!partNames.includes(name)) partNames.push(name);
2575
+ if (partNames.length > tileflowModelLimits.parts)
2576
+ throw new TileflowGltfError(
2577
+ `names ${partNames.length} parts in its materials; a building model takes at most ${tileflowModelLimits.parts} a level. Give materials of the same part the same name`
2578
+ );
2579
+ const part = partNames.indexOf(name);
2580
+ const offset = positions.length / 3;
2581
+ const role = primitive.lightingRole === void 0 ? tileflowModelDefaultLightingRole(name) : typeof primitive.lightingRole === "string" && Object.hasOwn(tileflowModelLightingRoles, primitive.lightingRole) ? tileflowModelLightingRoles[primitive.lightingRole] : void 0;
2582
+ if (role === void 0)
2583
+ throw new TileflowGltfError(
2584
+ "has an unknown lighting role; use unclassified, wall, door, roof, window, lamp or logo"
2585
+ );
2586
+ const count2 = primitive.positions.length / 3;
2587
+ positions.push(...primitive.positions);
2588
+ for (let vertex = 0; vertex < count2; vertex += 1) {
2589
+ parts.push(part);
2590
+ lightingRoles.push(role);
2591
+ normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
2592
+ const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
2593
+ luminance2.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
2594
+ }
2595
+ for (const index of primitive.indices) indices.push(offset + index);
2596
+ }
2597
+ const count = positions.length / 3;
2598
+ const triangles2 = indices.length / 3;
2599
+ if (triangles2 > tileflowModelLimits.triangles)
2600
+ throw new TileflowGltfError(
2601
+ `has ${triangles2.toLocaleString("en")} triangles; a building model takes at most ${tileflowModelLimits.triangles.toLocaleString("en")} a level. Simplify it and export it again`
2602
+ );
2603
+ if (count > tileflowModelLimits.vertices)
2604
+ throw new TileflowGltfError(
2605
+ `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`
2606
+ );
2607
+ let low = Infinity;
2608
+ let high = -Infinity;
2609
+ for (let vertex = 0; vertex < count; vertex += 1) {
2610
+ low = Math.min(low, positions[vertex * 3 + 2]);
2611
+ high = Math.max(high, positions[vertex * 3 + 2]);
2612
+ }
2613
+ if (low < -0.5)
2614
+ throw new TileflowGltfError(
2615
+ `reaches ${(-low).toFixed(1)} m below its origin; put the origin on the ground where the building stands, with the building above it`
2616
+ );
2617
+ if (!(high > 0)) throw new TileflowGltfError("has no height above its origin");
2618
+ const sums = new Array(count * 3).fill(0);
2619
+ for (let at = 0; at + 2 < indices.length; at += 3) {
2620
+ const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
2621
+ const n = cross3(
2622
+ sub3(point2(positions, b), point2(positions, a)),
2623
+ sub3(point2(positions, c), point2(positions, a))
2624
+ );
2625
+ for (const vertex of [a, b, c])
2626
+ for (let axis = 0; axis < 3; axis += 1) sums[vertex * 3 + axis] += n[axis];
2627
+ }
2628
+ const outNormals = [];
2629
+ for (let vertex = 0; vertex < count; vertex += 1) {
2630
+ const n = normals[vertex] ?? sums.slice(vertex * 3, vertex * 3 + 3);
2631
+ const size = Math.hypot(n[0], n[1], n[2]) || 1;
2632
+ outNormals.push(n[0] / size, n[1] / size, n[2] / size);
2633
+ }
2634
+ const shades = new Array(count).fill(0);
2635
+ for (const part of new Set(parts)) {
2636
+ const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
2637
+ const mean = members.reduce((sum, vertex) => sum + luminance2[vertex], 0) / members.length;
2638
+ const spread = Math.max(0.06, mean * 0.6);
2639
+ for (const vertex of members)
2640
+ shades[vertex] = clamp2((luminance2[vertex] - mean) / spread, -1, 1);
2641
+ }
2642
+ const texels = typeof lightmapTexels === "function" ? lightmapTexels(surfaceArea(positions, indices)) : lightmapTexels;
2643
+ const lightmap = ambientOcclusion && texels > 0 && surface && lightmapFits(surface) ? bakeBuildingModelLightmap({ positions, normals: outNormals, indices }, { texels }) : void 0;
2644
+ if (!lightmap)
2645
+ return {
2646
+ positions,
2647
+ normals: outNormals,
2648
+ parts,
2649
+ partNames,
2650
+ lightingRoles,
2651
+ shades,
2652
+ ...surface ? { surface } : {},
2653
+ occlusion: ambientOcclusion ? buildingModelAmbientVisibility({ positions, normals: outNormals, indices }) : new Array(count).fill(1),
2654
+ indices
2655
+ };
2656
+ const { sources } = lightmap;
2657
+ const from = (values, width = 1) => sources.flatMap((source) => values.slice(source * width, source * width + width));
2658
+ const attributes = from([...surface.attributes], 8);
2659
+ for (let vertex = 0; vertex < sources.length; vertex++) {
2660
+ attributes[vertex * 8 + 2] = lightmap.uvs[vertex * 2];
2661
+ attributes[vertex * 8 + 3] = lightmap.uvs[vertex * 2 + 1];
2662
+ }
2663
+ const texture = surface.textures.length;
2664
+ return {
2665
+ positions: from(positions, 3),
2666
+ normals: from(outNormals, 3),
2667
+ parts: from(parts),
2668
+ partNames,
2669
+ lightingRoles: from(lightingRoles),
2670
+ shades: from(shades),
2671
+ surface: {
2672
+ ...surface,
2673
+ attributes,
2674
+ materials: surface.materials.map((material) => ({
2675
+ ...material,
2676
+ occlusionTexture: { texture, texCoord: 1 }
2677
+ })),
2678
+ textures: [
2679
+ ...surface.textures,
2680
+ {
2681
+ mimeType: "image/jpeg",
2682
+ bytes: encodeGreyJpeg(lightmap.texels, lightmap.width, lightmap.height, lightmapQuality),
2683
+ width: lightmap.width,
2684
+ height: lightmap.height,
2685
+ wrapS: 33071,
2686
+ wrapT: 33071,
2687
+ minFilter: 9987,
2688
+ magFilter: 9729
2689
+ }
2690
+ ]
2691
+ },
2692
+ occlusion: new Array(sources.length).fill(1),
2693
+ indices: lightmap.indices
2694
+ };
2695
+ }
2696
+ var lightmapQuality = 35;
2697
+ function surfaceArea(positions, indices) {
2698
+ let area = 0;
2699
+ for (let at = 0; at + 2 < indices.length; at += 3) {
2700
+ const a = point2(positions, indices[at]);
2701
+ area += Math.hypot(
2702
+ ...cross3(
2703
+ sub3(point2(positions, indices[at + 1]), a),
2704
+ sub3(point2(positions, indices[at + 2]), a)
2705
+ )
2706
+ ) / 2;
2707
+ }
2708
+ return area;
2709
+ }
2710
+ function lightmapFits(surface) {
2711
+ return surface.textures.length < tileflowModelSurfaceLimits2.textures && surface.materials.every(
2712
+ (material) => !material.occlusionTexture && tileflowModelTextureChannels2.every((channel) => material[channel]?.texCoord !== 1)
2713
+ );
2714
+ }
2715
+ function tileflowBuildingModelFootprint(mesh) {
2716
+ const { positions, indices } = mesh;
2717
+ let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity];
2718
+ for (let at = 0; at < positions.length; at += 3) {
2719
+ minX = Math.min(minX, positions[at]);
2720
+ maxX = Math.max(maxX, positions[at]);
2721
+ minY = Math.min(minY, positions[at + 1]);
2722
+ maxY = Math.max(maxY, positions[at + 1]);
2723
+ }
2724
+ const cell = Math.max(1, Math.max(maxX - minX, maxY - minY) / 1024);
2725
+ const columns = Math.ceil((maxX - minX) / cell) + 5;
2726
+ const rows = Math.ceil((maxY - minY) / cell) + 5;
2727
+ const x0 = minX - 2 * cell;
2728
+ const y0 = minY - 2 * cell;
2729
+ const filled = new Uint8Array(columns * rows);
2730
+ for (let at = 0; at + 2 < indices.length; at += 3) {
2731
+ const corners = [indices[at], indices[at + 1], indices[at + 2]].map((vertex) => [
2732
+ (positions[vertex * 3] - x0) / cell,
2733
+ (positions[vertex * 3 + 1] - y0) / cell
2734
+ ]);
2735
+ for (const [cx, cy] of corners) filled[Math.floor(cy) * columns + Math.floor(cx)] = 1;
2736
+ const left = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[0]))));
2737
+ const right = Math.min(columns - 1, Math.ceil(Math.max(...corners.map((c2) => c2[0]))));
2738
+ const top = Math.max(0, Math.floor(Math.min(...corners.map((c2) => c2[1]))));
2739
+ const bottom = Math.min(rows - 1, Math.ceil(Math.max(...corners.map((c2) => c2[1]))));
2740
+ const [a, b, c] = corners;
2741
+ const area = edge(a, b, c);
2742
+ if (Math.abs(area) < 1e-9) continue;
2743
+ for (let y = top; y <= bottom; y += 1)
2744
+ for (let x = left; x <= right; x += 1) {
2745
+ const p = [x + 0.5, y + 0.5];
2746
+ const w0 = edge(b, c, p) / area;
2747
+ const w1 = edge(c, a, p) / area;
2748
+ const w2 = edge(a, b, p) / area;
2749
+ if (w0 >= 0 && w1 >= 0 && w2 >= 0) filled[y * columns + x] = 1;
2750
+ }
2751
+ }
2752
+ const grown = new Uint8Array(filled);
2753
+ for (let y = 1; y < rows - 1; y += 1)
2754
+ for (let x = 1; x < columns - 1; x += 1) {
2755
+ if (filled[y * columns + x]) continue;
2756
+ for (let dy = -1; dy <= 1 && !grown[y * columns + x]; dy += 1)
2757
+ for (let dx = -1; dx <= 1; dx += 1)
2758
+ if (filled[(y + dy) * columns + x + dx]) {
2759
+ grown[y * columns + x] = 1;
2760
+ break;
2761
+ }
2762
+ }
2763
+ const outside = new Uint8Array(columns * rows);
2764
+ const stack = [0];
2765
+ outside[0] = 1;
2766
+ while (stack.length) {
2767
+ const at = stack.pop();
2768
+ const x = at % columns;
2769
+ const y = (at - x) / columns;
2770
+ for (const [dx, dy] of [
2771
+ [1, 0],
2772
+ [-1, 0],
2773
+ [0, 1],
2774
+ [0, -1]
2775
+ ]) {
2776
+ const nx = x + dx;
2777
+ const ny = y + dy;
2778
+ if (nx < 0 || ny < 0 || nx >= columns || ny >= rows) continue;
2779
+ const next2 = ny * columns + nx;
2780
+ if (outside[next2] || grown[next2]) continue;
2781
+ outside[next2] = 1;
2782
+ stack.push(next2);
2783
+ }
2784
+ }
2785
+ const covered = (x, y) => x >= 0 && y >= 0 && x < columns && y < rows && !outside[y * columns + x];
2786
+ const next = /* @__PURE__ */ new Map();
2787
+ const add = (from, to) => {
2788
+ const key = `${from[0]},${from[1]}`;
2789
+ const list2 = next.get(key) ?? [];
2790
+ list2.push(to);
2791
+ next.set(key, list2);
2792
+ };
2793
+ for (let y = 0; y < rows; y += 1)
2794
+ for (let x = 0; x < columns; x += 1) {
2795
+ if (!covered(x, y)) continue;
2796
+ if (!covered(x, y - 1)) add([x, y], [x + 1, y]);
2797
+ if (!covered(x + 1, y)) add([x + 1, y], [x + 1, y + 1]);
2798
+ if (!covered(x, y + 1)) add([x + 1, y + 1], [x, y + 1]);
2799
+ if (!covered(x - 1, y)) add([x, y + 1], [x, y]);
2800
+ }
2801
+ const rings = [];
2802
+ for (const [startKey, targets] of next) {
2803
+ while (targets.length) {
2804
+ const start = startKey.split(",").map(Number);
2805
+ const ring = [start];
2806
+ let current = targets.pop();
2807
+ while (`${current[0]},${current[1]}` !== startKey) {
2808
+ ring.push(current);
2809
+ const outs = next.get(`${current[0]},${current[1]}`);
2810
+ if (!outs?.length) break;
2811
+ current = outs.pop();
2812
+ }
2813
+ const simplified = simplify(ring, 0.75);
2814
+ if (simplified.length >= 3)
2815
+ rings.push(simplified.flatMap(([x, y]) => [x0 + x * cell, y0 + y * cell]));
2816
+ }
2817
+ }
2818
+ rings.sort((a, b) => Math.abs(ringArea(b)) - Math.abs(ringArea(a)));
2819
+ const kept = [];
2820
+ let points = 0;
2821
+ for (const ring of rings) {
2822
+ if (points + ring.length / 2 > tileflowModelLimits.footprintPoints) break;
2823
+ kept.push(ring);
2824
+ points += ring.length / 2;
2825
+ }
2826
+ return kept;
2827
+ }
2828
+ function isFootprint(primitive) {
2829
+ return primitive.material?.trim().toLowerCase().replace(/\.\d+$/u, "") === "footprint";
2830
+ }
2831
+ function partOf2(material) {
2832
+ const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
2833
+ return isTileflowModelPartName(name) ? name : void 0;
2834
+ }
2835
+ function simplify(ring, tolerance) {
2836
+ if (ring.length <= 4) return [...ring];
2837
+ let far = 0;
2838
+ let distance = -1;
2839
+ for (const [index, [x, y]] of ring.entries()) {
2840
+ const d = Math.hypot(x - ring[0][0], y - ring[0][1]);
2841
+ if (d > distance) {
2842
+ distance = d;
2843
+ far = index;
2844
+ }
2845
+ }
2846
+ const half = (points) => {
2847
+ if (points.length <= 2) return [...points];
2848
+ const [a, b] = [points[0], points[points.length - 1]];
2849
+ let index = 0;
2850
+ let most = -1;
2851
+ for (let at = 1; at < points.length - 1; at += 1) {
2852
+ const d = segmentDistance(points[at], a, b);
2853
+ if (d > most) {
2854
+ most = d;
2855
+ index = at;
2856
+ }
2857
+ }
2858
+ if (most <= tolerance) return [a, b];
2859
+ const left = half(points.slice(0, index + 1));
2860
+ return [...left.slice(0, -1), ...half(points.slice(index))];
2861
+ };
2862
+ const first = half(ring.slice(0, far + 1));
2863
+ const second = half([...ring.slice(far), ring[0]]);
2864
+ return [...first.slice(0, -1), ...second.slice(0, -1)];
2865
+ }
2866
+ function segmentDistance(p, a, b) {
2867
+ const dx = b[0] - a[0];
2868
+ const dy = b[1] - a[1];
2869
+ const length = dx * dx + dy * dy;
2870
+ const t = length > 0 ? clamp2(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
2871
+ return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
2872
+ }
2873
+ function ringArea(ring) {
2874
+ let area = 0;
2875
+ for (let a = 0, b = ring.length - 2; a < ring.length; b = a, a += 2)
2876
+ area += ring[b] * ring[a + 1] - ring[a] * ring[b + 1];
2877
+ return area / 2;
2878
+ }
2879
+ function edge(a, b, p) {
2880
+ return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
2881
+ }
2882
+ function point2(list2, vertex) {
2883
+ return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
2884
+ }
2885
+ function sub3(a, b) {
2886
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2887
+ }
2888
+ function cross3(a, b) {
2889
+ return [
2890
+ a[1] * b[2] - a[2] * b[1],
2891
+ a[2] * b[0] - a[0] * b[2],
2892
+ a[0] * b[1] - a[1] * b[0]
2893
+ ];
2894
+ }
2895
+ function clamp2(value, low, high) {
2896
+ return Math.min(high, Math.max(low, value));
2897
+ }
2898
+
2899
+ // src/tree-models.ts
2900
+ var TileflowTreeModelError = class extends Error {
2901
+ code = "TREE_MODEL_INVALID";
2902
+ issues;
2903
+ phase = "tree-model-preparation";
2904
+ constructor(issues) {
2905
+ super(
2906
+ [
2907
+ "Invalid Tileflow tree models",
2908
+ ...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
2909
+ ].join("\n")
2910
+ );
2911
+ this.name = "TileflowTreeModelError";
2912
+ this.issues = issues;
2913
+ }
2914
+ };
2915
+ var tileflowBuildingModelLimit = 64;
2916
+ async function prepareTileflowBuildingModel(id, levels) {
2917
+ const model = buildingModelEntry(id, levels);
2918
+ await MeshoptEncoder.ready;
2919
+ const bytes = encodeTileflowModels([model], MeshoptEncoder);
2920
+ return { bytes, sha256: sha256Of(bytes) };
2921
+ }
2922
+ async function packageTileflowLandmarkTile(landmarks) {
2923
+ if (landmarks.length === 0) throw new RangeError("A landmark tile holds at least one landmark.");
2924
+ if (new Set(landmarks.map((landmark) => landmark.id)).size !== landmarks.length)
2925
+ throw new RangeError("Every landmark has an id of its own.");
2926
+ let tile;
2927
+ const models = [];
2928
+ for (const landmark of [...landmarks].sort((a, b) => compareCodeUnits4(a.id, b.id))) {
2929
+ if (!(landmark.heading >= 0 && landmark.heading < 360))
2930
+ throw new RangeError(`Landmark ${landmark.id} is turned 0 to 360 degrees.`);
2931
+ let own;
2932
+ try {
2933
+ own = tileflowLandmarkTileOf(landmark.at);
2934
+ } catch {
2935
+ throw new RangeError(`Landmark ${landmark.id} stands at no place on the map.`);
2936
+ }
2937
+ tile ??= own;
2938
+ if (own[0] !== tile[0] || own[1] !== tile[1])
2939
+ throw new RangeError(
2940
+ `Landmark ${landmark.id} stands in tile ${own.join("/")}, not ${tile.join("/")}.`
2941
+ );
2942
+ let model;
2943
+ try {
2944
+ model = buildingModelEntry(landmark.id, landmark.levels);
2945
+ } catch (error) {
2946
+ throw new RangeError(`Landmark ${landmark.id}: ${error.message}`);
2947
+ }
2948
+ models.push({ ...model, at: [landmark.at[0], landmark.at[1]], heading: landmark.heading });
2949
+ }
2950
+ await MeshoptEncoder.ready;
2951
+ const bytes = encodeTileflowModels(models, MeshoptEncoder);
2952
+ const footprints = readTileflowLandmarkTileFootprints(bytes);
2953
+ if (!footprints) throw new Error("The packaged landmark tile cannot be read back.");
2954
+ return { x: tile[0], y: tile[1], bytes, sha256: sha256Of(bytes), landmarks: footprints };
2955
+ }
2956
+ function encodeTileflowLandmarkRegion(region) {
2957
+ const bytes = serializeTileflowLandmarkRegion(region);
2958
+ return { bytes, sha256: sha256Of(bytes) };
2959
+ }
2960
+ var nearLightmapTexels = (area) => Math.min(
2961
+ 524288,
2962
+ Math.max(65536, Math.round(89 / 0.6 * area ** 0.54), Math.round(1.3 ** 2 * area / 0.6))
2963
+ );
2964
+ var buildingModelLightmapTexels = {
2965
+ near: nearLightmapTexels,
2966
+ mid: (area) => nearLightmapTexels(area) / 4
2967
+ };
2968
+ function buildingModelEntry(id, levels) {
2969
+ const primitives = readTileflowGltfTriangles(levels.near, { materials: true });
2970
+ if (isTileflowTreeModel(primitives))
2971
+ throw new TileflowGltfError(
2972
+ "is not a building model; its materials name only the parts of a tree (bark, leaves, fronds and stem)"
2973
+ );
2974
+ const meshes = [
2975
+ prepareTileflowBuildingModelMesh(primitives, {
2976
+ lightmapTexels: buildingModelLightmapTexels.near
2977
+ })
2978
+ ];
2979
+ if (levels.mid)
2980
+ meshes.push(
2981
+ prepareTileflowBuildingModelMesh(levels.mid, {
2982
+ lightmapTexels: buildingModelLightmapTexels.mid
2983
+ })
2984
+ );
2985
+ if (levels.far) meshes.push(prepareTileflowBuildingModelMesh(levels.far));
2986
+ return {
2987
+ id,
2988
+ levels: meshes,
2989
+ footprint: tileflowBuildingModelFootprint(
2990
+ tileflowBuildingModelDeclaredFootprint(primitives) ?? meshes[0]
2991
+ )
2992
+ };
2993
+ }
2994
+ function sha256Of(bytes) {
2995
+ return createHash("sha256").update(bytes).digest("hex");
2996
+ }
2997
+ var fileLimit = 8 * 1024 * 1024;
2998
+ var modelId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
2999
+ async function prepareTileflowMapTreeModels(directories, options) {
3000
+ const resolved = await resolveTileflowAssetDirectories(directories, {
3001
+ baseDirectory: options.baseDirectory,
3002
+ configPath: options.configPath,
3003
+ cwd: options.cwd,
3004
+ kind: "models",
3005
+ target: options.target
3006
+ });
3007
+ const issues = [];
3008
+ const files = /* @__PURE__ */ new Map();
3009
+ for (const directory of resolved) {
3010
+ let names;
3011
+ try {
3012
+ names = (await readdir2(directory.realPath)).sort();
3013
+ } catch {
3014
+ issues.push({ message: "is not a readable directory", path: directory.configPath });
3015
+ continue;
3016
+ }
3017
+ for (const name of names) {
3018
+ const display = `${directory.configPath}/${name}`;
3019
+ const lower = name.toLowerCase();
3020
+ if (lower.endsWith(".gltf")) {
3021
+ issues.push({
3022
+ message: "is a text glTF; export the model as a glTF binary (.glb)",
3023
+ path: display
3024
+ });
3025
+ continue;
3026
+ }
3027
+ if (!lower.endsWith(".glb")) continue;
3028
+ const level = name.endsWith(".far.glb") ? "far" : name.endsWith(".mid.glb") ? "mid" : "near";
3029
+ const id = name.slice(0, name.length - (level === "near" ? 4 : 8));
3030
+ if (!modelId.test(id) || id.length > 64) {
3031
+ issues.push({
3032
+ message: "a model's file name is its id: lowercase letters, digits and hyphens, up to 64, such as royal-palm.glb",
3033
+ path: display
3034
+ });
3035
+ continue;
3036
+ }
3037
+ const path = join3(directory.realPath, name);
3038
+ const info = await lstat3(path).catch(() => void 0);
3039
+ if (!info?.isFile()) {
3040
+ issues.push({ message: "is not a regular file", path: display });
3041
+ continue;
3042
+ }
3043
+ if (info.size > fileLimit) {
3044
+ issues.push({ message: "is larger than 8 MiB", path: display });
3045
+ continue;
3046
+ }
3047
+ const entry = files.get(id) ?? {};
3048
+ entry[level] = { path, display };
3049
+ files.set(id, entry);
3050
+ }
3051
+ }
3052
+ const models = {};
3053
+ const buildings = {};
3054
+ const identities = [];
3055
+ for (const [id, entry] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
3056
+ if (!entry.near) {
3057
+ issues.push({ message: `has no ${id}.glb beside it`, path: (entry.mid ?? entry.far).display });
3058
+ continue;
3059
+ }
3060
+ let nearBytes;
3061
+ let primitives;
3062
+ try {
3063
+ nearBytes = new Uint8Array(await readFile(entry.near.path));
3064
+ primitives = readTileflowGltfTriangles(nearBytes);
3065
+ } catch (error) {
3066
+ issues.push({
3067
+ message: error instanceof TileflowGltfError ? error.message : "cannot be read",
3068
+ path: entry.near.display
3069
+ });
3070
+ continue;
3071
+ }
3072
+ if (!isTileflowTreeModel(primitives)) {
3073
+ const files2 = { near: nearBytes };
3074
+ const sources = [];
3075
+ let failing = entry.near;
3076
+ try {
3077
+ for (const level of ["near", "mid", "far"]) {
3078
+ const file = entry[level];
3079
+ if (!file) continue;
3080
+ failing = file;
3081
+ const bytes = level === "near" ? nearBytes : new Uint8Array(await readFile(file.path));
3082
+ files2[level] = bytes;
3083
+ if (level !== "near") prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion: false });
3084
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
3085
+ identities.push({ id, level, sha256 });
3086
+ sources.push({ level, sha256, byteLength: bytes.byteLength });
3087
+ }
3088
+ failing = entry.near;
3089
+ const prepared2 = await prepareTileflowBuildingModel(id, files2);
3090
+ buildings[id] = {
3091
+ fileName: tileflowModelFileName(id, prepared2.sha256),
3092
+ bytes: prepared2.bytes,
3093
+ sha256: prepared2.sha256,
3094
+ sources
3095
+ };
3096
+ } catch (error) {
3097
+ issues.push({
3098
+ message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
3099
+ path: failing.display
3100
+ });
3101
+ }
3102
+ continue;
3103
+ }
3104
+ if (entry.mid) {
3105
+ issues.push({
3106
+ message: "is a tree model; trees have a near model and a far one (<id>.far.glb), not a mid one",
3107
+ path: entry.mid.display
3108
+ });
3109
+ continue;
3110
+ }
3111
+ const prepared = {};
3112
+ for (const level of ["near", "far"]) {
3113
+ const file = entry[level];
3114
+ if (!file) continue;
3115
+ try {
3116
+ const bytes = new Uint8Array(await readFile(file.path));
3117
+ prepared[level] = encodeTileflowTreeModel(
3118
+ prepareTileflowTreeModelMesh(bytes, level),
3119
+ level
3120
+ );
3121
+ identities.push({ id, level, sha256: createHash("sha256").update(bytes).digest("hex") });
3122
+ } catch (error) {
3123
+ issues.push({
3124
+ message: error instanceof TileflowGltfError || error instanceof RangeError ? error.message : "cannot be read",
3125
+ path: file.display
3126
+ });
3127
+ }
3128
+ }
3129
+ if (prepared.near)
3130
+ models[id] = { near: prepared.near, ...prepared.far ? { far: prepared.far } : {} };
3131
+ }
3132
+ if (Object.keys(models).length > tileflowTreeModelLimits2.models)
3133
+ issues.push({
3134
+ message: `holds ${Object.keys(models).length} tree models; a map takes at most ${tileflowTreeModelLimits2.models}`,
3135
+ path: options.configPath
3136
+ });
3137
+ if (Object.keys(buildings).length > tileflowBuildingModelLimit)
3138
+ issues.push({
3139
+ message: `holds ${Object.keys(buildings).length} building models; a map takes at most ${tileflowBuildingModelLimit}`,
3140
+ path: options.configPath
3141
+ });
3142
+ if (issues.length > 0) throw new TileflowTreeModelError(issues);
3143
+ return {
3144
+ models,
3145
+ buildings,
3146
+ identities,
3147
+ watchPaths: resolved.filter((directory) => directory.watch).map((directory) => directory.realPath)
3148
+ };
3149
+ }
3150
+
3151
+ // src/icon-preview.ts
3152
+ import { renderTileflowIconAtlasPixels } from "@tileflow/core";
3153
+ async function renderTileflowIconAtlasPreview(source, index) {
3154
+ if (!Object.values(index).some((entry) => entry.sdf)) return source;
3155
+ const sharp = await loadSharp();
3156
+ const { data, info } = await sharp(source, { limitInputPixels: 2048 * 2048 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
3157
+ const rendered = renderTileflowIconAtlasPixels(data, info.width, info.height, index);
3158
+ return sharp(rendered, { raw: { width: info.width, height: info.height, channels: 4 } }).png().toBuffer();
3159
+ }
3160
+
3161
+ // src/icons.ts
3162
+ var TileflowIconCompilationError = class extends Error {
3163
+ code = "ICON_INVALID";
3164
+ issues;
3165
+ phase = "icon-compilation";
3166
+ constructor(issues) {
3167
+ super(
3168
+ [
3169
+ "Invalid Tileflow icon package",
3170
+ ...issues.map((issue) => `- ${issue.path}: ${issue.message}`)
3171
+ ].join("\n")
3172
+ );
3173
+ this.name = "TileflowIconCompilationError";
3174
+ this.issues = issues;
3175
+ }
3176
+ };
3177
+ var iconFileExtensions = /* @__PURE__ */ new Set([".svg", ".png", ".jpg", ".jpeg", ".webp"]);
3178
+ var iconSpriteSize = 24;
3179
+ async function compileTileflowIconPackages(project, options) {
3180
+ const result = await composeMapIconSources(project, options);
3181
+ const packagesByHash = /* @__PURE__ */ new Map();
3182
+ for (const composed of result.composedBySequence.values()) {
3183
+ if (composed.package) packagesByHash.set(composed.package.contentHash, composed.package);
3184
+ }
3185
+ const compositions = {};
3186
+ const sourceIdentities = {};
3187
+ for (const request of result.mapRequests) {
3188
+ const composed = result.composedBySequence.get(request.sequenceKey);
3189
+ sourceIdentities[request.mapName] = composed?.sourceIdentities ?? [];
3190
+ if (composed?.composition) compositions[request.mapName] = composed.composition;
3191
+ }
3192
+ const bindings = result.mapRequests.flatMap((request) => {
3193
+ if (request.sources.length === 0) return [];
3194
+ const composed = result.composedBySequence.get(request.sequenceKey);
3195
+ if (!composed?.package) {
3196
+ throw new Error(`Missing compiled icon package for map ${request.mapName}`);
3197
+ }
3198
+ const appearances = Object.fromEntries(
3199
+ composed.package.manifest.renderedIcons.flatMap(
3200
+ (icon) => icon.appearance ? [[icon.name, icon.appearance]] : []
3201
+ )
3202
+ );
3203
+ return [
3204
+ {
3205
+ iconIds: composed.package.manifest.iconNames,
3206
+ ...Object.keys(appearances).length ? { appearances } : {},
3207
+ label: request.mapName,
3208
+ mapName: request.mapName,
3209
+ packageHash: composed.package.contentHash
3210
+ }
3211
+ ];
3212
+ });
3213
+ return {
3214
+ bindings,
3215
+ compositions,
3216
+ packages: [...packagesByHash.values()].sort(
3217
+ (left, right) => compareCodeUnits5(left.contentHash, right.contentHash)
3218
+ ),
3219
+ sourceIdentities,
3220
+ watchFiles: uniqueStrings(
3221
+ result.mapRequests.flatMap(
3222
+ (request) => result.composedBySequence.get(request.sequenceKey)?.watchFiles ?? []
3223
+ )
3224
+ ).sort(compareCodeUnits5),
3225
+ watchPaths: uniqueStrings(
3226
+ result.mapRequests.flatMap(
3227
+ (request) => result.composedBySequence.get(request.sequenceKey)?.watchPaths ?? []
3228
+ )
3229
+ ).sort(compareCodeUnits5)
3230
+ };
3231
+ }
3232
+ async function inspectTileflowIconCatalogs(project, options) {
3233
+ const result = await composeMapIconSources(project, {
3234
+ ...options,
3235
+ target: "local"
3236
+ });
3237
+ const maps = result.mapRequests.map((request) => {
3238
+ if (request.sources.length === 0) return { name: request.mapName, icons: { kind: "none" } };
3239
+ const composed = result.composedBySequence.get(request.sequenceKey);
3240
+ if (!composed?.package)
3241
+ throw new Error(`Missing compiled icon catalog for map ${request.mapName}`);
3242
+ return {
3243
+ name: request.mapName,
3244
+ icons: {
3245
+ composition: composed.composition,
3246
+ contributors: composed.contributors.map(describeComposedContributor),
3247
+ iconIds: [...composed.package.manifest.iconNames],
3248
+ kind: "sources",
3249
+ label: request.mapName,
3250
+ packageHash: composed.package.contentHash
3251
+ }
3252
+ };
3253
+ });
3254
+ const requestBySequence = new Map(
3255
+ result.mapRequests.filter((request) => request.sources.length > 0).map((request) => [request.sequenceKey, request])
3256
+ );
3257
+ const catalogs = await Promise.all(
3258
+ [...result.composedBySequence.entries()].map(
3259
+ async ([sequenceKey, composed]) => {
3260
+ const request = requestBySequence.get(sequenceKey);
3261
+ if (!request || !composed.package)
3262
+ throw new Error(`Missing icon contributor sequence ${sequenceKey}`);
3263
+ const atlas = await readComposedAtlasIndexes(composed.package);
3264
+ return {
3265
+ composition: composed.composition,
3266
+ compiledPackage: composed.package,
3267
+ contributors: composed.contributors.map(describeComposedContributor),
3268
+ icons: composed.winners.map((winner, index) => {
3269
+ const manifestIcon = composed.package.manifest.renderedIcons[index];
3270
+ const oneXAtlas = atlas.oneX[winner.id];
3271
+ const twoXAtlas = atlas.twoX[winner.id];
3272
+ const contributor = composed.contributors[winner.contributor];
3273
+ if (!manifestIcon || manifestIcon.name !== winner.id || !oneXAtlas || !twoXAtlas || !contributor) {
3274
+ throw new Error(`Missing compiled catalog metadata for icon ${winner.id}`);
3275
+ }
3276
+ return {
3277
+ id: winner.id,
3278
+ ...manifestIcon.appearance ? { appearance: manifestIcon.appearance } : {},
3279
+ ...manifestIcon.layout ? { layout: manifestIcon.layout } : {},
3280
+ rendered: {
3281
+ oneX: {
3282
+ atlas: atlasRectangle(oneXAtlas),
3283
+ height: oneXAtlas.height,
3284
+ pixelRatio: 1,
3285
+ pixelSha256: manifestIcon.pixelSha256.oneX,
3286
+ width: oneXAtlas.width
3287
+ },
3288
+ twoX: {
3289
+ atlas: atlasRectangle(twoXAtlas),
3290
+ height: twoXAtlas.height,
3291
+ pixelRatio: 2,
3292
+ pixelSha256: manifestIcon.pixelSha256.twoX,
3293
+ width: twoXAtlas.width
3294
+ }
3295
+ },
3296
+ source: winner.source === null ? {
3297
+ contributor: winner.contributor,
3298
+ kind: "icon-set",
3299
+ reference: contributor.kind === "icon-set" ? contributor.reference : "",
3300
+ version: contributor.kind === "icon-set" ? contributor.version : 0
3301
+ } : {
3302
+ byteLength: winner.source.byteLength,
3303
+ contributor: winner.contributor,
3304
+ dimensions: { ...winner.source.dimensions },
3305
+ format: winner.source.format,
3306
+ kind: "file",
3307
+ path: winner.source.path
3308
+ }
3309
+ };
3310
+ }),
3311
+ insideWorkingTree: composed.contributors.every(
3312
+ (contributor) => contributor.kind === "icon-set" || contributor.insideWorkingTree
3313
+ ),
3314
+ replacements: composed.replacements.map(
3315
+ (replacement) => describeComposedReplacement(replacement, composed.contributors)
3316
+ )
3317
+ };
3318
+ }
3319
+ )
3320
+ );
3321
+ catalogs.sort(
3322
+ (left, right) => compareCodeUnits5(
3323
+ left.contributors.map((contributor) => contributor.label).join("\0"),
3324
+ right.contributors.map((contributor) => contributor.label).join("\0")
3325
+ )
3326
+ );
3327
+ return { catalogs, maps };
3328
+ }
3329
+ async function composeMapIconSources(project, options) {
3330
+ const mapRequests = getMapIconRequests(project, options.mapNames);
3331
+ const workspaceRequests = options.mapNames === void 0 ? mapRequests : getMapIconRequests(project);
3332
+ const baseDirectory = options.baseDirectory ?? options.cwd;
3333
+ const workspaceLock = await readWorkspaceIconSetLock(workspaceRequests, baseDirectory);
3334
+ const lockPath = workspaceLock ? resolvePath(baseDirectory, tileflowIconsLockfileName3) : void 0;
3335
+ const issues = [];
3336
+ const composedBySequence = /* @__PURE__ */ new Map();
3337
+ for (const request of uniqueSequences(mapRequests)) {
3338
+ if (request.sources.length === 0) continue;
3339
+ try {
3340
+ const lock = workspaceLock ? selectMapIconSetLock(workspaceLock, request.sources) : void 0;
3341
+ const composed = await composeTileflowIconSources(request.sources, {
3342
+ ...options.icons,
3343
+ baseDirectory,
3344
+ configPath: `maps.${request.mapName}.icons`,
3345
+ cwd: options.cwd,
3346
+ ...lock ? { lock } : {},
3347
+ target: options.target
3348
+ });
3349
+ composedBySequence.set(
3350
+ request.sequenceKey,
3351
+ lockPath && lock ? { ...composed, watchFiles: uniqueStrings([...composed.watchFiles, lockPath]) } : composed
3352
+ );
3353
+ } catch (error) {
3354
+ if (error instanceof TileflowIconSetError5) throw error;
3355
+ if (error instanceof TileflowIconCompilationError) issues.push(...error.issues);
3356
+ else if (error instanceof TileflowAssetDirectoryError) issues.push(...error.issues);
3357
+ else
3358
+ issues.push({
3359
+ message: error instanceof Error ? error.message : "Icon compilation failed",
3360
+ path: `maps.${request.mapName}.icons`
3361
+ });
3362
+ }
3363
+ }
3364
+ if (issues.length > 0) throw new TileflowIconCompilationError(issues);
3365
+ return { composedBySequence, mapRequests };
3366
+ }
3367
+ async function readWorkspaceIconSetLock(requests, baseDirectory) {
3368
+ const references = /* @__PURE__ */ new Set();
3369
+ for (const request of requests) {
3370
+ for (const reference of collectTileflowIconSetReferences2(request.sources))
3371
+ references.add(reference);
3372
+ }
3373
+ if (references.size === 0) return void 0;
3374
+ const sources = [...references].sort(compareCodeUnits5).map((reference) => ({ kind: "icon-set", reference }));
3375
+ return readTileflowIconsLockfile(baseDirectory, sources);
3376
+ }
3377
+ function selectMapIconSetLock(lock, sources) {
3378
+ const references = collectTileflowIconSetReferences2(sources);
3379
+ if (references.length === 0) return void 0;
3380
+ return {
3381
+ lockfileVersion: 1,
3382
+ sets: Object.fromEntries(references.map((reference) => [reference, lock.sets[reference]]))
3383
+ };
3384
+ }
3385
+ async function prepareTileflowCatalogIcons(project, options) {
3386
+ const compiled = await compileTileflowIconPackages(project, {
3387
+ baseDirectory: options.baseDirectory,
3388
+ cwd: options.cwd,
3389
+ ...options.icons ? { icons: options.icons } : {},
3390
+ target: "local"
3391
+ });
3392
+ const packagesByHash = new Map(
3393
+ compiled.packages.map((iconPackage) => [iconPackage.contentHash, iconPackage])
3394
+ );
3395
+ const assets = [];
3396
+ const mapAssets = {};
3397
+ const mapIconCompositions = {};
3398
+ const mapIconSources = {};
3399
+ for (const mapName of Object.keys(project.maps)) mapIconSources[mapName] = [];
3400
+ for (const binding of compiled.bindings) {
3401
+ const iconPackage = packagesByHash.get(binding.packageHash);
3402
+ if (!iconPackage) throw new Error(`Missing compiled icon package for map ${binding.mapName}`);
3403
+ const spriteUrl = joinUrl(options.assetBaseUrl, `icons/${binding.mapName}/sprite`);
3404
+ mapAssets[binding.mapName] = {
3405
+ icons: {
3406
+ ids: binding.iconIds,
3407
+ sprite: spriteUrl,
3408
+ ...binding.appearances ? { appearances: binding.appearances } : {}
3409
+ }
3410
+ };
3411
+ mapIconSources[binding.mapName] = compiled.sourceIdentities[binding.mapName] ?? [];
3412
+ const composition = compiled.compositions[binding.mapName];
3413
+ if (composition) mapIconCompositions[binding.mapName] = composition;
3414
+ for (const file of iconPackage.files) {
3415
+ assets.push({
3416
+ contentType: file.contentType === "application/json" ? "application/json; charset=utf-8" : file.contentType,
3417
+ fileName: `icons/${binding.mapName}/${file.fileName}`,
3418
+ source: file.contentType === "application/json" ? new TextDecoder().decode(file.source) : file.source
3419
+ });
3420
+ }
3421
+ }
3422
+ const mapModelSources = {};
3423
+ const modelWatchPaths = [];
3424
+ for (const [mapName, map] of Object.entries(project.maps)) {
3425
+ if (!map.models?.length) continue;
3426
+ const models = await prepareTileflowMapTreeModels(map.models, {
3427
+ baseDirectory: options.baseDirectory ?? options.cwd,
3428
+ configPath: `maps.${mapName}.models`,
3429
+ cwd: options.cwd,
3430
+ target: "local"
3431
+ });
3432
+ const buildingModels = {};
3433
+ for (const [id, model] of Object.entries(models.buildings)) {
3434
+ const fileName = `models/${mapName}/${model.fileName}`;
3435
+ assets.push({ contentType: tileflowModelContentType, fileName, source: model.bytes });
3436
+ buildingModels[id] = joinUrl(options.assetBaseUrl, fileName);
3437
+ }
3438
+ mapAssets[mapName] = {
3439
+ ...mapAssets[mapName],
3440
+ models: models.models,
3441
+ ...Object.keys(buildingModels).length > 0 ? { buildingModels } : {}
3442
+ };
3443
+ mapModelSources[mapName] = models.identities;
3444
+ modelWatchPaths.push(...models.watchPaths);
3445
+ }
3446
+ return {
3447
+ assets,
3448
+ baseDirectory: await realpath3(options.baseDirectory ?? options.cwd),
3449
+ cwd: await realpath3(options.cwd),
3450
+ mapAssets,
3451
+ mapIconCompositions,
3452
+ mapIconSources,
3453
+ ...Object.keys(mapModelSources).length > 0 ? { mapModelSources } : {},
3454
+ project,
3455
+ sourceProject: project,
3456
+ watchFiles: compiled.watchFiles,
3457
+ watchPaths: [...compiled.watchPaths, ...modelWatchPaths]
3458
+ };
3459
+ }
3460
+ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
3461
+ const requests = getMapIconRequests(project);
3462
+ const paths = /* @__PURE__ */ new Set();
3463
+ let realBaseDirectory;
3464
+ for (const request of uniqueSequences(requests)) {
3465
+ for (const source of request.sources) {
3466
+ if (typeof source === "object" && source.kind === "icon-set") {
3467
+ realBaseDirectory ??= await realpath3(baseDirectory);
3468
+ paths.add(resolvePath(realBaseDirectory, tileflowIconsLockfileName3));
3469
+ continue;
3470
+ }
3471
+ try {
3472
+ const directory = await readTileflowIconDirectory(source, {
3473
+ baseDirectory,
3474
+ configPath: `maps.${request.mapName}.icons`,
3475
+ cwd,
3476
+ target: "local"
3477
+ });
3478
+ if (directory.watchPath) paths.add(directory.watchPath);
3479
+ } catch {
3480
+ }
3481
+ }
3482
+ }
3483
+ return [...paths].sort(compareCodeUnits5);
3484
+ }
3485
+ function getMapIconRequests(project, mapNames) {
3486
+ const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits5);
3487
+ return selectedNames.map((mapName) => {
3488
+ const mapConfig = project.maps[mapName];
3489
+ if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
3490
+ const sources = parseResolvedTileflowMap(mapConfig).icons ?? [];
3491
+ return { mapName, sequenceKey: serializeCanonicalJson3(sources), sources };
3492
+ });
3493
+ }
3494
+ function uniqueSequences(requests) {
3495
+ return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
3496
+ (left, right) => compareCodeUnits5(left.sequenceKey, right.sequenceKey)
3497
+ );
3498
+ }
3499
+ function describeComposedContributor(contributor) {
3500
+ return contributor.kind === "icon-set" ? {
3501
+ iconIds: [...contributor.iconIds],
3502
+ kind: "icon-set",
3503
+ label: contributor.label,
3504
+ reference: contributor.reference,
3505
+ version: contributor.version
3506
+ } : {
3507
+ iconIds: [...contributor.iconIds],
3508
+ insideWorkingTree: contributor.insideWorkingTree,
3509
+ kind: contributor.kind,
3510
+ label: contributor.label
3511
+ };
3512
+ }
3513
+ function describeComposedReplacement(replacement, contributors) {
3514
+ return {
3515
+ id: replacement.id,
3516
+ replaced: describeContributorExport(contributors[replacement.replaced], replacement.id),
3517
+ winner: describeContributorExport(contributors[replacement.winner], replacement.id)
3518
+ };
3519
+ }
3520
+ function describeContributorExport(contributor, id) {
3521
+ if (!contributor) return id;
3522
+ if (contributor.kind === "icon-set") return contributor.label;
3523
+ return contributor.entries.find((entry) => entry.id === id)?.path ?? contributor.label;
3524
+ }
3525
+ async function readComposedAtlasIndexes(iconPackage) {
3526
+ const decoder = new TextDecoder("utf-8", { fatal: true });
3527
+ const read = (fileName) => {
3528
+ const file = iconPackage.files.find((candidate) => candidate.fileName === fileName);
3529
+ if (!file) throw new Error(`Missing generated ${fileName}`);
3530
+ return tileflowIconSpriteIndexSchema.parse(
3531
+ parseTileflowIconJson2(
3532
+ decoder.decode(file.source),
3533
+ tileflowIconPackageLimits4.maxGeneratedFileBytes
3534
+ )
3535
+ );
3536
+ };
3537
+ return { oneX: read("sprite.json"), twoX: read("sprite@2x.json") };
3538
+ }
3539
+ function describeAssetDirectory(directory) {
3540
+ return typeof directory.authoring === "string" ? directory.authoring : `npm:${directory.authoring.package}/${directory.authoring.path}`;
3541
+ }
3542
+ function describeAssetFile(directory, fileName) {
3543
+ return `${describeAssetDirectory(directory).replace(/\/+$/u, "")}/${fileName}`;
3544
+ }
3545
+ async function inspectIconSource(directory, target, issues) {
3546
+ let entries;
3547
+ const issueCount = issues.length;
3548
+ try {
3549
+ entries = await readdir3(directory.realPath, { withFileTypes: true });
3550
+ } catch (error) {
3551
+ issues.push({
3552
+ message: describeFileSystemError(error, "Icon source could not be read"),
3553
+ path: directory.configPath
3554
+ });
3555
+ return null;
3556
+ }
3557
+ entries.sort((left, right) => compareCodeUnits5(left.name, right.name));
3558
+ const candidates = [];
3559
+ const names = /* @__PURE__ */ new Set();
3560
+ let metadata = {
3561
+ schemaVersion: 1,
3562
+ icons: {}
3563
+ };
3564
+ for (const entry of entries) {
3565
+ const entryPath = resolvePath(directory.realPath, entry.name);
3566
+ const path = `${directory.configPath}/${entry.name}`;
3567
+ if (entry.isDirectory()) {
3568
+ issues.push({ message: "Nested directories are not supported", path });
3569
+ continue;
3570
+ }
3571
+ let entryStat;
3572
+ let realEntryPath = entryPath;
3573
+ try {
3574
+ if (entry.isSymbolicLink()) {
3575
+ realEntryPath = await realpath3(entryPath);
3576
+ if (!isPathInside2(directory.containmentRoot, realEntryPath)) {
3577
+ issues.push({ message: "Icon symlink escapes its owning directory boundary", path });
3578
+ continue;
3579
+ }
3580
+ }
3581
+ entryStat = await stat(realEntryPath);
3582
+ } catch (error) {
3583
+ issues.push({ message: describeFileSystemError(error, "Icon entry could not be read"), path });
3584
+ continue;
3585
+ }
3586
+ if (!entryStat.isFile()) {
3587
+ issues.push({ message: "Icon entries must be regular files", path });
3588
+ continue;
3589
+ }
3590
+ const extension = extname(entry.name).toLowerCase();
3591
+ if (entry.name === tileflowIconMetadataFileName) {
3592
+ try {
3593
+ if (entryStat.size > tileflowIconPackageLimits4.maxSourceFileBytes)
3594
+ throw new Error("Icon metadata exceeds its byte limit");
3595
+ metadata = tileflowIconAuthorMetadataSchema.parse(
3596
+ parseTileflowIconJson2(
3597
+ await readFile2(realEntryPath, "utf8"),
3598
+ tileflowIconPackageLimits4.maxSourceFileBytes
3599
+ )
3600
+ );
3601
+ } catch (error) {
3602
+ issues.push({
3603
+ path,
3604
+ message: `Invalid icon metadata: ${error instanceof Error ? error.message : String(error)}`
3605
+ });
3606
+ }
3607
+ continue;
3608
+ }
3609
+ if (!iconFileExtensions.has(extension)) {
3610
+ continue;
3611
+ }
3612
+ const sourceName = entry.name.slice(0, -extname(entry.name).length);
3613
+ const kind = sourceName.endsWith(".pattern") ? "pattern" : "icon";
3614
+ const name = kind === "pattern" ? sourceName.slice(0, -".pattern".length) : sourceName;
3615
+ if (names.has(name)) {
3616
+ issues.push({
3617
+ message: `Duplicate icon basename "${name}"`,
3618
+ path
3619
+ });
3620
+ continue;
3621
+ }
3622
+ names.add(name);
3623
+ if (!tileflowIconIdPattern.test(name) || name.length > 64) {
3624
+ issues.push({
3625
+ message: "Icon filenames must produce a lower-kebab ID of at most 64 characters",
3626
+ path
3627
+ });
3628
+ }
3629
+ if (entryStat.size > tileflowIconPackageLimits4.maxSourceFileBytes) {
3630
+ issues.push({
3631
+ message: `Icon source exceeds ${tileflowIconPackageLimits4.maxSourceFileBytes} bytes`,
3632
+ path
3633
+ });
3634
+ }
3635
+ candidates.push({
3636
+ displayPath: describeAssetFile(directory, entry.name),
3637
+ fileName: entry.name,
3638
+ format: iconSourceFormat(extension),
3639
+ kind,
3640
+ name,
3641
+ path: realEntryPath,
3642
+ size: entryStat.size
3643
+ });
3644
+ }
3645
+ candidates.sort((left, right) => compareCodeUnits5(left.name, right.name));
3646
+ for (const id of Object.keys(metadata.icons)) {
3647
+ if (!names.has(id))
3648
+ issues.push({
3649
+ path: `${directory.configPath}/${tileflowIconMetadataFileName}`,
3650
+ message: `Icon metadata references unknown ID "${id}"`
3651
+ });
3652
+ }
3653
+ if (candidates.length === 0) {
3654
+ issues.push({
3655
+ message: "No supported .svg, .png, .jpg, .jpeg, or .webp icon files were found",
3656
+ path: directory.configPath
3657
+ });
3658
+ }
3659
+ if (issues.length > issueCount) {
3660
+ return null;
3661
+ }
3662
+ const icons = [];
3663
+ for (const candidate of candidates) {
3664
+ const displayPath = candidate.displayPath;
3665
+ try {
3666
+ const source = await readFile2(candidate.path);
3667
+ if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits4.maxSourceFileBytes) {
3668
+ throw new Error("Icon changed while it was being validated");
3669
+ }
3670
+ if (target === "hosted" && extname(candidate.fileName).toLowerCase() === ".svg") {
3671
+ validateHostedSvg(source, displayPath);
3672
+ }
3673
+ const declared = metadata.icons[candidate.name];
3674
+ icons.push({
3675
+ ...declared?.representation === "sdf" ? { appearance: { representation: declared.representation, defaults: declared.defaults } } : {},
3676
+ ...declared?.layout ? { layout: declared.layout } : {},
3677
+ displayPath,
3678
+ fileName: candidate.fileName,
3679
+ format: candidate.format,
3680
+ kind: candidate.kind,
3681
+ name: candidate.name,
3682
+ path: candidate.path,
3683
+ source
3684
+ });
3685
+ } catch (error) {
3686
+ issues.push({
3687
+ message: error instanceof Error ? error.message : "Icon could not be read",
3688
+ path: displayPath
3689
+ });
3690
+ }
3691
+ }
3692
+ if (issues.length > issueCount) {
3693
+ return null;
3694
+ }
3695
+ return { icons };
3696
+ }
3697
+ async function validateDecodedDimensions(icon) {
3698
+ const sharp = await loadSharp();
3699
+ const metadata = await sharp(icon.source, {
3700
+ density: 72,
3701
+ failOn: "error",
3702
+ limitInputPixels: tileflowIconPackageLimits4.maxDecodedPixelsPerIcon
3703
+ }).metadata();
3704
+ const width = metadata.width ?? 0;
3705
+ const height = metadata.height ?? 0;
3706
+ if (width <= 0 || height <= 0) {
3707
+ throw new Error(`Unable to determine decoded dimensions for ${icon.fileName}`);
3708
+ }
3709
+ if (width * height > tileflowIconPackageLimits4.maxDecodedPixelsPerIcon) {
3710
+ throw new Error(
3711
+ `${icon.fileName} exceeds ${tileflowIconPackageLimits4.maxDecodedPixelsPerIcon} decoded pixels`
3712
+ );
3713
+ }
3714
+ return { height, width };
3715
+ }
3716
+ async function renderIcon(icon, dimensions, pixelRatio) {
3717
+ const sharp = await loadSharp();
3718
+ const targetWidth = (icon.kind === "pattern" ? dimensions.width : iconSpriteSize) * pixelRatio;
3719
+ const targetHeight = (icon.kind === "pattern" ? dimensions.height : iconSpriteSize) * pixelRatio;
3720
+ const { data, info } = await sharp(icon.source, {
3721
+ density: 72 * pixelRatio,
3722
+ failOn: "error",
3723
+ limitInputPixels: tileflowIconPackageLimits4.maxDecodedPixelsPerIcon
3724
+ }).resize(targetWidth, targetHeight, {
3725
+ background: { alpha: 0, b: 0, g: 0, r: 0 },
3726
+ fit: "contain"
3727
+ }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
3728
+ if (info.channels !== 4 || info.width !== targetWidth || info.height !== targetHeight) {
3729
+ throw new Error(`Rendered icon ${icon.fileName} did not produce canonical RGBA pixels`);
3730
+ }
3731
+ let rgba;
3732
+ try {
3733
+ if (icon.appearance && icon.kind === "pattern")
3734
+ throw new Error("SDF metadata is only supported for icons, not patterns.");
3735
+ if (icon.layout) {
3736
+ if (icon.kind === "pattern")
3737
+ throw new Error("Text-fitting metadata is only supported for icons, not patterns.");
3738
+ assertTileflowIconLayoutBounds2(
3739
+ icon.layout,
3740
+ info.width / pixelRatio,
3741
+ info.height / pixelRatio
3742
+ );
3743
+ }
3744
+ rgba = icon.appearance ? createIconDistanceField(new Uint8Array(data), info.width, info.height, pixelRatio) : new Uint8Array(data);
3745
+ } catch (error) {
3746
+ throw new Error(
3747
+ `${icon.displayPath}: ${error instanceof Error ? error.message : "Invalid icon capabilities"}`,
3748
+ { cause: error }
3749
+ );
3750
+ }
3751
+ const png = await sharp(rgba, {
3752
+ raw: { channels: 4, height: info.height, width: info.width }
3753
+ }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
3754
+ return { height: info.height, png, rgba, width: info.width };
3755
+ }
3756
+ function validateHostedSvg(source, displayPath) {
3757
+ const parser = new SaxesParser({ fileName: displayPath, xmlns: true });
3758
+ const decoder = new TextDecoder("utf-8", { fatal: true });
3759
+ let styleDepth = 0;
3760
+ let styleText = "";
3761
+ parser.on("doctype", () => {
3762
+ throw new Error("Hosted SVGs cannot contain a document type or entity declarations");
3763
+ });
3764
+ parser.on("processinginstruction", (instruction) => {
3765
+ if (instruction.target.toLowerCase() === "xml-stylesheet") {
3766
+ throw new Error("Hosted SVGs cannot load external stylesheets");
3767
+ }
3768
+ });
3769
+ parser.on("opentag", (tag) => {
3770
+ const localName = tag.local.toLowerCase();
3771
+ if (localName === "script") {
3772
+ throw new Error("Hosted SVGs cannot contain scripts");
3773
+ }
3774
+ if (localName === "style") {
3775
+ styleDepth += 1;
3776
+ }
3777
+ for (const attribute of Object.values(tag.attributes)) {
3778
+ const attributeName = attribute.local.toLowerCase();
3779
+ const value = attribute.value.trim();
3780
+ if (attributeName.startsWith("on")) {
3781
+ throw new Error("Hosted SVGs cannot contain script event handlers");
3782
+ }
3783
+ if (attributeName === "href" && value.length > 0 && !value.startsWith("#")) {
3784
+ throw new Error("Hosted SVG resource references must use local fragments");
3785
+ }
3786
+ if (containsExternalCssUrl(value)) {
3787
+ throw new Error("Hosted SVG resource references must use local fragments");
3788
+ }
3789
+ }
3790
+ });
3791
+ parser.on("text", (text) => {
3792
+ if (styleDepth > 0) {
3793
+ styleText += text;
3794
+ }
3795
+ });
3796
+ parser.on("cdata", (text) => {
3797
+ if (styleDepth > 0) {
3798
+ styleText += text;
3799
+ }
3800
+ });
3801
+ parser.on("closetag", (tag) => {
3802
+ if (tag.local.toLowerCase() !== "style") {
3803
+ return;
3804
+ }
3805
+ styleDepth -= 1;
3806
+ if (styleDepth === 0) {
3807
+ if (styleText.toLowerCase().includes("@import") || containsExternalCssUrl(styleText)) {
3808
+ throw new Error("Hosted SVGs cannot load external stylesheets or resources");
3809
+ }
3810
+ styleText = "";
3811
+ }
3812
+ });
3813
+ parser.on("error", (error) => {
3814
+ throw error;
3815
+ });
3816
+ const chunkBytes = 64 * 1024;
3817
+ for (let offset = 0; offset < source.byteLength; offset += chunkBytes) {
3818
+ parser.write(decoder.decode(source.subarray(offset, offset + chunkBytes), { stream: true }));
3819
+ }
3820
+ parser.write(decoder.decode());
3821
+ parser.close();
3822
+ }
3823
+ function containsExternalCssUrl(value) {
3824
+ const lowerValue = value.toLowerCase();
3825
+ let cursor = 0;
3826
+ while (cursor < value.length) {
3827
+ const start = lowerValue.indexOf("url(", cursor);
3828
+ if (start === -1) {
3829
+ return false;
3830
+ }
3831
+ const end = value.indexOf(")", start + 4);
3832
+ if (end === -1) {
3833
+ return true;
3834
+ }
3835
+ const rawTarget = value.slice(start + 4, end).trim();
3836
+ const target = stripMatchingQuotes(rawTarget);
3837
+ if (!target.startsWith("#")) {
3838
+ return true;
3839
+ }
3840
+ cursor = end + 1;
3841
+ }
3842
+ return false;
3843
+ }
3844
+ function stripMatchingQuotes(value) {
3845
+ if (value.length < 2) {
3846
+ return value;
3847
+ }
3848
+ const first = value[0];
3849
+ const last = value[value.length - 1];
3850
+ return first === '"' && last === '"' || first === "'" && last === "'" ? value.slice(1, -1).trim() : value;
3851
+ }
3852
+ async function mapWithConcurrency(values, concurrency, map) {
3853
+ const results = new Array(values.length);
3854
+ let nextIndex = 0;
3855
+ async function worker() {
3856
+ while (nextIndex < values.length) {
3857
+ const index = nextIndex;
3858
+ nextIndex += 1;
3859
+ const value = values[index];
3860
+ if (value !== void 0) {
3861
+ results[index] = await map(value, index);
3862
+ }
3863
+ }
3864
+ }
3865
+ await Promise.all(
3866
+ Array.from({ length: Math.min(concurrency, values.length) }, async () => worker())
3867
+ );
3868
+ return results;
3869
+ }
3870
+ function atlasRectangle(entry) {
3871
+ return { height: entry.height, width: entry.width, x: entry.x, y: entry.y };
3872
+ }
3873
+ function iconSourceFormat(extension) {
3874
+ switch (extension) {
3875
+ case ".svg":
3876
+ return "svg";
3877
+ case ".png":
3878
+ return "png";
3879
+ case ".jpg":
3880
+ case ".jpeg":
3881
+ return "jpeg";
3882
+ case ".webp":
3883
+ return "webp";
3884
+ default:
3885
+ throw new Error(`Unsupported icon source format: ${extension}`);
3886
+ }
3887
+ }
3888
+ function isPathInside2(root, candidate) {
3889
+ const pathFromRoot = relative2(root, candidate);
3890
+ return pathFromRoot === "" || !isAbsolute2(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep2}`);
3891
+ }
3892
+ function describeFileSystemError(error, fallback) {
3893
+ if (error && typeof error === "object" && "code" in error) {
3894
+ const code = String(error.code ?? "");
3895
+ if (code === "ENOENT") {
3896
+ return fallback;
3897
+ }
3898
+ if (code === "EACCES" || code === "EPERM") {
3899
+ return "Icon source is not readable";
3900
+ }
3901
+ }
3902
+ return error instanceof Error && error.message ? error.message : fallback;
3903
+ }
3904
+ function joinUrl(base, path) {
3905
+ const trimmedBase = base.replace(/\/+$/g, "");
3906
+ const trimmedPath = path.replace(/^\/+/g, "");
3907
+ return trimmedBase ? `${trimmedBase}/${trimmedPath}` : trimmedPath;
3908
+ }
3909
+ function uniqueStrings(values) {
3910
+ return [...new Set(values)];
3911
+ }
3912
+ async function renderIconInputs(inputs) {
3913
+ let renderedOneXPixels = 0;
3914
+ return mapWithConcurrency(inputs, tileflowIconPackageLimits4.decodeConcurrency, async (icon) => {
3915
+ const dimensions = await validateDecodedDimensions(icon);
3916
+ const oneXWidth = icon.kind === "pattern" ? dimensions.width : iconSpriteSize;
3917
+ const oneXHeight = icon.kind === "pattern" ? dimensions.height : iconSpriteSize;
3918
+ const maximumOneXDimension = tileflowIconPackageLimits4.maxAtlasDimension / 2;
3919
+ renderedOneXPixels += oneXWidth * oneXHeight;
3920
+ if (oneXWidth > maximumOneXDimension || oneXHeight > maximumOneXDimension || renderedOneXPixels > maximumOneXDimension ** 2) {
3921
+ throw new Error(
3922
+ "Rendered icon inputs exceed the paired sprite capacity before rasterization"
3923
+ );
3924
+ }
3925
+ if (icon.kind === "pattern" && (dimensions.width < 2 || dimensions.width > 512 || (dimensions.width & dimensions.width - 1) !== 0)) {
3926
+ throw new Error(
3927
+ `${icon.fileName} pattern width must be a power of two from 2 through 512 pixels`
3928
+ );
3929
+ }
3930
+ const oneX = await renderIcon(icon, dimensions, 1);
3931
+ const twoX = await renderIcon(icon, dimensions, 2);
3932
+ return {
3933
+ dimensions,
3934
+ input: icon,
3935
+ oneX,
3936
+ pixelSha256: {
3937
+ oneX: await hashTileflowRenderedIconPixels3({
3938
+ height: oneX.height,
3939
+ pixelRatio: 1,
3940
+ rgba: oneX.rgba,
3941
+ width: oneX.width
3942
+ }),
3943
+ twoX: await hashTileflowRenderedIconPixels3({
3944
+ height: twoX.height,
3945
+ pixelRatio: 2,
3946
+ rgba: twoX.rgba,
3947
+ width: twoX.width
3948
+ })
3949
+ },
3950
+ sourceSha256: icon.appearance || icon.layout ? await sha256Hex3(
3951
+ `tileflow-icon-source-v2\0${await sha256Hex3(icon.source)}\0${serializeCanonicalJson3({ ...icon.appearance ? { appearance: icon.appearance } : {}, ...icon.layout ? { layout: icon.layout } : {} })}`
3952
+ ) : await sha256Hex3(icon.source),
3953
+ twoX
3954
+ };
3955
+ });
3956
+ }
3957
+ async function readTileflowIconDirectory(source, options) {
3958
+ const configPath = options.configPath ?? "icons";
3959
+ const directory = (await resolveTileflowAssetDirectories([source], {
3960
+ baseDirectory: options.baseDirectory,
3961
+ configPath,
3962
+ cwd: options.cwd,
3963
+ kind: "icons",
3964
+ ordinalOffset: options.ordinal ?? 0,
3965
+ target: options.target
3966
+ }))[0];
3967
+ if (!directory)
3968
+ throw new TileflowIconCompilationError([{ path: configPath, message: "Missing icon directory" }]);
3969
+ const issues = [];
3970
+ const inspected = await inspectIconSource(directory, options.target, issues);
3971
+ if (!inspected || issues.length) throw new TileflowIconCompilationError(issues);
3972
+ if (inspected.icons.length > tileflowIconPackageLimits4.maxIconCount)
3973
+ throw new TileflowIconCompilationError([
3974
+ {
3975
+ path: configPath,
3976
+ message: `One icon contributor supports at most ${tileflowIconPackageLimits4.maxIconCount} exports`
3977
+ }
3978
+ ]);
3979
+ const entries = inspected.icons.map((icon) => ({
3980
+ byteLength: icon.source.byteLength,
3981
+ format: icon.format,
3982
+ id: icon.name,
3983
+ kind: icon.kind,
3984
+ path: icon.displayPath
3985
+ }));
3986
+ return {
3987
+ containmentRoot: directory.containmentRoot,
3988
+ entries,
3989
+ iconIds: entries.map((entry) => entry.id),
3990
+ label: describeAssetDirectory(directory),
3991
+ realPath: directory.realPath,
3992
+ ...directory.watch ? { watchPath: directory.realPath } : {},
3993
+ render: async (ids) => {
3994
+ const selected = new Set(ids);
3995
+ const inputs = inspected.icons.filter((icon) => selected.has(icon.name));
3996
+ if (inputs.length !== ids.length || inputs.reduce((sum, icon) => sum + icon.source.byteLength, 0) > tileflowIconPackageLimits4.maxSourceBytes)
3997
+ throw new TileflowIconCompilationError([
3998
+ {
3999
+ path: configPath,
4000
+ message: "Selected icon inputs exceed their source budget or have invalid IDs"
4001
+ }
4002
+ ]);
4003
+ return (await renderIconInputs(inputs)).map((rendered) => ({
4004
+ icon: {
4005
+ id: rendered.input.name,
4006
+ ...rendered.input.appearance ? { appearance: rendered.input.appearance } : {},
4007
+ ...rendered.input.layout ? { layout: rendered.input.layout } : {},
4008
+ oneX: {
4009
+ height: rendered.oneX.height,
4010
+ width: rendered.oneX.width,
4011
+ rgba: rendered.oneX.rgba
4012
+ },
4013
+ twoX: {
4014
+ height: rendered.twoX.height,
4015
+ width: rendered.twoX.width,
4016
+ rgba: rendered.twoX.rgba
4017
+ }
4018
+ },
4019
+ // Canonical key order keeps the in-memory build manifest byte-identical to its artifact.
4020
+ identity: {
4021
+ format: rendered.input.format,
4022
+ id: rendered.input.name,
4023
+ kind: rendered.input.kind,
4024
+ sha256: rendered.sourceSha256
4025
+ },
4026
+ source: {
4027
+ byteLength: rendered.input.source.byteLength,
4028
+ dimensions: { ...rendered.dimensions },
4029
+ format: rendered.input.format,
4030
+ id: rendered.input.name,
4031
+ kind: rendered.input.kind,
4032
+ path: rendered.input.displayPath
4033
+ },
4034
+ sourceBytes: rendered.input.source.byteLength
4035
+ }));
4036
+ }
4037
+ };
4038
+ }
4039
+
4040
+ export {
4041
+ packTileflowRenderedIcons,
4042
+ verifyTileflowIconArtifact,
4043
+ getTileflowIconCacheDirectory,
4044
+ loadTileflowIconSetArtifact,
4045
+ storeTileflowIconSetArtifact,
4046
+ readTileflowIconsLockfileText,
4047
+ readTileflowIconsLockfile,
4048
+ writeTileflowIconsLockfile,
4049
+ composeTileflowIconSources,
4050
+ prepareTileflowBuildingModel,
4051
+ packageTileflowLandmarkTile,
4052
+ encodeTileflowLandmarkRegion,
4053
+ prepareTileflowMapTreeModels,
4054
+ renderTileflowIconAtlasPreview,
4055
+ TileflowIconCompilationError,
4056
+ iconFileExtensions,
4057
+ compileTileflowIconPackages,
4058
+ inspectTileflowIconCatalogs,
4059
+ prepareTileflowCatalogIcons,
4060
+ getTileflowIconWatchPaths,
4061
+ readTileflowIconDirectory
4062
+ };