@tileflow/dev 0.1.0-alpha.23 → 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.
- package/README.md +130 -515
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +2 -2
- package/dist/{chunk-CENQR5Q2.js → chunk-NUHSYLLS.js} +312 -170
- package/dist/{chunk-G5FUSWNN.js → chunk-XS5Q4IS2.js} +1 -1
- package/dist/{chunk-XTRHREXO.js → chunk-XXVI42UZ.js} +1 -1
- package/dist/fonts.d.ts +1 -1
- package/dist/icons-CkLVtaP-.d.ts +164 -0
- package/dist/icons.d.ts +3 -136
- package/dist/icons.js +5 -3
- package/dist/index.d.ts +52 -3
- package/dist/index.js +587 -4
- package/dist/server.d.ts +1 -1
- package/dist/server.js +3 -3
- package/docs/artifact-lifecycle.md +153 -0
- package/docs/bounded-pmtiles-resource-inspection.md +21 -0
- package/docs/bounded-vector-feature-inspection.md +101 -0
- package/docs/local-comparison-workbench-and-compiler-inspection.md +250 -0
- package/docs/structured-config-inspection-and-diagnostics.md +39 -0
- package/package.json +4 -3
package/dist/artifacts.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TileflowStyleOptions, MapLibreStyle } from '@tileflow/core';
|
|
2
2
|
import { TileflowBuildCatalog, TileflowBuildStyles, TileflowMapBuildManifestV1, TileflowStyleInspection, TileflowMapBuildProvenanceV1, TileflowPreparedMapAssets } from '@tileflow/core/build';
|
|
3
3
|
import { TileflowRuntimeManifest } from '@tileflow/core/manifest';
|
|
4
|
-
import { TileflowBuildAsset, PreparedTileflowBuildCatalog, TileflowSourceCatalog, PreparedTileflowCatalog } from './icons
|
|
4
|
+
import { e as TileflowBuildAsset, P as PreparedTileflowBuildCatalog, r as TileflowSourceCatalog, d as PreparedTileflowCatalog } from './icons-CkLVtaP-.js';
|
|
5
5
|
export { defaultTileflowConfigPath } from './config.js';
|
|
6
6
|
|
|
7
7
|
type TileflowLocalTilesetFile = Readonly<{
|
package/dist/artifacts.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
tileflowArtifactSessionSchemaVersion,
|
|
23
23
|
writeTileflowArtifactPlan,
|
|
24
24
|
writeTileflowBuildArtifacts
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-XXVI42UZ.js";
|
|
26
26
|
import "./chunk-ONYQNLNA.js";
|
|
27
27
|
import {
|
|
28
28
|
getTileflowAssetBasePath,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
normalizeTileflowBasePath,
|
|
32
32
|
resolveTileflowArtifactPublicUrls
|
|
33
33
|
} from "./chunk-RSBV4L2Y.js";
|
|
34
|
-
import "./chunk-
|
|
34
|
+
import "./chunk-NUHSYLLS.js";
|
|
35
35
|
import "./chunk-BL3FK4GO.js";
|
|
36
36
|
import "./chunk-KDFTE4Q5.js";
|
|
37
37
|
import {
|
|
@@ -4,20 +4,184 @@ import {
|
|
|
4
4
|
} from "./chunk-BL3FK4GO.js";
|
|
5
5
|
|
|
6
6
|
// src/icons.ts
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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) =>
|
|
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(
|
|
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) =>
|
|
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(
|
|
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(
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 >
|
|
480
|
+
if (icons.length > tileflowIconPackageLimits2.maxIconCount) {
|
|
308
481
|
throw new Error(
|
|
309
|
-
`Composed icon set contains more than ${
|
|
482
|
+
`Composed icon set contains more than ${tileflowIconPackageLimits2.maxIconCount} icons`
|
|
310
483
|
);
|
|
311
484
|
}
|
|
312
|
-
if (aggregateBytes >
|
|
485
|
+
if (aggregateBytes > tileflowIconPackageLimits2.maxSourceBytes) {
|
|
313
486
|
throw new Error(
|
|
314
|
-
`Composed icon set exceeds ${
|
|
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) =>
|
|
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 >
|
|
560
|
+
if (entryStat.size > tileflowIconPackageLimits2.maxSourceFileBytes) {
|
|
388
561
|
issues.push({
|
|
389
|
-
message: `Icon source exceeds ${
|
|
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) =>
|
|
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 >
|
|
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
|
|
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(`${
|
|
638
|
+
const oneXJson = new TextEncoder().encode(`${serializeCanonicalJson2(layoutOneX.index)}
|
|
500
639
|
`);
|
|
501
|
-
const twoXJson = new TextEncoder().encode(`${
|
|
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) =>
|
|
511
|
-
const manifest =
|
|
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
|
|
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:
|
|
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 >
|
|
695
|
+
if (width * height > tileflowIconPackageLimits2.maxDecodedPixelsPerIcon) {
|
|
604
696
|
throw new Error(
|
|
605
|
-
`${icon.fileName} exceeds ${
|
|
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:
|
|
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
|
};
|