@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.
Files changed (71) hide show
  1. package/README.md +34 -0
  2. package/dist/controls/camera-api.d.ts +28 -0
  3. package/dist/controls/camera-api.js +33 -10
  4. package/dist/controls/hooks.js +2 -0
  5. package/dist/core/InstanceUniforms.d.ts +15 -0
  6. package/dist/core/InstanceUniforms.js +32 -0
  7. package/dist/core/batch/BatchedMesh.js +6 -0
  8. package/dist/core/instanced/instanced-material.js +5 -1
  9. package/dist/core/lines/BatchedLines.d.ts +9 -7
  10. package/dist/core/lines/BatchedLines.js +9 -7
  11. package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
  12. package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
  13. package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
  14. package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
  15. package/dist/core/lines/lines-material.js +10 -1
  16. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  17. package/dist/core/sprites/BatchedSprites.js +205 -0
  18. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  19. package/dist/core/sprites/RaycastingSprites.js +71 -0
  20. package/dist/core/sprites/sprite-material.d.ts +59 -0
  21. package/dist/core/sprites/sprite-material.js +192 -0
  22. package/dist/core/text/BatchedText.d.ts +6 -0
  23. package/dist/core/text/BatchedText.js +6 -0
  24. package/dist/core/text/MemberTexture.js +55 -1
  25. package/dist/core/text/Text.d.ts +16 -1
  26. package/dist/core/text/Text.js +15 -0
  27. package/dist/core/text/batched-text-material.js +4 -2
  28. package/dist/objects/containers/hooks.d.ts +7 -2
  29. package/dist/objects/containers/hooks.js +20 -5
  30. package/dist/objects/containers/mesh-writes.js +3 -3
  31. package/dist/objects/images/ImageInstance.js +111 -43
  32. package/dist/objects/images/Images.d.ts +19 -7
  33. package/dist/objects/images/Images.js +266 -83
  34. package/dist/objects/images/atlas-packer.d.ts +92 -91
  35. package/dist/objects/images/atlas-packer.js +230 -258
  36. package/dist/objects/images/image-container.d.ts +14 -3
  37. package/dist/objects/images/image-container.js +19 -5
  38. package/dist/objects/images/pack-sources.d.ts +190 -0
  39. package/dist/objects/images/pack-sources.js +272 -0
  40. package/dist/objects/images/texture-budget.js +2 -2
  41. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  42. package/dist/objects/images/worker/atlas-worker.js +123 -0
  43. package/dist/objects/images/worker/client.d.ts +62 -0
  44. package/dist/objects/images/worker/client.js +249 -0
  45. package/dist/objects/images/worker/index.d.ts +10 -0
  46. package/dist/objects/images/worker/index.js +1 -0
  47. package/dist/objects/layer/Layer.d.ts +7 -6
  48. package/dist/objects/layer/Layer.js +40 -30
  49. package/dist/objects/layer/layer-context.d.ts +41 -0
  50. package/dist/objects/layer/layer-context.js +45 -0
  51. package/dist/objects/materials/ColorMaterial.js +16 -3
  52. package/dist/objects/materials/LineMaterial.js +4 -2
  53. package/dist/objects/materials/TextureMaterial.js +14 -2
  54. package/dist/objects/materials/effects/color-dim.d.ts +1 -10
  55. package/dist/objects/materials/effects/color-dim.js +21 -18
  56. package/dist/objects/materials/effects/effects-context.d.ts +6 -5
  57. package/dist/objects/materials/shared.d.ts +18 -0
  58. package/dist/objects/materials/shared.js +21 -0
  59. package/dist/objects/models/Models.js +1 -8
  60. package/dist/objects/shapes/Shapes.js +2 -9
  61. package/dist/objects/text/TextInstance.js +1 -1
  62. package/dist/objects/text/text-layout.d.ts +13 -4
  63. package/dist/objects/text/text-layout.js +30 -4
  64. package/dist/space/Space.d.ts +15 -0
  65. package/dist/space/Space.js +16 -10
  66. package/dist/types/defs.d.ts +53 -2
  67. package/dist/utils/logging.d.ts +6 -3
  68. package/dist/utils/logging.js +15 -7
  69. package/dist/utils/math.d.ts +11 -0
  70. package/dist/utils/math.js +14 -0
  71. package/package.json +2 -2
@@ -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
+ };
@@ -0,0 +1,62 @@
1
+ import { type SourceReport } from '../pack-sources';
2
+ import { type WorkerAtlas, type WorkerSource } from './atlas-worker';
3
+ /** Options for {@link packInWorker}. */
4
+ export interface PackInWorkerOptions {
5
+ /** Abandons the pack. The worker is told to drop it, and a reply that still arrives is thrown away. */
6
+ signal?: AbortSignal;
7
+ /**
8
+ * Makes the pooled worker, on the first pack that needs it. Defaults to the bundled atlas worker,
9
+ * and exists so a spec can drive the protocol against a fake one without a browser. It is ignored
10
+ * once a worker is pooled.
11
+ */
12
+ createWorker?: () => Worker;
13
+ }
14
+ /**
15
+ * Packs a set of sources in a web worker.
16
+ *
17
+ * **One worker serves every pack, and no pack ever stops it.** The atlases come back as
18
+ * `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
19
+ * with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
20
+ * move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
21
+ * bitmap it made, not only the first upload of it. Terminating the worker used to run before this
22
+ * function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
23
+ *
24
+ * The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
25
+ * before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
26
+ * left running. The worker script is inlined into the bundle, so starting one resolves no URL at
27
+ * runtime, which matters because the SDK is loaded from origins it does not control.
28
+ *
29
+ * Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
30
+ * has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
31
+ * that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
32
+ * a layer's worth of GPU memory.
33
+ *
34
+ * The sources go over in batches so the main thread is never blocked for long by one structured
35
+ * clone, and so the worker can decode the early batches while the later ones are still being sent.
36
+ * A closing message tells it every source has arrived.
37
+ * @param sources - The unique sources to pack, in the caller's order; placements address them by
38
+ * their index in this array.
39
+ * @param maxDimension - Largest atlas edge in pixels.
40
+ * @param options - The abort signal, and the worker factory a spec overrides.
41
+ * @returns The packed atlases and one report per source.
42
+ */
43
+ export declare function packInWorker(sources: readonly WorkerSource[], maxDimension: number, options?: PackInWorkerOptions): Promise<{
44
+ atlases: WorkerAtlas[];
45
+ reports: SourceReport[];
46
+ }>;
47
+ /**
48
+ * Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
49
+ *
50
+ * Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
51
+ * the first spec's fake to every spec after it.
52
+ */
53
+ export declare function resetAtlasWorkerPool(): void;
54
+ /**
55
+ * How many sources one `postMessage` carries.
56
+ *
57
+ * Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
58
+ * CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
59
+ * the number that matters most in this file — tune it against the benchmark, not by taste.
60
+ */
61
+ export declare const SOURCE_CHUNK_SIZE = 25;
62
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,249 @@
1
+ /* eslint-disable-next-line import/default -- Vite answers this specifier with a module it
2
+ generates, whose default export is a `Worker` subclass. The ESLint resolver drops the query and
3
+ reads the worker entry instead, which exports no default of its own. `vite/client` declares the
4
+ real shape, so tsc sees it. */
5
+ import AtlasWorker from './atlas-worker?worker&inline';
6
+ /**
7
+ * Packs a set of sources in a web worker.
8
+ *
9
+ * **One worker serves every pack, and no pack ever stops it.** The atlases come back as
10
+ * `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
11
+ * with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
12
+ * move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
13
+ * bitmap it made, not only the first upload of it. Terminating the worker used to run before this
14
+ * function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
15
+ *
16
+ * The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
17
+ * before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
18
+ * left running. The worker script is inlined into the bundle, so starting one resolves no URL at
19
+ * runtime, which matters because the SDK is loaded from origins it does not control.
20
+ *
21
+ * Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
22
+ * has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
23
+ * that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
24
+ * a layer's worth of GPU memory.
25
+ *
26
+ * The sources go over in batches so the main thread is never blocked for long by one structured
27
+ * clone, and so the worker can decode the early batches while the later ones are still being sent.
28
+ * A closing message tells it every source has arrived.
29
+ * @param sources - The unique sources to pack, in the caller's order; placements address them by
30
+ * their index in this array.
31
+ * @param maxDimension - Largest atlas edge in pixels.
32
+ * @param options - The abort signal, and the worker factory a spec overrides.
33
+ * @returns The packed atlases and one report per source.
34
+ */
35
+ export function packInWorker(sources, maxDimension, options = {}) {
36
+ const { signal, createWorker = createAtlasWorker } = options;
37
+ return new Promise((resolve, reject) => {
38
+ // Read the signal before anything is sent. A pack that is already abandoned must cost the
39
+ // worker nothing, so it posts no message at all.
40
+ if (signal?.aborted) {
41
+ reject(abandoned());
42
+ return;
43
+ }
44
+ let worker;
45
+ try {
46
+ worker = pooledWorker(createWorker);
47
+ }
48
+ catch (error) {
49
+ // A host page whose `script-src` forbids `blob:` and `data:` makes `new Worker()` throw. The
50
+ // caller falls back to an inline pack.
51
+ reject(asError(error));
52
+ return;
53
+ }
54
+ const id = ++lastRequestId;
55
+ // Every exit runs through here. Dropping this pack's entry is what stops delivery, because the
56
+ // worker is shared and goes on running.
57
+ const done = (settle) => {
58
+ signal?.removeEventListener('abort', onAbort);
59
+ pending.delete(id);
60
+ settle();
61
+ };
62
+ // A plain Error, not `signal.reason`, which Chrome 66 and Safari 15 do not have. Nothing
63
+ // branches on the error's type: every caller reads `signal.aborted` instead.
64
+ const onAbort = () => {
65
+ done(() => {
66
+ cancel(worker, id);
67
+ reject(abandoned());
68
+ });
69
+ };
70
+ pending.set(id, (message) => {
71
+ done(() => {
72
+ if (message.type === 'failed')
73
+ reject(new Error(message.message));
74
+ else
75
+ resolve({ atlases: message.atlases, reports: message.reports });
76
+ });
77
+ });
78
+ signal?.addEventListener('abort', onAbort);
79
+ try {
80
+ for (let start = 0; start < sources.length; start += SOURCE_CHUNK_SIZE) {
81
+ post(worker, {
82
+ type: 'sources',
83
+ id,
84
+ sources: sources.slice(start, start + SOURCE_CHUNK_SIZE),
85
+ });
86
+ }
87
+ post(worker, { type: 'pack', id, maxDimension });
88
+ }
89
+ catch (error) {
90
+ // `postMessage` throws when the platform refuses to copy a source. The worker keeps the
91
+ // batches that did arrive until it is told otherwise, and the closing message never comes.
92
+ done(() => {
93
+ cancel(worker, id);
94
+ reject(asError(error));
95
+ });
96
+ }
97
+ });
98
+ }
99
+ /**
100
+ * Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
101
+ *
102
+ * Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
103
+ * the first spec's fake to every spec after it.
104
+ */
105
+ export function resetAtlasWorkerPool() {
106
+ pool = null;
107
+ pending.clear();
108
+ }
109
+ /**
110
+ * Hands back the one worker every pack runs on, starting it on the first call.
111
+ *
112
+ * The reply handler lives on the worker and not on a pack, because the worker outlives every pack
113
+ * it serves and a reply for a pack nobody wants still has pixels to free.
114
+ * @param createWorker - Makes the worker, if there is not one yet.
115
+ * @returns The pooled worker.
116
+ */
117
+ function pooledWorker(createWorker) {
118
+ if (pool)
119
+ return pool.worker;
120
+ // Not stored until it exists: a factory that throws must be retried by the next pack, not cached.
121
+ const started = { worker: createWorker(), produced: false };
122
+ started.worker.onmessage = (event) => {
123
+ const message = event.data;
124
+ const deliver = pending.get(message.id);
125
+ if (deliver) {
126
+ // These pixels are about to be mounted, so from here on this worker must not be stopped.
127
+ if (message.type === 'packed' && message.atlases.length > 0)
128
+ started.produced = true;
129
+ deliver(message);
130
+ return;
131
+ }
132
+ // Nobody is waiting: this pack was abandoned after the worker had already replied. An
133
+ // `ImageBitmap` holds its pixels outside the JavaScript heap and nothing else will ever close
134
+ // these, so every superseded pack would leak a layer's worth of GPU memory.
135
+ if (message.type === 'packed')
136
+ for (const atlas of message.atlases)
137
+ atlas.bitmap.close();
138
+ };
139
+ started.worker.onerror = (event) => {
140
+ retire(started);
141
+ failAll(event.message || 'atlas-worker: failed to start');
142
+ };
143
+ started.worker.onmessageerror = () => {
144
+ // This never fires on the primary target. `Worker`'s `messageerror` is `false` for Chrome,
145
+ // Chrome Android and Android WebView, and Safari only added it in 16.4, which is above the
146
+ // `safari15`/`ios15` floor (checked against `@mdn/browser-compat-data@6.1.5`, as installed
147
+ // here). Assigning it is free and Firefox has had it since 57, so it is kept for the browsers
148
+ // that do support it. It is not a guard the build targets can rely on.
149
+ //
150
+ // A reply that does not deserialize is lost, and it carries no id, so there is no way to tell
151
+ // whose it was. Without this the layer waits for a reply that never comes and stays blank.
152
+ retire(started);
153
+ failAll('atlas-worker: a reply could not be read');
154
+ };
155
+ pool = started;
156
+ return started.worker;
157
+ }
158
+ /**
159
+ * Drops a broken worker, so the next pack starts a fresh one.
160
+ *
161
+ * A worker that has handed out an atlas is dropped and left running. Stopping it would destroy the
162
+ * GPU memory behind every bitmap it made, which is the failure this whole file is shaped around. A
163
+ * worker that produced nothing is stopped, because nothing depends on it.
164
+ * @param broken - The pooled worker to drop.
165
+ */
166
+ function retire(broken) {
167
+ if (pool === broken)
168
+ pool = null;
169
+ if (!broken.produced)
170
+ broken.worker.terminate();
171
+ }
172
+ /**
173
+ * Abandons every pack waiting on the worker that just broke.
174
+ * @param message - What to reject each of them with.
175
+ */
176
+ function failAll(message) {
177
+ for (const [id, deliver] of [...pending])
178
+ deliver({ type: 'failed', id, message });
179
+ }
180
+ /**
181
+ * Tells the worker to drop a pack nobody wants any more.
182
+ *
183
+ * Termination used to do this. Now that the worker outlives its packs, a superseded pack would
184
+ * otherwise run to the end and hold the worker off the next one.
185
+ * @param worker - The pooled worker.
186
+ * @param id - The pack to drop.
187
+ */
188
+ function cancel(worker, id) {
189
+ try {
190
+ post(worker, { type: 'cancel', id });
191
+ }
192
+ catch {
193
+ // A worker that will not take this message is already unusable, and the pack it would have
194
+ // dropped is dropped either way.
195
+ }
196
+ }
197
+ /**
198
+ * Sends one request to the worker.
199
+ *
200
+ * **Nothing is ever transferred.** A `Blob` is a handle to an immutable, reference-counted byte
201
+ * store, so a structured clone copies the handle and not the bytes. Being cheap to clone is the
202
+ * property that matters here; being transferable is not, and transferring a Blob is not a thing the
203
+ * platform offers. A `Blob` is also the only source that reaches this function, so no object a def
204
+ * owns is ever detached by a pack.
205
+ *
206
+ * The typed parameter is why this function exists: `postMessage` takes `any`, so a message built at
207
+ * the call site would go unchecked.
208
+ * @param worker - The worker to send to.
209
+ * @param request - The message.
210
+ */
211
+ function post(worker, request) {
212
+ worker.postMessage(request);
213
+ }
214
+ /**
215
+ * Makes the bundled atlas worker.
216
+ * @returns A fresh worker.
217
+ */
218
+ function createAtlasWorker() {
219
+ return new AtlasWorker();
220
+ }
221
+ /**
222
+ * The error every abandoned pack rejects with.
223
+ * @returns The error.
224
+ */
225
+ function abandoned() {
226
+ return new Error('atlas-worker: the pack was abandoned');
227
+ }
228
+ /**
229
+ * Turns whatever was thrown into an `Error`.
230
+ * @param error - What was thrown.
231
+ * @returns An error.
232
+ */
233
+ function asError(error) {
234
+ return error instanceof Error ? error : new Error(String(error));
235
+ }
236
+ /**
237
+ * How many sources one `postMessage` carries.
238
+ *
239
+ * Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
240
+ * CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
241
+ * the number that matters most in this file — tune it against the benchmark, not by taste.
242
+ */
243
+ export const SOURCE_CHUNK_SIZE = 25;
244
+ /** Every pack that has been sent and is waiting for its reply, by request id. */
245
+ const pending = new Map();
246
+ /** The worker every pack runs on, once the first pack has started it. */
247
+ let pool = null;
248
+ /** The last id handed to a pack. Ids only ever climb, so no reply can find the wrong caller. */
249
+ let lastRequestId = 0;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The worker's public face: the main thread's half only.
3
+ *
4
+ * The worker entry is deliberately absent. It imports `packSources` and carries the
5
+ * `?worker&inline` specifier, and nothing outside this directory should pull either in by
6
+ * reaching past the barrel. Types are the exception, because they are erased at emit.
7
+ */
8
+ export { type WorkerAtlas, type WorkerSource } from './atlas-worker';
9
+ export { packInWorker, type PackInWorkerOptions, SOURCE_CHUNK_SIZE } from './client';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { packInWorker, SOURCE_CHUNK_SIZE } from './client';
@@ -5,10 +5,10 @@ export interface LayerProps {
5
5
  /** The layer definition to render, together with its descendants. */
6
6
  layer: LayerDef;
7
7
  /**
8
- * Rendered inside the root group, before the tree's own content — under the root layer's dim
9
- * provider, which is what the scene's background quad mounts here for: the root's dim reaches it
10
- * like any layer's, and its material exists before every layer's, which its draw order relies
11
- * on.
8
+ * Rendered inside the root group, before the tree's own content. This sits under the root
9
+ * layer's dim and overlay providers. The scene's background quad mounts here for that reason:
10
+ * the root's dim reaches it like any layer's, and its material exists before every layer's,
11
+ * which its draw order relies on.
12
12
  */
13
13
  children?: ReactNode;
14
14
  }
@@ -18,8 +18,9 @@ export interface LayerProps {
18
18
  * This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
19
19
  * "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
20
20
  * scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
21
- * carries: nothing above the root can hide it, and the whole tree's draw order lives inside
22
- * `[0, 1)`.
21
+ * carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
22
+ * `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
23
+ * me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
23
24
  * @param props - The layer props.
24
25
  * @returns The tree's root group.
25
26
  */
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, use } from 'react';
2
+ import { Suspense } from 'react';
3
3
  import { useLayoutTargets } from '../../events/layout-targets';
4
4
  import { usePointerProps } from '../../events/pointer';
5
5
  import { isImageDefs, isLayerDefs, isLineDefs, isModelDefs, isShapeDefs, isTextDefs, } from '../../types/guards';
6
6
  import { Images } from '../images';
7
7
  import { Lines } from '../lines';
8
- import { LayerDimContext } from '../materials/effects/color-dim';
9
8
  import { Models } from '../models';
10
9
  import { Shapes } from '../shapes';
11
10
  import { Texts } from '../text';
11
+ import { LayerDimContext, LayerOverlayContext, useLayerDim, useLayerOverlay, } from './layer-context';
12
12
  import { useStagedChildren } from './mount-scheduler';
13
13
  import { useVisibilityGui } from './visibility-gui';
14
14
  /**
@@ -17,15 +17,16 @@ import { useVisibilityGui } from './visibility-gui';
17
17
  * This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
18
18
  * "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
19
19
  * scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
20
- * carries: nothing above the root can hide it, and the whole tree's draw order lives inside
21
- * `[0, 1)`.
20
+ * carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
21
+ * `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
22
+ * me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
22
23
  * @param props - The layer props.
23
24
  * @returns The tree's root group.
24
25
  */
25
26
  export function Layer(props) {
26
27
  const { layer, children } = props;
27
28
  useVisibilityGui(layer);
28
- return (_jsx(LayerNode, { layer: layer, parentVisible: true, orderBase: 0, orderStep: 1, children: children }));
29
+ return (_jsx(LayerNode, { layer: layer, hidden: false, orderBase: 0, orderStep: 1, children: children }));
29
30
  }
30
31
  /**
31
32
  * One layer of the tree as a three.js `<group>`, recursing into its child layers.
@@ -43,45 +44,43 @@ export function Layer(props) {
43
44
  * whose membership depends on the camera re-derives it. From then on a pixel-size change reaches
44
45
  * those defs through the frame loop, with no render of this component involved.
45
46
  *
46
- * **Draw order is a group prop.** three propagates a group's `renderOrder` down its subtree as
47
- * `groupOrder`, which `painterSortStable` compares before `renderOrder` and `material.id` — so
48
- * numbering the groups is all it takes to make array order the draw order. A nested group
49
- * overwrites `groupOrder` for its own subtree, so the value that decides a mesh's place is its
50
- * nearest enclosing layer's; the numbers on layers above it are overwritten and simply do not
51
- * matter. Without this the order falls out of `material.id`, which ascends in mount order and
52
- * therefore puts any material built after first paint — every model's, since they exist only once
53
- * its GLB has loaded — last in the scene regardless of where its layer sits.
47
+ * Draw order is a pair: the tier, then the layer's own place. `overlay` adds a whole band above
48
+ * every plan-tier value. Array order still decides strictly within each band.
54
49
  * @param props - The node props.
55
50
  * @returns The `<group>` for a layer def.
56
51
  */
57
52
  function LayerNode(props) {
58
- const { layer, children: prelude, parentVisible, orderBase, orderStep } = props;
59
- // On screen only if nothing above hides it either — the value picking hangs off, and what the
60
- // children inherit in turn.
61
- const visible = parentVisible && !layer.hidden;
62
- // Nearest declaration wins and reaches every material beneath through context — so only
63
- // `undefined` inherits, and a layer's own `0` un-dims it inside a dimmed ancestor. Unlike
64
- // visibility this is never a prop: nothing between here and the material has any use for it.
65
- const inheritedDim = use(LayerDimContext);
66
- const dim = layer.dim ?? inheritedDim;
53
+ const { layer, children: prelude, hidden: parentHidden, orderBase, orderStep } = props;
54
+ // What the nearest ancestor already resolved for dim and overlay. A layer cannot see its
55
+ // ancestors any other way.
56
+ const parentDim = useLayerDim();
57
+ const parentOverlay = useLayerOverlay();
58
+ // Once hidden or once in the overlay tier, a subtree stays that way. A descendant that dropped
59
+ // back into the plan tier would invert the nesting, and a descendant of a hidden layer is not on
60
+ // screen whatever it says about itself.
61
+ const hidden = parentHidden || layer.hidden === true;
62
+ const overlay = parentOverlay || layer.overlay === true;
63
+ // Nearest declaration wins, so only `undefined` inherits. A layer's own 0 un-dims it inside a
64
+ // dimmed ancestor.
65
+ const dim = layer.dim ?? parentDim;
66
+ // On screen only if nothing above hides it either. This is the value picking hangs off.
67
+ const visible = !hidden;
67
68
  const { children, mounted } = useStagedChildren(layer);
68
69
  useLayoutTargets(layer, children);
69
70
  const pointerProps = usePointerProps(layer, visible);
70
71
  let contents = null;
71
72
  if (isLayerDefs(children)) {
72
73
  const childStep = orderStep / children.length;
73
- contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, parentVisible: visible, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
74
+ contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, hidden: hidden, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
74
75
  }
75
76
  else if (mounted > 0) {
76
77
  contents = _jsx(LeafContent, { defs: children, mounted: mounted });
77
78
  }
78
- const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: orderBase, ...pointerProps, children: [prelude, contents] }));
79
- // Always provided, never conditionally on the layer declaring a `dim`: making the provider appear
80
- // when one is first set would change this component's element type, and React would remount the
81
- // subtree — releasing and re-claiming every GPU slot beneath it, exactly what an eased dim must
82
- // not do. A layer that declares none provides what it inherited, which no consumer can tell apart
83
- // from not providing at all.
84
- return _jsx(LayerDimContext, { value: dim, children: group });
79
+ const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: (overlay ? OVERLAY_TIER : PLAN_TIER) + orderBase, ...pointerProps, children: [prelude, contents] }));
80
+ // Always provided, never only when the layer declares one. Making a provider appear on first use
81
+ // would change this component's element type and remount the subtree, which releases and re-claims
82
+ // every GPU slot beneath it.
83
+ return (_jsx(LayerDimContext, { value: dim, children: _jsx(LayerOverlayContext, { value: overlay, children: group }) }));
85
84
  }
86
85
  /**
87
86
  * The content of one leaf layer: the container for its defs' kind, inside a Suspense boundary of
@@ -112,3 +111,14 @@ function LeafContent(props) {
112
111
  content = _jsx(Models, { defs: defs, mounted: mounted });
113
112
  return _jsx(Suspense, { fallback: null, children: content });
114
113
  }
114
+ /**
115
+ * Where the plan's own layers are numbered from. The tree subdivides `[0, 1)` depth first, so the
116
+ * mantissa stays free for nesting.
117
+ */
118
+ const PLAN_TIER = 0;
119
+ /**
120
+ * Where an overlay subtree is numbered from, which puts it in `[1, 2)`. That is after every
121
+ * plan-tier layer, whatever renderOrder those layers hold. The whole cost is one bit of mantissa for
122
+ * subdivision.
123
+ */
124
+ const OVERLAY_TIER = 1;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The dim amount a layer draws at, which every material beneath it reads.
3
+ * @returns The nearest provided amount, or `0` with no layer above.
4
+ */
5
+ export declare function useLayerDim(): number;
6
+ /**
7
+ * Whether the current layer draws in the overlay tier.
8
+ * @returns `true` if this layer or an ancestor opted into the overlay tier, `false` otherwise.
9
+ */
10
+ export declare function useLayerOverlay(): boolean;
11
+ /**
12
+ * The dim amount the current layer hands down to its descendants.
13
+ *
14
+ * Context rather than a prop, because the value crosses the whole subtree and concerns none of the
15
+ * components between. A container hands its material nothing about layers, and a material that
16
+ * never asked about them still dims correctly. Threading it as a prop would put a `dim` on every
17
+ * material, every container, and every one of their inner components.
18
+ *
19
+ * Read through {@link useLayerDim} instead of directly. A `LayerNode` provides it, always, never
20
+ * only when it declares a `dim` of its own. See `Layer.tsx` for why.
21
+ *
22
+ * Defaults to `0`, undimmed, which is what "no layer above me" means for a component rendered
23
+ * outside a `Layer` tree. A fourth inherited field belongs in a context and a hook of its own here,
24
+ * not folded into this one.
25
+ */
26
+ export declare const LayerDimContext: import("react").Context<number>;
27
+ /**
28
+ * Whether the current layer hands its overlay tier down to its descendants.
29
+ *
30
+ * Context rather than a prop, for the reason {@link LayerDimContext} is. It is also not a shading
31
+ * term, which is why `overlayParams`, the material parameters this tier unlocks, stays in
32
+ * `objects/materials/shared.ts` instead of moving here.
33
+ *
34
+ * Read through {@link useLayerOverlay} instead of directly. A `LayerNode` provides it, always,
35
+ * never only when it declares an `overlay` of its own. See `Layer.tsx` for why.
36
+ *
37
+ * Defaults to `false`, the plan tier, which is what "no layer above me" means for a component
38
+ * rendered outside a `Layer` tree.
39
+ */
40
+ export declare const LayerOverlayContext: import("react").Context<boolean>;
41
+ //# sourceMappingURL=layer-context.d.ts.map