@tileflow/dev 0.1.0-alpha.22 → 0.1.0-alpha.24

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.
@@ -4,20 +4,184 @@ import {
4
4
  } from "./chunk-BL3FK4GO.js";
5
5
 
6
6
  // src/icons.ts
7
- import { readdir, readFile, realpath, stat } from "fs/promises";
8
- import { extname, isAbsolute, relative, resolve, sep } from "path";
9
- import { SaxesParser } from "saxes";
7
+ import {
8
+ isTileflowIconSetSource,
9
+ TileflowIconSetError
10
+ } from "@tileflow/core";
11
+
12
+ // src/icon-sprite.ts
10
13
  import {
11
14
  compareCodeUnits,
12
15
  hashTileflowIconPackageManifest,
13
16
  hashTileflowRenderedIconPixels,
14
- parseResolvedTileflowMap,
15
17
  serializeCanonicalJson,
16
18
  sha256Hex,
17
- tileflowIconIdPattern,
19
+ tileflowIconIdSchema,
18
20
  tileflowIconPackageLimits,
19
21
  tileflowIconPackageManifestSchema
20
22
  } from "@tileflow/core";
23
+ async function packTileflowRenderedIcons(input) {
24
+ const icons = [...input].sort((left, right) => compareCodeUnits(left.id, right.id));
25
+ if (icons.length < 1 || icons.length > tileflowIconPackageLimits.maxIconCount || new Set(icons.map((icon) => icon.id)).size !== icons.length)
26
+ throw new Error("Expected 1 through 256 unique rendered icons");
27
+ const renderedIcons = [];
28
+ for (const icon of icons) {
29
+ tileflowIconIdSchema.parse(icon.id);
30
+ if (icon.twoX.width !== icon.oneX.width * 2 || icon.twoX.height !== icon.oneX.height * 2)
31
+ throw new Error("Rendered density geometry must double exactly");
32
+ renderedIcons.push({
33
+ name: icon.id,
34
+ pixelSha256: {
35
+ oneX: await hashTileflowRenderedIconPixels({ ...icon.oneX, pixelRatio: 1 }),
36
+ twoX: await hashTileflowRenderedIconPixels({ ...icon.twoX, pixelRatio: 2 })
37
+ }
38
+ });
39
+ }
40
+ const oneX = icons.map((icon) => ({ ...icon.oneX, name: icon.id }));
41
+ const twoX = icons.map((icon) => ({ ...icon.twoX, name: icon.id }));
42
+ const layoutOneX = createSpriteLayout(oneX, 1);
43
+ const layoutTwoX = createSpriteLayout(twoX, 2);
44
+ if (layoutTwoX.width !== layoutOneX.width * 2 || layoutTwoX.height !== layoutOneX.height * 2)
45
+ throw new Error("Composed atlas exceeds the paired density geometry limit");
46
+ const files = [
47
+ {
48
+ contentType: "application/json",
49
+ fileName: "sprite.json",
50
+ source: new TextEncoder().encode(`${serializeCanonicalJson(layoutOneX.index)}
51
+ `)
52
+ },
53
+ {
54
+ contentType: "image/png",
55
+ fileName: "sprite.png",
56
+ source: await createSpriteImage(oneX, layoutOneX)
57
+ },
58
+ {
59
+ contentType: "application/json",
60
+ fileName: "sprite@2x.json",
61
+ source: new TextEncoder().encode(`${serializeCanonicalJson(layoutTwoX.index)}
62
+ `)
63
+ },
64
+ {
65
+ contentType: "image/png",
66
+ fileName: "sprite@2x.png",
67
+ source: await createSpriteImage(twoX, layoutTwoX)
68
+ }
69
+ ];
70
+ assertGeneratedFileLimits(files);
71
+ const manifest = tileflowIconPackageManifestSchema.parse({
72
+ format: "tileflow-icon-package-v1",
73
+ files: await Promise.all(
74
+ files.map(async (file) => ({
75
+ name: file.fileName,
76
+ byteLength: file.source.byteLength,
77
+ contentType: file.contentType,
78
+ sha256: await sha256Hex(file.source)
79
+ }))
80
+ ),
81
+ iconNames: icons.map((icon) => icon.id),
82
+ renderedIcons,
83
+ sprites: {
84
+ oneX: { width: layoutOneX.width, height: layoutOneX.height, pixelRatio: 1 },
85
+ twoX: { width: layoutTwoX.width, height: layoutTwoX.height, pixelRatio: 2 }
86
+ }
87
+ });
88
+ return { contentHash: await hashTileflowIconPackageManifest(manifest), manifest, files };
89
+ }
90
+ function createSpriteLayout(icons, pixelRatio) {
91
+ const columns = Math.ceil(Math.sqrt(icons.length));
92
+ const widest = Math.max(...icons.map((icon) => icon.width));
93
+ const targetWidth = Math.min(tileflowIconPackageLimits.maxAtlasDimension, columns * widest);
94
+ const placements = [];
95
+ let left = 0;
96
+ let rowHeight = 0;
97
+ let top = 0;
98
+ for (const icon of icons) {
99
+ if (icon.width > tileflowIconPackageLimits.maxAtlasDimension)
100
+ throw new Error(
101
+ `Generated sprite exceeds ${tileflowIconPackageLimits.maxAtlasDimension} pixels per dimension`
102
+ );
103
+ if (left > 0 && left + icon.width > targetWidth) {
104
+ top += rowHeight;
105
+ left = 0;
106
+ rowHeight = 0;
107
+ }
108
+ placements.push({ left, top });
109
+ left += icon.width;
110
+ rowHeight = Math.max(rowHeight, icon.height);
111
+ }
112
+ const width = targetWidth;
113
+ const height = top + rowHeight;
114
+ if (width > tileflowIconPackageLimits.maxAtlasDimension || height > tileflowIconPackageLimits.maxAtlasDimension)
115
+ throw new Error(
116
+ `Generated sprite exceeds ${tileflowIconPackageLimits.maxAtlasDimension} pixels per dimension`
117
+ );
118
+ const index = Object.fromEntries(
119
+ icons.map((icon, ordinal) => {
120
+ const placement = placements[ordinal];
121
+ return [
122
+ icon.name,
123
+ { height: icon.height, pixelRatio, width: icon.width, x: placement.left, y: placement.top }
124
+ ];
125
+ })
126
+ );
127
+ return { height, index, width };
128
+ }
129
+ async function createSpriteImage(icons, layout) {
130
+ const sharp = await loadSharp();
131
+ const atlas = new Uint8Array(layout.width * layout.height * 4);
132
+ for (const icon of icons) {
133
+ const placement = layout.index[icon.name];
134
+ if (!placement || icon.width !== placement.width || icon.height !== placement.height || icon.rgba.byteLength !== icon.width * icon.height * 4)
135
+ throw new Error("Rendered icon dimensions do not match the sprite layout");
136
+ const rowBytes = icon.width * 4;
137
+ for (let row = 0; row < icon.height; row += 1) {
138
+ const sourceStart = row * rowBytes;
139
+ const targetStart = ((placement.y + row) * layout.width + placement.x) * 4;
140
+ atlas.set(icon.rgba.subarray(sourceStart, sourceStart + rowBytes), targetStart);
141
+ }
142
+ }
143
+ return sharp(atlas, { raw: { channels: 4, height: layout.height, width: layout.width } }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
144
+ }
145
+ function assertGeneratedFileLimits(files) {
146
+ let totalBytes = 0;
147
+ for (const file of files) {
148
+ if (file.source.byteLength > tileflowIconPackageLimits.maxGeneratedFileBytes)
149
+ throw new Error(
150
+ `${file.fileName} exceeds ${tileflowIconPackageLimits.maxGeneratedFileBytes} generated bytes`
151
+ );
152
+ totalBytes += file.source.byteLength;
153
+ }
154
+ if (totalBytes > tileflowIconPackageLimits.maxGeneratedPackageBytes)
155
+ throw new Error(
156
+ `Generated package exceeds ${tileflowIconPackageLimits.maxGeneratedPackageBytes} bytes`
157
+ );
158
+ }
159
+ async function loadSharp() {
160
+ try {
161
+ return (await import("sharp")).default;
162
+ } catch (cause) {
163
+ throw new Error(
164
+ 'Local icon sprites require the optional "sharp" package. Install sharp or disable local icons.',
165
+ { cause }
166
+ );
167
+ }
168
+ }
169
+
170
+ // src/icons.ts
171
+ import { readdir, readFile, realpath, stat } from "fs/promises";
172
+ import { extname, isAbsolute, relative, resolve, sep } from "path";
173
+ import { SaxesParser } from "saxes";
174
+ import {
175
+ compareCodeUnits as compareCodeUnits2,
176
+ hashTileflowIconPackageManifest as hashTileflowIconPackageManifest2,
177
+ hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels2,
178
+ parseResolvedTileflowMap,
179
+ serializeCanonicalJson as serializeCanonicalJson2,
180
+ sha256Hex as sha256Hex2,
181
+ tileflowIconIdPattern,
182
+ tileflowIconPackageLimits as tileflowIconPackageLimits2,
183
+ tileflowIconPackageManifestSchema as tileflowIconPackageManifestSchema2
184
+ } from "@tileflow/core";
21
185
  var TileflowIconCompilationError = class extends Error {
22
186
  code = "ICON_INVALID";
23
187
  issues;
@@ -69,14 +233,14 @@ async function compileTileflowIconPackages(project, options) {
69
233
  return {
70
234
  bindings,
71
235
  packages: [...packagesByHash.values()].sort(
72
- (left, right) => compareCodeUnits(left.contentHash, right.contentHash)
236
+ (left, right) => compareCodeUnits2(left.contentHash, right.contentHash)
73
237
  ),
74
238
  sourceIdentities,
75
239
  watchPaths: uniqueStrings(
76
240
  result.mapRequests.flatMap(
77
241
  (request) => request.directories.filter((directory) => directory.watch).map((directory) => directory.realPath)
78
242
  )
79
- ).sort(compareCodeUnits)
243
+ ).sort(compareCodeUnits2)
80
244
  };
81
245
  }
82
246
  async function inspectTileflowIconCatalogs(project, options) {
@@ -150,7 +314,7 @@ async function inspectTileflowIconCatalogs(project, options) {
150
314
  replacements: [...compiled.sourceReplacements]
151
315
  };
152
316
  }).sort(
153
- (left, right) => compareCodeUnits(left.directories.join("\0"), right.directories.join("\0"))
317
+ (left, right) => compareCodeUnits2(left.directories.join("\0"), right.directories.join("\0"))
154
318
  );
155
319
  return { catalogs, maps };
156
320
  }
@@ -229,23 +393,32 @@ async function getTileflowIconWatchPaths(project, cwd, baseDirectory = cwd) {
229
393
  requests.flatMap(
230
394
  (request) => request.directories.filter((directory) => directory.watch).map((directory) => directory.realPath)
231
395
  )
232
- ).sort(compareCodeUnits);
396
+ ).sort(compareCodeUnits2);
233
397
  }
234
398
  async function getMapIconRequests(project, cwd, baseDirectory, target, mapNames) {
235
- const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits);
399
+ const selectedNames = (mapNames === void 0 ? Object.keys(project.maps) : [...new Set(mapNames)]).sort(compareCodeUnits2);
236
400
  const requests = [];
237
401
  for (const mapName of selectedNames) {
238
402
  const mapConfig = project.maps[mapName];
239
403
  if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
240
404
  const resolvedMap = parseResolvedTileflowMap(mapConfig);
241
405
  try {
242
- const directories = await resolveTileflowAssetDirectories(resolvedMap.icons ?? [], {
243
- baseDirectory,
244
- configPath: `maps.${mapName}.icons`,
245
- cwd,
246
- kind: "icons",
247
- target
248
- });
406
+ const sourceDirectories = resolvedMap.icons ?? [];
407
+ if (sourceDirectories.some(isTileflowIconSetSource))
408
+ throw new TileflowIconSetError(
409
+ "ICON_SET_INVALID",
410
+ "Shared icon sets require the explicit composeTileflowIconSources build port; command integration is not installed"
411
+ );
412
+ const directories = await resolveTileflowAssetDirectories(
413
+ sourceDirectories,
414
+ {
415
+ baseDirectory,
416
+ configPath: `maps.${mapName}.icons`,
417
+ cwd,
418
+ kind: "icons",
419
+ target
420
+ }
421
+ );
249
422
  requests.push({
250
423
  directories,
251
424
  mapName,
@@ -267,11 +440,11 @@ function uniqueDirectories(requests) {
267
440
  (request) => request.directories.map((directory) => [directory.realPath, directory])
268
441
  )
269
442
  ).values()
270
- ].sort((left, right) => compareCodeUnits(left.realPath, right.realPath));
443
+ ].sort((left, right) => compareCodeUnits2(left.realPath, right.realPath));
271
444
  }
272
445
  function uniqueSequences(requests) {
273
446
  return [...new Map(requests.map((request) => [request.sequenceKey, request])).values()].sort(
274
- (left, right) => compareCodeUnits(left.sequenceKey, right.sequenceKey)
447
+ (left, right) => compareCodeUnits2(left.sequenceKey, right.sequenceKey)
275
448
  );
276
449
  }
277
450
  function composeIconDirectories(request, inspectedBySource) {
@@ -301,17 +474,17 @@ function composeIconDirectories(request, inspectedBySource) {
301
474
  }
302
475
  }
303
476
  const icons = [...iconsById.values()].sort(
304
- (left, right) => compareCodeUnits(left.name, right.name)
477
+ (left, right) => compareCodeUnits2(left.name, right.name)
305
478
  );
306
479
  const aggregateBytes = icons.reduce((total, icon) => total + icon.source.byteLength, 0);
307
- if (icons.length > tileflowIconPackageLimits.maxIconCount) {
480
+ if (icons.length > tileflowIconPackageLimits2.maxIconCount) {
308
481
  throw new Error(
309
- `Composed icon set contains more than ${tileflowIconPackageLimits.maxIconCount} icons`
482
+ `Composed icon set contains more than ${tileflowIconPackageLimits2.maxIconCount} icons`
310
483
  );
311
484
  }
312
- if (aggregateBytes > tileflowIconPackageLimits.maxSourceBytes) {
485
+ if (aggregateBytes > tileflowIconPackageLimits2.maxSourceBytes) {
313
486
  throw new Error(
314
- `Composed icon set exceeds ${tileflowIconPackageLimits.maxSourceBytes} aggregate bytes`
487
+ `Composed icon set exceeds ${tileflowIconPackageLimits2.maxSourceBytes} aggregate bytes`
315
488
  );
316
489
  }
317
490
  return { icons, replacements };
@@ -334,7 +507,7 @@ async function inspectIconSource(directory, target, issues) {
334
507
  });
335
508
  return null;
336
509
  }
337
- entries.sort((left, right) => compareCodeUnits(left.name, right.name));
510
+ entries.sort((left, right) => compareCodeUnits2(left.name, right.name));
338
511
  const candidates = [];
339
512
  const names = /* @__PURE__ */ new Set();
340
513
  for (const entry of entries) {
@@ -384,9 +557,9 @@ async function inspectIconSource(directory, target, issues) {
384
557
  path
385
558
  });
386
559
  }
387
- if (entryStat.size > tileflowIconPackageLimits.maxSourceFileBytes) {
560
+ if (entryStat.size > tileflowIconPackageLimits2.maxSourceFileBytes) {
388
561
  issues.push({
389
- message: `Icon source exceeds ${tileflowIconPackageLimits.maxSourceFileBytes} bytes`,
562
+ message: `Icon source exceeds ${tileflowIconPackageLimits2.maxSourceFileBytes} bytes`,
390
563
  path
391
564
  });
392
565
  }
@@ -400,7 +573,7 @@ async function inspectIconSource(directory, target, issues) {
400
573
  size: entryStat.size
401
574
  });
402
575
  }
403
- candidates.sort((left, right) => compareCodeUnits(left.name, right.name));
576
+ candidates.sort((left, right) => compareCodeUnits2(left.name, right.name));
404
577
  if (candidates.length === 0) {
405
578
  issues.push({
406
579
  message: "No supported .svg, .png, .jpg, .jpeg, or .webp icon files were found",
@@ -415,7 +588,7 @@ async function inspectIconSource(directory, target, issues) {
415
588
  const displayPath = candidate.displayPath;
416
589
  try {
417
590
  const source = await readFile(candidate.path);
418
- if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits.maxSourceFileBytes) {
591
+ if (source.byteLength !== candidate.size || source.byteLength > tileflowIconPackageLimits2.maxSourceFileBytes) {
419
592
  throw new Error("Icon changed while it was being validated");
420
593
  }
421
594
  if (target === "hosted" && extname(candidate.fileName).toLowerCase() === ".svg") {
@@ -443,41 +616,7 @@ async function inspectIconSource(directory, target, issues) {
443
616
  return { icons };
444
617
  }
445
618
  async function compileInspectedIconSource(inspected) {
446
- const rendered = await mapWithConcurrency(
447
- inspected.icons,
448
- tileflowIconPackageLimits.decodeConcurrency,
449
- async (icon) => {
450
- const dimensions = await validateDecodedDimensions(icon);
451
- if (icon.kind === "pattern" && (dimensions.width < 2 || dimensions.width > 512 || (dimensions.width & dimensions.width - 1) !== 0)) {
452
- throw new Error(
453
- `${icon.fileName} pattern width must be a power of two from 2 through 512 pixels`
454
- );
455
- }
456
- const oneX = await renderIcon(icon, dimensions, 1);
457
- const twoX = await renderIcon(icon, dimensions, 2);
458
- return {
459
- dimensions,
460
- input: icon,
461
- oneX,
462
- pixelSha256: {
463
- oneX: await hashTileflowRenderedIconPixels({
464
- height: oneX.height,
465
- pixelRatio: 1,
466
- rgba: oneX.rgba,
467
- width: oneX.width
468
- }),
469
- twoX: await hashTileflowRenderedIconPixels({
470
- height: twoX.height,
471
- pixelRatio: 2,
472
- rgba: twoX.rgba,
473
- width: twoX.width
474
- })
475
- },
476
- sourceSha256: await sha256Hex(icon.source),
477
- twoX
478
- };
479
- }
480
- );
619
+ const rendered = await renderIconInputs(inspected.icons);
481
620
  const layoutOneX = createSpriteLayout(
482
621
  rendered.map(({ input, oneX }) => ({ ...oneX, name: input.name })),
483
622
  1
@@ -496,9 +635,9 @@ async function compileInspectedIconSource(inspected) {
496
635
  layoutTwoX
497
636
  )
498
637
  ]);
499
- const oneXJson = new TextEncoder().encode(`${serializeCanonicalJson(layoutOneX.index)}
638
+ const oneXJson = new TextEncoder().encode(`${serializeCanonicalJson2(layoutOneX.index)}
500
639
  `);
501
- const twoXJson = new TextEncoder().encode(`${serializeCanonicalJson(layoutTwoX.index)}
640
+ const twoXJson = new TextEncoder().encode(`${serializeCanonicalJson2(layoutTwoX.index)}
502
641
  `);
503
642
  const files = [
504
643
  { contentType: "application/json", fileName: "sprite.json", source: oneXJson },
@@ -507,8 +646,8 @@ async function compileInspectedIconSource(inspected) {
507
646
  { contentType: "image/png", fileName: "sprite@2x.png", source: twoXImage }
508
647
  ];
509
648
  assertGeneratedFileLimits(files);
510
- const fileDigests = await Promise.all(files.map((file) => sha256Hex(file.source)));
511
- const manifest = tileflowIconPackageManifestSchema.parse({
649
+ const fileDigests = await Promise.all(files.map((file) => sha256Hex2(file.source)));
650
+ const manifest = tileflowIconPackageManifestSchema2.parse({
512
651
  files: files.map((file, index) => ({
513
652
  byteLength: file.source.byteLength,
514
653
  contentType: file.contentType,
@@ -534,75 +673,28 @@ async function compileInspectedIconSource(inspected) {
534
673
  layoutOneX,
535
674
  layoutTwoX,
536
675
  package: {
537
- contentHash: await hashTileflowIconPackageManifest(manifest),
676
+ contentHash: await hashTileflowIconPackageManifest2(manifest),
538
677
  files,
539
678
  manifest
540
679
  },
541
680
  sourceReplacements: inspected.replacements ?? []
542
681
  };
543
682
  }
544
- function createSpriteLayout(icons, pixelRatio) {
545
- const columns = Math.ceil(Math.sqrt(icons.length));
546
- const widest = Math.max(...icons.map((icon) => icon.width));
547
- const targetWidth = Math.min(tileflowIconPackageLimits.maxAtlasDimension, columns * widest);
548
- const placements = [];
549
- let left = 0;
550
- let rowHeight = 0;
551
- let top = 0;
552
- for (const icon of icons) {
553
- if (icon.width > tileflowIconPackageLimits.maxAtlasDimension) {
554
- throw new Error(
555
- `Generated sprite exceeds ${tileflowIconPackageLimits.maxAtlasDimension} pixels per dimension`
556
- );
557
- }
558
- if (left > 0 && left + icon.width > targetWidth) {
559
- top += rowHeight;
560
- left = 0;
561
- rowHeight = 0;
562
- }
563
- placements.push({ left, top });
564
- left += icon.width;
565
- rowHeight = Math.max(rowHeight, icon.height);
566
- }
567
- const width = targetWidth;
568
- const height = top + rowHeight;
569
- if (width > tileflowIconPackageLimits.maxAtlasDimension || height > tileflowIconPackageLimits.maxAtlasDimension) {
570
- throw new Error(
571
- `Generated sprite exceeds ${tileflowIconPackageLimits.maxAtlasDimension} pixels per dimension`
572
- );
573
- }
574
- const index = Object.fromEntries(
575
- icons.map((icon, indexNumber) => {
576
- const placement = placements[indexNumber];
577
- return [
578
- icon.name,
579
- {
580
- height: icon.height,
581
- pixelRatio,
582
- width: icon.width,
583
- x: placement.left,
584
- y: placement.top
585
- }
586
- ];
587
- })
588
- );
589
- return { height, index, width };
590
- }
591
683
  async function validateDecodedDimensions(icon) {
592
684
  const sharp = await loadSharp();
593
685
  const metadata = await sharp(icon.source, {
594
686
  density: 72,
595
687
  failOn: "error",
596
- limitInputPixels: tileflowIconPackageLimits.maxDecodedPixelsPerIcon
688
+ limitInputPixels: tileflowIconPackageLimits2.maxDecodedPixelsPerIcon
597
689
  }).metadata();
598
690
  const width = metadata.width ?? 0;
599
691
  const height = metadata.height ?? 0;
600
692
  if (width <= 0 || height <= 0) {
601
693
  throw new Error(`Unable to determine decoded dimensions for ${icon.fileName}`);
602
694
  }
603
- if (width * height > tileflowIconPackageLimits.maxDecodedPixelsPerIcon) {
695
+ if (width * height > tileflowIconPackageLimits2.maxDecodedPixelsPerIcon) {
604
696
  throw new Error(
605
- `${icon.fileName} exceeds ${tileflowIconPackageLimits.maxDecodedPixelsPerIcon} decoded pixels`
697
+ `${icon.fileName} exceeds ${tileflowIconPackageLimits2.maxDecodedPixelsPerIcon} decoded pixels`
606
698
  );
607
699
  }
608
700
  return { height, width };
@@ -614,7 +706,7 @@ async function renderIcon(icon, dimensions, pixelRatio) {
614
706
  const { data, info } = await sharp(icon.source, {
615
707
  density: 72 * pixelRatio,
616
708
  failOn: "error",
617
- limitInputPixels: tileflowIconPackageLimits.maxDecodedPixelsPerIcon
709
+ limitInputPixels: tileflowIconPackageLimits2.maxDecodedPixelsPerIcon
618
710
  }).resize(targetWidth, targetHeight, {
619
711
  background: { alpha: 0, b: 0, g: 0, r: 0 },
620
712
  fit: "contain"
@@ -628,45 +720,6 @@ async function renderIcon(icon, dimensions, pixelRatio) {
628
720
  }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
629
721
  return { height: info.height, png, rgba, width: info.width };
630
722
  }
631
- async function createSpriteImage(icons, layout) {
632
- const sharp = await loadSharp();
633
- const atlas = new Uint8Array(layout.width * layout.height * 4);
634
- for (const icon of icons) {
635
- const placement = layout.index[icon.name];
636
- if (!placement || icon.width !== placement.width || icon.height !== placement.height) {
637
- throw new Error("Rendered icon dimensions do not match the sprite layout");
638
- }
639
- const rowBytes = icon.width * 4;
640
- for (let row = 0; row < icon.height; row += 1) {
641
- const sourceStart = row * rowBytes;
642
- const targetStart = ((placement.y + row) * layout.width + placement.x) * 4;
643
- atlas.set(icon.rgba.subarray(sourceStart, sourceStart + rowBytes), targetStart);
644
- }
645
- }
646
- return sharp(atlas, {
647
- raw: {
648
- channels: 4,
649
- height: layout.height,
650
- width: layout.width
651
- }
652
- }).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
653
- }
654
- function assertGeneratedFileLimits(files) {
655
- let totalBytes = 0;
656
- for (const file of files) {
657
- if (file.source.byteLength > tileflowIconPackageLimits.maxGeneratedFileBytes) {
658
- throw new Error(
659
- `${file.fileName} exceeds ${tileflowIconPackageLimits.maxGeneratedFileBytes} generated bytes`
660
- );
661
- }
662
- totalBytes += file.source.byteLength;
663
- }
664
- if (totalBytes > tileflowIconPackageLimits.maxGeneratedPackageBytes) {
665
- throw new Error(
666
- `Generated package exceeds ${tileflowIconPackageLimits.maxGeneratedPackageBytes} bytes`
667
- );
668
- }
669
- }
670
723
  function validateHostedSvg(source, displayPath) {
671
724
  const parser = new SaxesParser({ fileName: displayPath, xmlns: true });
672
725
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -781,16 +834,6 @@ async function mapWithConcurrency(values, concurrency, map) {
781
834
  );
782
835
  return results;
783
836
  }
784
- async function loadSharp() {
785
- try {
786
- return (await import("sharp")).default;
787
- } catch (error) {
788
- throw new Error(
789
- 'Local icon sprites require the optional "sharp" package. Install sharp or disable local icons.',
790
- { cause: error }
791
- );
792
- }
793
- }
794
837
  function atlasRectangle(entry) {
795
838
  return { height: entry.height, width: entry.width, x: entry.x, y: entry.y };
796
839
  }
@@ -833,12 +876,111 @@ function joinUrl(base, path) {
833
876
  function uniqueStrings(values) {
834
877
  return [...new Set(values)];
835
878
  }
879
+ async function renderIconInputs(inputs) {
880
+ let renderedOneXPixels = 0;
881
+ return mapWithConcurrency(inputs, tileflowIconPackageLimits2.decodeConcurrency, async (icon) => {
882
+ const dimensions = await validateDecodedDimensions(icon);
883
+ const oneXWidth = icon.kind === "pattern" ? dimensions.width : iconSpriteSize;
884
+ const oneXHeight = icon.kind === "pattern" ? dimensions.height : iconSpriteSize;
885
+ const maximumOneXDimension = tileflowIconPackageLimits2.maxAtlasDimension / 2;
886
+ renderedOneXPixels += oneXWidth * oneXHeight;
887
+ if (oneXWidth > maximumOneXDimension || oneXHeight > maximumOneXDimension || renderedOneXPixels > maximumOneXDimension ** 2) {
888
+ throw new Error(
889
+ "Rendered icon inputs exceed the paired sprite capacity before rasterization"
890
+ );
891
+ }
892
+ if (icon.kind === "pattern" && (dimensions.width < 2 || dimensions.width > 512 || (dimensions.width & dimensions.width - 1) !== 0)) {
893
+ throw new Error(
894
+ `${icon.fileName} pattern width must be a power of two from 2 through 512 pixels`
895
+ );
896
+ }
897
+ const oneX = await renderIcon(icon, dimensions, 1);
898
+ const twoX = await renderIcon(icon, dimensions, 2);
899
+ return {
900
+ dimensions,
901
+ input: icon,
902
+ oneX,
903
+ pixelSha256: {
904
+ oneX: await hashTileflowRenderedIconPixels2({
905
+ height: oneX.height,
906
+ pixelRatio: 1,
907
+ rgba: oneX.rgba,
908
+ width: oneX.width
909
+ }),
910
+ twoX: await hashTileflowRenderedIconPixels2({
911
+ height: twoX.height,
912
+ pixelRatio: 2,
913
+ rgba: twoX.rgba,
914
+ width: twoX.width
915
+ })
916
+ },
917
+ sourceSha256: await sha256Hex2(icon.source),
918
+ twoX
919
+ };
920
+ });
921
+ }
922
+ async function readTileflowIconDirectory(source, options) {
923
+ const directory = (await resolveTileflowAssetDirectories([source], {
924
+ ...options,
925
+ configPath: "icons",
926
+ kind: "icons"
927
+ }))[0];
928
+ if (!directory)
929
+ throw new TileflowIconCompilationError([{ path: "icons", message: "Missing icon directory" }]);
930
+ const issues = [];
931
+ const inspected = await inspectIconSource(directory, options.target, issues);
932
+ if (!inspected || issues.length) throw new TileflowIconCompilationError(issues);
933
+ if (inspected.icons.length > tileflowIconPackageLimits2.maxIconCount)
934
+ throw new TileflowIconCompilationError([
935
+ { path: "icons", message: "One icon contributor supports at most 256 exports" }
936
+ ]);
937
+ return {
938
+ iconIds: inspected.icons.map((icon) => icon.name),
939
+ ...directory.watch ? { watchPath: directory.realPath } : {},
940
+ render: async (ids) => {
941
+ const selected = new Set(ids);
942
+ const inputs = inspected.icons.filter((icon) => selected.has(icon.name));
943
+ if (inputs.length !== ids.length || inputs.reduce((sum, icon) => sum + icon.source.byteLength, 0) > tileflowIconPackageLimits2.maxSourceBytes)
944
+ throw new TileflowIconCompilationError([
945
+ {
946
+ path: "icons",
947
+ message: "Selected icon inputs exceed their source budget or have invalid IDs"
948
+ }
949
+ ]);
950
+ return (await renderIconInputs(inputs)).map((rendered) => ({
951
+ icon: {
952
+ id: rendered.input.name,
953
+ oneX: {
954
+ height: rendered.oneX.height,
955
+ width: rendered.oneX.width,
956
+ rgba: rendered.oneX.rgba
957
+ },
958
+ twoX: {
959
+ height: rendered.twoX.height,
960
+ width: rendered.twoX.width,
961
+ rgba: rendered.twoX.rgba
962
+ }
963
+ },
964
+ identity: {
965
+ id: rendered.input.name,
966
+ kind: rendered.input.kind,
967
+ format: rendered.input.format,
968
+ sha256: rendered.sourceSha256
969
+ },
970
+ sourceBytes: rendered.input.source.byteLength
971
+ }));
972
+ }
973
+ };
974
+ }
836
975
 
837
976
  export {
977
+ packTileflowRenderedIcons,
978
+ loadSharp,
838
979
  TileflowIconCompilationError,
839
980
  iconFileExtensions,
840
981
  compileTileflowIconPackages,
841
982
  inspectTileflowIconCatalogs,
842
983
  prepareTileflowCatalogIcons,
843
- getTileflowIconWatchPaths
984
+ getTileflowIconWatchPaths,
985
+ readTileflowIconDirectory
844
986
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createTileflowArtifactSession,
3
3
  tileflowArtifactSessionSchemaVersion
4
- } from "./chunk-XTRHREXO.js";
4
+ } from "./chunk-XXVI42UZ.js";
5
5
  import {
6
6
  normalizeTileflowBasePath
7
7
  } from "./chunk-RSBV4L2Y.js";
@@ -1742,7 +1742,7 @@ function createTileflowDevRequestHandler(options = {}) {
1742
1742
  return options.session ? eventStreamResponse(options.session) : jsonResponse2({ error: "Live events require a development session." }, 404);
1743
1743
  }
1744
1744
  if (path.startsWith("/__runtime/")) {
1745
- const runtimeResponse = (await import("./preview-assets-6LYL7GP7.js")).getTileflowPreviewRuntimeResponse(path);
1745
+ const runtimeResponse = (await import("./preview-assets-UTUP3OML.js")).getTileflowPreviewRuntimeResponse(path);
1746
1746
  if (runtimeResponse) return runtimeResponse;
1747
1747
  }
1748
1748
  const session = options.session ?? await loadOwnedSession(url.origin);
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  getTileflowIconWatchPaths,
12
12
  prepareTileflowCatalogIcons
13
- } from "./chunk-CENQR5Q2.js";
13
+ } from "./chunk-NUHSYLLS.js";
14
14
  import {
15
15
  TileflowStyleValidationError,
16
16
  normalizeTileflowStyleValidationIssues,