@expofp/renderer 3.37.0 → 3.38.0

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.
@@ -0,0 +1,190 @@
1
+ /**
2
+ * A source that the packer can draw from without decoding it further. It is already decoded, and
3
+ * it reports its own pixel size. A `Blob` is the one image source that meets neither condition, so
4
+ * code decodes each `Blob` into one of these types before it reaches this module.
5
+ */
6
+ export type DecodedSource = ImageBitmap | HTMLCanvasElement | OffscreenCanvas;
7
+ /**
8
+ * A canvas that one atlas is drawn onto. This type includes `OffscreenCanvas`, so packing can
9
+ * later move into a worker.
10
+ */
11
+ export type AtlasCanvas = HTMLCanvasElement | OffscreenCanvas;
12
+ /**
13
+ * Creates the backing canvas for one atlas, sized to that atlas exactly.
14
+ * @param width - The atlas width in pixels.
15
+ * @param height - The atlas height in pixels.
16
+ * @returns The canvas to draw on.
17
+ */
18
+ export type CreateAtlasCanvas = (width: number, height: number) => AtlasCanvas;
19
+ /**
20
+ * A region of a source image, in **source pixels with a top-left origin**. These are the same axes
21
+ * as `ImageDef.bounds`, and they are the exact `sx, sy, sw, sh` values that the packer passes to
22
+ * `drawImage`. This type keeps pixels instead of normalized values, so code cannot mistake it for
23
+ * a {@link UvRect}, whose origin is the opposite corner.
24
+ */
25
+ export interface SourceRect {
26
+ /** Distance from the source's left edge, in pixels. */
27
+ x: number;
28
+ /** Distance from the source's top edge, in pixels. */
29
+ y: number;
30
+ /** Region width in pixels. */
31
+ width: number;
32
+ /** Region height in pixels. */
33
+ height: number;
34
+ }
35
+ /**
36
+ * A region of an atlas, **normalized in the canvas's own axes: a top-left origin, with v running
37
+ * down**.
38
+ *
39
+ * A texture with `flipY = false` consumes these values. That setting maps canvas row 0 to `v = 0`,
40
+ * so these numbers feed a uv transform directly, with no vertical inversion. This is deliberate.
41
+ * The scene already uses a y axis that points down, because it follows SVG coordinates and the
42
+ * camera matches them. A uv space that also runs down keeps every axis in the pipeline pointing
43
+ * the same way. If only one end inverts, images come out upside down.
44
+ */
45
+ export interface UvRect {
46
+ /** Left edge, `0` at the atlas's left. */
47
+ u0: number;
48
+ /** Top edge, `0` at the atlas's top. */
49
+ v0: number;
50
+ /** Right edge. */
51
+ u1: number;
52
+ /** Bottom edge. */
53
+ v1: number;
54
+ }
55
+ /**
56
+ * One source, or one tile of a split source, placed on an atlas. Each placement carries an array
57
+ * index instead of the source object as its address, because this data crosses a `postMessage`
58
+ * boundary, and object identity does not survive that boundary.
59
+ */
60
+ export interface Placement {
61
+ /** Index into the `sources` array that the caller gave {@link packSources}. */
62
+ index: number;
63
+ /** Where the pixels sit on the atlas. */
64
+ uv: UvRect;
65
+ /**
66
+ * Which part of the source this placement holds. This is undefined when the source packs
67
+ * whole, which is the common case. It is set only on the tiles of a split source.
68
+ */
69
+ source?: SourceRect;
70
+ }
71
+ /** One packed atlas, before its placements are mapped back onto the defs that wanted them. */
72
+ export interface SourceAtlas {
73
+ /** The canvas holding the packed pixels. Its `width`/`height` are the atlas's size. */
74
+ canvas: AtlasCanvas;
75
+ /** Every placement on this atlas. */
76
+ placements: Placement[];
77
+ /**
78
+ * Whether this atlas's pixels can shrink to fit a memory budget. Bitmaps pack into resizable
79
+ * atlases, canvases pack into fixed ones, and an atlas never mixes the two.
80
+ */
81
+ resizable: boolean;
82
+ }
83
+ /**
84
+ * What one source contributed to a pack. The caller holds the defs, so only the caller can warn
85
+ * about them. This report gives the caller the measurements it needs to do that.
86
+ */
87
+ export interface SourceReport {
88
+ /** The source's intrinsic width in pixels. */
89
+ width: number;
90
+ /** The source's intrinsic height in pixels. */
91
+ height: number;
92
+ /**
93
+ * How many rects this source packed into. It is `0` when the source had no pixels and code
94
+ * dropped it. It is `1` when the source packed whole. Otherwise it is the tile count, for a
95
+ * source too big for one atlas that code had to cut.
96
+ */
97
+ tiles: number;
98
+ }
99
+ /**
100
+ * Whether a source is a `Blob`, which is also true of a `File`.
101
+ *
102
+ * The test looks for `slice`, and it must keep looking for `slice`. `slice` is Chrome 21, Chrome
103
+ * Android 25, Safari 7 and iOS Safari 7, so every build target has it. `arrayBuffer` reads better
104
+ * and is wrong: it is Chrome 76 and Chrome Android 76, above this package's `chrome66` floor, so on
105
+ * the old Android WebViews it is `undefined` and every real `Blob` would fail this test and reach
106
+ * the packer undecoded (`packages/floorplan/src/components/Maplibre/utils/pmtiles-range-fallback.ts`
107
+ * works around the same floor). No other image source carries either method: not an `<img>`, a
108
+ * canvas, an offscreen canvas or a bitmap.
109
+ *
110
+ * A property test rather than `instanceof`, so this never reaches for a global that can be absent.
111
+ *
112
+ * This is the one home of the test. It decides both what needs a decode and what may cross to the
113
+ * atlas worker, so a second copy could drift from this one, and the drift would only show on the
114
+ * oldest targets.
115
+ * @param source - The source to classify.
116
+ * @returns Whether the source needs a decode.
117
+ */
118
+ export declare function isBlob(source: object): source is Blob;
119
+ /**
120
+ * Whether a source's pixels can shrink to fit a memory budget. This is `true` for decoded images
121
+ * and bitmaps. It is `false` for canvases, because a canvas's pixels belong to the caller. The
122
+ * code counts a canvas at full size and never compresses it (see the scene's `memoryLimit`).
123
+ *
124
+ * The check looks for the absence of `getContext`. `HTMLCanvasElement` and `OffscreenCanvas` both
125
+ * have it. `ImageBitmap` and `Blob` do not. This is a property test, not an
126
+ * `instanceof` check, because the SDK runs in WebViews. There, `OffscreenCanvas` and `ImageBitmap`
127
+ * can be missing as globals, and an `instanceof` check against a missing global throws an error.
128
+ * @param source - The source to classify.
129
+ * @returns Whether the source can shrink.
130
+ */
131
+ export declare function isResizableSource(source: object): boolean;
132
+ /**
133
+ * Makes the canvas that one atlas draws onto. The function returns an `OffscreenCanvas` where one
134
+ * exists, and a DOM canvas otherwise.
135
+ *
136
+ * This is the only place in the package that builds an atlas canvas, so the `OffscreenCanvas`
137
+ * guard lives here alone. The guard uses this plain form because `eslint-plugin-compat`
138
+ * understands it and flags only an unguarded construction. Nothing here needs hiding from the
139
+ * linter.
140
+ * @param width - The canvas width in pixels.
141
+ * @param height - The canvas height in pixels.
142
+ * @returns The canvas.
143
+ */
144
+ export declare function createAtlasCanvas(width: number, height: number): AtlasCanvas;
145
+ /**
146
+ * Packs decoded sources into atlas canvases and reports where each one landed.
147
+ *
148
+ * The code splits sources by kind first: resizable ones (bitmaps and images) apart from fixed ones
149
+ * (canvases). Each kind packs on its own, so no atlas ever mixes the two kinds. This lets a memory
150
+ * budget scale a whole resizable atlas without touching a canvas's pixels. Within one kind, the
151
+ * code offers each source to the packer whole. Anything too big for an atlas comes back parked in
152
+ * a bin of its own natural size, and the packer flags that bin. Those are the only sources that get
153
+ * cut into tiles, and the tiles go back through the same packer. Letting the packer decide keeps
154
+ * the guarantee that no atlas exceeds `maxDimension`, with no need to measure every image up
155
+ * front.
156
+ *
157
+ * This function does **not** deduplicate sources. The caller must collect unique sources before it
158
+ * decodes them, so every entry in `sources` is already distinct.
159
+ *
160
+ * This module runs unchanged on the main thread and inside the atlas worker. It must not use
161
+ * `document`, the def types, or the logger.
162
+ * @param sources - The decoded sources to pack, in the order the caller gives them.
163
+ * @param maxDimension - The largest atlas edge in pixels. The function floors this to a positive
164
+ * integer.
165
+ * @param createCanvas - Makes each atlas canvas. Defaults to {@link createAtlasCanvas}.
166
+ * @returns The atlases in packing order (resizable first, then fixed), and one report for each
167
+ * source, aligned by index with `sources`.
168
+ */
169
+ export declare function packSources(sources: readonly (DecodedSource | null)[], maxDimension: number, createCanvas?: CreateAtlasCanvas): {
170
+ atlases: SourceAtlas[];
171
+ reports: SourceReport[];
172
+ };
173
+ /**
174
+ * Resolves an atlas canvas's 2D context, asking one canvas kind at a time.
175
+ *
176
+ * Deliberately not `canvas.getContext('2d')` on the union. The two kinds' overload lists disagree
177
+ * at the end — `HTMLCanvasElement` closes with `getContext(id: string): RenderingContext | null`,
178
+ * `OffscreenCanvas` with an `OffscreenRenderingContextId` one — and resolving across the union can
179
+ * combine one kind's `"2d"` overload with the other's catch-all, yielding
180
+ * `OffscreenCanvasRenderingContext2D | RenderingContext`: a type covering bitmap and WebGL contexts
181
+ * that have no `drawImage`. It surfaces only on incremental rebuilds (any compile error in this
182
+ * file leaves the bad resolution cached in `.tsbuildinfo`, where it outlives the error that caused
183
+ * it), so a clean build hides it and a working tree quietly breaks. Narrowed first, each branch has
184
+ * one concrete type whose `"2d"` overload is the only candidate, and the result is stable either
185
+ * way.
186
+ * @param canvas - The atlas canvas.
187
+ * @returns Its 2D context, or `null` when the canvas cannot provide one.
188
+ */
189
+ export declare function contextOf(canvas: AtlasCanvas): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;
190
+ //# sourceMappingURL=pack-sources.d.ts.map
@@ -0,0 +1,272 @@
1
+ import { MaxRectsPacker } from 'maxrects-packer';
2
+ /**
3
+ * Whether a source is a `Blob`, which is also true of a `File`.
4
+ *
5
+ * The test looks for `slice`, and it must keep looking for `slice`. `slice` is Chrome 21, Chrome
6
+ * Android 25, Safari 7 and iOS Safari 7, so every build target has it. `arrayBuffer` reads better
7
+ * and is wrong: it is Chrome 76 and Chrome Android 76, above this package's `chrome66` floor, so on
8
+ * the old Android WebViews it is `undefined` and every real `Blob` would fail this test and reach
9
+ * the packer undecoded (`packages/floorplan/src/components/Maplibre/utils/pmtiles-range-fallback.ts`
10
+ * works around the same floor). No other image source carries either method: not an `<img>`, a
11
+ * canvas, an offscreen canvas or a bitmap.
12
+ *
13
+ * A property test rather than `instanceof`, so this never reaches for a global that can be absent.
14
+ *
15
+ * This is the one home of the test. It decides both what needs a decode and what may cross to the
16
+ * atlas worker, so a second copy could drift from this one, and the drift would only show on the
17
+ * oldest targets.
18
+ * @param source - The source to classify.
19
+ * @returns Whether the source needs a decode.
20
+ */
21
+ export function isBlob(source) {
22
+ return typeof source.slice === 'function';
23
+ }
24
+ /**
25
+ * Whether a source's pixels can shrink to fit a memory budget. This is `true` for decoded images
26
+ * and bitmaps. It is `false` for canvases, because a canvas's pixels belong to the caller. The
27
+ * code counts a canvas at full size and never compresses it (see the scene's `memoryLimit`).
28
+ *
29
+ * The check looks for the absence of `getContext`. `HTMLCanvasElement` and `OffscreenCanvas` both
30
+ * have it. `ImageBitmap` and `Blob` do not. This is a property test, not an
31
+ * `instanceof` check, because the SDK runs in WebViews. There, `OffscreenCanvas` and `ImageBitmap`
32
+ * can be missing as globals, and an `instanceof` check against a missing global throws an error.
33
+ * @param source - The source to classify.
34
+ * @returns Whether the source can shrink.
35
+ */
36
+ export function isResizableSource(source) {
37
+ return !('getContext' in source);
38
+ }
39
+ /**
40
+ * Makes the canvas that one atlas draws onto. The function returns an `OffscreenCanvas` where one
41
+ * exists, and a DOM canvas otherwise.
42
+ *
43
+ * This is the only place in the package that builds an atlas canvas, so the `OffscreenCanvas`
44
+ * guard lives here alone. The guard uses this plain form because `eslint-plugin-compat`
45
+ * understands it and flags only an unguarded construction. Nothing here needs hiding from the
46
+ * linter.
47
+ * @param width - The canvas width in pixels.
48
+ * @param height - The canvas height in pixels.
49
+ * @returns The canvas.
50
+ */
51
+ export function createAtlasCanvas(width, height) {
52
+ if (typeof OffscreenCanvas !== 'undefined')
53
+ return new OffscreenCanvas(width, height);
54
+ const canvas = document.createElement('canvas');
55
+ canvas.width = width;
56
+ canvas.height = height;
57
+ return canvas;
58
+ }
59
+ /**
60
+ * Packs decoded sources into atlas canvases and reports where each one landed.
61
+ *
62
+ * The code splits sources by kind first: resizable ones (bitmaps and images) apart from fixed ones
63
+ * (canvases). Each kind packs on its own, so no atlas ever mixes the two kinds. This lets a memory
64
+ * budget scale a whole resizable atlas without touching a canvas's pixels. Within one kind, the
65
+ * code offers each source to the packer whole. Anything too big for an atlas comes back parked in
66
+ * a bin of its own natural size, and the packer flags that bin. Those are the only sources that get
67
+ * cut into tiles, and the tiles go back through the same packer. Letting the packer decide keeps
68
+ * the guarantee that no atlas exceeds `maxDimension`, with no need to measure every image up
69
+ * front.
70
+ *
71
+ * This function does **not** deduplicate sources. The caller must collect unique sources before it
72
+ * decodes them, so every entry in `sources` is already distinct.
73
+ *
74
+ * This module runs unchanged on the main thread and inside the atlas worker. It must not use
75
+ * `document`, the def types, or the logger.
76
+ * @param sources - The decoded sources to pack, in the order the caller gives them.
77
+ * @param maxDimension - The largest atlas edge in pixels. The function floors this to a positive
78
+ * integer.
79
+ * @param createCanvas - Makes each atlas canvas. Defaults to {@link createAtlasCanvas}.
80
+ * @returns The atlases in packing order (resizable first, then fixed), and one report for each
81
+ * source, aligned by index with `sources`.
82
+ */
83
+ export function packSources(sources, maxDimension, createCanvas = createAtlasCanvas) {
84
+ const limit = Math.max(1, Math.floor(maxDimension));
85
+ // Every member of `DecodedSource` reports its own pixel size, so nothing here needs to duck-type
86
+ // a size. That was only ever needed because an `<img>` reports its layout size as `width`. A
87
+ // source that fails to decode arrives as `null` and reports `0 x 0`. Code skips that source the
88
+ // same way it skips a zero-sized canvas.
89
+ const reports = sources.map((source) => source
90
+ ? { width: source.width, height: source.height, tiles: 0 }
91
+ : { width: 0, height: 0, tiles: 0 });
92
+ const resizable = [];
93
+ const fixed = [];
94
+ for (let index = 0; index < sources.length; index++) {
95
+ const source = sources[index];
96
+ // A source that failed to decode is dropped later by the size check in `packGroup`. Code
97
+ // groups it with the resizable sources, so it never lands in an atlas of canvases on its way
98
+ // to being skipped.
99
+ (!source || isResizableSource(source) ? resizable : fixed).push(index);
100
+ }
101
+ // One packer serves both groups, reset between them. The configuration (size, padding, options)
102
+ // is the same for each, so reusing the packer spares an allocation per pack. The packer cannot
103
+ // carry over across pack calls, though, because `limit` is fixed at construction and changes
104
+ // from call to call.
105
+ // `pot: false`, because WebGL 2 samples non-power-of-two textures without restriction. Rounding
106
+ // each atlas up only wastes memory. `smart` stays on (the default), which shrinks every bin to
107
+ // its content. That is why the code reads the size back off the bin instead of assuming
108
+ // `limit`.
109
+ const packer = new MaxRectsPacker(limit, limit, PADDING, { pot: false });
110
+ const atlases = [
111
+ ...packGroup(packer, sources, resizable, reports, limit, createCanvas, true),
112
+ ...packGroup(packer, sources, fixed, reports, limit, createCanvas, false),
113
+ ];
114
+ return { atlases, reports };
115
+ }
116
+ /**
117
+ * The gap between neighboring images on an atlas. This gap stops linear filtering at one image's
118
+ * edge from reaching into the next image's pixels. The packer applies this gap *between* rects
119
+ * only, so atlas edges stay flush against the pixels. This is also why a tile exactly
120
+ * `maxDimension` across still fits in a bin.
121
+ */
122
+ const PADDING = 1;
123
+ /**
124
+ * Packs one kind of source, either all resizable or all fixed, into atlases. The function tags
125
+ * each atlas with the group's kind and records each source's tile count in `reports`.
126
+ * @param packer - The shared packer. It is reset to empty before this group is added.
127
+ * @param sources - Every source, indexed by the entries in `indices`.
128
+ * @param indices - The indices of this kind. This list can be empty, and then nothing packs.
129
+ * @param reports - Per-source reports. This function fills in each report's `tiles` count.
130
+ * @param limit - The largest atlas edge, in pixels.
131
+ * @param createCanvas - Makes each atlas canvas.
132
+ * @param resizable - The kind, stamped onto every atlas that this call produces.
133
+ * @returns The atlases this group produced.
134
+ */
135
+ function packGroup(packer, sources, indices, reports, limit, createCanvas, resizable) {
136
+ const rects = [];
137
+ for (const index of indices) {
138
+ const { width, height } = reports[index];
139
+ // A `drawImage` call throws in some browsers when the source region has zero width or zero
140
+ // height. So code drops an image that has not decoded, or a canvas sized to nothing, instead
141
+ // of drawing it. Its report keeps `tiles: 0`, and that value tells the caller to warn about
142
+ // it.
143
+ if (width <= 0 || height <= 0)
144
+ continue;
145
+ rects.push({ width, height, x: 0, y: 0, hash: `${index}`, data: { index } });
146
+ }
147
+ // Clear whatever the previous group left in the packer, keeping its configuration.
148
+ packer.reset();
149
+ // `addArray` sorts by longest edge before it places rects, which packs far tighter than
150
+ // insertion order.
151
+ packer.addArray(rects);
152
+ // The packer tags whatever it must park in an oversized bin. The packer decides what needs
153
+ // cutting, not a separate measurement in this code. The tiles are ordinary rects, and they pack
154
+ // into the bins that the first pass already opened.
155
+ const oversized = rects.filter((rect) => rect.oversized);
156
+ if (oversized.length > 0) {
157
+ packer.addArray(oversized.flatMap((rect) => cutTiles(rect, reports[rect.data.index], limit)));
158
+ }
159
+ // Every oversized bin now has its tiles standing in for it, so the code drops the bin instead of
160
+ // returning it. Handing back a canvas larger than the caller's limit is the exact problem this
161
+ // code exists to prevent.
162
+ const bins = packer.bins.filter((bin) => !bin.rects.some((rect) => rect.oversized));
163
+ const atlases = bins.map((bin) => ({
164
+ ...renderAtlas(bin, sources, reports, createCanvas),
165
+ resizable,
166
+ }));
167
+ // Count what actually landed. This lets code tell a dropped source apart from a split one.
168
+ for (const bin of bins)
169
+ for (const rect of bin.rects)
170
+ reports[rect.data.index].tiles++;
171
+ return atlases;
172
+ }
173
+ /**
174
+ * Cuts a source that did not fit in the packer into a grid of tiles. No tile is larger than one
175
+ * atlas. Tiles are exact and do not overlap. Each tile ends up alone, or beside its own sibling
176
+ * tiles, in an atlas. Nothing can bleed across the cut, so the code needs no overlap column to
177
+ * hide a seam.
178
+ * @param rect - The oversized rect that names the source to cut.
179
+ * @param report - That source's measured size.
180
+ * @param limit - The largest tile edge, in pixels.
181
+ * @returns The tiles to pack in its place.
182
+ */
183
+ function cutTiles(rect, report, limit) {
184
+ const columns = Math.ceil(report.width / limit);
185
+ const rows = Math.ceil(report.height / limit);
186
+ const tiles = [];
187
+ for (let row = 0; row < rows; row++) {
188
+ const y = row * limit;
189
+ const height = Math.min(limit, report.height - y);
190
+ for (let column = 0; column < columns; column++) {
191
+ const x = column * limit;
192
+ const width = Math.min(limit, report.width - x);
193
+ tiles.push({
194
+ width,
195
+ height,
196
+ x: 0,
197
+ y: 0,
198
+ hash: `${rect.hash}:${column},${row}`,
199
+ data: { index: rect.data.index, source: { x, y, width, height } },
200
+ });
201
+ }
202
+ }
203
+ return tiles;
204
+ }
205
+ /**
206
+ * Draws one packed bin onto a canvas and records where each rect landed.
207
+ * @param bin - The packed bin, already sized to its content.
208
+ * @param sources - Every source, indexed by each rect's tile.
209
+ * @param reports - Per-source sizes, for the whole-image draw region.
210
+ * @param createCanvas - Makes the canvas to draw on.
211
+ * @returns The atlas, less its `resizable` flag.
212
+ */
213
+ function renderAtlas(bin, sources, reports, createCanvas) {
214
+ const canvas = createCanvas(bin.width, bin.height);
215
+ const context = contextOf(canvas);
216
+ // Nothing downstream can recover from a missing context. Every atlas then uploads blank, and
217
+ // the images silently draw as nothing at all. It is better to fail here, where the cause stays
218
+ // visible.
219
+ if (!context)
220
+ throw new Error(`pack-sources: no 2d context for a ${bin.width}x${bin.height} atlas`);
221
+ const placements = [];
222
+ for (const rect of bin.rects) {
223
+ const { index, source } = rect.data;
224
+ // The copied region is the tile when the rect has one, and the whole image otherwise. Both
225
+ // read from the source's own axes. This region happens to match the packed rect's size today,
226
+ // because tiles are placed 1:1 and unrotated. Reading the size from the destination instead
227
+ // is correct only by accident, not by design. The code keeps this region apart from `source`
228
+ // itself, which stays undefined for an unsplit image, so the placement below does not report
229
+ // a region that the caller has no use for.
230
+ const { width, height } = reports[index];
231
+ const region = source ?? { x: 0, y: 0, width, height };
232
+ // This value is never null, by construction. A source that fails to decode reports `0 x 0`
233
+ // and never reaches the packer, so every rect in a bin has a drawable source behind it.
234
+ const decoded = sources[index];
235
+ if (!decoded)
236
+ continue;
237
+ context.drawImage(decoded, region.x, region.y, region.width, region.height, rect.x, rect.y, rect.width, rect.height);
238
+ // The rect uses canvas pixels, and uv keeps the canvas's axes, so this is a plain
239
+ // normalization with no vertical inversion (see {@link UvRect}).
240
+ const uv = {
241
+ u0: rect.x / bin.width,
242
+ v0: rect.y / bin.height,
243
+ u1: (rect.x + rect.width) / bin.width,
244
+ v1: (rect.y + rect.height) / bin.height,
245
+ };
246
+ placements.push(source ? { index, uv, source } : { index, uv });
247
+ }
248
+ return { canvas, placements };
249
+ }
250
+ /**
251
+ * Resolves an atlas canvas's 2D context, asking one canvas kind at a time.
252
+ *
253
+ * Deliberately not `canvas.getContext('2d')` on the union. The two kinds' overload lists disagree
254
+ * at the end — `HTMLCanvasElement` closes with `getContext(id: string): RenderingContext | null`,
255
+ * `OffscreenCanvas` with an `OffscreenRenderingContextId` one — and resolving across the union can
256
+ * combine one kind's `"2d"` overload with the other's catch-all, yielding
257
+ * `OffscreenCanvasRenderingContext2D | RenderingContext`: a type covering bitmap and WebGL contexts
258
+ * that have no `drawImage`. It surfaces only on incremental rebuilds (any compile error in this
259
+ * file leaves the bad resolution cached in `.tsbuildinfo`, where it outlives the error that caused
260
+ * it), so a clean build hides it and a working tree quietly breaks. Narrowed first, each branch has
261
+ * one concrete type whose `"2d"` overload is the only candidate, and the result is stable either
262
+ * way.
263
+ * @param canvas - The atlas canvas.
264
+ * @returns Its 2D context, or `null` when the canvas cannot provide one.
265
+ */
266
+ export function contextOf(canvas) {
267
+ // `in` rather than `instanceof`: in a worker there is no `HTMLCanvasElement` global to test
268
+ // against, and an `instanceof` against a missing global throws.
269
+ if ('convertToBlob' in canvas)
270
+ return canvas.getContext('2d');
271
+ return canvas.getContext('2d');
272
+ }
@@ -203,8 +203,8 @@ export function useResizeFactor(budget, atlas) {
203
203
  if (!active)
204
204
  return;
205
205
  const entry = {
206
- width: atlas.canvas.width,
207
- height: atlas.canvas.height,
206
+ width: atlas.image.width,
207
+ height: atlas.image.height,
208
208
  resizable: atlas.resizable,
209
209
  };
210
210
  budget.register(entry);
@@ -0,0 +1,56 @@
1
+ import { type Placement, type SourceReport } from '../pack-sources';
2
+ /**
3
+ * The one source the worker takes: a `Blob`.
4
+ *
5
+ * The worker could draw a bitmap or an offscreen canvas too, but it never receives one. A `Blob` is
6
+ * the only source that crosses by copy. Its handle clones and the bytes stay put, so the def that
7
+ * owns the source keeps it. Any other source would have to move, a moved source is dead on the main
8
+ * thread, and the next repack of that layer would then find nothing to draw. Tier selection keeps
9
+ * those sources on the main thread, where they pack inline.
10
+ */
11
+ export type WorkerSource = Blob;
12
+ /**
13
+ * What the main thread sends.
14
+ *
15
+ * Every message carries the `id` of the pack it belongs to. One worker serves every pack, so two
16
+ * layers can be in flight at once and the worker must keep their sources apart.
17
+ */
18
+ export type AtlasWorkerRequest =
19
+ /** One batch of sources, in the order they were collected. Sent as many times as needed. */
20
+ {
21
+ type: 'sources';
22
+ id: number;
23
+ sources: WorkerSource[];
24
+ }
25
+ /** No more batches: decode what has arrived, pack it, and reply. */
26
+ | {
27
+ type: 'pack';
28
+ id: number;
29
+ maxDimension: number;
30
+ }
31
+ /** Nobody wants this pack any more. Drop it and reply nothing. */
32
+ | {
33
+ type: 'cancel';
34
+ id: number;
35
+ };
36
+ /** One atlas the worker packed. The bitmap is transferred out, not cloned. */
37
+ export interface WorkerAtlas {
38
+ /** The atlas's pixels, moved out of the offscreen canvas they were drawn into. */
39
+ bitmap: ImageBitmap;
40
+ /** Every placement on this atlas, addressing sources by their index in the batches. */
41
+ placements: Placement[];
42
+ /** Whether these pixels may be downscaled to fit a memory budget. */
43
+ resizable: boolean;
44
+ }
45
+ /** What the worker sends back — at most one of these per pack, carrying that pack's `id`. */
46
+ export type AtlasWorkerResponse = {
47
+ type: 'packed';
48
+ id: number;
49
+ atlases: WorkerAtlas[];
50
+ reports: SourceReport[];
51
+ } | {
52
+ type: 'failed';
53
+ id: number;
54
+ message: string;
55
+ };
56
+ //# sourceMappingURL=atlas-worker.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { packSources, } from '../pack-sources';
2
+ /**
3
+ * Waits for every batch to decode, packs them, and moves each atlas's pixels out of its canvas.
4
+ * @param id - Which pack to run.
5
+ * @param maxDimension - Largest atlas edge in pixels.
6
+ * @returns The reply to post back, or null when the pack was canceled and nothing is owed.
7
+ */
8
+ async function pack(id, maxDimension) {
9
+ // `requestFor` and not `requests.get`, because a layer with no sources sends its closing message
10
+ // and nothing else. That is an empty pack, and it still owes a reply: without one the layer
11
+ // waits forever and draws nothing. The only other way to reach this with no state is a cancel
12
+ // that arrived first, and that happens only when the closing message was never sent, so the
13
+ // empty reply it produces goes to a caller that has already forgotten the id and is discarded.
14
+ const request = requestFor(id);
15
+ request.packing = true;
16
+ const decoded = await Promise.all(request.decoding);
17
+ requests.delete(id);
18
+ // The only yield point in a pack. `packSources` runs straight through once it starts, so this is
19
+ // the last moment a cancel can save the worker's time for the next pack.
20
+ if (request.canceled) {
21
+ closeDecoded(decoded);
22
+ return null;
23
+ }
24
+ // `createAtlasCanvas` returns an `OffscreenCanvas` here and never takes its fallback branch. A
25
+ // worker runs at all only because `selectTier` found `OffscreenCanvas` on the main thread, and
26
+ // the constructor is exposed to both globals or to neither. No fallback is worth writing: a
27
+ // worker has no `document`, so without `OffscreenCanvas` it could build no canvas at all.
28
+ const { atlases, reports } = packSources(decoded, maxDimension);
29
+ // `transferToImageBitmap` moves the canvas's backing bitmap out instead of copying it, which is
30
+ // what makes an atlas cheap to return. The canvas is left blank, and this code drops it anyway.
31
+ const packed = atlases.map((atlas) => ({
32
+ bitmap: atlas.canvas.transferToImageBitmap(),
33
+ placements: atlas.placements,
34
+ resizable: atlas.resizable,
35
+ }));
36
+ // The atlases hold the pixels now, so every decoded bitmap is dead. This worker decoded all of
37
+ // them and owns all of them. Close them rather than leaving a thousand to the collector.
38
+ closeDecoded(decoded);
39
+ return { type: 'packed', id, atlases: packed, reports };
40
+ }
41
+ /**
42
+ * Abandons one pack.
43
+ *
44
+ * A pack that has not reached {@link pack} yet is dropped here, because nothing else will ever read
45
+ * its decodes. A pack that is already running is only marked: it reads the mark at its one yield
46
+ * point and cleans up there.
47
+ * @param id - Which pack to abandon.
48
+ */
49
+ function cancel(id) {
50
+ const request = requests.get(id);
51
+ // Already replied, or never started. Either way this worker holds nothing for that pack.
52
+ if (!request)
53
+ return;
54
+ request.canceled = true;
55
+ if (request.packing)
56
+ return;
57
+ requests.delete(id);
58
+ void Promise.all(request.decoding).then(closeDecoded);
59
+ }
60
+ /**
61
+ * Frees decoded pixels this worker owns.
62
+ *
63
+ * An `ImageBitmap` holds its pixels outside the JavaScript heap, where collection does not reclaim
64
+ * them promptly. A canceled pack of a thousand logos would otherwise sit on a thousand of them.
65
+ * @param decoded - What the sources decoded into.
66
+ */
67
+ function closeDecoded(decoded) {
68
+ for (const source of decoded)
69
+ if (source && 'close' in source)
70
+ source.close();
71
+ }
72
+ /**
73
+ * Finds a pack's state, starting it on the first batch that arrives.
74
+ * @param id - Which pack the batch belongs to.
75
+ * @returns That pack's state.
76
+ */
77
+ function requestFor(id) {
78
+ let request = requests.get(id);
79
+ if (!request) {
80
+ request = { decoding: [], canceled: false, packing: false };
81
+ requests.set(id, request);
82
+ }
83
+ return request;
84
+ }
85
+ /**
86
+ * Every pack this worker is working on, by request id.
87
+ *
88
+ * The main thread does not terminate this worker once it has returned an atlas, because those
89
+ * atlases are backed by GPU memory the worker owns. It stops a worker only when that worker broke
90
+ * before it produced anything. So one worker serves every pack of every layer, for the life of the
91
+ * page, and each pack's state has to be kept apart from the others'. An entry is deleted as soon
92
+ * as its pack replies or is canceled.
93
+ */
94
+ const requests = new Map();
95
+ /** This worker's global scope. */
96
+ const scope = self;
97
+ scope.onmessage = (event) => {
98
+ const message = event.data;
99
+ if (message.type === 'sources') {
100
+ const request = requestFor(message.id);
101
+ // Null rather than a rejection, so one set of bytes that is not an image drops its own def
102
+ // instead of failing the whole layer's pack. `packSources` skips it as a source with no pixels.
103
+ for (const source of message.sources) {
104
+ request.decoding.push(createImageBitmap(source).catch(() => null));
105
+ }
106
+ return;
107
+ }
108
+ if (message.type === 'cancel') {
109
+ cancel(message.id);
110
+ return;
111
+ }
112
+ const { id } = message;
113
+ pack(id, message.maxDimension).then((response) => {
114
+ // Nothing is owed for a canceled pack. The main thread has already settled it.
115
+ if (!response)
116
+ return;
117
+ // The bitmaps move rather than copy. After this the worker holds nothing for this pack.
118
+ scope.postMessage(response, response.atlases.map((atlas) => atlas.bitmap));
119
+ }, (error) => {
120
+ requests.delete(id);
121
+ scope.postMessage({ type: 'failed', id, message: String(error) });
122
+ });
123
+ };