@expofp/renderer 3.34.1 → 3.35.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/dist/Renderer.d.ts +14 -0
- package/dist/Renderer.js +16 -4
- package/dist/core/shader-source.d.ts +20 -0
- package/dist/core/shader-source.js +26 -0
- package/dist/debug/components/Gui.d.ts +2 -0
- package/dist/debug/components/Gui.js +11 -1
- package/dist/external/ExternalCanvas.js +5 -6
- package/dist/external/external-camera.js +26 -11
- package/dist/index.d.ts +1 -0
- package/dist/objects/Background.js +1 -1
- package/dist/objects/containers/SlotContainer.js +7 -1
- package/dist/objects/containers/hooks.js +1 -1
- package/dist/objects/containers/props.d.ts +10 -0
- package/dist/objects/images/Images.js +9 -2
- package/dist/objects/images/image-container.js +1 -1
- package/dist/objects/layer/Layer.js +10 -10
- package/dist/objects/layer/mount-scheduler.d.ts +83 -54
- package/dist/objects/layer/mount-scheduler.js +204 -60
- package/dist/objects/lighting/Lighting.d.ts +92 -0
- package/dist/objects/lighting/Lighting.js +385 -0
- package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
- package/dist/objects/lighting/OcclusionPasses.js +271 -0
- package/dist/objects/lighting/index.d.ts +11 -0
- package/dist/objects/lighting/index.js +10 -0
- package/dist/objects/lighting/lighting-gui.d.ts +24 -0
- package/dist/objects/lighting/lighting-gui.js +114 -0
- package/dist/objects/lighting/occluder.d.ts +34 -0
- package/dist/objects/lighting/occluder.js +62 -0
- package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
- package/dist/objects/lighting/occlusion-targets.js +276 -0
- package/dist/objects/lines/Lines.js +3 -1
- package/dist/objects/materials/ColorMaterial.d.ts +58 -51
- package/dist/objects/materials/ColorMaterial.js +74 -52
- package/dist/objects/materials/LineMaterial.js +1 -1
- package/dist/objects/materials/ModelMaterial.d.ts +30 -18
- package/dist/objects/materials/ModelMaterial.js +68 -41
- package/dist/objects/materials/TextureMaterial.js +1 -1
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
- package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
- package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
- package/dist/objects/materials/effects/ao/index.d.ts +129 -0
- package/dist/objects/materials/effects/ao/index.js +72 -0
- package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +4 -4
- package/dist/objects/materials/{dim.js → effects/color-dim.js} +6 -24
- package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
- package/dist/objects/materials/effects/contact-shadow.js +167 -0
- package/dist/objects/materials/effects/effects-context.d.ts +52 -0
- package/dist/objects/materials/effects/effects-context.js +24 -0
- package/dist/objects/models/Models.js +18 -2
- package/dist/objects/models/model-container.js +1 -1
- package/dist/objects/shapes/Shapes.js +20 -2
- package/dist/objects/text/Texts.js +3 -1
- package/dist/objects/text/text-container.js +1 -1
- package/dist/space/Scene.d.ts +3 -0
- package/dist/space/Scene.js +17 -5
- package/package.json +2 -2
- package/dist/objects/Lighting.d.ts +0 -20
- package/dist/objects/Lighting.js +0 -85
|
@@ -1,42 +1,60 @@
|
|
|
1
1
|
import { type LayerDef, type RenderableDef, type RenderableDefCollection } from '../../types/defs';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Lets a shape layer mount its children a slice at a time, in registration order.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* register from a passive mount effect, and React runs those children-first, in sibling order.
|
|
10
|
-
* Pacing is one admission per animation frame, granted after the previously admitted leaf reports
|
|
11
|
-
* its content committed — so each layer's frame actually presents before the next layer's commit
|
|
12
|
-
* begins.
|
|
5
|
+
* React cannot split a commit. `useDeferredValue` splits a render phase, but the commit that
|
|
6
|
+
* follows runs to the end. So the unit of progressive mounting is whatever one commit mounts, and
|
|
7
|
+
* that unit is a slice of one leaf layer's children. Without slices, a layer holding thousands of
|
|
8
|
+
* defs mounts them in one commit, and no pacing of whole layers makes that frame shorter.
|
|
13
9
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* Priority is registration order, which is the flattened leaf order of the tree. Leaves register
|
|
11
|
+
* from a passive mount effect, and React runs those children first, in sibling order.
|
|
12
|
+
*
|
|
13
|
+
* A frame keeps taking slices until its budget runs out, and hands the rest to the next frame. One
|
|
14
|
+
* slice per frame was measured as 64% of the staging spent waiting rather than mounting.
|
|
15
|
+
*
|
|
16
|
+
* **What the budget covers.** Each slice is timed from the grant to the report, so the measurement
|
|
17
|
+
* holds React's work: the render phase, the commit, and the effects that claim slots and write
|
|
18
|
+
* their state. It does not hold what the frame does next. There three culls and sorts, uploads
|
|
19
|
+
* buffers and textures, compiles a new material's program and draws, and then the browser paints.
|
|
20
|
+
* Those cost more than the mount on a busy startup frame, and the budget cannot see any of them.
|
|
21
|
+
* Treat the budget as a cap on what mounting adds to a frame, not as a frame time.
|
|
22
|
+
*
|
|
23
|
+
* The slice size comes from measurement rather than from a fixed count, because a child of an image
|
|
24
|
+
* layer costs about ten times what a child of a shape layer costs. Every slice is timed, and the
|
|
25
|
+
* next one is sized from what the last one cost per child. One floor overrides the budget: a layer
|
|
26
|
+
* is never cut into more than {@link MAX_SLICES_PER_LEAF} slices, because a layer re-renders its
|
|
27
|
+
* whole mounted prefix on every slice.
|
|
28
|
+
*
|
|
29
|
+
* An empty leaf is exempt. It has no instances to commit, so it is admitted when it registers and
|
|
30
|
+
* is never paced. Without that, a layer filled after mount, such as wayfinding's route, the markers
|
|
31
|
+
* or the traffic, waits out the staging of every leaf ahead of it before it can draw. A leaf that
|
|
32
|
+
* finishes staging is exempt from then on, for the same reason: whatever it gains later mounts at
|
|
33
|
+
* once, as the one small commit it is.
|
|
18
34
|
*/
|
|
19
35
|
export interface MountScheduler {
|
|
20
36
|
/**
|
|
21
|
-
* Queues a leaf for admission. Call from a mount effect so call order is the flattened
|
|
22
|
-
* order. Admission is sticky: a leaf that
|
|
23
|
-
*
|
|
37
|
+
* Queues a leaf for admission. Call it from a mount effect, so the call order is the flattened
|
|
38
|
+
* leaf order. Admission is sticky: a leaf that registers again, after a StrictMode effect cycle
|
|
39
|
+
* or a remount, keeps every child it was already granted.
|
|
24
40
|
* @param leaf - The shape layer to queue.
|
|
25
41
|
* @returns An unregister function.
|
|
26
42
|
*/
|
|
27
43
|
register(leaf: LayerDef): () => void;
|
|
28
44
|
/**
|
|
29
|
-
* The queued leaves, in registration order
|
|
30
|
-
* queue
|
|
45
|
+
* The queued leaves, in registration order, which is the flattened leaf order of the mounted
|
|
46
|
+
* tree. The queue is also the tree's leaf census, and that is what the layer-visibility GUI
|
|
47
|
+
* lists.
|
|
31
48
|
* @returns A snapshot of the queue.
|
|
32
49
|
*/
|
|
33
50
|
leaves(): readonly LayerDef[];
|
|
34
51
|
/**
|
|
35
|
-
*
|
|
52
|
+
* How many of a leaf's children can mount their content now, counted from the start of its list.
|
|
53
|
+
* This is the `useSyncExternalStore` snapshot.
|
|
36
54
|
* @param leaf - The shape layer to check.
|
|
37
|
-
* @returns
|
|
55
|
+
* @returns The admitted count, which is every child once the leaf finishes staging.
|
|
38
56
|
*/
|
|
39
|
-
|
|
57
|
+
admittedCount(leaf: LayerDef): number;
|
|
40
58
|
/**
|
|
41
59
|
* Subscribes to scheduler changes: admissions, registrations, and unregistrations.
|
|
42
60
|
* @param listener - Called after every change.
|
|
@@ -44,59 +62,70 @@ export interface MountScheduler {
|
|
|
44
62
|
*/
|
|
45
63
|
subscribe(listener: () => void): () => void;
|
|
46
64
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
65
|
+
* Reports that the admitted slice committed its content. The scheduler records what the slice
|
|
66
|
+
* cost and releases the next one. It releases it inside this frame while the budget holds, and on
|
|
67
|
+
* the next frame once it does not.
|
|
49
68
|
* @param leaf - The shape layer whose content committed.
|
|
50
69
|
*/
|
|
51
70
|
reportMounted(leaf: LayerDef): void;
|
|
52
71
|
}
|
|
53
|
-
/** What {@link useStagedChildren} hands a layer: the children to render, and
|
|
72
|
+
/** What {@link useStagedChildren} hands a layer: the children to render, and how many can mount. */
|
|
54
73
|
export interface StagedChildren<T extends RenderableDef> {
|
|
55
|
-
/** The layer's children
|
|
74
|
+
/** The layer's children, as the deferred and version-keyed snapshot the hook describes. */
|
|
56
75
|
children: Readonly<RenderableDefCollection<T>>;
|
|
57
|
-
/**
|
|
58
|
-
|
|
76
|
+
/**
|
|
77
|
+
* How many of `children` can mount their instances, counted from the start of the list. A layer
|
|
78
|
+
* of layers is not staged, so it always gets all of them.
|
|
79
|
+
*/
|
|
80
|
+
mounted: number;
|
|
59
81
|
}
|
|
60
82
|
/**
|
|
61
83
|
* Builds the scheduler a `Layer` tree's root provides.
|
|
84
|
+
* @param budgetMs - How much of a frame mounting can take, in milliseconds, across as many slices
|
|
85
|
+
* as fit. The frame also has to draw, so this is a fraction of a frame rather than the whole of
|
|
86
|
+
* one. A budget smaller than one slice paces one slice per frame, which is what a spec wants when
|
|
87
|
+
* its commits cost nothing measurable.
|
|
62
88
|
* @returns The scheduler.
|
|
63
89
|
*/
|
|
64
|
-
export declare function createMountScheduler(): MountScheduler;
|
|
90
|
+
export declare function createMountScheduler(budgetMs?: number): MountScheduler;
|
|
65
91
|
/**
|
|
66
|
-
* A layer's children as they
|
|
67
|
-
*
|
|
68
|
-
*
|
|
92
|
+
* A layer's children as they render now, together with how many of them can mount.
|
|
93
|
+
*
|
|
94
|
+
* Three behaviors fold together here, because their consumer needs all three, and because the
|
|
95
|
+
* values in between are traps. Rendering the snapshot before the deferral defeats the deferral.
|
|
69
96
|
*
|
|
70
|
-
* - **Subscription.** The store's version-keyed snapshot of `layer.children
|
|
71
|
-
* exactly when the layer is updated, the cached one otherwise. The layer's own array is
|
|
72
|
-
* in place, so passing it down
|
|
73
|
-
* `layer.hidden = true; update(layer)` re-render the layer while child identities
|
|
74
|
-
* keyed reconciliation mounts and unmounts only the membership delta.
|
|
75
|
-
* - **Deferral.** Children flow through `useDeferredValue`, so
|
|
76
|
-
* is the layer component alone
|
|
77
|
-
* priority,
|
|
78
|
-
* unaffected
|
|
79
|
-
* - **Admission.**
|
|
80
|
-
*
|
|
81
|
-
* at its full size and never reallocates
|
|
82
|
-
* against a not
|
|
83
|
-
*
|
|
97
|
+
* - **Subscription.** The store's version-keyed snapshot of `layer.children`. It is a new identity
|
|
98
|
+
* exactly when the layer is updated, and the cached one otherwise. The layer's own array is
|
|
99
|
+
* mutated in place, so passing it down leaves a memoized container stale. The snapshot is what
|
|
100
|
+
* makes `layer.hidden = true; update(layer)` re-render the layer while the child identities
|
|
101
|
+
* survive, so keyed reconciliation mounts and unmounts only the membership delta.
|
|
102
|
+
* - **Deferral.** Children flow through `useDeferredValue`, so the urgent render of a membership
|
|
103
|
+
* change is the layer component alone. The container's instance-list sweep re-renders at
|
|
104
|
+
* transition priority, where React can interrupt it, and the frames keep flowing. `update(def)`
|
|
105
|
+
* writes on leaf defs are unaffected, because they never pass through `children`.
|
|
106
|
+
* - **Admission.** How many of a leaf's children can mount their content, per {@link useStagedMount}.
|
|
107
|
+
* A leaf granted nothing yet renders `null` rather than an empty container, so each batch is
|
|
108
|
+
* still created at its full size and never reallocates as the granted prefix grows. Defs stay the
|
|
109
|
+
* source of truth: `update()` against a def that has not mounted yet is harmless, because its
|
|
110
|
+
* instance writes whatever the def holds when it does mount.
|
|
84
111
|
* @param layer - The layer to observe.
|
|
85
|
-
* @returns The current children snapshot and
|
|
112
|
+
* @returns The current children snapshot and how many of them can mount.
|
|
86
113
|
*/
|
|
87
114
|
export declare function useStagedChildren<T extends RenderableDef>(layer: LayerDef<T>): StagedChildren<T>;
|
|
88
115
|
/**
|
|
89
|
-
*
|
|
116
|
+
* How many of a layer's children can mount, per the tree's scheduler.
|
|
90
117
|
*
|
|
91
|
-
* Consumes the scheduler {@link MountSchedulerContext} provides. Without one
|
|
92
|
-
* mount
|
|
93
|
-
* scheduler, a leaf is
|
|
94
|
-
* admission commit so the next
|
|
118
|
+
* Consumes the scheduler {@link MountSchedulerContext} provides. Without one, which is a plain
|
|
119
|
+
* `<Layer>` mount or staging turned off, everything is admitted at once and nothing queues. Under a
|
|
120
|
+
* scheduler, a leaf is granted a slice of its children at a time, in flattened leaf order. It
|
|
121
|
+
* reports back from each admission commit, so the next slice is released only after this one's
|
|
122
|
+
* content committed.
|
|
95
123
|
* @param layer - The layer def.
|
|
96
|
-
* @param leaf - Whether the layer is a shape layer
|
|
97
|
-
* @
|
|
124
|
+
* @param leaf - Whether the layer is a shape layer. Only leaves are staged.
|
|
125
|
+
* @param total - How many children the layer holds, which is what an unstaged layer mounts.
|
|
126
|
+
* @returns How many of the layer's children can mount their content.
|
|
98
127
|
*/
|
|
99
|
-
export declare function useStagedMount(layer: LayerDef, leaf: boolean):
|
|
128
|
+
export declare function useStagedMount(layer: LayerDef, leaf: boolean, total: number): number;
|
|
100
129
|
/** The tree's scheduler, provided by `Scene` and inherited by every `Layer` beneath it. */
|
|
101
130
|
export declare const MountSchedulerContext: import("react").Context<MountScheduler | null>;
|
|
102
131
|
//# sourceMappingURL=mount-scheduler.d.ts.map
|
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import { createContext, use, useDeferredValue, useEffect, useSyncExternalStore } from 'react';
|
|
2
2
|
import { isLayerDefs } from '../../types/guards';
|
|
3
3
|
import { childrenOf, subscribeDef } from '../../update';
|
|
4
|
+
import { createLogger } from '../../utils/logging';
|
|
5
|
+
const logger = createLogger('mount');
|
|
4
6
|
/**
|
|
5
7
|
* Builds the scheduler a `Layer` tree's root provides.
|
|
8
|
+
* @param budgetMs - How much of a frame mounting can take, in milliseconds, across as many slices
|
|
9
|
+
* as fit. The frame also has to draw, so this is a fraction of a frame rather than the whole of
|
|
10
|
+
* one. A budget smaller than one slice paces one slice per frame, which is what a spec wants when
|
|
11
|
+
* its commits cost nothing measurable.
|
|
6
12
|
* @returns The scheduler.
|
|
7
13
|
*/
|
|
8
|
-
export function createMountScheduler() {
|
|
14
|
+
export function createMountScheduler(budgetMs = FRAME_BUDGET_MS) {
|
|
9
15
|
const queue = [];
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
16
|
+
// How many children each leaf has been granted. Weak, so an entry goes when its def goes. Sticky,
|
|
17
|
+
// so an unregister keeps the count: a StrictMode effect cycle must not unmount content that is
|
|
18
|
+
// already on screen.
|
|
19
|
+
const granted = new WeakMap();
|
|
20
|
+
// Leaves that are not paced at all. A leaf lands here when it registers empty, and when it
|
|
21
|
+
// finishes staging. Both mount whatever they hold the moment they hold it.
|
|
22
|
+
const unpaced = new WeakSet();
|
|
23
|
+
// What one child of this leaf cost to mount, in milliseconds, as last measured. Kept per leaf
|
|
24
|
+
// because the spread is wide: a plain shape costs a fraction of what an image costs.
|
|
25
|
+
const rateByLeaf = new WeakMap();
|
|
13
26
|
const listeners = new Set();
|
|
14
27
|
let awaiting = null;
|
|
28
|
+
let awaitingSize = 0;
|
|
29
|
+
let awaitingSince = 0;
|
|
15
30
|
let pacing = false;
|
|
16
|
-
//
|
|
31
|
+
// What this frame already spent on mounting, and how many slices it took. A frame keeps taking
|
|
32
|
+
// slices until one of the two runs out. That is what stops the pacing from costing a whole frame
|
|
33
|
+
// per slice, which on a large plan leaves most of the staging waiting instead of mounting.
|
|
34
|
+
let frameSpent = 0;
|
|
35
|
+
let frameSlices = 0;
|
|
36
|
+
// The leaves this frame granted to, for the one debug line each of them gets when the frame ends.
|
|
37
|
+
const framed = [];
|
|
38
|
+
// Seeds the first slice of a leaf that nothing has measured yet. Every measurement refreshes it,
|
|
39
|
+
// so the seed follows the scene being mounted rather than a number chosen here.
|
|
40
|
+
let rate = INITIAL_MS_PER_CHILD;
|
|
41
|
+
// Frame pacing in the browser. A macrotask in headless spec environments, which have no rAF.
|
|
17
42
|
const nextFrame = typeof requestAnimationFrame === 'function'
|
|
18
43
|
? (callback) => void requestAnimationFrame(callback)
|
|
19
44
|
: (callback) => void setTimeout(callback, 0);
|
|
@@ -21,25 +46,97 @@ export function createMountScheduler() {
|
|
|
21
46
|
for (const listener of [...listeners])
|
|
22
47
|
listener();
|
|
23
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* How many more of a leaf's children the next commit can take.
|
|
51
|
+
* @param leaf - The leaf being staged.
|
|
52
|
+
* @param from - How many of its children are already granted.
|
|
53
|
+
* @param available - What is left of this frame's mounting budget, in milliseconds.
|
|
54
|
+
* @returns The slice size, at least one child and never past the end.
|
|
55
|
+
*/
|
|
56
|
+
const sliceFor = (leaf, from, available) => {
|
|
57
|
+
const measured = rateByLeaf.get(leaf);
|
|
58
|
+
const fit = Math.max(1, Math.round(available / Math.max(measured ?? rate, MIN_MS_PER_CHILD)));
|
|
59
|
+
// A leaf that nothing has measured gets a capped first slice. That slice pays the fixed costs
|
|
60
|
+
// of the layer, which are the material's compile, the mesh's allocation and the first atlas
|
|
61
|
+
// upload. A seed taken from a cheaper layer sizes it far too generously.
|
|
62
|
+
const size = measured === undefined ? Math.min(fit, FIRST_SLICE) : fit;
|
|
63
|
+
// The floor is what keeps slicing from turning quadratic. A layer renders its whole mounted
|
|
64
|
+
// prefix on every slice, so the elements React builds over a staging run come to slices times
|
|
65
|
+
// prefix. That is cheap over a handful of slices and ruinous over hundreds. The floor caps a
|
|
66
|
+
// layer at MAX_SLICES_PER_LEAF slices and overrides the budget to do it, so a big layer takes
|
|
67
|
+
// one frame longer than the budget instead of paying that cost hundreds of times.
|
|
68
|
+
const floor = Math.ceil(leaf.children.length / MAX_SLICES_PER_LEAF);
|
|
69
|
+
return Math.min(leaf.children.length - from, Math.max(size, floor));
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* How many of a leaf's children can mount now.
|
|
73
|
+
* @param leaf - The leaf to count.
|
|
74
|
+
* @returns The admitted count.
|
|
75
|
+
*/
|
|
76
|
+
const countOf = (leaf) => unpaced.has(leaf)
|
|
77
|
+
? leaf.children.length
|
|
78
|
+
: Math.min(granted.get(leaf) ?? 0, leaf.children.length);
|
|
79
|
+
/** Reports what the frame admitted, one line per layer it touched, and clears the tally. */
|
|
80
|
+
const logFrame = () => {
|
|
81
|
+
if (framed.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
for (const leaf of framed) {
|
|
84
|
+
logger.debug(`${leaf.name ?? '?'}, admitted ${countOf(leaf)}/${leaf.children.length}`);
|
|
85
|
+
}
|
|
86
|
+
framed.length = 0;
|
|
87
|
+
};
|
|
88
|
+
/** Hands the rest of the queue to the next frame, and starts that frame's budget over. */
|
|
89
|
+
const yieldFrame = () => {
|
|
90
|
+
if (pacing)
|
|
91
|
+
return;
|
|
92
|
+
pacing = true;
|
|
93
|
+
logFrame();
|
|
94
|
+
nextFrame(() => {
|
|
95
|
+
pacing = false;
|
|
96
|
+
frameSpent = 0;
|
|
97
|
+
frameSlices = 0;
|
|
98
|
+
pump();
|
|
99
|
+
});
|
|
100
|
+
};
|
|
24
101
|
const pump = () => {
|
|
25
102
|
if (awaiting || pacing)
|
|
26
103
|
return;
|
|
27
|
-
const next = queue.find((leaf) => !
|
|
28
|
-
if (!next)
|
|
104
|
+
const next = queue.find((leaf) => !unpaced.has(leaf) && (granted.get(leaf) ?? 0) < leaf.children.length);
|
|
105
|
+
if (!next) {
|
|
106
|
+
logFrame();
|
|
29
107
|
return;
|
|
30
|
-
|
|
108
|
+
}
|
|
109
|
+
const available = budgetMs - frameSpent;
|
|
110
|
+
// Out of budget, or enough slices for one frame, so the rest waits for the next frame. A frame
|
|
111
|
+
// always takes its first slice, however small the budget is, so the scheduler can never stall.
|
|
112
|
+
// The re-layout drain follows the same rule. The slice cap is a guard rather than a policy:
|
|
113
|
+
// every grant is a React update raised from an effect, and React stops a component that raises
|
|
114
|
+
// them without end.
|
|
115
|
+
const spent = available < MIN_SLICE_BUDGET_MS || frameSlices >= MAX_SLICES_PER_FRAME;
|
|
116
|
+
if (spent && frameSlices > 0) {
|
|
117
|
+
yieldFrame();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const from = granted.get(next) ?? 0;
|
|
121
|
+
const size = sliceFor(next, from, available);
|
|
122
|
+
granted.set(next, from + size);
|
|
123
|
+
if (from + size >= next.children.length)
|
|
124
|
+
unpaced.add(next);
|
|
31
125
|
awaiting = next;
|
|
126
|
+
awaitingSize = size;
|
|
127
|
+
awaitingSince = performance.now();
|
|
128
|
+
if (!framed.includes(next))
|
|
129
|
+
framed.push(next);
|
|
32
130
|
notifyAll();
|
|
33
131
|
};
|
|
34
132
|
return {
|
|
35
133
|
register: (leaf) => {
|
|
36
|
-
// An empty leaf has no instances to commit
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
admitted.add(leaf);
|
|
134
|
+
// An empty leaf has no instances to commit. It costs nothing to admit and nothing to pace, so
|
|
135
|
+
// it never becomes `awaiting`. This is what lets a layer filled after mount, such as
|
|
136
|
+
// wayfinding's route, the markers or the traffic, draw without waiting out the whole staging.
|
|
137
|
+
// It stays exempt, so the later fill mounts at once, as the one small commit it is.
|
|
138
|
+
if (leaf.children.length === 0 && !unpaced.has(leaf)) {
|
|
139
|
+
unpaced.add(leaf);
|
|
43
140
|
notifyAll();
|
|
44
141
|
}
|
|
45
142
|
if (!queue.includes(leaf)) {
|
|
@@ -53,7 +150,7 @@ export function createMountScheduler() {
|
|
|
53
150
|
queue.splice(index, 1);
|
|
54
151
|
notifyAll();
|
|
55
152
|
}
|
|
56
|
-
// An admitted leaf that unmounts before
|
|
153
|
+
// An admitted leaf that unmounts before it reports must not stall the queue for good.
|
|
57
154
|
if (awaiting === leaf) {
|
|
58
155
|
awaiting = null;
|
|
59
156
|
pump();
|
|
@@ -61,7 +158,9 @@ export function createMountScheduler() {
|
|
|
61
158
|
};
|
|
62
159
|
},
|
|
63
160
|
leaves: () => [...queue],
|
|
64
|
-
|
|
161
|
+
admittedCount: (leaf) => unpaced.has(leaf)
|
|
162
|
+
? leaf.children.length
|
|
163
|
+
: Math.min(granted.get(leaf) ?? 0, leaf.children.length),
|
|
65
164
|
subscribe: (listener) => {
|
|
66
165
|
listeners.add(listener);
|
|
67
166
|
return () => {
|
|
@@ -71,75 +170,120 @@ export function createMountScheduler() {
|
|
|
71
170
|
reportMounted: (leaf) => {
|
|
72
171
|
if (awaiting !== leaf)
|
|
73
172
|
return;
|
|
173
|
+
// Timed from the grant. The effect this runs from is inside the commit the grant caused, and
|
|
174
|
+
// it runs after the children's own effects, so the span covers that commit's React work. It
|
|
175
|
+
// does not cover the draw that follows, which the interface doc explains.
|
|
176
|
+
const cost = performance.now() - awaitingSince;
|
|
177
|
+
if (awaitingSize > 0) {
|
|
178
|
+
rate = Math.max(cost / awaitingSize, MIN_MS_PER_CHILD);
|
|
179
|
+
rateByLeaf.set(leaf, rate);
|
|
180
|
+
}
|
|
74
181
|
awaiting = null;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pump();
|
|
81
|
-
});
|
|
182
|
+
frameSpent += cost;
|
|
183
|
+
frameSlices++;
|
|
184
|
+
// Straight on to the next slice while this frame still has budget. `pump` hands the queue to
|
|
185
|
+
// the next frame itself once the budget or the slice count runs out.
|
|
186
|
+
pump();
|
|
82
187
|
},
|
|
83
188
|
};
|
|
84
189
|
}
|
|
85
190
|
/**
|
|
86
|
-
* A layer's children as they
|
|
87
|
-
* content. Three behaviors fold together here, because their consumer needs all of them and the
|
|
88
|
-
* intermediate values are footguns — rendering the un-deferred snapshot would defeat the deferral:
|
|
191
|
+
* A layer's children as they render now, together with how many of them can mount.
|
|
89
192
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
193
|
+
* Three behaviors fold together here, because their consumer needs all three, and because the
|
|
194
|
+
* values in between are traps. Rendering the snapshot before the deferral defeats the deferral.
|
|
195
|
+
*
|
|
196
|
+
* - **Subscription.** The store's version-keyed snapshot of `layer.children`. It is a new identity
|
|
197
|
+
* exactly when the layer is updated, and the cached one otherwise. The layer's own array is
|
|
198
|
+
* mutated in place, so passing it down leaves a memoized container stale. The snapshot is what
|
|
199
|
+
* makes `layer.hidden = true; update(layer)` re-render the layer while the child identities
|
|
200
|
+
* survive, so keyed reconciliation mounts and unmounts only the membership delta.
|
|
201
|
+
* - **Deferral.** Children flow through `useDeferredValue`, so the urgent render of a membership
|
|
202
|
+
* change is the layer component alone. The container's instance-list sweep re-renders at
|
|
203
|
+
* transition priority, where React can interrupt it, and the frames keep flowing. `update(def)`
|
|
204
|
+
* writes on leaf defs are unaffected, because they never pass through `children`.
|
|
205
|
+
* - **Admission.** How many of a leaf's children can mount their content, per {@link useStagedMount}.
|
|
206
|
+
* A leaf granted nothing yet renders `null` rather than an empty container, so each batch is
|
|
207
|
+
* still created at its full size and never reallocates as the granted prefix grows. Defs stay the
|
|
208
|
+
* source of truth: `update()` against a def that has not mounted yet is harmless, because its
|
|
209
|
+
* instance writes whatever the def holds when it does mount.
|
|
104
210
|
* @param layer - The layer to observe.
|
|
105
|
-
* @returns The current children snapshot and
|
|
211
|
+
* @returns The current children snapshot and how many of them can mount.
|
|
106
212
|
*/
|
|
107
213
|
export function useStagedChildren(layer) {
|
|
108
214
|
const snapshot = useSyncExternalStore((listener) => subscribeDef(layer, listener), () => childrenOf(layer));
|
|
109
215
|
const children = useDeferredValue(snapshot);
|
|
110
|
-
const
|
|
111
|
-
return { children,
|
|
216
|
+
const mounted = useStagedMount(layer, !isLayerDefs(children), children.length);
|
|
217
|
+
return { children, mounted };
|
|
112
218
|
}
|
|
113
219
|
/**
|
|
114
|
-
*
|
|
220
|
+
* How many of a layer's children can mount, per the tree's scheduler.
|
|
115
221
|
*
|
|
116
|
-
* Consumes the scheduler {@link MountSchedulerContext} provides. Without one
|
|
117
|
-
* mount
|
|
118
|
-
* scheduler, a leaf is
|
|
119
|
-
* admission commit so the next
|
|
222
|
+
* Consumes the scheduler {@link MountSchedulerContext} provides. Without one, which is a plain
|
|
223
|
+
* `<Layer>` mount or staging turned off, everything is admitted at once and nothing queues. Under a
|
|
224
|
+
* scheduler, a leaf is granted a slice of its children at a time, in flattened leaf order. It
|
|
225
|
+
* reports back from each admission commit, so the next slice is released only after this one's
|
|
226
|
+
* content committed.
|
|
120
227
|
* @param layer - The layer def.
|
|
121
|
-
* @param leaf - Whether the layer is a shape layer
|
|
122
|
-
* @
|
|
228
|
+
* @param leaf - Whether the layer is a shape layer. Only leaves are staged.
|
|
229
|
+
* @param total - How many children the layer holds, which is what an unstaged layer mounts.
|
|
230
|
+
* @returns How many of the layer's children can mount their content.
|
|
123
231
|
*/
|
|
124
|
-
export function useStagedMount(layer, leaf) {
|
|
232
|
+
export function useStagedMount(layer, leaf, total) {
|
|
125
233
|
const scheduler = use(MountSchedulerContext);
|
|
126
|
-
const
|
|
127
|
-
// Registration from a passive effect, deliberately
|
|
128
|
-
//
|
|
234
|
+
const mounted = useSyncExternalStore((onStoreChange) => (scheduler ? scheduler.subscribe(onStoreChange) : noopUnsubscribe), () => (!leaf || !scheduler ? total : scheduler.admittedCount(layer)));
|
|
235
|
+
// Registration from a passive effect, deliberately. Effect order across the tree is the flattened
|
|
236
|
+
// leaf order, and that order is the admission priority.
|
|
129
237
|
useEffect(() => {
|
|
130
238
|
if (!scheduler || !leaf)
|
|
131
239
|
return undefined;
|
|
132
240
|
return scheduler.register(layer);
|
|
133
241
|
}, [scheduler, layer, leaf]);
|
|
134
|
-
// Runs in the admission commit, after the children's effects
|
|
135
|
-
// by the time the next
|
|
242
|
+
// Runs in the admission commit, after the children's effects, so the slice has claimed its slots
|
|
243
|
+
// by the time the next one is released. Keyed on the count, so every slice reports, not only the
|
|
244
|
+
// first.
|
|
136
245
|
useEffect(() => {
|
|
137
|
-
if (scheduler && leaf &&
|
|
246
|
+
if (scheduler && leaf && mounted > 0)
|
|
138
247
|
scheduler.reportMounted(layer);
|
|
139
|
-
}, [scheduler, layer, leaf,
|
|
140
|
-
return
|
|
248
|
+
}, [scheduler, layer, leaf, mounted]);
|
|
249
|
+
return mounted;
|
|
141
250
|
}
|
|
142
251
|
/** The tree's scheduler, provided by `Scene` and inherited by every `Layer` beneath it. */
|
|
143
252
|
export const MountSchedulerContext = createContext(null);
|
|
253
|
+
/**
|
|
254
|
+
* How much of a frame mounting can take, in milliseconds, across as many slices as fit. Two thirds
|
|
255
|
+
* of a 60Hz frame, which leaves the draw and the paint their share. The budget is what a frame
|
|
256
|
+
* spends in total, not what one slice spends: a budget of one slice per frame leaves a large scene
|
|
257
|
+
* waiting on frames for most of its staging.
|
|
258
|
+
*/
|
|
259
|
+
const FRAME_BUDGET_MS = 11;
|
|
260
|
+
/**
|
|
261
|
+
* The least budget worth starting a slice on, in milliseconds. Below it the frame counts as spent.
|
|
262
|
+
* The alternative is a one-child slice, which still costs a whole React commit.
|
|
263
|
+
*/
|
|
264
|
+
const MIN_SLICE_BUDGET_MS = 2;
|
|
265
|
+
/**
|
|
266
|
+
* The most slices one frame takes, however cheap they are. Every grant is a React update raised
|
|
267
|
+
* from an effect, and React stops a component that raises them without end.
|
|
268
|
+
*/
|
|
269
|
+
const MAX_SLICES_PER_FRAME = 12;
|
|
270
|
+
/**
|
|
271
|
+
* What a child is assumed to cost before anything is measured, in milliseconds. Deliberately
|
|
272
|
+
* pessimistic, because the first slice of the first leaf is the one grant no measurement informs.
|
|
273
|
+
*/
|
|
274
|
+
const INITIAL_MS_PER_CHILD = 0.25;
|
|
275
|
+
/**
|
|
276
|
+
* The cheapest a child is ever assumed to be, in milliseconds. Without a floor, a layer whose slice
|
|
277
|
+
* measured as instant sizes the next one at every child it holds.
|
|
278
|
+
*/
|
|
279
|
+
const MIN_MS_PER_CHILD = 0.02;
|
|
280
|
+
/** The most children a leaf is granted before one of its own slices has been measured. */
|
|
281
|
+
const FIRST_SLICE = 64;
|
|
282
|
+
/**
|
|
283
|
+
* The most slices one leaf is ever cut into. A layer re-renders its whole mounted prefix per slice,
|
|
284
|
+
* so the number of slices decides how much of that cost the staging pays. Eight keeps it to a few
|
|
285
|
+
* thousand elements even for a layer of several thousand defs.
|
|
286
|
+
*/
|
|
287
|
+
const MAX_SLICES_PER_LEAF = 8;
|
|
144
288
|
// What subscribing without a scheduler returns: nothing to unsubscribe from.
|
|
145
289
|
const noopUnsubscribe = () => undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for {@link Lighting}: every value the debug GUI can move, apart from the two that aim the key light.
|
|
3
|
+
*
|
|
4
|
+
* Each one is optional, and each one overrides the design's own default for as long as it is given.
|
|
5
|
+
* The pitch and `trackPitch` are deliberately absent. Where the key light points is the design's
|
|
6
|
+
* decision rather than a caller's, and the GUI keeps both of them for settling it.
|
|
7
|
+
*
|
|
8
|
+
* **A prop beats the GUI only when the prop changes.** {@link Lighting} writes these into the rig
|
|
9
|
+
* and into the scene's effect state from an effect keyed on the values themselves, so a control the
|
|
10
|
+
* GUI moved stays moved until a caller passes something different.
|
|
11
|
+
*/
|
|
12
|
+
export interface LightingProps extends Partial<Omit<LightSettings, 'trackPitch' | 'pitchDegrees'>> {
|
|
13
|
+
/** The shade both darkening terms blend toward, and each term's own numbers. */
|
|
14
|
+
shadow?: ShadowProps;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The darkening values: the shade both terms share, the contact shadow's two numbers, and ambient
|
|
18
|
+
* occlusion's switch and two numbers.
|
|
19
|
+
*
|
|
20
|
+
* One object rather than six loose props, because they are one visual decision. The names are the
|
|
21
|
+
* GUI's own labels, so a caller and a slider talk about the same thing — and so that the two
|
|
22
|
+
* strengths cannot be mistaken for each other, which is the reason the GUI renamed them.
|
|
23
|
+
*
|
|
24
|
+
* Omit a value to keep the design's own default. `CONTACT_SHADOW_DEFAULTS` and `OCCLUSION_DEFAULTS`
|
|
25
|
+
* are where those live, each beside the term that owns it.
|
|
26
|
+
*/
|
|
27
|
+
export interface ShadowProps {
|
|
28
|
+
/** The shade both terms blend toward, as a CSS hex string. */
|
|
29
|
+
color?: string;
|
|
30
|
+
/** The contact shadow's reach above a volume's base, in plan units. */
|
|
31
|
+
contactDistance?: number;
|
|
32
|
+
/** How far the contact shadow blends a volume's base toward the shade, from `0` to `1`. */
|
|
33
|
+
contactStrength?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Whether ambient occlusion runs at all.
|
|
36
|
+
*
|
|
37
|
+
* A switch rather than a capability: the term also turns itself off for good on a context that
|
|
38
|
+
* cannot give it a depth texture, and there is nothing here to say about that.
|
|
39
|
+
*/
|
|
40
|
+
ambientOcclusion?: boolean;
|
|
41
|
+
/** How far ambient occlusion looks for an occluder, in plan units. */
|
|
42
|
+
aoRadius?: number;
|
|
43
|
+
/** How far ambient occlusion blends an occluded fragment toward the shade, from `0` to `1`. */
|
|
44
|
+
aoStrength?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The light rig, as the debug GUI edits it.
|
|
48
|
+
*
|
|
49
|
+
* One object per mounted `Lighting`, mutated in place: the frame reads it, so a slider drag costs a
|
|
50
|
+
* few numbers and no render. A remount gives a fresh rig rather than reusing module state.
|
|
51
|
+
*/
|
|
52
|
+
export interface LightSettings {
|
|
53
|
+
/** Whether the light copies the camera's backward axis in full, the pre-overhaul behavior. */
|
|
54
|
+
trackPitch: boolean;
|
|
55
|
+
/** The light's angle from the plan's up axis, in degrees. Unread while `trackPitch` is on. */
|
|
56
|
+
pitchDegrees: number;
|
|
57
|
+
/**
|
|
58
|
+
* How directional the rig is, from `0` (pure hemisphere, flat) to `1` (the most directional light
|
|
59
|
+
* this pitch and these colors take before a wall clips). Normalized rather than an intensity ratio,
|
|
60
|
+
* whose usable range moves with the pitch. See {@link intensitiesFromContrast}.
|
|
61
|
+
*/
|
|
62
|
+
contrast: number;
|
|
63
|
+
/** Hemisphere sky color, as a CSS hex string. */
|
|
64
|
+
skyColor: string;
|
|
65
|
+
/** Hemisphere ground color, which every downward face receives. */
|
|
66
|
+
groundColor: string;
|
|
67
|
+
/** Directional color, warm against the cool sky so lit faces separate by temperature too. */
|
|
68
|
+
directionalColor: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Scene lighting: a hemisphere fill along the plan's up axis, and a warm directional light at a
|
|
72
|
+
* fixed pitch on the camera's heading. The one intensity control is `contrast`, and `Lighting`
|
|
73
|
+
* derives both `π`-unit intensities from it every frame — so an up-facing surface (a roof) lands on
|
|
74
|
+
* its authored color and no surface the design accounts for clips.
|
|
75
|
+
*
|
|
76
|
+
* That is also why the renderer skips tone mapping (see `Renderer.tsx` and `ExternalCanvas.tsx`): a
|
|
77
|
+
* curve brings an unbounded light sum back into range, and `contrast` keeps this rig's sum in range
|
|
78
|
+
* to begin with, at every pitch.
|
|
79
|
+
*
|
|
80
|
+
* Only lit materials see the lights — solid shape batches and models. Every 2D primitive draws
|
|
81
|
+
* through a `MeshBasicMaterial`, so lines, text and images are unaffected.
|
|
82
|
+
*
|
|
83
|
+
* **The lights sit inside the scene's world matrix**, so every direction below is in the plan's own
|
|
84
|
+
* space, taken there through {@link Space.matrixInverse}. Three reads a light's direction from where
|
|
85
|
+
* it ends up in the *world*, so the two transforms cancel and the light points where it was aimed
|
|
86
|
+
* under any fit. The directional light needs its target under the same matrix for that, since its
|
|
87
|
+
* direction is the difference of two world positions.
|
|
88
|
+
* @param props - The values a caller set, any subset; see {@link LightingProps}.
|
|
89
|
+
* @returns The two lights, and the directional light's target.
|
|
90
|
+
*/
|
|
91
|
+
export declare function Lighting(props: LightingProps): import("react").JSX.Element;
|
|
92
|
+
//# sourceMappingURL=Lighting.d.ts.map
|