@carto/api-client 0.5.31 → 0.5.32-alpha.02a4470.133

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 (57) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/build/api-client.cjs +527 -3
  3. package/build/api-client.cjs.map +1 -1
  4. package/build/api-client.d.cts +42 -5
  5. package/build/api-client.d.ts +42 -5
  6. package/build/api-client.js +520 -3
  7. package/build/api-client.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/fetch-map/layer-map.ts +48 -0
  10. package/src/fetch-map/parse-map.ts +133 -10
  11. package/src/fetch-map/pattern-atlas.ts +461 -0
  12. package/src/fetch-map/patterns/checker-large.png +0 -0
  13. package/src/fetch-map/patterns/checker-large.svg +1 -0
  14. package/src/fetch-map/patterns/checker-medium.png +0 -0
  15. package/src/fetch-map/patterns/checker-medium.svg +1 -0
  16. package/src/fetch-map/patterns/checker-small.png +0 -0
  17. package/src/fetch-map/patterns/checker-small.svg +1 -0
  18. package/src/fetch-map/patterns/cross-hatch-large.png +0 -0
  19. package/src/fetch-map/patterns/cross-hatch-large.svg +1 -0
  20. package/src/fetch-map/patterns/cross-hatch-medium.png +0 -0
  21. package/src/fetch-map/patterns/cross-hatch-medium.svg +1 -0
  22. package/src/fetch-map/patterns/cross-hatch-small.png +0 -0
  23. package/src/fetch-map/patterns/cross-hatch-small.svg +1 -0
  24. package/src/fetch-map/patterns/diag-left-large.png +0 -0
  25. package/src/fetch-map/patterns/diag-left-large.svg +1 -0
  26. package/src/fetch-map/patterns/diag-left-medium.png +0 -0
  27. package/src/fetch-map/patterns/diag-left-medium.svg +1 -0
  28. package/src/fetch-map/patterns/diag-left-small.png +0 -0
  29. package/src/fetch-map/patterns/diag-left-small.svg +1 -0
  30. package/src/fetch-map/patterns/diag-right-large.png +0 -0
  31. package/src/fetch-map/patterns/diag-right-large.svg +1 -0
  32. package/src/fetch-map/patterns/diag-right-medium.png +0 -0
  33. package/src/fetch-map/patterns/diag-right-medium.svg +1 -0
  34. package/src/fetch-map/patterns/diag-right-small.png +0 -0
  35. package/src/fetch-map/patterns/diag-right-small.svg +1 -0
  36. package/src/fetch-map/patterns/dots-large.png +0 -0
  37. package/src/fetch-map/patterns/dots-large.svg +1 -0
  38. package/src/fetch-map/patterns/dots-medium.png +0 -0
  39. package/src/fetch-map/patterns/dots-medium.svg +1 -0
  40. package/src/fetch-map/patterns/dots-small.png +0 -0
  41. package/src/fetch-map/patterns/dots-small.svg +1 -0
  42. package/src/fetch-map/patterns/hlines-large.png +0 -0
  43. package/src/fetch-map/patterns/hlines-large.svg +1 -0
  44. package/src/fetch-map/patterns/hlines-medium.png +0 -0
  45. package/src/fetch-map/patterns/hlines-medium.svg +1 -0
  46. package/src/fetch-map/patterns/hlines-small.png +0 -0
  47. package/src/fetch-map/patterns/hlines-small.svg +1 -0
  48. package/src/fetch-map/patterns/solid.png +0 -0
  49. package/src/fetch-map/patterns/solid.svg +1 -0
  50. package/src/fetch-map/patterns/vlines-large.png +0 -0
  51. package/src/fetch-map/patterns/vlines-large.svg +1 -0
  52. package/src/fetch-map/patterns/vlines-medium.png +0 -0
  53. package/src/fetch-map/patterns/vlines-medium.svg +1 -0
  54. package/src/fetch-map/patterns/vlines-small.png +0 -0
  55. package/src/fetch-map/patterns/vlines-small.svg +1 -0
  56. package/src/fetch-map/types.ts +44 -0
  57. package/src/global.d.ts +10 -0
@@ -0,0 +1,461 @@
1
+ // Fill-pattern atlas — internal to carto-api-client.
2
+ //
3
+ // The patterns live as individual, developer-editable tiles (src/fetch-map/patterns/):
4
+ // Design's original 64px raster masks (*.png) plus vector twins (*.svg), both inlined
5
+ // by tsup's `dataurl` loader, with procedural canvas painters as a third, asset-free
6
+ // source — selectable via a debug knob while the final asset strategy is evaluated.
7
+ // Tiles are composited into a sprite sheet on a canvas the first time a pattern is
8
+ // needed. Each atlas cell is filled with side-by-side copies of its tile (raster art
9
+ // never resampled), and is surrounded by a gutter holding the tile's own wrapped
10
+ // content — so linear sampling stays seamless at repeat boundaries and never bleeds a
11
+ // neighboring cell.
12
+ //
13
+ // parse-map sets `result.fillPatternAtlas = getPatternAtlas()`. deck.gl's
14
+ // `fillPatternAtlas` prop is async — but the Promise must resolve to a decoded image,
15
+ // never a data-URL string: deck URL-loads string prop values, while a promise-resolved
16
+ // string goes straight to texture creation, where luma.gl rejects it.
17
+ //
18
+ // Debug knobs (set in the browser console, then reload the map):
19
+ // globalThis.__CARTO_PATTERN_CELL_SIZE__ = 64|128|256 // atlas cell px, default 128
20
+ // globalThis.__CARTO_PATTERN_TEXTURE_PARAMS__ = {magFilter: 'nearest', ...}
21
+ // // merged into the atlas texture's sampler via deck's `textureParameters`;
22
+ // // unset -> deck defaults (linear).
23
+ // globalThis.__CARTO_PATTERN_ASSET_SOURCE__ = 'png' | 'svg' | 'procedural'
24
+ // // 'png' (default): Design's original 64px raster masks, tiled at native
25
+ // // resolution. 'svg': in-repo vector tiles rasterized at atlas-build time
26
+ // // (2x texel density per repeat). 'procedural': canvas painters, no assets.
27
+
28
+ import hlinesLarge from './patterns/hlines-large.png';
29
+ import hlinesMedium from './patterns/hlines-medium.png';
30
+ import hlinesSmall from './patterns/hlines-small.png';
31
+ import vlinesLarge from './patterns/vlines-large.png';
32
+ import vlinesMedium from './patterns/vlines-medium.png';
33
+ import vlinesSmall from './patterns/vlines-small.png';
34
+ import diagLeftLarge from './patterns/diag-left-large.png';
35
+ import diagLeftMedium from './patterns/diag-left-medium.png';
36
+ import diagLeftSmall from './patterns/diag-left-small.png';
37
+ import diagRightLarge from './patterns/diag-right-large.png';
38
+ import diagRightMedium from './patterns/diag-right-medium.png';
39
+ import diagRightSmall from './patterns/diag-right-small.png';
40
+ import crossHatchLarge from './patterns/cross-hatch-large.png';
41
+ import crossHatchMedium from './patterns/cross-hatch-medium.png';
42
+ import crossHatchSmall from './patterns/cross-hatch-small.png';
43
+ import dotsLarge from './patterns/dots-large.png';
44
+ import dotsMedium from './patterns/dots-medium.png';
45
+ import dotsSmall from './patterns/dots-small.png';
46
+ import checkerLarge from './patterns/checker-large.png';
47
+ import checkerMedium from './patterns/checker-medium.png';
48
+ import checkerSmall from './patterns/checker-small.png';
49
+ import solid from './patterns/solid.png';
50
+
51
+ import hlinesLargeSvg from './patterns/hlines-large.svg';
52
+ import hlinesMediumSvg from './patterns/hlines-medium.svg';
53
+ import hlinesSmallSvg from './patterns/hlines-small.svg';
54
+ import vlinesLargeSvg from './patterns/vlines-large.svg';
55
+ import vlinesMediumSvg from './patterns/vlines-medium.svg';
56
+ import vlinesSmallSvg from './patterns/vlines-small.svg';
57
+ import diagLeftLargeSvg from './patterns/diag-left-large.svg';
58
+ import diagLeftMediumSvg from './patterns/diag-left-medium.svg';
59
+ import diagLeftSmallSvg from './patterns/diag-left-small.svg';
60
+ import diagRightLargeSvg from './patterns/diag-right-large.svg';
61
+ import diagRightMediumSvg from './patterns/diag-right-medium.svg';
62
+ import diagRightSmallSvg from './patterns/diag-right-small.svg';
63
+ import crossHatchLargeSvg from './patterns/cross-hatch-large.svg';
64
+ import crossHatchMediumSvg from './patterns/cross-hatch-medium.svg';
65
+ import crossHatchSmallSvg from './patterns/cross-hatch-small.svg';
66
+ import dotsLargeSvg from './patterns/dots-large.svg';
67
+ import dotsMediumSvg from './patterns/dots-medium.svg';
68
+ import dotsSmallSvg from './patterns/dots-small.svg';
69
+ import checkerLargeSvg from './patterns/checker-large.svg';
70
+ import checkerMediumSvg from './patterns/checker-medium.svg';
71
+ import checkerSmallSvg from './patterns/checker-small.svg';
72
+ import solidSvg from './patterns/solid.svg';
73
+
74
+ const DEFAULT_CELL_SIZE = 128;
75
+ // px of the original raster masks; also the design grid the on-screen size is
76
+ // defined against — one tile spans 64 design units whatever the asset source.
77
+ const SOURCE_TILE_SIZE = 64;
78
+ // Vector sources (svg/procedural) rasterize each repeat at up to this many px,
79
+ // doubling texel density per repeat vs the 64px masks.
80
+ const VECTOR_RENDER_SIZE = 128;
81
+
82
+ export type PatternAssetSource = 'png' | 'svg' | 'procedural';
83
+
84
+ type PatternDebugGlobals = {
85
+ __CARTO_PATTERN_CELL_SIZE__?: number;
86
+ __CARTO_PATTERN_TEXTURE_PARAMS__?: Record<string, unknown>;
87
+ __CARTO_PATTERN_ASSET_SOURCE__?: PatternAssetSource;
88
+ };
89
+
90
+ export type PatternAtlasFrame = {
91
+ x: number;
92
+ y: number;
93
+ width: number;
94
+ height: number;
95
+ mask: boolean;
96
+ };
97
+
98
+ // Atlas grid: rows follow PATTERN_ROWS, columns are density sparse->dense
99
+ // [large, medium, small]; last row holds `none` (transparent) and `solid`.
100
+ const PATTERN_ROWS = [
101
+ 'hlines',
102
+ 'vlines',
103
+ 'diag-left',
104
+ 'diag-right',
105
+ 'cross-hatch',
106
+ 'dots',
107
+ 'checker',
108
+ ] as const;
109
+ const DENSITY_COLUMNS = ['large', 'medium', 'small'] as const;
110
+
111
+ // atlas key -> inlined data URL of its editable source tile
112
+ const CELL_URLS: Record<string, string> = {
113
+ 'hlines-large': hlinesLarge,
114
+ 'hlines-medium': hlinesMedium,
115
+ 'hlines-small': hlinesSmall,
116
+ 'vlines-large': vlinesLarge,
117
+ 'vlines-medium': vlinesMedium,
118
+ 'vlines-small': vlinesSmall,
119
+ 'diag-left-large': diagLeftLarge,
120
+ 'diag-left-medium': diagLeftMedium,
121
+ 'diag-left-small': diagLeftSmall,
122
+ 'diag-right-large': diagRightLarge,
123
+ 'diag-right-medium': diagRightMedium,
124
+ 'diag-right-small': diagRightSmall,
125
+ 'cross-hatch-large': crossHatchLarge,
126
+ 'cross-hatch-medium': crossHatchMedium,
127
+ 'cross-hatch-small': crossHatchSmall,
128
+ 'dots-large': dotsLarge,
129
+ 'dots-medium': dotsMedium,
130
+ 'dots-small': dotsSmall,
131
+ 'checker-large': checkerLarge,
132
+ 'checker-medium': checkerMedium,
133
+ 'checker-small': checkerSmall,
134
+ solid,
135
+ };
136
+
137
+ // atlas key -> inlined data URL of its vector source tile
138
+ const CELL_SVG_URLS: Record<string, string> = {
139
+ 'hlines-large': hlinesLargeSvg,
140
+ 'hlines-medium': hlinesMediumSvg,
141
+ 'hlines-small': hlinesSmallSvg,
142
+ 'vlines-large': vlinesLargeSvg,
143
+ 'vlines-medium': vlinesMediumSvg,
144
+ 'vlines-small': vlinesSmallSvg,
145
+ 'diag-left-large': diagLeftLargeSvg,
146
+ 'diag-left-medium': diagLeftMediumSvg,
147
+ 'diag-left-small': diagLeftSmallSvg,
148
+ 'diag-right-large': diagRightLargeSvg,
149
+ 'diag-right-medium': diagRightMediumSvg,
150
+ 'diag-right-small': diagRightSmallSvg,
151
+ 'cross-hatch-large': crossHatchLargeSvg,
152
+ 'cross-hatch-medium': crossHatchMediumSvg,
153
+ 'cross-hatch-small': crossHatchSmallSvg,
154
+ 'dots-large': dotsLargeSvg,
155
+ 'dots-medium': dotsMediumSvg,
156
+ 'dots-small': dotsSmallSvg,
157
+ 'checker-large': checkerLargeSvg,
158
+ 'checker-medium': checkerMediumSvg,
159
+ 'checker-small': checkerSmallSvg,
160
+ solid: solidSvg,
161
+ };
162
+
163
+ function debugGlobals(): PatternDebugGlobals {
164
+ return globalThis as PatternDebugGlobals;
165
+ }
166
+
167
+ export function getPatternCellSize(): number {
168
+ const cell = debugGlobals().__CARTO_PATTERN_CELL_SIZE__;
169
+ return typeof cell === 'number' && cell > 0 ? cell : DEFAULT_CELL_SIZE;
170
+ }
171
+
172
+ export function getPatternAssetSource(): PatternAssetSource {
173
+ const source = debugGlobals().__CARTO_PATTERN_ASSET_SOURCE__;
174
+ return source === 'svg' || source === 'procedural' ? source : 'png';
175
+ }
176
+
177
+ /** Sampler overrides for the atlas texture; undefined keeps deck's defaults (linear). */
178
+ export function getPatternTextureParameters():
179
+ | Record<string, unknown>
180
+ | undefined {
181
+ const params = debugGlobals().__CARTO_PATTERN_TEXTURE_PARAMS__;
182
+ return params && typeof params === 'object' ? params : undefined;
183
+ }
184
+
185
+ // Copies of the source tile laid side by side inside one atlas cell, per axis. Raster
186
+ // tiles draw at native resolution (no resampling); vector sources rasterize each
187
+ // repeat at up to VECTOR_RENDER_SIZE px. The UV wrap — where sampling seams live —
188
+ // happens 1/reps as often.
189
+ function getPatternRepeats(
190
+ cell: number,
191
+ source: PatternAssetSource = getPatternAssetSource()
192
+ ): number {
193
+ const repeatSize = source === 'png' ? SOURCE_TILE_SIZE : VECTOR_RENDER_SIZE;
194
+ return Math.max(1, Math.floor(cell / repeatSize));
195
+ }
196
+
197
+ // deck's fill-pattern shader sizes the on-screen repeat proportionally to the mapping
198
+ // frame's texel size (scale = FILL_UV_SCALE * getFillPatternScale * frame.wh). This
199
+ // factor compensates for the cell size and the repeats inside it, normalized to the
200
+ // 64px design grid, so the pattern keeps a constant on-screen size across cell sizes
201
+ // and asset sources.
202
+ export function getPatternScaleAdjustment(
203
+ cell: number = getPatternCellSize()
204
+ ): number {
205
+ return (SOURCE_TILE_SIZE * getPatternRepeats(cell)) / cell;
206
+ }
207
+
208
+ // Gutter width around each cell; sized so a few mip levels stay bleed-free.
209
+ function getPatternCellPadding(cell: number): number {
210
+ return Math.max(2, Math.round(cell / 16));
211
+ }
212
+
213
+ export function getPatternAtlasMapping(
214
+ cell: number = getPatternCellSize()
215
+ ): Record<string, PatternAtlasFrame> {
216
+ const pad = getPatternCellPadding(cell);
217
+ const pitch = cell + 2 * pad;
218
+ const mapping: Record<string, PatternAtlasFrame> = {};
219
+ const frame = (col: number, row: number): PatternAtlasFrame => ({
220
+ x: pad + col * pitch,
221
+ y: pad + row * pitch,
222
+ width: cell,
223
+ height: cell,
224
+ mask: true,
225
+ });
226
+ PATTERN_ROWS.forEach((pattern, row) => {
227
+ DENSITY_COLUMNS.forEach((density, col) => {
228
+ mapping[`${pattern}-${density}`] = frame(col, row);
229
+ });
230
+ });
231
+ // `none` must be a real transparent cell — a null pattern key resolves to atlas
232
+ // bounds [0,0,0,0] and samples the wrong (0,0) cell.
233
+ mapping.none = frame(0, PATTERN_ROWS.length);
234
+ mapping.solid = frame(1, PATTERN_ROWS.length);
235
+ return mapping;
236
+ }
237
+
238
+ export type AssembledAtlas = ImageBitmap | HTMLCanvasElement;
239
+
240
+ type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
241
+
242
+ function createCanvas(w: number, h: number): AnyCanvas {
243
+ if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h);
244
+ if (typeof document !== 'undefined') {
245
+ const c = document.createElement('canvas');
246
+ c.width = w;
247
+ c.height = h;
248
+ return c;
249
+ }
250
+ throw new Error(
251
+ 'carto-api-client: no Canvas available to assemble the pattern atlas'
252
+ );
253
+ }
254
+
255
+ async function loadImage(dataUrl: string): Promise<CanvasImageSource> {
256
+ if (
257
+ typeof createImageBitmap !== 'undefined' &&
258
+ typeof fetch !== 'undefined'
259
+ ) {
260
+ const blob = await (await fetch(dataUrl)).blob();
261
+ return createImageBitmap(blob);
262
+ }
263
+ return new Promise((resolve, reject) => {
264
+ const img = new Image();
265
+ img.onload = () => resolve(img);
266
+ img.onerror = reject;
267
+ img.src = dataUrl;
268
+ });
269
+ }
270
+
271
+ // SVG images always go through an Image element: createImageBitmap on SVG blobs is
272
+ // inconsistent across engines, and drawImage from an SVG image rasterizes from the
273
+ // vector at the destination size in modern browsers.
274
+ function loadSvgImage(dataUrl: string): Promise<CanvasImageSource> {
275
+ return new Promise((resolve, reject) => {
276
+ const img = new Image();
277
+ img.onload = () => resolve(img);
278
+ img.onerror = reject;
279
+ img.src = dataUrl;
280
+ });
281
+ }
282
+
283
+ // Procedural painters — the same measured design-grid geometry as the assets, drawn
284
+ // in 64-unit design coordinates onto a pre-scaled context (canvas AA does the rest).
285
+ const DENSITY_STEP: Record<string, number> = {small: 1, medium: 2, large: 4};
286
+
287
+ function paintDesignTile(
288
+ ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,
289
+ key: string
290
+ ): void {
291
+ const [, name, density] =
292
+ key.match(/^(.*?)(?:-(small|medium|large))?$/) ?? [];
293
+ const step = DENSITY_STEP[density ?? ''] ?? 1;
294
+ const linePeriod = 4 * step;
295
+ const diagPeriod = 16 * step;
296
+ const square = 2 * step;
297
+ ctx.fillStyle = '#000';
298
+ ctx.strokeStyle = '#000';
299
+ const diag = (dir: 1 | -1) => {
300
+ // dir 1: "\" lines (x - y = k*p); dir -1: "/" lines (x + y = k*p)
301
+ ctx.lineWidth = Math.SQRT2; // 2px horizontal width at 45 degrees
302
+ ctx.beginPath();
303
+ for (
304
+ let k = -Math.ceil(64 / diagPeriod);
305
+ k <= 2 * Math.ceil(64 / diagPeriod);
306
+ k++
307
+ ) {
308
+ ctx.moveTo(k * diagPeriod - 8 * dir, -8);
309
+ ctx.lineTo(k * diagPeriod + 72 * dir, 72);
310
+ }
311
+ ctx.stroke();
312
+ };
313
+ switch (name) {
314
+ case 'solid':
315
+ ctx.fillRect(0, 0, 64, 64);
316
+ break;
317
+ case 'hlines':
318
+ for (let y = 0; y < 64; y += linePeriod) ctx.fillRect(0, y, 64, 2);
319
+ break;
320
+ case 'vlines':
321
+ for (let x = 0; x < 64; x += linePeriod) ctx.fillRect(x, 0, 2, 64);
322
+ break;
323
+ case 'dots':
324
+ for (let y = 0; y < 64; y += linePeriod)
325
+ for (let x = 0; x < 64; x += linePeriod) ctx.fillRect(x, y, 2, 2);
326
+ break;
327
+ case 'checker':
328
+ for (let j = 0; j * square < 64; j++)
329
+ for (let i = 0; i * square < 64; i++)
330
+ if ((i + j) % 2 === 0)
331
+ ctx.fillRect(i * square, j * square, square, square);
332
+ break;
333
+ case 'diag-left':
334
+ diag(1);
335
+ break;
336
+ case 'diag-right':
337
+ diag(-1);
338
+ break;
339
+ case 'cross-hatch':
340
+ diag(1);
341
+ diag(-1);
342
+ break;
343
+ }
344
+ }
345
+
346
+ function paintTileCanvas(key: string, size: number): CanvasImageSource {
347
+ const canvas = createCanvas(size, size);
348
+ const ctx = canvas.getContext('2d');
349
+ if (!ctx)
350
+ throw new Error(
351
+ 'carto-api-client: 2D context unavailable for pattern tile'
352
+ );
353
+ ctx.scale(size / 64, size / 64);
354
+ paintDesignTile(ctx, key);
355
+ return canvas;
356
+ }
357
+
358
+ type TileImages = Record<string, CanvasImageSource>;
359
+
360
+ // Shared compositor: lay out the per-key tile images on the atlas grid. `none` is
361
+ // left transparent (no tile). Every other cell is filled with reps x reps copies of
362
+ // its tile, with one extra ring clipped into the gutter, so the padding holds the
363
+ // tile's own wrapped content.
364
+ async function composeAtlas(
365
+ cell: number,
366
+ reps: number,
367
+ images: TileImages
368
+ ): Promise<AssembledAtlas> {
369
+ const mapping = getPatternAtlasMapping(cell);
370
+ const pad = getPatternCellPadding(cell);
371
+ const pitch = cell + 2 * pad;
372
+ const step = cell / reps;
373
+ const canvas = createCanvas(pitch * 3, pitch * (PATTERN_ROWS.length + 1));
374
+ const ctx = canvas.getContext('2d');
375
+ if (!ctx)
376
+ throw new Error(
377
+ 'carto-api-client: 2D context unavailable for pattern atlas'
378
+ );
379
+
380
+ for (const [key, frame] of Object.entries(mapping)) {
381
+ const img = images[key];
382
+ if (!img) continue;
383
+ ctx.save();
384
+ ctx.beginPath();
385
+ ctx.rect(frame.x - pad, frame.y - pad, cell + 2 * pad, cell + 2 * pad);
386
+ ctx.clip();
387
+ for (let i = -1; i <= reps; i++) {
388
+ for (let j = -1; j <= reps; j++) {
389
+ ctx.drawImage(img, frame.x + i * step, frame.y + j * step, step, step);
390
+ }
391
+ }
392
+ ctx.restore();
393
+ }
394
+
395
+ if (typeof createImageBitmap !== 'undefined')
396
+ return createImageBitmap(canvas);
397
+ return canvas as HTMLCanvasElement;
398
+ }
399
+
400
+ // One builder per asset source, all funneling into composeAtlas.
401
+
402
+ // Design's original 64px raster masks, tiled at native resolution (never resampled).
403
+ async function buildPngAtlas(cell: number): Promise<AssembledAtlas> {
404
+ const images: TileImages = {};
405
+ await Promise.all(
406
+ Object.entries(CELL_URLS).map(async ([key, url]) => {
407
+ images[key] = await loadImage(url);
408
+ })
409
+ );
410
+ return composeAtlas(cell, getPatternRepeats(cell, 'png'), images);
411
+ }
412
+
413
+ // In-repo vector tiles, rasterized from the vector at the repeat size.
414
+ async function buildSvgAtlas(cell: number): Promise<AssembledAtlas> {
415
+ const images: TileImages = {};
416
+ await Promise.all(
417
+ Object.entries(CELL_SVG_URLS).map(async ([key, url]) => {
418
+ images[key] = await loadSvgImage(url);
419
+ })
420
+ );
421
+ return composeAtlas(cell, getPatternRepeats(cell, 'svg'), images);
422
+ }
423
+
424
+ // Asset-free canvas painters drawing the measured design-grid geometry.
425
+ async function buildProceduralAtlas(cell: number): Promise<AssembledAtlas> {
426
+ const reps = getPatternRepeats(cell, 'procedural');
427
+ const step = cell / reps;
428
+ const images: TileImages = {};
429
+ for (const key of Object.keys(CELL_URLS)) {
430
+ images[key] = paintTileCanvas(key, step);
431
+ }
432
+ return composeAtlas(cell, reps, images);
433
+ }
434
+
435
+ const ATLAS_BUILDERS: Record<
436
+ PatternAssetSource,
437
+ (cell: number) => Promise<AssembledAtlas>
438
+ > = {
439
+ png: buildPngAtlas,
440
+ svg: buildSvgAtlas,
441
+ procedural: buildProceduralAtlas,
442
+ };
443
+
444
+ const atlasPromises = new Map<string, Promise<AssembledAtlas>>();
445
+
446
+ /** Assemble the sprite sheet for the active asset source and cell size. Memoized. */
447
+ export function getPatternAtlas(
448
+ cell: number = getPatternCellSize()
449
+ ): Promise<AssembledAtlas> {
450
+ const source = getPatternAssetSource();
451
+ const memoKey = `${source}:${cell}`;
452
+ let promise = atlasPromises.get(memoKey);
453
+ if (!promise) {
454
+ promise = ATLAS_BUILDERS[source](cell);
455
+ // Keep a rejection observed even if no consumer attaches a handler (e.g. Node,
456
+ // where there is no canvas) — the returned promise still rejects for real callers.
457
+ promise.catch(() => {});
458
+ atlasPromises.set(memoKey, promise);
459
+ }
460
+ return promise;
461
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="16" height="16" patternUnits="userSpaceOnUse"><rect width="8" height="8"/><rect x="8" y="8" width="8" height="8"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="8" height="8" patternUnits="userSpaceOnUse"><rect width="4" height="4"/><rect x="4" y="4" width="4" height="4"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="2" height="2"/><rect x="2" y="2" width="2" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="a" width="45.2548" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="1.4142" height="64"/></pattern><pattern id="b" width="45.2548" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#a)"/><rect width="64" height="64" fill="url(#b)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="a" width="22.6274" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="1.4142" height="64"/></pattern><pattern id="b" width="22.6274" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#a)"/><rect width="64" height="64" fill="url(#b)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="a" width="11.3137" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="1.4142" height="64"/></pattern><pattern id="b" width="11.3137" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#a)"/><rect width="64" height="64" fill="url(#b)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="45.2548" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="22.6274" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="11.3137" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="45.2548" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="22.6274" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="11.3137" height="64" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="1.4142" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="16" height="16" patternUnits="userSpaceOnUse"><rect width="2" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="8" height="8" patternUnits="userSpaceOnUse"><rect width="2" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="2" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="64" height="16" patternUnits="userSpaceOnUse"><rect width="64" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="64" height="8" patternUnits="userSpaceOnUse"><rect width="64" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="64" height="4" patternUnits="userSpaceOnUse"><rect width="64" height="2"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs></defs><rect width="64" height="64"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="16" height="64" patternUnits="userSpaceOnUse"><rect width="2" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="8" height="64" patternUnits="userSpaceOnUse"><rect width="2" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><defs><pattern id="p" width="4" height="64" patternUnits="userSpaceOnUse"><rect width="2" height="64"/></pattern></defs><rect width="64" height="64" fill="url(#p)"/></svg>