@expofp/renderer 3.38.0 → 3.39.1
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/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/events/LayoutDriver.d.ts +6 -6
- package/dist/events/LayoutDriver.js +9 -12
- package/dist/events/layout-targets.d.ts +35 -34
- package/dist/events/layout-targets.js +127 -107
- package/dist/objects/containers/hooks.d.ts +19 -4
- package/dist/objects/containers/hooks.js +101 -23
- package/dist/objects/containers/mesh-writes.js +3 -3
- package/dist/objects/images/ImageInstance.js +104 -36
- package/dist/objects/images/Images.d.ts +8 -7
- package/dist/objects/images/Images.js +94 -28
- package/dist/objects/images/image-container.d.ts +14 -3
- package/dist/objects/images/image-container.js +19 -5
- package/dist/objects/layer/Layer.d.ts +7 -6
- package/dist/objects/layer/Layer.js +77 -57
- package/dist/objects/layer/layer-context.d.ts +74 -0
- package/dist/objects/layer/layer-context.js +82 -0
- package/dist/objects/materials/ColorMaterial.js +16 -3
- package/dist/objects/materials/LineMaterial.js +4 -2
- package/dist/objects/materials/TextureMaterial.js +4 -2
- package/dist/objects/materials/effects/color-dim.d.ts +3 -11
- package/dist/objects/materials/effects/color-dim.js +17 -17
- 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/ShapeInstance.js +1 -2
- package/dist/objects/shapes/Shapes.js +2 -9
- package/dist/objects/text/TextInstance.js +2 -2
- 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 +37 -0
- 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
|
@@ -52,9 +52,9 @@ export function useLayoutTargets(layer, children) {
|
|
|
52
52
|
export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
53
53
|
const groups = new Set();
|
|
54
54
|
/**
|
|
55
|
-
* The pixel size each def was last laid out at — the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* The pixel size each def was last laid out at — the registry's layout state, because the rule
|
|
56
|
+
* that lays a def out is the def's own. Keyed by def rather than by group, so a membership change
|
|
57
|
+
* re-lays out only the defs that joined.
|
|
58
58
|
*
|
|
59
59
|
* A def whose `onScale` is *replaced* after it was laid out is not noticed: the record says only
|
|
60
60
|
* "laid out at this size". Reassigning a handler is a thing nothing does — a def is built with its
|
|
@@ -63,21 +63,25 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
63
63
|
const layouts = new WeakMap();
|
|
64
64
|
/**
|
|
65
65
|
* The roll each def's `onRoll` last ran at — the other axis's record, kept apart from
|
|
66
|
-
* {@link layouts} because the two move independently
|
|
67
|
-
* and a turn leaves every scale record current.
|
|
66
|
+
* {@link layouts} because the two move independently.
|
|
68
67
|
*/
|
|
69
68
|
const rolls = new WeakMap();
|
|
70
69
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* at drain time by the per-axis records above, so a def stale on both axes is dequeued once and
|
|
74
|
-
* laid out on both, and a new axis adds a record and a rule, never another queue. `deferred`
|
|
75
|
-
* holds the ones a drain pushed behind the visible — hidden defs, and defs the frustum
|
|
76
|
-
* rejected — worked off with whatever budget the stale pass leaves. Sets, not arrays, because one
|
|
77
|
-
* def can be re-marked while queued and must not be laid out twice.
|
|
70
|
+
* Whether each def was hidden when it was last written — what tells a visibility flip from a
|
|
71
|
+
* resize. Seeded at registration, which is when the def's instance wrote what it held.
|
|
78
72
|
*/
|
|
79
|
-
const
|
|
73
|
+
const drawnHidden = new WeakMap();
|
|
74
|
+
/**
|
|
75
|
+
* The write queue, in priority order: defs a rule flipped between hidden and shown, then every
|
|
76
|
+
* other def a rule changed, then the ones a drain found outside the frustum. Sets, not arrays,
|
|
77
|
+
* because a def re-reported before its write must still be written once — with whatever its
|
|
78
|
+
* fields hold by then.
|
|
79
|
+
*/
|
|
80
|
+
const flipped = new Set();
|
|
81
|
+
const queued = new Set();
|
|
80
82
|
const deferred = new Set();
|
|
83
|
+
/** What the sweep in flight has cost so far, for the log line that closes it. */
|
|
84
|
+
let sweep;
|
|
81
85
|
let current = pixelSize;
|
|
82
86
|
let currentRoll = roll;
|
|
83
87
|
/**
|
|
@@ -87,81 +91,90 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
87
91
|
*/
|
|
88
92
|
const view = { pixelSize: current, roll: currentRoll };
|
|
89
93
|
/**
|
|
90
|
-
* Runs a def's
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `update(def)` — so a handler mutating def fields needs no `update()` call of its own, and the two
|
|
94
|
-
* paths cannot disagree about what a def looks like. A def whose instance has not mounted yet syncs
|
|
95
|
-
* to nothing here and picks the mutation up when it does. A layer among the returned defs is
|
|
96
|
-
* reconciled instead, which is how a handler changes its own membership.
|
|
94
|
+
* Runs whatever of a def's rules are stale by their records, scale before roll so a rule that
|
|
95
|
+
* turns a label re-fits it at the size just applied. Each rule is recorded as run before anything
|
|
96
|
+
* it reported is handled, because handling can reach back in here.
|
|
97
97
|
*
|
|
98
|
-
* A
|
|
99
|
-
* the
|
|
100
|
-
* the handler over it again.
|
|
98
|
+
* A rule reporting nothing is still recorded as run, so a membership change does not run it over
|
|
99
|
+
* the def again — reporting nothing is how a rule skips the write, which is the half that costs.
|
|
101
100
|
* @param def - The def to lay out.
|
|
102
|
-
* @
|
|
101
|
+
* @param report - Receives what each rule reported changed.
|
|
103
102
|
*/
|
|
104
|
-
function
|
|
103
|
+
function layOutStale(def, report) {
|
|
105
104
|
// Hidden defs are laid out too. Whether a def draws at all can be exactly what its rule decides
|
|
106
105
|
// — a booth's dot appears only once the plan is far enough out for its label to stop fitting —
|
|
107
|
-
// so a registry that skipped them would never run the rule that would bring them back.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
// so a registry that skipped them would never run the rule that would bring them back.
|
|
107
|
+
if (def.onScale && layouts.get(def) !== current) {
|
|
108
|
+
const changed = def.onScale(view);
|
|
109
|
+
layouts.set(def, current);
|
|
110
|
+
if (changed?.length)
|
|
111
|
+
report(changed);
|
|
112
|
+
}
|
|
113
|
+
if (def.onRoll && rolls.get(def) !== currentRoll) {
|
|
114
|
+
const changed = def.onRoll(view);
|
|
115
|
+
rolls.set(def, currentRoll);
|
|
116
|
+
if (changed?.length)
|
|
117
|
+
report(changed);
|
|
118
|
+
}
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* @param def - The def to
|
|
124
|
-
* @returns Whether anything was written.
|
|
121
|
+
* Writes a def now — the layer path or the slot path, per `writeChanged` — and records whether
|
|
122
|
+
* what it drew was hidden.
|
|
123
|
+
* @param def - The def to write.
|
|
125
124
|
*/
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const changed = def.onRoll(view);
|
|
130
|
-
rolls.set(def, currentRoll);
|
|
131
|
-
if (!changed || changed.length === 0)
|
|
132
|
-
return false;
|
|
133
|
-
writeChanged(changed);
|
|
134
|
-
return true;
|
|
125
|
+
function write(def) {
|
|
126
|
+
drawnHidden.set(def, def.hidden === true);
|
|
127
|
+
writeChanged([def]);
|
|
135
128
|
}
|
|
136
129
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* @param
|
|
140
|
-
* @returns Whether anything was written.
|
|
130
|
+
* Queues what a rule reported changed. A layer is written at once: its change is a membership
|
|
131
|
+
* change, which only React reconciles, and React already defers the sweep behind it.
|
|
132
|
+
* @param changed - The defs the rule reported.
|
|
141
133
|
*/
|
|
142
|
-
function
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
134
|
+
function enqueue(changed) {
|
|
135
|
+
for (const def of changed) {
|
|
136
|
+
if (isLayerDef(def)) {
|
|
137
|
+
write(def);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const drawn = drawnHidden.get(def);
|
|
141
|
+
if (drawn !== undefined && drawn !== (def.hidden === true)) {
|
|
142
|
+
queued.delete(def);
|
|
143
|
+
deferred.delete(def);
|
|
144
|
+
flipped.add(def);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Flipped and flipped back before its write: against what the slot holds it is a resize.
|
|
148
|
+
flipped.delete(def);
|
|
149
|
+
deferred.delete(def);
|
|
150
|
+
queued.add(def);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
146
153
|
}
|
|
147
154
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* @param carriesRule - Whether a def declares the moved axis's rule.
|
|
155
|
+
* Runs the stale rules of every registered def the moved axis concerns, queueing their writes.
|
|
156
|
+
* @param axis - The axis that moved, for the log line.
|
|
157
|
+
* @param handler - The moved axis's handler, `onScale` or `onRoll`.
|
|
152
158
|
*/
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Rule presence is read at marking time, so a rule cleared after registration — a hidden
|
|
158
|
-
// bookmark's — simply stops queueing, with nothing to unregister.
|
|
159
|
+
function runHandlers(axis, handler) {
|
|
160
|
+
const start = performance.now();
|
|
161
|
+
const before = flipped.size + queued.size + deferred.size;
|
|
162
|
+
let ran = 0;
|
|
159
163
|
for (const defs of groups) {
|
|
160
164
|
for (const def of defs) {
|
|
161
|
-
if (
|
|
162
|
-
|
|
165
|
+
if (!def[handler])
|
|
166
|
+
continue;
|
|
167
|
+
ran++;
|
|
168
|
+
layOutStale(def, enqueue);
|
|
163
169
|
}
|
|
164
170
|
}
|
|
171
|
+
const pending = flipped.size + queued.size + deferred.size;
|
|
172
|
+
if (pending > before) {
|
|
173
|
+
sweep ??= { defs: 0, drains: 0, writeMs: 0 };
|
|
174
|
+
sweep.defs += pending - before;
|
|
175
|
+
}
|
|
176
|
+
logger.debug(`${axis}: ran ${ran} ${handler} handlers in ${(performance.now() - start).toFixed(2)}ms, ` +
|
|
177
|
+
`${pending} defs to write (${flipped.size} visibility flips)`);
|
|
165
178
|
}
|
|
166
179
|
return {
|
|
167
180
|
get pixelSize() {
|
|
@@ -171,25 +184,33 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
171
184
|
return currentRoll;
|
|
172
185
|
},
|
|
173
186
|
get hasWork() {
|
|
174
|
-
return
|
|
187
|
+
return flipped.size > 0 || queued.size > 0 || deferred.size > 0;
|
|
175
188
|
},
|
|
176
189
|
register(defs) {
|
|
177
190
|
groups.add(defs);
|
|
178
|
-
let
|
|
191
|
+
let wrote = false;
|
|
179
192
|
for (const def of defs) {
|
|
180
|
-
if (
|
|
181
|
-
|
|
193
|
+
if (!drawnHidden.has(def))
|
|
194
|
+
drawnHidden.set(def, def.hidden === true);
|
|
195
|
+
// Written at once, not queued: a def is current from the moment it registers, so one
|
|
196
|
+
// mounting mid-zoom draws at the size the plan is at.
|
|
197
|
+
layOutStale(def, (changed) => {
|
|
198
|
+
for (const each of changed)
|
|
199
|
+
write(each);
|
|
200
|
+
wrote = true;
|
|
201
|
+
});
|
|
182
202
|
}
|
|
183
203
|
// Registration happens in an effect rather than inside the frame loop, so anything written here
|
|
184
204
|
// needs a frame asking for it.
|
|
185
|
-
if (
|
|
205
|
+
if (wrote)
|
|
186
206
|
invalidate();
|
|
187
207
|
return () => {
|
|
188
208
|
groups.delete(defs);
|
|
189
|
-
// A queued def whose layer is gone must not be
|
|
190
|
-
// unmounting
|
|
209
|
+
// A queued def whose layer is gone must not be written from the backlog: its instances are
|
|
210
|
+
// unmounting.
|
|
191
211
|
for (const def of defs) {
|
|
192
|
-
|
|
212
|
+
flipped.delete(def);
|
|
213
|
+
queued.delete(def);
|
|
193
214
|
deferred.delete(def);
|
|
194
215
|
}
|
|
195
216
|
};
|
|
@@ -197,59 +218,58 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
197
218
|
applyScale(nextPixelSize) {
|
|
198
219
|
current = nextPixelSize;
|
|
199
220
|
view.pixelSize = nextPixelSize;
|
|
200
|
-
|
|
221
|
+
runHandlers(`pxToSvgScale ${+nextPixelSize.toFixed(3)}`, 'onScale');
|
|
201
222
|
},
|
|
202
223
|
applyRoll(nextRoll) {
|
|
203
224
|
currentRoll = nextRoll;
|
|
204
225
|
view.roll = nextRoll;
|
|
205
|
-
|
|
226
|
+
runHandlers(`roll ${+nextRoll.toFixed(3)}`, 'onRoll');
|
|
206
227
|
},
|
|
207
228
|
drain(frustum, budgetMs = -1) {
|
|
208
|
-
if (
|
|
229
|
+
if (flipped.size === 0 && queued.size === 0 && deferred.size === 0)
|
|
209
230
|
return false;
|
|
210
231
|
const start = performance.now();
|
|
211
|
-
let wrote = 0;
|
|
212
232
|
/**
|
|
213
|
-
* Defs this drain has taken off a queue,
|
|
233
|
+
* Defs this drain has taken off a queue, written or deferred.
|
|
214
234
|
*
|
|
215
235
|
* It gates the budget so that the **first** def of a drain is always taken. Without that, a
|
|
216
236
|
* budget already spent before the loop began — the worker descheduled between `start` and the
|
|
217
237
|
* check, which a loaded machine does routinely — dequeues nothing at all, and a drain that
|
|
218
|
-
* dequeues nothing is the stall this budget exists to prevent.
|
|
219
|
-
* the guarantee is one def per drain rather than one per pass.
|
|
238
|
+
* dequeues nothing is the stall this budget exists to prevent.
|
|
220
239
|
*/
|
|
221
240
|
let processed = 0;
|
|
222
241
|
const overBudget = () => processed > 0 && budgetMs > 0 && performance.now() - start >= budgetMs;
|
|
223
|
-
// Sets iterate in insertion order and tolerate deletion mid-walk, which is what makes
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
// Sets iterate in insertion order and tolerate deletion mid-walk, which is what makes each pass
|
|
243
|
+
// a plain loop: a written def leaves its set before its write runs.
|
|
244
|
+
for (const queue of [flipped, queued]) {
|
|
245
|
+
for (const def of queue) {
|
|
246
|
+
if (overBudget())
|
|
247
|
+
break;
|
|
248
|
+
processed++;
|
|
249
|
+
queue.delete(def);
|
|
250
|
+
// Deferral is priority, never a skip: a def the camera cannot see joins the last pass.
|
|
251
|
+
if (frustum !== undefined && culledBy(def, frustum)) {
|
|
252
|
+
deferred.add(def);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
write(def);
|
|
237
256
|
}
|
|
238
|
-
if (layOutStale(def))
|
|
239
|
-
wrote++;
|
|
240
257
|
}
|
|
241
258
|
for (const def of deferred) {
|
|
242
259
|
if (overBudget())
|
|
243
260
|
break;
|
|
244
261
|
processed++;
|
|
245
262
|
deferred.delete(def);
|
|
246
|
-
|
|
247
|
-
wrote++;
|
|
263
|
+
write(def);
|
|
248
264
|
}
|
|
249
|
-
const remaining =
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
265
|
+
const remaining = flipped.size + queued.size + deferred.size;
|
|
266
|
+
if (sweep) {
|
|
267
|
+
sweep.drains++;
|
|
268
|
+
sweep.writeMs += performance.now() - start;
|
|
269
|
+
if (remaining === 0) {
|
|
270
|
+
logger.debug(`wrote ${sweep.defs} defs over ${sweep.drains} frames in ${sweep.writeMs.toFixed(2)}ms`);
|
|
271
|
+
sweep = undefined;
|
|
272
|
+
}
|
|
253
273
|
}
|
|
254
274
|
return remaining > 0;
|
|
255
275
|
},
|
|
@@ -3,16 +3,21 @@ import { type RenderableDef } from '../../types/defs';
|
|
|
3
3
|
import { type SlotContainer, type SlotMesh } from './SlotContainer';
|
|
4
4
|
/**
|
|
5
5
|
* Creates one layer's container and keeps it wired to its layer for as long as the component is
|
|
6
|
-
* mounted: the membership's paint order,
|
|
6
|
+
* mounted: the membership's paint order, how hits on its mesh resolve back to defs, and what runs
|
|
7
|
+
* when a member no longer belongs in this container's kind.
|
|
7
8
|
*
|
|
8
9
|
* The container is built once, from the first render's defs, and outlives every render after —
|
|
9
10
|
* membership changes grow it in place rather than rebuilding it, which is what lets the mesh R3F
|
|
10
11
|
* built from its args stand too. `create` is therefore called once and its later identities ignored.
|
|
11
12
|
* @param create - Builds the container. Called on the first render only.
|
|
12
13
|
* @param defs - The layer's defs, in authored paint order.
|
|
14
|
+
* @param onKindMismatch - Re-splits the layer, for a container that finds a member of the wrong
|
|
15
|
+
* kind. Registered as {@link SlotContainer.setKindMismatchHandler} and cleared on unmount or
|
|
16
|
+
* before a new identity replaces it. A container with no kind of its own to leave omits it, and
|
|
17
|
+
* nothing is registered.
|
|
13
18
|
* @returns The container.
|
|
14
19
|
*/
|
|
15
|
-
export declare function useContainer<T extends RenderableDef, C extends SlotContainer<unknown, SlotMesh, T>>(create: () => C, defs: readonly T[]): C;
|
|
20
|
+
export declare function useContainer<T extends RenderableDef, C extends SlotContainer<unknown, SlotMesh, T>>(create: () => C, defs: readonly T[], onKindMismatch?: () => void): C;
|
|
16
21
|
/**
|
|
17
22
|
* Claims a slot for one instance, releasing on unmount. Paint order is not this hook's concern —
|
|
18
23
|
* the container writes it for the whole sibling list in one pass (see {@link useContainer}).
|
|
@@ -37,10 +42,20 @@ export declare function useSlot<R>(container: SlotContainer<R>, def: RenderableD
|
|
|
37
42
|
* never drawn, so computing its transform, color, or glyphs would be wasted; the update that shows
|
|
38
43
|
* it re-runs this sync, which writes the then-current state. Everything type-specific stays in
|
|
39
44
|
* `write`, which is what keeps this hook generic across every def type.
|
|
45
|
+
*
|
|
46
|
+
* **An `update(def)` renders nothing unless the slot's resource moved.** The only thing a render of
|
|
47
|
+
* an instance can do that a sync cannot is re-claim its slot, and that is needed only when the
|
|
48
|
+
* resource backing it changed — a text's line count, a shape's geometry. So the store's snapshot is
|
|
49
|
+
* the resource itself, not a version counter: an update that leaves it alone (a dim, a color, a
|
|
50
|
+
* move) is written by the subscription's own listener, straight into the slot, and React bails out
|
|
51
|
+
* of the render because the snapshot is unchanged. An update that moves it renders, and the render's
|
|
52
|
+
* re-claim writes the def into the new slot.
|
|
40
53
|
* @param container - The container holding the slot.
|
|
41
54
|
* @param def - The def this instance renders.
|
|
42
|
-
* @param
|
|
55
|
+
* @param resourceOf - Reads the per-instance resource backing the slot off the def; a new identity
|
|
56
|
+
* re-claims. Called on every update, so it must be cheap and return the same identity for an
|
|
57
|
+
* unchanged def. `null` for a container whose slots need no resource, which never re-claims.
|
|
43
58
|
* @param write - Writes the def's type-specific state into its slot. Called only while visible.
|
|
44
59
|
*/
|
|
45
|
-
export declare function useDefSync<R>(container: SlotContainer<R>, def: RenderableDef,
|
|
60
|
+
export declare function useDefSync<R>(container: SlotContainer<R>, def: RenderableDef, resourceOf: (() => R) | null, write: (slot: number) => void): void;
|
|
46
61
|
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -1,38 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useStore } from '@react-three/fiber';
|
|
2
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
3
|
import { registerHitResolver } from '../../events/pointer';
|
|
4
|
-
import { registerSlotWriter, subscribeDef, syncNow
|
|
5
|
-
import {
|
|
4
|
+
import { registerSlotWriter, subscribeDef, syncNow } from '../../update';
|
|
5
|
+
import { resolveDim, subscribeDim, useDimChain } from '../layer/layer-context';
|
|
6
6
|
/**
|
|
7
7
|
* Creates one layer's container and keeps it wired to its layer for as long as the component is
|
|
8
|
-
* mounted: the membership's paint order,
|
|
8
|
+
* mounted: the membership's paint order, how hits on its mesh resolve back to defs, and what runs
|
|
9
|
+
* when a member no longer belongs in this container's kind.
|
|
9
10
|
*
|
|
10
11
|
* The container is built once, from the first render's defs, and outlives every render after —
|
|
11
12
|
* membership changes grow it in place rather than rebuilding it, which is what lets the mesh R3F
|
|
12
13
|
* built from its args stand too. `create` is therefore called once and its later identities ignored.
|
|
13
14
|
* @param create - Builds the container. Called on the first render only.
|
|
14
15
|
* @param defs - The layer's defs, in authored paint order.
|
|
16
|
+
* @param onKindMismatch - Re-splits the layer, for a container that finds a member of the wrong
|
|
17
|
+
* kind. Registered as {@link SlotContainer.setKindMismatchHandler} and cleared on unmount or
|
|
18
|
+
* before a new identity replaces it. A container with no kind of its own to leave omits it, and
|
|
19
|
+
* nothing is registered.
|
|
15
20
|
* @returns The container.
|
|
16
21
|
*/
|
|
17
|
-
export function useContainer(create, defs) {
|
|
22
|
+
export function useContainer(create, defs, onKindMismatch) {
|
|
18
23
|
const [container] = useState(create);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
24
|
+
const chain = useDimChain();
|
|
25
|
+
const invalidate = useInvalidate();
|
|
26
|
+
// The membership the dim subscription below re-orders, current as of the last commit.
|
|
27
|
+
const defsRef = useRef(defs);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
defsRef.current = defs;
|
|
30
|
+
});
|
|
23
31
|
// Paint order for the whole membership, one pass per structural change. Child effects run before
|
|
24
32
|
// this one, so every instance the change mounted has already claimed its slot; defs whose
|
|
25
33
|
// instances bailed out (memo) never re-ran, which is fine — order lives in the container, not in
|
|
26
34
|
// their props. Running inside the same effect flush as the claims is what keeps a frame from
|
|
27
35
|
// drawing between a claim's default order and this pass.
|
|
28
36
|
useEffect(() => {
|
|
29
|
-
container.setPaintOrder(defs,
|
|
37
|
+
container.setPaintOrder(defs, resolveDim(chain) > 0);
|
|
30
38
|
invalidate();
|
|
31
|
-
}, [container, defs,
|
|
39
|
+
}, [container, defs, chain, invalidate]);
|
|
40
|
+
// And again as the layer's dim crosses zero — only whether it is dimmed, not by how much, since the
|
|
41
|
+
// order pass splits two ways. Followed by subscription, not render: an eased dim crosses zero once
|
|
42
|
+
// for every container in the plan in the same frame, and rendering each of them to get here cost
|
|
43
|
+
// that frame more than the passes themselves.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let dimmed = resolveDim(chain) > 0;
|
|
46
|
+
return subscribeDim(chain, () => {
|
|
47
|
+
const next = resolveDim(chain) > 0;
|
|
48
|
+
if (next === dimmed)
|
|
49
|
+
return;
|
|
50
|
+
dimmed = next;
|
|
51
|
+
container.setPaintOrder(defsRef.current, next);
|
|
52
|
+
invalidate();
|
|
53
|
+
});
|
|
54
|
+
}, [container, chain, invalidate]);
|
|
32
55
|
// How a hit on this container's mesh becomes a def. From an effect, so the mesh is bound by now;
|
|
33
56
|
// the container answers which field of an intersection names its slot, so nothing here has to know
|
|
34
57
|
// one mesh kind from another.
|
|
35
58
|
useEffect(() => registerHitResolver(container.mesh, (hit) => container.defFromHit(hit)), [container]);
|
|
59
|
+
// Assigned after mount rather than passed to `create`, because the container is built once and
|
|
60
|
+
// outlives every render, so it cannot close over a fresh callback. A caller with nothing to run on
|
|
61
|
+
// a mismatch passes no handler, and this then does nothing: no call into the container and no
|
|
62
|
+
// cleanup closure either.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!onKindMismatch)
|
|
65
|
+
return;
|
|
66
|
+
container.setKindMismatchHandler(onKindMismatch);
|
|
67
|
+
return () => container.setKindMismatchHandler(undefined);
|
|
68
|
+
}, [container, onKindMismatch]);
|
|
36
69
|
return container;
|
|
37
70
|
}
|
|
38
71
|
/**
|
|
@@ -46,7 +79,7 @@ export function useContainer(create, defs) {
|
|
|
46
79
|
*/
|
|
47
80
|
export function useSlot(container, def, resource) {
|
|
48
81
|
const slotRef = useRef(-1);
|
|
49
|
-
const invalidate =
|
|
82
|
+
const invalidate = useInvalidate();
|
|
50
83
|
useEffect(() => {
|
|
51
84
|
const slot = container.claim(def, resource);
|
|
52
85
|
slotRef.current = slot;
|
|
@@ -76,15 +109,36 @@ export function useSlot(container, def, resource) {
|
|
|
76
109
|
* never drawn, so computing its transform, color, or glyphs would be wasted; the update that shows
|
|
77
110
|
* it re-runs this sync, which writes the then-current state. Everything type-specific stays in
|
|
78
111
|
* `write`, which is what keeps this hook generic across every def type.
|
|
112
|
+
*
|
|
113
|
+
* **An `update(def)` renders nothing unless the slot's resource moved.** The only thing a render of
|
|
114
|
+
* an instance can do that a sync cannot is re-claim its slot, and that is needed only when the
|
|
115
|
+
* resource backing it changed — a text's line count, a shape's geometry. So the store's snapshot is
|
|
116
|
+
* the resource itself, not a version counter: an update that leaves it alone (a dim, a color, a
|
|
117
|
+
* move) is written by the subscription's own listener, straight into the slot, and React bails out
|
|
118
|
+
* of the render because the snapshot is unchanged. An update that moves it renders, and the render's
|
|
119
|
+
* re-claim writes the def into the new slot.
|
|
79
120
|
* @param container - The container holding the slot.
|
|
80
121
|
* @param def - The def this instance renders.
|
|
81
|
-
* @param
|
|
122
|
+
* @param resourceOf - Reads the per-instance resource backing the slot off the def; a new identity
|
|
123
|
+
* re-claims. Called on every update, so it must be cheap and return the same identity for an
|
|
124
|
+
* unchanged def. `null` for a container whose slots need no resource, which never re-claims.
|
|
82
125
|
* @param write - Writes the def's type-specific state into its slot. Called only while visible.
|
|
83
126
|
*/
|
|
84
|
-
export function useDefSync(container, def,
|
|
127
|
+
export function useDefSync(container, def, resourceOf, write) {
|
|
128
|
+
// A container without resources is a `SlotContainer<null>`, so `null` is the resource it claims.
|
|
129
|
+
const readResource = (resourceOf ?? readNull);
|
|
130
|
+
const invalidate = useInvalidate();
|
|
131
|
+
// Refreshed after every render below; declared first because the subscription reaches for it.
|
|
132
|
+
const syncRef = useRef(noop);
|
|
133
|
+
const resource = useSyncExternalStore((listener) => subscribeDef(def, () => {
|
|
134
|
+
// This instance's own write rather than `syncNow`, which runs every instance drawing the
|
|
135
|
+
// def: each of them holds a subscription of its own, so `syncNow` here would write a def
|
|
136
|
+
// with N instances N² times.
|
|
137
|
+
syncRef.current();
|
|
138
|
+
invalidate();
|
|
139
|
+
listener();
|
|
140
|
+
}), readResource);
|
|
85
141
|
const slotRef = useSlot(container, def, resource);
|
|
86
|
-
const version = useSyncExternalStore((listener) => subscribeDef(def, listener), () => versionOf(def));
|
|
87
|
-
const invalidate = useThree((state) => state.invalidate);
|
|
88
142
|
/**
|
|
89
143
|
* Writes the def's current state into its slot: visibility always, the rest only when shown.
|
|
90
144
|
*/
|
|
@@ -92,6 +146,11 @@ export function useDefSync(container, def, resource, write) {
|
|
|
92
146
|
const slot = slotRef.current;
|
|
93
147
|
if (slot < 0)
|
|
94
148
|
return;
|
|
149
|
+
// The slot was claimed for the resource this render saw. When the def has since moved on to
|
|
150
|
+
// another — more lines than the claimed group holds — the write would overrun it; the render
|
|
151
|
+
// the change triggers re-claims and writes then.
|
|
152
|
+
if (!Object.is(readResource(), resource))
|
|
153
|
+
return;
|
|
95
154
|
const visible = !def.hidden;
|
|
96
155
|
container.setVisibleAt(slot, visible);
|
|
97
156
|
// Written even while hidden, unlike everything in `write`: it is one number, and writing it
|
|
@@ -103,16 +162,35 @@ export function useDefSync(container, def, resource, write) {
|
|
|
103
162
|
write(slot);
|
|
104
163
|
}
|
|
105
164
|
// Held in a ref and refreshed after every render: `sync` closes over a fresh `write` each time,
|
|
106
|
-
// and the effect below must not re-run just because that identity changed — only a
|
|
107
|
-
// means the
|
|
108
|
-
const syncRef = useRef(sync);
|
|
165
|
+
// and the effect below must not re-run just because that identity changed — only a new def or a
|
|
166
|
+
// new resource means the slot needs writing from here.
|
|
109
167
|
useEffect(() => {
|
|
110
168
|
syncRef.current = sync;
|
|
111
169
|
});
|
|
112
170
|
useEffect(() => registerSlotWriter(def, () => syncRef.current(), invalidate), [def, invalidate]);
|
|
113
|
-
//
|
|
114
|
-
//
|
|
171
|
+
// The first write, and the one after a re-claim. Declared after the ref refresh above, so it runs
|
|
172
|
+
// the closure that knows the new resource — `useSlot`'s own sync ran the previous render's, which
|
|
173
|
+
// the resource check turned into a no-op. Through the registry, so the frame request that shows it
|
|
174
|
+
// comes with it.
|
|
115
175
|
useEffect(() => {
|
|
116
176
|
syncNow(def);
|
|
117
|
-
}, [def,
|
|
177
|
+
}, [def, resource]);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The renderer's frame request, without subscribing to R3F's store for it.
|
|
181
|
+
*
|
|
182
|
+
* `useThree(selector)` subscribes, and every instance holds this hook's result — so with a selector
|
|
183
|
+
* each R3F state change (a canvas re-render re-configures the root) ran one selector per instance,
|
|
184
|
+
* tens of thousands on a large plan, for a value that never changes. The store handle is context
|
|
185
|
+
* alone, and reading `invalidate` at call time also follows a host that replaces it (external mode).
|
|
186
|
+
* @returns A stable function requesting a frame.
|
|
187
|
+
*/
|
|
188
|
+
function useInvalidate() {
|
|
189
|
+
const store = useStore();
|
|
190
|
+
const [invalidate] = useState(() => () => store.getState().invalidate());
|
|
191
|
+
return invalidate;
|
|
118
192
|
}
|
|
193
|
+
// The resource of an instance whose container needs none.
|
|
194
|
+
const readNull = () => null;
|
|
195
|
+
// What an instance's sync ref holds before its first render has committed.
|
|
196
|
+
const noop = () => undefined;
|
|
@@ -82,9 +82,9 @@ export function writeColorIfChanged(mesh, slot, def, logger) {
|
|
|
82
82
|
mesh.setColorAt(slot, _colorWithAlpha);
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
|
-
// Scratch shared by every write
|
|
86
|
-
// statement, so a write costs buffer uploads and no allocation. `_matrix` is the one callers see
|
|
87
|
-
//
|
|
85
|
+
// Scratch shared by every write. Each is filled and consumed within a single synchronous
|
|
86
|
+
// statement, so a write costs buffer uploads and no allocation. `_matrix` is the one callers see,
|
|
87
|
+
// through `shapeToMatrix`. The `_previous*` values are only ever read back and compared inside the
|
|
88
88
|
// write helpers, so a caller still holding `_matrix` never collides with them.
|
|
89
89
|
const _position = new Vector3();
|
|
90
90
|
const _quaternion = new Quaternion();
|