@tileflow/dev 0.1.0-alpha.25 → 0.1.0-alpha.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/THIRD_PARTY_NOTICES.md +27 -9
- package/dist/artifacts.d.ts +32 -5
- package/dist/artifacts.js +4 -2
- package/dist/{chunk-MKPSQSMX.js → chunk-73YPFDPC.js} +1 -1
- package/dist/{chunk-D2QWYYIF.js → chunk-EJPVUSX4.js} +242 -22
- package/dist/{chunk-W3X2JXYS.js → chunk-QAMFAM3S.js} +1168 -35
- package/dist/{chunk-3WCEHA27.js → chunk-UCBGTEQV.js} +1 -1
- package/dist/{chunk-LNCTVFUD.js → chunk-XHJBCVBB.js} +6 -1
- package/dist/icons.d.ts +10 -2
- package/dist/icons.js +3 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +9 -5
- package/dist/inspect.js +2 -2
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -3
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +1 -1
- package/docs/bounded-vector-feature-inspection.md +4 -4
- package/docs/local-comparison-workbench-and-compiler-inspection.md +4 -5
- package/docs/native-artifacts.md +258 -0
- package/docs/native-icons.md +129 -0
- package/package.json +3 -3
|
@@ -8,7 +8,10 @@ import { readdir as readdir2, readFile, realpath as realpath3, stat } from "fs/p
|
|
|
8
8
|
import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath, sep as sep2 } from "path";
|
|
9
9
|
import { SaxesParser } from "saxes";
|
|
10
10
|
import {
|
|
11
|
+
assertTileflowIconLayoutBounds as assertTileflowIconLayoutBounds2,
|
|
11
12
|
collectTileflowIconSetReferences as collectTileflowIconSetReferences2,
|
|
13
|
+
tileflowIconAuthorMetadataSchema,
|
|
14
|
+
tileflowIconMetadataFileName,
|
|
12
15
|
TileflowIconSetError as TileflowIconSetError5,
|
|
13
16
|
tileflowIconsLockfileName as tileflowIconsLockfileName3,
|
|
14
17
|
tileflowIconSpriteIndexSchema
|
|
@@ -18,7 +21,7 @@ import {
|
|
|
18
21
|
hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels3,
|
|
19
22
|
parseResolvedTileflowMap,
|
|
20
23
|
parseTileflowIconJson as parseTileflowIconJson2,
|
|
21
|
-
serializeCanonicalJson as
|
|
24
|
+
serializeCanonicalJson as serializeCanonicalJson3,
|
|
22
25
|
sha256Hex as sha256Hex3,
|
|
23
26
|
tileflowIconIdPattern,
|
|
24
27
|
tileflowIconPackageLimits as tileflowIconPackageLimits4
|
|
@@ -33,8 +36,8 @@ import {
|
|
|
33
36
|
compareCodeUnits as compareCodeUnits3,
|
|
34
37
|
parseTileflowIconComposition,
|
|
35
38
|
parseTileflowIconsLockfile as parseTileflowIconsLockfile2,
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
tileflowIconPackageLimits as tileflowIconPackageLimits3,
|
|
40
|
+
TileflowIconSetError as TileflowIconSetError4
|
|
38
41
|
} from "@tileflow/core";
|
|
39
42
|
|
|
40
43
|
// src/icon-cache.ts
|
|
@@ -53,21 +56,28 @@ import {
|
|
|
53
56
|
tileflowIconSpriteIndexSchema as indexSchema
|
|
54
57
|
} from "@tileflow/core";
|
|
55
58
|
import {
|
|
59
|
+
assertTileflowSdfPixels as assertTileflowSdfPixels2,
|
|
56
60
|
compareCodeUnits as compareCodeUnits2,
|
|
57
61
|
hashTileflowIconPackageManifest as hashTileflowIconPackageManifest2,
|
|
58
62
|
hashTileflowRenderedIconPixels as hashTileflowRenderedIconPixels2,
|
|
59
63
|
parseTileflowIconJson,
|
|
64
|
+
readTileflowIconLayout,
|
|
65
|
+
scaleTileflowIconLayout as scaleTileflowIconLayout2,
|
|
66
|
+
serializeCanonicalJson as serializeCanonicalJson2,
|
|
60
67
|
sha256Hex as sha256Hex2,
|
|
61
|
-
TileflowIconSetError,
|
|
62
68
|
tileflowIconPackageLimits as tileflowIconPackageLimits2,
|
|
63
|
-
tileflowIconPackageManifestSchema as tileflowIconPackageManifestSchema2
|
|
69
|
+
tileflowIconPackageManifestSchema as tileflowIconPackageManifestSchema2,
|
|
70
|
+
TileflowIconSetError
|
|
64
71
|
} from "@tileflow/core";
|
|
65
72
|
|
|
66
73
|
// src/icon-sprite.ts
|
|
67
74
|
import {
|
|
75
|
+
assertTileflowIconLayoutBounds,
|
|
76
|
+
assertTileflowSdfPixels,
|
|
68
77
|
compareCodeUnits,
|
|
69
78
|
hashTileflowIconPackageManifest,
|
|
70
79
|
hashTileflowRenderedIconPixels,
|
|
80
|
+
scaleTileflowIconLayout,
|
|
71
81
|
serializeCanonicalJson,
|
|
72
82
|
sha256Hex,
|
|
73
83
|
tileflowIconIdSchema,
|
|
@@ -77,22 +87,41 @@ import {
|
|
|
77
87
|
async function packTileflowRenderedIcons(input) {
|
|
78
88
|
const icons = [...input].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
79
89
|
if (icons.length < 1 || icons.length > tileflowIconPackageLimits.maxIconCount || new Set(icons.map((icon) => icon.id)).size !== icons.length)
|
|
80
|
-
throw new Error(
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Expected 1 through ${tileflowIconPackageLimits.maxIconCount} unique rendered icons`
|
|
92
|
+
);
|
|
81
93
|
const renderedIcons = [];
|
|
82
94
|
for (const icon of icons) {
|
|
83
95
|
tileflowIconIdSchema.parse(icon.id);
|
|
96
|
+
if (icon.layout) assertTileflowIconLayoutBounds(icon.layout, icon.oneX.width, icon.oneX.height);
|
|
97
|
+
if (icon.appearance) {
|
|
98
|
+
assertTileflowSdfPixels({ ...icon.oneX, pixelRatio: 1 });
|
|
99
|
+
assertTileflowSdfPixels({ ...icon.twoX, pixelRatio: 2 });
|
|
100
|
+
}
|
|
84
101
|
if (icon.twoX.width !== icon.oneX.width * 2 || icon.twoX.height !== icon.oneX.height * 2)
|
|
85
102
|
throw new Error("Rendered density geometry must double exactly");
|
|
86
103
|
renderedIcons.push({
|
|
87
104
|
name: icon.id,
|
|
105
|
+
...icon.appearance ? { appearance: icon.appearance } : {},
|
|
106
|
+
...icon.layout ? { layout: icon.layout } : {},
|
|
88
107
|
pixelSha256: {
|
|
89
108
|
oneX: await hashTileflowRenderedIconPixels({ ...icon.oneX, pixelRatio: 1 }),
|
|
90
109
|
twoX: await hashTileflowRenderedIconPixels({ ...icon.twoX, pixelRatio: 2 })
|
|
91
110
|
}
|
|
92
111
|
});
|
|
93
112
|
}
|
|
94
|
-
const oneX = icons.map((icon) => ({
|
|
95
|
-
|
|
113
|
+
const oneX = icons.map((icon) => ({
|
|
114
|
+
...icon.oneX,
|
|
115
|
+
name: icon.id,
|
|
116
|
+
appearance: icon.appearance,
|
|
117
|
+
layout: icon.layout
|
|
118
|
+
}));
|
|
119
|
+
const twoX = icons.map((icon) => ({
|
|
120
|
+
...icon.twoX,
|
|
121
|
+
name: icon.id,
|
|
122
|
+
appearance: icon.appearance,
|
|
123
|
+
layout: icon.layout
|
|
124
|
+
}));
|
|
96
125
|
const layoutOneX = createSpriteLayout(oneX, 1);
|
|
97
126
|
const layoutTwoX = createSpriteLayout(twoX, 2);
|
|
98
127
|
if (layoutTwoX.width !== layoutOneX.width * 2 || layoutTwoX.height !== layoutOneX.height * 2)
|
|
@@ -123,7 +152,7 @@ async function packTileflowRenderedIcons(input) {
|
|
|
123
152
|
];
|
|
124
153
|
assertGeneratedFileLimits(files);
|
|
125
154
|
const manifest = tileflowIconPackageManifestSchema.parse({
|
|
126
|
-
format: "tileflow-icon-package-v1",
|
|
155
|
+
format: icons.some((icon) => icon.appearance || icon.layout) ? "tileflow-icon-package-v2" : "tileflow-icon-package-v1",
|
|
127
156
|
files: await Promise.all(
|
|
128
157
|
files.map(async (file) => ({
|
|
129
158
|
name: file.fileName,
|
|
@@ -174,7 +203,15 @@ function createSpriteLayout(icons, pixelRatio) {
|
|
|
174
203
|
const placement = placements[ordinal];
|
|
175
204
|
return [
|
|
176
205
|
icon.name,
|
|
177
|
-
{
|
|
206
|
+
{
|
|
207
|
+
height: icon.height,
|
|
208
|
+
pixelRatio,
|
|
209
|
+
width: icon.width,
|
|
210
|
+
x: placement.left,
|
|
211
|
+
y: placement.top,
|
|
212
|
+
...icon.appearance ? { sdf: true, tileflow: icon.appearance } : {},
|
|
213
|
+
...icon.layout ? scaleTileflowIconLayout(icon.layout, pixelRatio) : {}
|
|
214
|
+
}
|
|
178
215
|
];
|
|
179
216
|
})
|
|
180
217
|
);
|
|
@@ -279,6 +316,16 @@ async function verifyTileflowIconArtifact(input) {
|
|
|
279
316
|
for (const name of manifest.iconNames) {
|
|
280
317
|
const one = oneIndex[name];
|
|
281
318
|
const two = twoIndex[name];
|
|
319
|
+
const declared = manifest.renderedIcons.find((icon) => icon.name === name).appearance;
|
|
320
|
+
const declaredLayout = manifest.renderedIcons.find((icon) => icon.name === name).layout;
|
|
321
|
+
const oneLayout = readTileflowIconLayout(one);
|
|
322
|
+
const twoLayout = readTileflowIconLayout(two);
|
|
323
|
+
if (serializeCanonicalJson2(oneLayout ?? null) !== serializeCanonicalJson2(declaredLayout ?? null) || serializeCanonicalJson2(twoLayout ?? null) !== serializeCanonicalJson2(oneLayout ? scaleTileflowIconLayout2(oneLayout, 2) : null))
|
|
324
|
+
throw new Error(
|
|
325
|
+
"Sprite layout must match the manifest and scale exactly between densities"
|
|
326
|
+
);
|
|
327
|
+
if (serializeCanonicalJson2(one.tileflow ?? null) !== serializeCanonicalJson2(two.tileflow ?? null) || serializeCanonicalJson2(one.tileflow ?? null) !== serializeCanonicalJson2(declared ?? null))
|
|
328
|
+
throw new Error("Sprite appearance must match at both densities and in the manifest");
|
|
282
329
|
if (two.x !== one.x * 2 || two.y !== one.y * 2 || two.width !== one.width * 2 || two.height !== one.height * 2)
|
|
283
330
|
throw new Error("Sprite densities must have exactly doubled geometry");
|
|
284
331
|
}
|
|
@@ -291,7 +338,19 @@ async function verifyTileflowIconArtifact(input) {
|
|
|
291
338
|
const expected = manifest.renderedIcons[ordinal].pixelSha256;
|
|
292
339
|
if (await hashTileflowRenderedIconPixels2({ ...oneX, pixelRatio: 1 }) !== expected.oneX || await hashTileflowRenderedIconPixels2({ ...twoX, pixelRatio: 2 }) !== expected.twoX)
|
|
293
340
|
throw new Error(`Rendered pixels do not match the manifest for ${id}`);
|
|
294
|
-
|
|
341
|
+
const appearance = oneIndex[id].tileflow;
|
|
342
|
+
if (appearance) {
|
|
343
|
+
assertTileflowSdfPixels2({ ...oneX, pixelRatio: 1 });
|
|
344
|
+
assertTileflowSdfPixels2({ ...twoX, pixelRatio: 2 });
|
|
345
|
+
}
|
|
346
|
+
const layout = readTileflowIconLayout(oneIndex[id]);
|
|
347
|
+
icons.push({
|
|
348
|
+
id,
|
|
349
|
+
oneX,
|
|
350
|
+
twoX,
|
|
351
|
+
...appearance ? { appearance } : {},
|
|
352
|
+
...layout ? { layout } : {}
|
|
353
|
+
});
|
|
295
354
|
}
|
|
296
355
|
return { package: { contentHash, manifest, files }, icons };
|
|
297
356
|
} catch (cause) {
|
|
@@ -702,7 +761,9 @@ async function composeTileflowIconSources(input, options) {
|
|
|
702
761
|
id: icon.id,
|
|
703
762
|
width: icon.oneX.width,
|
|
704
763
|
height: icon.oneX.height,
|
|
705
|
-
pixelSha256: { ...pixels }
|
|
764
|
+
pixelSha256: { ...pixels },
|
|
765
|
+
...icon.appearance ? { appearance: icon.appearance } : {},
|
|
766
|
+
...icon.layout ? { layout: icon.layout } : {}
|
|
706
767
|
},
|
|
707
768
|
null
|
|
708
769
|
);
|
|
@@ -731,7 +792,7 @@ async function composeTileflowIconSources(input, options) {
|
|
|
731
792
|
if (winners.size + selected.length > tileflowIconPackageLimits3.maxIconCount)
|
|
732
793
|
throw new TileflowIconSetError4(
|
|
733
794
|
"ICON_COMPOSITION_INVALID",
|
|
734
|
-
|
|
795
|
+
`Composed icon set exceeds ${tileflowIconPackageLimits3.maxIconCount} effective icons`
|
|
735
796
|
);
|
|
736
797
|
for (const item of await directory.render(selected)) {
|
|
737
798
|
localSourceBytes += item.sourceBytes;
|
|
@@ -749,8 +810,8 @@ async function composeTileflowIconSources(input, options) {
|
|
|
749
810
|
);
|
|
750
811
|
const iconPackage = sorted.length ? reused ?? await packTileflowRenderedIcons(sorted.map((winner) => winner.icon)) : null;
|
|
751
812
|
const composition = references.length && iconPackage ? parseTileflowIconComposition({
|
|
752
|
-
format: "tileflow-icon-composition-v1",
|
|
753
|
-
compositionVersion: 1,
|
|
813
|
+
format: sorted.some((winner) => winner.icon.appearance || winner.icon.layout) ? "tileflow-icon-composition-v2" : "tileflow-icon-composition-v1",
|
|
814
|
+
compositionVersion: sorted.some((winner) => winner.icon.appearance || winner.icon.layout) ? 2 : 1,
|
|
754
815
|
packageHash: iconPackage.contentHash,
|
|
755
816
|
contributors,
|
|
756
817
|
winners: sorted.map(({ ordinal, icon }, index) => ({
|
|
@@ -758,7 +819,9 @@ async function composeTileflowIconSources(input, options) {
|
|
|
758
819
|
contributor: ordinal,
|
|
759
820
|
width: icon.oneX.width,
|
|
760
821
|
height: icon.oneX.height,
|
|
761
|
-
pixelSha256: iconPackage.manifest.renderedIcons[index].pixelSha256
|
|
822
|
+
pixelSha256: iconPackage.manifest.renderedIcons[index].pixelSha256,
|
|
823
|
+
...icon.appearance ? { appearance: icon.appearance } : {},
|
|
824
|
+
...icon.layout ? { layout: icon.layout } : {}
|
|
762
825
|
}))
|
|
763
826
|
}) : null;
|
|
764
827
|
const previous = /* @__PURE__ */ new Map();
|
|
@@ -801,6 +864,90 @@ function isPathInside(root, candidate) {
|
|
|
801
864
|
return pathFromRoot === "" || !isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`);
|
|
802
865
|
}
|
|
803
866
|
|
|
867
|
+
// src/icon-distance.ts
|
|
868
|
+
var infinity = 1e20;
|
|
869
|
+
function createIconDistanceField(rgba, width, height, ratio) {
|
|
870
|
+
if (width !== 24 * ratio || height !== 24 * ratio || rgba.length !== width * height * 4)
|
|
871
|
+
throw new Error("SDF icons require a 24px logical cell at each density.");
|
|
872
|
+
const outer = new Float64Array(width * height);
|
|
873
|
+
const inner = new Float64Array(width * height);
|
|
874
|
+
let occupied = false;
|
|
875
|
+
for (let y = 0; y < height; y++) {
|
|
876
|
+
for (let x = 0; x < width; x++) {
|
|
877
|
+
const index = y * width + x;
|
|
878
|
+
const alpha = rgba[index * 4 + 3] / 255;
|
|
879
|
+
if (alpha && (x < 6 * ratio || y < 6 * ratio || x >= width - 6 * ratio || y >= height - 6 * ratio))
|
|
880
|
+
throw new Error(
|
|
881
|
+
"SDF icons require six transparent pixels of padding inside the 24px cell. Add a transparent border to the source."
|
|
882
|
+
);
|
|
883
|
+
occupied ||= alpha > 0;
|
|
884
|
+
outer[index] = alpha === 1 ? 0 : alpha === 0 ? infinity : Math.max(0, 0.5 - alpha) ** 2;
|
|
885
|
+
inner[index] = alpha === 1 ? infinity : alpha === 0 ? 0 : Math.max(0, alpha - 0.5) ** 2;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (!occupied) throw new Error("An SDF silhouette must contain visible pixels.");
|
|
889
|
+
transform(outer, width, height);
|
|
890
|
+
transform(inner, width, height);
|
|
891
|
+
const result = new Uint8Array(rgba.length);
|
|
892
|
+
for (let i = 0; i < outer.length; i++) {
|
|
893
|
+
const distance = Math.sqrt(outer[i]) - Math.sqrt(inner[i]);
|
|
894
|
+
const alpha = Math.round(255 * Math.max(0, Math.min(1, 0.75 - distance / (8 * ratio))));
|
|
895
|
+
result.set([255, 255, 255, alpha], i * 4);
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
function transform(grid, width, height) {
|
|
900
|
+
const size = Math.max(width, height);
|
|
901
|
+
const samples = new Float64Array(size);
|
|
902
|
+
const result = new Float64Array(size);
|
|
903
|
+
const sites = new Uint32Array(size);
|
|
904
|
+
const bounds = new Float64Array(size + 1);
|
|
905
|
+
const line = (length) => {
|
|
906
|
+
let count = 0;
|
|
907
|
+
sites[0] = 0;
|
|
908
|
+
bounds[0] = -infinity;
|
|
909
|
+
bounds[1] = infinity;
|
|
910
|
+
for (let position = 1; position < length; position++) {
|
|
911
|
+
let intersection;
|
|
912
|
+
do {
|
|
913
|
+
const site = sites[count];
|
|
914
|
+
intersection = (samples[position] + position ** 2 - samples[site] - site ** 2) / (2 * (position - site));
|
|
915
|
+
if (intersection > bounds[count]) break;
|
|
916
|
+
count--;
|
|
917
|
+
} while (count >= 0);
|
|
918
|
+
count++;
|
|
919
|
+
sites[count] = position;
|
|
920
|
+
bounds[count] = intersection;
|
|
921
|
+
bounds[count + 1] = infinity;
|
|
922
|
+
}
|
|
923
|
+
count = 0;
|
|
924
|
+
for (let position = 0; position < length; position++) {
|
|
925
|
+
while (bounds[count + 1] < position) count++;
|
|
926
|
+
result[position] = (position - sites[count]) ** 2 + samples[sites[count]];
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
for (let x = 0; x < width; x++) {
|
|
930
|
+
for (let y = 0; y < height; y++) samples[y] = grid[y * width + x];
|
|
931
|
+
line(height);
|
|
932
|
+
for (let y = 0; y < height; y++) grid[y * width + x] = result[y];
|
|
933
|
+
}
|
|
934
|
+
for (let y = 0; y < height; y++) {
|
|
935
|
+
samples.set(grid.subarray(y * width, (y + 1) * width));
|
|
936
|
+
line(width);
|
|
937
|
+
grid.set(result.subarray(0, width), y * width);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/icon-preview.ts
|
|
942
|
+
import { renderTileflowIconAtlasPixels } from "@tileflow/core";
|
|
943
|
+
async function renderTileflowIconAtlasPreview(source, index) {
|
|
944
|
+
if (!Object.values(index).some((entry) => entry.sdf)) return source;
|
|
945
|
+
const sharp = await loadSharp();
|
|
946
|
+
const { data, info } = await sharp(source, { limitInputPixels: 2048 * 2048 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
947
|
+
const rendered = renderTileflowIconAtlasPixels(data, info.width, info.height, index);
|
|
948
|
+
return sharp(rendered, { raw: { width: info.width, height: info.height, channels: 4 } }).png().toBuffer();
|
|
949
|
+
}
|
|
950
|
+
|
|
804
951
|
// src/icons.ts
|
|
805
952
|
var TileflowIconCompilationError = class extends Error {
|
|
806
953
|
code = "ICON_INVALID";
|
|
@@ -838,9 +985,15 @@ async function compileTileflowIconPackages(project, options) {
|
|
|
838
985
|
if (!composed?.package) {
|
|
839
986
|
throw new Error(`Missing compiled icon package for map ${request.mapName}`);
|
|
840
987
|
}
|
|
988
|
+
const appearances = Object.fromEntries(
|
|
989
|
+
composed.package.manifest.renderedIcons.flatMap(
|
|
990
|
+
(icon) => icon.appearance ? [[icon.name, icon.appearance]] : []
|
|
991
|
+
)
|
|
992
|
+
);
|
|
841
993
|
return [
|
|
842
994
|
{
|
|
843
995
|
iconIds: composed.package.manifest.iconNames,
|
|
996
|
+
...Object.keys(appearances).length ? { appearances } : {},
|
|
844
997
|
label: request.mapName,
|
|
845
998
|
mapName: request.mapName,
|
|
846
999
|
packageHash: composed.package.contentHash
|
|
@@ -912,6 +1065,8 @@ async function inspectTileflowIconCatalogs(project, options) {
|
|
|
912
1065
|
}
|
|
913
1066
|
return {
|
|
914
1067
|
id: winner.id,
|
|
1068
|
+
...manifestIcon.appearance ? { appearance: manifestIcon.appearance } : {},
|
|
1069
|
+
...manifestIcon.layout ? { layout: manifestIcon.layout } : {},
|
|
915
1070
|
rendered: {
|
|
916
1071
|
oneX: {
|
|
917
1072
|
atlas: atlasRectangle(oneXAtlas),
|
|
@@ -1036,7 +1191,13 @@ async function prepareTileflowCatalogIcons(project, options) {
|
|
|
1036
1191
|
const iconPackage = packagesByHash.get(binding.packageHash);
|
|
1037
1192
|
if (!iconPackage) throw new Error(`Missing compiled icon package for map ${binding.mapName}`);
|
|
1038
1193
|
const spriteUrl = joinUrl(options.assetBaseUrl, `icons/${binding.mapName}/sprite`);
|
|
1039
|
-
mapAssets[binding.mapName] = {
|
|
1194
|
+
mapAssets[binding.mapName] = {
|
|
1195
|
+
icons: {
|
|
1196
|
+
ids: binding.iconIds,
|
|
1197
|
+
sprite: spriteUrl,
|
|
1198
|
+
...binding.appearances ? { appearances: binding.appearances } : {}
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1040
1201
|
mapIconSources[binding.mapName] = compiled.sourceIdentities[binding.mapName] ?? [];
|
|
1041
1202
|
const composition = compiled.compositions[binding.mapName];
|
|
1042
1203
|
if (composition) mapIconCompositions[binding.mapName] = composition;
|
|
@@ -1092,7 +1253,7 @@ function getMapIconRequests(project, mapNames) {
|
|
|
1092
1253
|
const mapConfig = project.maps[mapName];
|
|
1093
1254
|
if (!mapConfig) throw new Error(`Unknown Tileflow map: ${mapName}`);
|
|
1094
1255
|
const sources = parseResolvedTileflowMap(mapConfig).icons ?? [];
|
|
1095
|
-
return { mapName, sequenceKey:
|
|
1256
|
+
return { mapName, sequenceKey: serializeCanonicalJson3(sources), sources };
|
|
1096
1257
|
});
|
|
1097
1258
|
}
|
|
1098
1259
|
function uniqueSequences(requests) {
|
|
@@ -1161,6 +1322,10 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1161
1322
|
entries.sort((left, right) => compareCodeUnits4(left.name, right.name));
|
|
1162
1323
|
const candidates = [];
|
|
1163
1324
|
const names = /* @__PURE__ */ new Set();
|
|
1325
|
+
let metadata = {
|
|
1326
|
+
schemaVersion: 1,
|
|
1327
|
+
icons: {}
|
|
1328
|
+
};
|
|
1164
1329
|
for (const entry of entries) {
|
|
1165
1330
|
const entryPath = resolvePath(directory.realPath, entry.name);
|
|
1166
1331
|
const path = `${directory.configPath}/${entry.name}`;
|
|
@@ -1188,6 +1353,24 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1188
1353
|
continue;
|
|
1189
1354
|
}
|
|
1190
1355
|
const extension = extname(entry.name).toLowerCase();
|
|
1356
|
+
if (entry.name === tileflowIconMetadataFileName) {
|
|
1357
|
+
try {
|
|
1358
|
+
if (entryStat.size > tileflowIconPackageLimits4.maxSourceFileBytes)
|
|
1359
|
+
throw new Error("Icon metadata exceeds its byte limit");
|
|
1360
|
+
metadata = tileflowIconAuthorMetadataSchema.parse(
|
|
1361
|
+
parseTileflowIconJson2(
|
|
1362
|
+
await readFile(realEntryPath, "utf8"),
|
|
1363
|
+
tileflowIconPackageLimits4.maxSourceFileBytes
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
issues.push({
|
|
1368
|
+
path,
|
|
1369
|
+
message: `Invalid icon metadata: ${error instanceof Error ? error.message : String(error)}`
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1191
1374
|
if (!iconFileExtensions.has(extension)) {
|
|
1192
1375
|
continue;
|
|
1193
1376
|
}
|
|
@@ -1225,6 +1408,13 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1225
1408
|
});
|
|
1226
1409
|
}
|
|
1227
1410
|
candidates.sort((left, right) => compareCodeUnits4(left.name, right.name));
|
|
1411
|
+
for (const id of Object.keys(metadata.icons)) {
|
|
1412
|
+
if (!names.has(id))
|
|
1413
|
+
issues.push({
|
|
1414
|
+
path: `${directory.configPath}/${tileflowIconMetadataFileName}`,
|
|
1415
|
+
message: `Icon metadata references unknown ID "${id}"`
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1228
1418
|
if (candidates.length === 0) {
|
|
1229
1419
|
issues.push({
|
|
1230
1420
|
message: "No supported .svg, .png, .jpg, .jpeg, or .webp icon files were found",
|
|
@@ -1245,7 +1435,10 @@ async function inspectIconSource(directory, target, issues) {
|
|
|
1245
1435
|
if (target === "hosted" && extname(candidate.fileName).toLowerCase() === ".svg") {
|
|
1246
1436
|
validateHostedSvg(source, displayPath);
|
|
1247
1437
|
}
|
|
1438
|
+
const declared = metadata.icons[candidate.name];
|
|
1248
1439
|
icons.push({
|
|
1440
|
+
...declared?.representation === "sdf" ? { appearance: { representation: declared.representation, defaults: declared.defaults } } : {},
|
|
1441
|
+
...declared?.layout ? { layout: declared.layout } : {},
|
|
1249
1442
|
displayPath,
|
|
1250
1443
|
fileName: candidate.fileName,
|
|
1251
1444
|
format: candidate.format,
|
|
@@ -1300,8 +1493,27 @@ async function renderIcon(icon, dimensions, pixelRatio) {
|
|
|
1300
1493
|
if (info.channels !== 4 || info.width !== targetWidth || info.height !== targetHeight) {
|
|
1301
1494
|
throw new Error(`Rendered icon ${icon.fileName} did not produce canonical RGBA pixels`);
|
|
1302
1495
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1496
|
+
let rgba;
|
|
1497
|
+
try {
|
|
1498
|
+
if (icon.appearance && icon.kind === "pattern")
|
|
1499
|
+
throw new Error("SDF metadata is only supported for icons, not patterns.");
|
|
1500
|
+
if (icon.layout) {
|
|
1501
|
+
if (icon.kind === "pattern")
|
|
1502
|
+
throw new Error("Text-fitting metadata is only supported for icons, not patterns.");
|
|
1503
|
+
assertTileflowIconLayoutBounds2(
|
|
1504
|
+
icon.layout,
|
|
1505
|
+
info.width / pixelRatio,
|
|
1506
|
+
info.height / pixelRatio
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
rgba = icon.appearance ? createIconDistanceField(new Uint8Array(data), info.width, info.height, pixelRatio) : new Uint8Array(data);
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
throw new Error(
|
|
1512
|
+
`${icon.displayPath}: ${error instanceof Error ? error.message : "Invalid icon capabilities"}`,
|
|
1513
|
+
{ cause: error }
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const png = await sharp(rgba, {
|
|
1305
1517
|
raw: { channels: 4, height: info.height, width: info.width }
|
|
1306
1518
|
}).png({ adaptiveFiltering: false, compressionLevel: 9, palette: false }).toBuffer();
|
|
1307
1519
|
return { height: info.height, png, rgba, width: info.width };
|
|
@@ -1500,7 +1712,9 @@ async function renderIconInputs(inputs) {
|
|
|
1500
1712
|
width: twoX.width
|
|
1501
1713
|
})
|
|
1502
1714
|
},
|
|
1503
|
-
sourceSha256: await sha256Hex3(
|
|
1715
|
+
sourceSha256: icon.appearance || icon.layout ? await sha256Hex3(
|
|
1716
|
+
`tileflow-icon-source-v2\0${await sha256Hex3(icon.source)}\0${serializeCanonicalJson3({ ...icon.appearance ? { appearance: icon.appearance } : {}, ...icon.layout ? { layout: icon.layout } : {} })}`
|
|
1717
|
+
) : await sha256Hex3(icon.source),
|
|
1504
1718
|
twoX
|
|
1505
1719
|
};
|
|
1506
1720
|
});
|
|
@@ -1522,7 +1736,10 @@ async function readTileflowIconDirectory(source, options) {
|
|
|
1522
1736
|
if (!inspected || issues.length) throw new TileflowIconCompilationError(issues);
|
|
1523
1737
|
if (inspected.icons.length > tileflowIconPackageLimits4.maxIconCount)
|
|
1524
1738
|
throw new TileflowIconCompilationError([
|
|
1525
|
-
{
|
|
1739
|
+
{
|
|
1740
|
+
path: configPath,
|
|
1741
|
+
message: `One icon contributor supports at most ${tileflowIconPackageLimits4.maxIconCount} exports`
|
|
1742
|
+
}
|
|
1526
1743
|
]);
|
|
1527
1744
|
const entries = inspected.icons.map(
|
|
1528
1745
|
(icon) => ({
|
|
@@ -1553,6 +1770,8 @@ async function readTileflowIconDirectory(source, options) {
|
|
|
1553
1770
|
return (await renderIconInputs(inputs)).map((rendered) => ({
|
|
1554
1771
|
icon: {
|
|
1555
1772
|
id: rendered.input.name,
|
|
1773
|
+
...rendered.input.appearance ? { appearance: rendered.input.appearance } : {},
|
|
1774
|
+
...rendered.input.layout ? { layout: rendered.input.layout } : {},
|
|
1556
1775
|
oneX: {
|
|
1557
1776
|
height: rendered.oneX.height,
|
|
1558
1777
|
width: rendered.oneX.width,
|
|
@@ -1595,6 +1814,7 @@ export {
|
|
|
1595
1814
|
readTileflowIconsLockfile,
|
|
1596
1815
|
writeTileflowIconsLockfile,
|
|
1597
1816
|
composeTileflowIconSources,
|
|
1817
|
+
renderTileflowIconAtlasPreview,
|
|
1598
1818
|
TileflowIconCompilationError,
|
|
1599
1819
|
iconFileExtensions,
|
|
1600
1820
|
compileTileflowIconPackages,
|