@expofp/renderer 3.37.1 → 3.39.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.
- package/README.md +34 -0
- package/dist/controls/camera-api.d.ts +28 -0
- package/dist/controls/camera-api.js +33 -10
- package/dist/controls/hooks.js +2 -0
- package/dist/core/InstanceUniforms.d.ts +15 -0
- package/dist/core/InstanceUniforms.js +32 -0
- package/dist/core/batch/BatchedMesh.js +6 -0
- package/dist/core/instanced/instanced-material.js +5 -1
- package/dist/core/lines/BatchedLines.d.ts +9 -7
- package/dist/core/lines/BatchedLines.js +9 -7
- package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
- package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
- package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
- package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
- package/dist/core/lines/lines-material.js +10 -1
- package/dist/core/sprites/BatchedSprites.d.ts +115 -0
- package/dist/core/sprites/BatchedSprites.js +205 -0
- package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
- package/dist/core/sprites/RaycastingSprites.js +71 -0
- package/dist/core/sprites/sprite-material.d.ts +59 -0
- package/dist/core/sprites/sprite-material.js +192 -0
- package/dist/core/text/BatchedText.d.ts +6 -0
- package/dist/core/text/BatchedText.js +6 -0
- package/dist/core/text/MemberTexture.js +55 -1
- package/dist/core/text/Text.d.ts +16 -1
- package/dist/core/text/Text.js +15 -0
- package/dist/core/text/batched-text-material.js +4 -2
- package/dist/objects/containers/hooks.d.ts +7 -2
- package/dist/objects/containers/hooks.js +20 -5
- package/dist/objects/containers/mesh-writes.js +3 -3
- package/dist/objects/images/ImageInstance.js +111 -43
- package/dist/objects/images/Images.d.ts +19 -7
- package/dist/objects/images/Images.js +266 -83
- package/dist/objects/images/atlas-packer.d.ts +92 -91
- package/dist/objects/images/atlas-packer.js +230 -258
- package/dist/objects/images/image-container.d.ts +14 -3
- package/dist/objects/images/image-container.js +19 -5
- package/dist/objects/images/pack-sources.d.ts +190 -0
- package/dist/objects/images/pack-sources.js +272 -0
- package/dist/objects/images/texture-budget.js +2 -2
- package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
- package/dist/objects/images/worker/atlas-worker.js +123 -0
- package/dist/objects/images/worker/client.d.ts +62 -0
- package/dist/objects/images/worker/client.js +249 -0
- package/dist/objects/images/worker/index.d.ts +10 -0
- package/dist/objects/images/worker/index.js +1 -0
- package/dist/objects/layer/Layer.d.ts +7 -6
- package/dist/objects/layer/Layer.js +40 -30
- package/dist/objects/layer/layer-context.d.ts +41 -0
- package/dist/objects/layer/layer-context.js +45 -0
- package/dist/objects/materials/ColorMaterial.js +16 -3
- package/dist/objects/materials/LineMaterial.js +4 -2
- package/dist/objects/materials/TextureMaterial.js +14 -2
- package/dist/objects/materials/effects/color-dim.d.ts +1 -10
- package/dist/objects/materials/effects/color-dim.js +21 -18
- package/dist/objects/materials/effects/effects-context.d.ts +6 -5
- package/dist/objects/materials/shared.d.ts +18 -0
- package/dist/objects/materials/shared.js +21 -0
- package/dist/objects/models/Models.js +1 -8
- package/dist/objects/shapes/Shapes.js +2 -9
- package/dist/objects/text/TextInstance.js +1 -1
- package/dist/objects/text/text-layout.d.ts +13 -4
- package/dist/objects/text/text-layout.js +30 -4
- package/dist/space/Space.d.ts +15 -0
- package/dist/space/Space.js +16 -10
- package/dist/types/defs.d.ts +53 -2
- package/dist/utils/logging.d.ts +6 -3
- package/dist/utils/logging.js +15 -7
- package/dist/utils/math.d.ts +11 -0
- package/dist/utils/math.js +14 -0
- package/package.json +2 -2
|
@@ -1,315 +1,287 @@
|
|
|
1
|
-
import { MaxRectsPacker } from 'maxrects-packer';
|
|
2
1
|
import { createLogger } from '../../utils/logging';
|
|
2
|
+
import { createAtlasCanvas, isBlob, packSources, } from './pack-sources';
|
|
3
|
+
import { packInWorker } from './worker';
|
|
3
4
|
const logger = createLogger('atlas-packer');
|
|
4
|
-
// Need to reduce blocking time on the main thread
|
|
5
|
-
// Naive approach of offloading to web worker is not feasible:
|
|
6
|
-
// HTMLImageElement can't be easily transferred, and createImageBitmap() on sources takes more time
|
|
7
|
-
// than actual atlas packing. Needs to be investigated further
|
|
8
5
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*/
|
|
14
|
-
const PADDING = 1;
|
|
15
|
-
/**
|
|
16
|
-
* How many source pixels per square unit of `bounds` a def may carry before it is called
|
|
17
|
-
* over-sampled. Past this the extra resolution cannot reach the screen at any sane zoom, so the
|
|
18
|
-
* atlas space and texture memory are simply wasted — usually a full-size logo standing in for a
|
|
19
|
-
* marker icon.
|
|
6
|
+
* How many source pixels per square unit of `bounds` a def can carry before code calls it
|
|
7
|
+
* over-sampled. Past this limit, the extra resolution cannot reach the screen at any reasonable
|
|
8
|
+
* zoom. The atlas space and texture memory are then wasted, usually on a full-size logo that
|
|
9
|
+
* stands in for a marker icon.
|
|
20
10
|
*/
|
|
21
11
|
const MAX_AREA_RATIO = 1000;
|
|
12
|
+
export { isResizableSource, } from './pack-sources';
|
|
22
13
|
/**
|
|
23
|
-
*
|
|
24
|
-
* bitmaps, `false` for canvases — a canvas's pixels are the caller's, counted at full size but never
|
|
25
|
-
* compressed (see the scene's `memoryLimit`).
|
|
14
|
+
* Packs image defs into atlas canvases and reports where each def's pixels landed.
|
|
26
15
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*/
|
|
34
|
-
export function isResizableSource(source) {
|
|
35
|
-
return !('getContext' in source);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Packs image defs into atlas canvases and reports where every def's pixels landed.
|
|
16
|
+
* The code first dedupes the defs. Two defs that point at the same URL, or that hold the same
|
|
17
|
+
* object, draw once and share one region. The code then decodes every source that is not drawable
|
|
18
|
+
* yet, which is what makes this function asynchronous: a `Blob` holds bytes and not pixels. The
|
|
19
|
+
* code then hands the decoded sources to {@link packSources}, which does the pixel work. The
|
|
20
|
+
* placements come back addressed by source index, and this function expands them into one entry
|
|
21
|
+
* per def.
|
|
39
22
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* offered to the packer whole. Anything too big for an atlas comes back parked in a natural-size bin
|
|
45
|
-
* of its own, which the packer flags; those are the only sources cut into tiles, and the tiles go
|
|
46
|
-
* back through the same packer. Letting the packer decide is what keeps the guarantee that no atlas
|
|
47
|
-
* exceeds `maxDimension` without measuring every image up front.
|
|
23
|
+
* Packing reads only the sources' pixel dimensions, never `bounds`. So a def that later moves or
|
|
24
|
+
* resizes keeps its region: the quad's matrix scales it, and nothing needs repacking. A change in
|
|
25
|
+
* membership is different. This is a batch call with no incremental path, so an added or removed
|
|
26
|
+
* def needs a fresh call.
|
|
48
27
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* changes do, though — this is a batch call with no incremental path.
|
|
28
|
+
* A worker that fails for any reason other than an abort falls back to the inline tier once. The
|
|
29
|
+
* worker is an optimization, and no host page may cost a layer its images by refusing one.
|
|
52
30
|
* @param defs - The images to pack.
|
|
53
|
-
* @param maxDimension -
|
|
54
|
-
*
|
|
55
|
-
* @
|
|
31
|
+
* @param maxDimension - The largest atlas edge, in pixels. The function floors this to a positive
|
|
32
|
+
* integer.
|
|
33
|
+
* @param options - Overrides for how atlas canvases are made, and the signal that abandons the
|
|
34
|
+
* pack.
|
|
35
|
+
* @returns One entry for each atlas, in packing order: resizable atlases first, then fixed ones.
|
|
56
36
|
*/
|
|
57
|
-
export function packAtlases(defs, maxDimension, options = {}) {
|
|
37
|
+
export async function packAtlases(defs, maxDimension, options = {}) {
|
|
58
38
|
const startedAt = performance.now();
|
|
59
|
-
const { createCanvas =
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
39
|
+
const { createCanvas = createAtlasCanvas, signal } = options;
|
|
40
|
+
const entries = collectSources(defs);
|
|
41
|
+
const sources = entries.map((entry) => entry.source);
|
|
42
|
+
let tier = selectTier(sources);
|
|
43
|
+
let packed = null;
|
|
44
|
+
if (tier === 'worker') {
|
|
45
|
+
try {
|
|
46
|
+
packed = await packWorkerTier(entries, sources, maxDimension, signal);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// An abort is the caller's own decision, and nothing may run after it.
|
|
50
|
+
if (signal?.aborted)
|
|
51
|
+
throw error;
|
|
52
|
+
// Any other failure is the worker alone failing, and the main thread can still do the work.
|
|
53
|
+
// The case that matters is a host page whose `script-src` forbids `blob:` and `data:`:
|
|
54
|
+
// `new Worker()` throws there, this SDK runs on origins it does not control, and without
|
|
55
|
+
// this the layer would stay blank on that site.
|
|
56
|
+
logger.warn('the worker could not pack, packing inline instead %O', error);
|
|
57
|
+
tier = 'inline';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Only if the worker tier did not run, or ran and failed. `??=` does not evaluate its right side
|
|
61
|
+
// when the left side already holds the atlases.
|
|
62
|
+
packed ??= await packInlineTier(entries, sources, maxDimension, createCanvas, signal);
|
|
75
63
|
const elapsed = (performance.now() - startedAt).toFixed(1);
|
|
76
|
-
logger.debug('packed defs: %d
|
|
77
|
-
return
|
|
64
|
+
logger.debug('packed defs: %d, sources: %d, atlases: %d, tier: %s (%sms)', packed.reduce((count, atlas) => count + atlas.images.length, 0), entries.length, packed.length, tier, elapsed);
|
|
65
|
+
return packed;
|
|
78
66
|
}
|
|
79
67
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
68
|
+
* Picks the higher tier when this environment and this set of sources both allow it.
|
|
69
|
+
*
|
|
70
|
+
* A layer packs in the worker only when **every** source is a `Blob`. One source of any other kind
|
|
71
|
+
* keeps the whole layer on the main thread.
|
|
72
|
+
*
|
|
73
|
+
* The test names what may cross, never what must stay. A test that only excludes the known-bad
|
|
74
|
+
* kinds admits every kind nobody thought of, and this code shipped that mistake once: it asked
|
|
75
|
+
* whether a source was not a DOM canvas, which let an `<img>` through. An `<img>` neither clones
|
|
76
|
+
* nor moves, so `postMessage` threw on it and the layer drew nothing.
|
|
77
|
+
*
|
|
78
|
+
* A `Blob` is also the only source the worker can take without taking it from the def that owns it.
|
|
79
|
+
* Its handle clones, so the def keeps its bytes. Every other kind would have to move, and a moved
|
|
80
|
+
* source is dead on the main thread, so the next repack would find nothing to draw. Those sources
|
|
81
|
+
* lose nothing by packing inline. A canvas cannot cross at all — `transferControlToOffscreen` throws
|
|
82
|
+
* on a canvas that already holds a 2D context, and every floorplan canvas source holds one — and
|
|
83
|
+
* `createImageBitmap(canvas)` costs more than a pack of that canvas on the main thread (this is
|
|
84
|
+
* measured, see the design spec).
|
|
85
|
+
* @param sources - The unique sources that this pack will draw.
|
|
86
|
+
* @returns The tier to run.
|
|
88
87
|
*/
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
// Clear whatever the previous group left in the packer, keeping its configuration.
|
|
93
|
-
packer.reset();
|
|
94
|
-
// `addArray` sorts by longest edge before placing, which packs far tighter than insertion order.
|
|
95
|
-
packer.addArray(rects);
|
|
96
|
-
// The packer tags whatever it had to park in an oversized bin, so it — not a measurement of our
|
|
97
|
-
// own — decides what needs cutting. The tiles are ordinary rects, and pack into the bins the
|
|
98
|
-
// first pass already opened.
|
|
99
|
-
const oversized = rects.filter((rect) => rect.oversized);
|
|
100
|
-
if (oversized.length > 0)
|
|
101
|
-
packer.addArray(oversized.flatMap((rect) => cutTiles(rect, limit)));
|
|
102
|
-
// Every oversized bin is now represented by its tiles, so it is dropped rather than returned:
|
|
103
|
-
// handing back a canvas larger than the caller's limit is the thing this all exists to prevent.
|
|
104
|
-
const bins = packer.bins.filter((bin) => !bin.rects.some((rect) => rect.oversized));
|
|
105
|
-
const atlases = bins.map((bin) => ({ ...renderAtlas(bin, createCanvas), resizable }));
|
|
106
|
-
return {
|
|
107
|
-
atlases,
|
|
108
|
-
defs: defs.length - skipped.defs,
|
|
109
|
-
sources: sources.size - skipped.sources,
|
|
110
|
-
tiles: bins.reduce((count, bin) => count + bin.rects.length, 0),
|
|
111
|
-
};
|
|
88
|
+
export function selectTier(sources) {
|
|
89
|
+
const offthread = typeof Worker === 'function' && typeof OffscreenCanvas !== 'undefined' && sources.every(isBlob);
|
|
90
|
+
return offthread ? 'worker' : 'inline';
|
|
112
91
|
}
|
|
113
92
|
/**
|
|
114
|
-
*
|
|
93
|
+
* Packs in a worker, off the main thread.
|
|
115
94
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* @
|
|
119
|
-
*
|
|
95
|
+
* The worker decodes every source itself and builds its own offscreen canvases, so this tier
|
|
96
|
+
* ignores {@link AtlasPackerOptions.createCanvas}. A test that passes one is testing the inline
|
|
97
|
+
* tier: {@link selectTier} never picks this tier in jsdom, which has neither `Worker` nor
|
|
98
|
+
* `OffscreenCanvas`.
|
|
99
|
+
* @param entries - The unique sources, and the defs that draw each one.
|
|
100
|
+
* @param sources - The unique sources, aligned by index with `entries`.
|
|
101
|
+
* @param maxDimension - The largest atlas edge, in pixels.
|
|
102
|
+
* @param signal - Abandons the pack. The worker is told to drop it and goes on running.
|
|
103
|
+
* @returns One entry for each atlas, in packing order.
|
|
120
104
|
*/
|
|
121
|
-
|
|
122
|
-
// `
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
105
|
+
async function packWorkerTier(entries, sources, maxDimension, signal) {
|
|
106
|
+
// `selectTier` picked this tier only because every source is a `Blob`, which is what a
|
|
107
|
+
// `WorkerSource` is. The compiler cannot follow that, so the cast states it.
|
|
108
|
+
const { atlases, reports } = await packInWorker(sources, maxDimension, {
|
|
109
|
+
signal,
|
|
110
|
+
});
|
|
111
|
+
// `packInWorker` already rejects on abort, so the pack still stands here. The code checks again
|
|
112
|
+
// because an abort raised in the same turn as the reply is a real ordering.
|
|
113
|
+
if (signal?.aborted) {
|
|
114
|
+
// Nothing will mount these atlases, and an `ImageBitmap` holds its pixels outside the
|
|
115
|
+
// JavaScript heap, so close them here.
|
|
116
|
+
for (const atlas of atlases)
|
|
117
|
+
atlas.bitmap.close();
|
|
118
|
+
throw new Error('atlas-packer: the pack was aborted');
|
|
119
|
+
}
|
|
120
|
+
reportSources(entries, reports);
|
|
121
|
+
return atlases.map((atlas) => ({
|
|
122
|
+
image: atlas.bitmap,
|
|
123
|
+
resizable: atlas.resizable,
|
|
124
|
+
images: expandPlacements(atlas.placements, entries, reports),
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Packs on the main thread. This tier decodes the sources here and draws the atlases here.
|
|
129
|
+
* @param entries - The unique sources, and the defs that draw each one.
|
|
130
|
+
* @param sources - The unique sources, aligned by index with `entries`.
|
|
131
|
+
* @param maxDimension - The largest atlas edge, in pixels.
|
|
132
|
+
* @param createCanvas - Makes each atlas canvas.
|
|
133
|
+
* @param signal - Abandons the pack.
|
|
134
|
+
* @returns One entry for each atlas, in packing order.
|
|
135
|
+
*/
|
|
136
|
+
async function packInlineTier(entries, sources, maxDimension, createCanvas, signal) {
|
|
137
|
+
const decoded = await decodeSources(sources);
|
|
138
|
+
// The caller abandoned the pack while the decodes were in flight, so nothing here is wanted any
|
|
139
|
+
// more. The code reads `signal.aborted` and not `throwIfAborted()`, which Chrome 66 and Safari
|
|
140
|
+
// 15 do not have.
|
|
141
|
+
if (signal?.aborted) {
|
|
142
|
+
closeDecoded(sources, decoded);
|
|
143
|
+
throw new Error('atlas-packer: the pack was aborted');
|
|
144
|
+
}
|
|
145
|
+
const { atlases, reports } = packSources(decoded, maxDimension, createCanvas);
|
|
146
|
+
// The atlases hold the pixels now, so the decoded bitmaps are dead.
|
|
147
|
+
closeDecoded(sources, decoded);
|
|
148
|
+
reportSources(entries, reports);
|
|
149
|
+
return atlases.map((atlas) => ({
|
|
150
|
+
image: atlas.canvas,
|
|
151
|
+
resizable: atlas.resizable,
|
|
152
|
+
images: expandPlacements(atlas.placements, entries, reports),
|
|
153
|
+
}));
|
|
128
154
|
}
|
|
129
155
|
/**
|
|
130
|
-
* Groups defs by the source they draw, so identical images
|
|
156
|
+
* Groups defs by the source they draw, so the pack draws identical images once.
|
|
131
157
|
*
|
|
132
|
-
* Identity is the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
158
|
+
* Identity is the source object itself. Two defs share a region only when they hold the exact same
|
|
159
|
+
* object. That is why floorplan hands two booths with one logo a single `Blob`, instead of two
|
|
160
|
+
* Blobs that hold the same bytes.
|
|
135
161
|
* @param defs - The defs to group.
|
|
136
162
|
* @returns One entry per unique source, in first-seen order.
|
|
137
163
|
*/
|
|
138
164
|
function collectSources(defs) {
|
|
139
165
|
const sources = new Map();
|
|
140
166
|
for (const def of defs) {
|
|
141
|
-
|
|
142
|
-
const key = typeof src === 'string' && src !== '' ? src : def.source;
|
|
143
|
-
let entry = sources.get(key);
|
|
167
|
+
let entry = sources.get(def.source);
|
|
144
168
|
if (!entry) {
|
|
145
|
-
entry = {
|
|
146
|
-
|
|
147
|
-
...imageSourceSize(def.source),
|
|
148
|
-
defs: [],
|
|
149
|
-
bounds: { width: 0, height: 0 },
|
|
150
|
-
};
|
|
151
|
-
sources.set(key, entry);
|
|
169
|
+
entry = { source: def.source, defs: [], bounds: { width: 0, height: 0 } };
|
|
170
|
+
sources.set(def.source, entry);
|
|
152
171
|
}
|
|
153
172
|
entry.defs.push(def);
|
|
154
|
-
//
|
|
155
|
-
//
|
|
173
|
+
// The code copies this size instead of keeping a reference to it. `Rect` exposes an in-place
|
|
174
|
+
// `set` method. A kept reference lets a later bounds change rewrite what this function
|
|
175
|
+
// already measured.
|
|
156
176
|
const { width, height } = def.bounds.size;
|
|
157
177
|
if (width * height > entry.bounds.width * entry.bounds.height) {
|
|
158
178
|
entry.bounds = { width, height };
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
|
-
return sources;
|
|
181
|
+
return [...sources.values()];
|
|
162
182
|
}
|
|
163
183
|
/**
|
|
164
|
-
* Turns
|
|
165
|
-
*
|
|
166
|
-
* @param
|
|
167
|
-
* @
|
|
184
|
+
* Turns the packer's index-addressed placements into one entry per def. Deduped defs all share
|
|
185
|
+
* the one region, which is the whole point of deduping them.
|
|
186
|
+
* @param placements - One atlas's placements.
|
|
187
|
+
* @param entries - The unique sources, aligned by index with what the code gave the packer.
|
|
188
|
+
* @param reports - Per-source measurements, aligned by index with `entries`. Supplies each
|
|
189
|
+
* placement's `size`.
|
|
190
|
+
* @returns One packed image for each def at each placement.
|
|
168
191
|
*/
|
|
169
|
-
function
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
skipped.sources++;
|
|
177
|
-
skipped.defs += entry.defs.length;
|
|
178
|
-
logger.warn('skipping source with no pixels (%dx%d) %O', entry.width, entry.height, entry.source);
|
|
179
|
-
continue;
|
|
192
|
+
function expandPlacements(placements, entries, reports) {
|
|
193
|
+
const images = [];
|
|
194
|
+
for (const { index, uv, source } of placements) {
|
|
195
|
+
const { width, height } = reports[index];
|
|
196
|
+
const size = { width, height };
|
|
197
|
+
for (const def of entries[index].defs) {
|
|
198
|
+
images.push(source ? { def, uv, source, size } : { def, uv, size });
|
|
180
199
|
}
|
|
181
|
-
warnIfOverSampled(entry);
|
|
182
|
-
rects.push({
|
|
183
|
-
width: entry.width,
|
|
184
|
-
height: entry.height,
|
|
185
|
-
x: 0,
|
|
186
|
-
y: 0,
|
|
187
|
-
hash: `${rects.length}`,
|
|
188
|
-
data: { entry },
|
|
189
|
-
});
|
|
190
200
|
}
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Reports a source carrying far more pixels than the bounds it is drawn into can ever show. Once
|
|
195
|
-
* per source rather than once per def, since deduped defs all share the one texture region.
|
|
196
|
-
* @param entry - The source to check.
|
|
197
|
-
*/
|
|
198
|
-
function warnIfOverSampled(entry) {
|
|
199
|
-
const boundsArea = entry.bounds.width * entry.bounds.height;
|
|
200
|
-
// Degenerate bounds would divide out to Infinity and warn about every such def, which says
|
|
201
|
-
// nothing about resolution — that is a bounds bug, and not this function's to report.
|
|
202
|
-
if (boundsArea <= 0)
|
|
203
|
-
return;
|
|
204
|
-
const ratio = (entry.width * entry.height) / boundsArea;
|
|
205
|
-
if (ratio <= MAX_AREA_RATIO)
|
|
206
|
-
return;
|
|
207
|
-
logger.warn('over-sampled source: %dx%d px drawn into %sx%s units — %d px per unit², limit %d', entry.width, entry.height,
|
|
208
|
-
// At most two decimals, trailing zeros dropped: 4 → 4, 4.1 → 4.1, 4.2999 → 4.3.
|
|
209
|
-
Number(entry.bounds.width.toFixed(2)), Number(entry.bounds.height.toFixed(2)), Math.round(ratio), MAX_AREA_RATIO);
|
|
201
|
+
return images;
|
|
210
202
|
}
|
|
211
203
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* @param
|
|
216
|
-
* @param
|
|
217
|
-
* @returns The tiles to pack in its place.
|
|
204
|
+
* Reports what the pack was unable to use. This covers sources with no pixels, and sources that
|
|
205
|
+
* carry far more resolution than their bounds can ever show. The function warns once for each
|
|
206
|
+
* source, not once for each def, because deduped defs all share the one texture region.
|
|
207
|
+
* @param entries - The unique sources, aligned by index with `reports`.
|
|
208
|
+
* @param reports - What the packer measured and placed.
|
|
218
209
|
*/
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const x = column * limit;
|
|
230
|
-
const width = Math.min(limit, entry.width - x);
|
|
231
|
-
tiles.push({
|
|
232
|
-
width,
|
|
233
|
-
height,
|
|
234
|
-
x: 0,
|
|
235
|
-
y: 0,
|
|
236
|
-
hash: `${rect.hash}:${column},${row}`,
|
|
237
|
-
data: { entry, source: { x, y, width, height } },
|
|
238
|
-
});
|
|
210
|
+
function reportSources(entries, reports) {
|
|
211
|
+
for (let index = 0; index < entries.length; index++) {
|
|
212
|
+
const entry = entries[index];
|
|
213
|
+
const { width, height, tiles } = reports[index];
|
|
214
|
+
if (tiles === 0) {
|
|
215
|
+
logger.warn('skipping source with no pixels (%dx%d) %O', width, height, entry.source);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (tiles > 1) {
|
|
219
|
+
logger.warn('source %dx%d exceeds the atlas limit, split into %d tiles', width, height, tiles);
|
|
239
220
|
}
|
|
221
|
+
const boundsArea = entry.bounds.width * entry.bounds.height;
|
|
222
|
+
// Degenerate bounds divide out to Infinity. That triggers a warning for every such def, but
|
|
223
|
+
// the warning says nothing about resolution. It only points to a bug in the bounds, and this
|
|
224
|
+
// function does not report that kind of bug.
|
|
225
|
+
if (boundsArea <= 0)
|
|
226
|
+
continue;
|
|
227
|
+
const ratio = (width * height) / boundsArea;
|
|
228
|
+
if (ratio <= MAX_AREA_RATIO)
|
|
229
|
+
continue;
|
|
230
|
+
logger.warn('over-sampled source: %dx%d px drawn into %sx%s units — %d px per unit², limit %d', width, height,
|
|
231
|
+
// At most two decimals, trailing zeros dropped: 4 → 4, 4.1 → 4.1, 4.2999 → 4.3.
|
|
232
|
+
Number(entry.bounds.width.toFixed(2)), Number(entry.bounds.height.toFixed(2)), Math.round(ratio), MAX_AREA_RATIO);
|
|
240
233
|
}
|
|
241
|
-
return tiles;
|
|
242
234
|
}
|
|
243
235
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
236
|
+
* Decodes every source that is not drawable yet, and leaves the drawable ones as they are.
|
|
237
|
+
*
|
|
238
|
+
* The code starts every decode before it waits for any one of them, so the browser decoder
|
|
239
|
+
* overlaps them. Each decode gives back a bitmap. The packer draws that bitmap, and the caller
|
|
240
|
+
* closes it.
|
|
241
|
+
*
|
|
242
|
+
* One source that does not decode must not take the layer with it. A drawing can carry an
|
|
243
|
+
* `<image>` whose bytes are not an image at all. A `Promise.all` over the decodes rejects the whole
|
|
244
|
+
* pack, so a plan with one bad graphic draws none of its graphics. A failed decode becomes `null`
|
|
245
|
+
* instead. {@link packSources} already treats `null` as a source with no pixels: it skips that
|
|
246
|
+
* source, and `reportSources` warns about it by name.
|
|
247
|
+
* @param sources - The unique sources to decode.
|
|
248
|
+
* @returns The decoded sources, aligned by index with the input. An entry is `null` where the bytes
|
|
249
|
+
* did not decode.
|
|
249
250
|
*/
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
const context = contextOf(canvas);
|
|
253
|
-
// Nothing downstream can recover from this: every atlas would upload blank, so the images would
|
|
254
|
-
// silently draw as nothing at all. Better to fail where the cause is still visible.
|
|
255
|
-
if (!context)
|
|
256
|
-
throw new Error(`atlas-packer: no 2d context for a ${bin.width}x${bin.height} atlas`);
|
|
257
|
-
const images = [];
|
|
258
|
-
for (const rect of bin.rects) {
|
|
259
|
-
const { entry, source } = rect.data;
|
|
260
|
-
// The copied region is the tile when there is one and the whole image otherwise — both read
|
|
261
|
-
// from the source's own axes. It happens to equal the packed rect's size today (tiles are
|
|
262
|
-
// placed 1:1, unrotated), but taking it from the destination would only be right by accident.
|
|
263
|
-
// Kept apart from `source` itself, which stays undefined for an unsplit image so the placement
|
|
264
|
-
// below does not report a region the caller has no use for.
|
|
265
|
-
const region = source ?? { x: 0, y: 0, width: entry.width, height: entry.height };
|
|
266
|
-
context.drawImage(entry.source, region.x, region.y, region.width, region.height, rect.x, rect.y, rect.width, rect.height);
|
|
267
|
-
// The rect is in canvas pixels and uv keeps the canvas's axes, so this is a plain normalization
|
|
268
|
-
// with no vertical inversion (see {@link UvRect}). Shared by every def on this rect — dedup is
|
|
269
|
-
// the whole point.
|
|
270
|
-
const uv = {
|
|
271
|
-
u0: rect.x / bin.width,
|
|
272
|
-
v0: rect.y / bin.height,
|
|
273
|
-
u1: (rect.x + rect.width) / bin.width,
|
|
274
|
-
v1: (rect.y + rect.height) / bin.height,
|
|
275
|
-
};
|
|
276
|
-
for (const def of entry.defs)
|
|
277
|
-
images.push(source ? { def, uv, source } : { def, uv });
|
|
278
|
-
}
|
|
279
|
-
return { canvas, images };
|
|
251
|
+
async function decodeSources(sources) {
|
|
252
|
+
return Promise.all(sources.map((source) => decodeSource(source).catch(() => null)));
|
|
280
253
|
}
|
|
281
254
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
255
|
+
* Frees the bitmaps that this pack decoded.
|
|
256
|
+
*
|
|
257
|
+
* An `ImageBitmap` holds its pixels outside the JavaScript heap, and garbage collection does not
|
|
258
|
+
* reclaim that memory promptly. A pack of a thousand logos would otherwise sit on a thousand
|
|
259
|
+
* decoded images until it did.
|
|
260
|
+
*
|
|
261
|
+
* The pack closes only what it decoded itself, which is every source that arrived as a `Blob`. A
|
|
262
|
+
* def is free to hold an `ImageBitmap` of its own, and {@link decodeSource} hands such a source
|
|
263
|
+
* straight back. Those pixels belong to the def: the next repack draws them again, and a pack that
|
|
264
|
+
* closed them would empty the layer instead.
|
|
265
|
+
* @param sources - The unique sources, aligned by index with `decoded`.
|
|
266
|
+
* @param decoded - What each source decoded into.
|
|
286
267
|
*/
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
268
|
+
function closeDecoded(sources, decoded) {
|
|
269
|
+
for (let index = 0; index < decoded.length; index++) {
|
|
270
|
+
const source = decoded[index];
|
|
271
|
+
if (source && isBlob(sources[index]) && 'close' in source)
|
|
272
|
+
source.close();
|
|
273
|
+
}
|
|
292
274
|
}
|
|
293
275
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* `OffscreenCanvas` with an `OffscreenRenderingContextId` one — and resolving across the union can
|
|
299
|
-
* combine one kind's `"2d"` overload with the other's catch-all, yielding
|
|
300
|
-
* `OffscreenCanvasRenderingContext2D | RenderingContext`: a type covering bitmap and WebGL contexts
|
|
301
|
-
* that have no `drawImage`. It surfaces only on incremental rebuilds (any compile error in this
|
|
302
|
-
* file leaves the bad resolution cached in `.tsbuildinfo`, where it outlives the error that caused
|
|
303
|
-
* it), so a clean build hides it and a working tree quietly breaks. Narrowed first, each branch has
|
|
304
|
-
* one concrete type whose `"2d"` overload is the only candidate, and the result is stable either
|
|
305
|
-
* way.
|
|
306
|
-
* @param canvas - The atlas canvas.
|
|
307
|
-
* @returns Its 2D context, or `null` when the canvas cannot provide one.
|
|
276
|
+
* Decodes one source. Anything that is not a `Blob` is drawable already and comes back as it went
|
|
277
|
+
* in. A `Blob` goes through `createImageBitmap`, which every build target has.
|
|
278
|
+
* @param source - The source to decode.
|
|
279
|
+
* @returns A source that the packer can draw.
|
|
308
280
|
*/
|
|
309
|
-
function
|
|
310
|
-
// `
|
|
311
|
-
//
|
|
312
|
-
if (
|
|
313
|
-
return
|
|
314
|
-
return
|
|
281
|
+
async function decodeSource(source) {
|
|
282
|
+
// A `Blob` is the only member of the union with no size and no draw. Everything else is drawable
|
|
283
|
+
// already.
|
|
284
|
+
if (!isBlob(source))
|
|
285
|
+
return source;
|
|
286
|
+
return createImageBitmap(source);
|
|
315
287
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type InstancedMeshImpl, type InstancedMeshImplParams } from '../../core/instanced/InstancedMesh';
|
|
2
2
|
import { type ImageDef } from '../../types/defs';
|
|
3
3
|
import { SlotContainer } from '../containers';
|
|
4
|
-
import { type
|
|
4
|
+
import { type PackedImage } from './atlas-packer';
|
|
5
5
|
/**
|
|
6
6
|
* The per-instance uniform each atlas mesh reads its texture region from. The container owns the
|
|
7
7
|
* name: it writes each slot's region here (`setUniformAt`) and hands it to the mesh's
|
|
@@ -42,9 +42,20 @@ export declare class ImageContainer extends SlotContainer<PackedImage, Instanced
|
|
|
42
42
|
* component renders `<instancedMesh args={container.args}>` with the geometry and material as
|
|
43
43
|
* children, and hands the mesh back through {@link SlotContainer.bind} — so R3F owns their
|
|
44
44
|
* lifetimes, including disposal on unmount and survival across StrictMode's simulated remount.
|
|
45
|
-
* @param
|
|
45
|
+
* @param capacity - How many instances to size the mesh for. This is the atlas's whole placement
|
|
46
|
+
* count, not one kind's share of it, so moving a def between the two meshes never resizes a
|
|
47
|
+
* buffer. The mesh grows past it on demand in any case.
|
|
46
48
|
*/
|
|
47
|
-
constructor(
|
|
49
|
+
constructor(capacity: number);
|
|
50
|
+
/**
|
|
51
|
+
* Whether the bound mesh draws its members facing the camera.
|
|
52
|
+
*
|
|
53
|
+
* Read off the mesh rather than stored, so one field cannot disagree with another. `screenSpace` is
|
|
54
|
+
* an ordinary def field, so flipping it re-renders nothing and only an instance can notice that it
|
|
55
|
+
* is drawing from the wrong mesh.
|
|
56
|
+
* @returns Whether this container holds a sprite batch.
|
|
57
|
+
*/
|
|
58
|
+
get facesCamera(): boolean;
|
|
48
59
|
protected addInstance(packed: PackedImage): number;
|
|
49
60
|
protected removeInstance(slot: number): void;
|
|
50
61
|
}
|