@expofp/renderer 3.34.2 → 3.35.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/Renderer.d.ts +14 -0
- package/dist/Renderer.js +16 -4
- package/dist/core/SquareDataTexture.d.ts +2 -2
- package/dist/core/SquareDataTexture.js +2 -2
- package/dist/core/batch/BatchedMesh.d.ts +3 -3
- package/dist/core/batch/BatchedMesh.js +3 -3
- package/dist/core/instanced/InstancedMesh.d.ts +7 -7
- package/dist/core/instanced/InstancedMesh.js +4 -4
- package/dist/core/instanced/instanced-material.js +1 -1
- package/dist/core/lines/BatchedLines.d.ts +4 -4
- package/dist/core/lines/BatchedLines.js +4 -4
- package/dist/core/shader-source.d.ts +20 -0
- package/dist/core/shader-source.js +26 -0
- package/dist/core/text/BatchedText.d.ts +14 -14
- package/dist/core/text/BatchedText.js +9 -9
- package/dist/core/text/MemberTexture.d.ts +2 -2
- package/dist/core/text/Text.d.ts +5 -5
- package/dist/core/text/Text.js +4 -4
- package/dist/core/text/glyph-packing.d.ts +2 -2
- package/dist/debug/components/Gui.d.ts +2 -0
- package/dist/debug/components/Gui.js +11 -1
- package/dist/events/layout-targets.d.ts +7 -3
- package/dist/events/layout-targets.js +13 -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.d.ts +27 -2
- package/dist/objects/containers/SlotContainer.js +36 -1
- package/dist/objects/containers/hooks.js +1 -1
- package/dist/objects/containers/mesh-writes.d.ts +1 -1
- package/dist/objects/containers/props.d.ts +10 -0
- package/dist/objects/images/Images.js +10 -3
- package/dist/objects/images/image-container.d.ts +5 -5
- package/dist/objects/images/image-container.js +2 -2
- 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/lines/line-container.d.ts +4 -4
- package/dist/objects/materials/ColorMaterial.d.ts +58 -51
- package/dist/objects/materials/ColorMaterial.js +74 -52
- package/dist/objects/materials/LineMaterial.d.ts +1 -1
- 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} +5 -5
- package/dist/objects/materials/{dim.js → effects/color-dim.js} +7 -25
- 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 +20 -4
- package/dist/objects/models/model-container.d.ts +4 -20
- package/dist/objects/models/model-container.js +2 -22
- package/dist/objects/shapes/Shapes.js +20 -2
- package/dist/objects/shapes/shape-container.d.ts +3 -20
- package/dist/objects/shapes/shape-container.js +0 -21
- package/dist/objects/text/Texts.js +3 -1
- package/dist/objects/text/text-container.d.ts +6 -6
- package/dist/objects/text/text-container.js +4 -4
- package/dist/objects/text/text-layout.d.ts +2 -2
- 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,5 +1,5 @@
|
|
|
1
1
|
import { type GlyphsGeometry, type TextRenderInfo } from 'troika-three-text';
|
|
2
|
-
import { type
|
|
2
|
+
import { type TextImpl } from './Text';
|
|
3
3
|
/**
|
|
4
4
|
* Rewrites a batch's glyph attributes from its members' render info.
|
|
5
5
|
*
|
|
@@ -13,5 +13,5 @@ import { type TextExt } from './Text';
|
|
|
13
13
|
* @returns The render info the batch should adopt as its own — any member's will do, since a batch
|
|
14
14
|
* shares one SDF atlas — or `undefined` when no member has synced.
|
|
15
15
|
*/
|
|
16
|
-
export declare function packGlyphs(geometry: GlyphsGeometry, ordered: readonly number[], textAt: (memberId: number) =>
|
|
16
|
+
export declare function packGlyphs(geometry: GlyphsGeometry, ordered: readonly number[], textAt: (memberId: number) => TextImpl | undefined): TextRenderInfo | undefined;
|
|
17
17
|
//# sourceMappingURL=glyph-packing.d.ts.map
|
|
@@ -12,6 +12,8 @@ import { type PropsWithChildren } from 'react';
|
|
|
12
12
|
* unmount/remount, the same element is re-attached and the scene's own effects
|
|
13
13
|
* re-add their folders. Rendered as an ancestor of the scene, so on unmount
|
|
14
14
|
* React tears the scene's folders down before this component destroys the panel.
|
|
15
|
+
* It also registers the panel's single change handler, which asks for a redraw. Every folder in the
|
|
16
|
+
* panel inherits it, so no controller carries one of its own.
|
|
15
17
|
* @param props - Component props; `children` is the scene subtree that consumes
|
|
16
18
|
* the GUI via `useGui`.
|
|
17
19
|
* @returns The children wrapped with the GUI provided through context.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import './Gui.module.css'; // Global overrides of lil-gui's stock styling (checkbox row layout).
|
|
3
|
+
import { useThree } from '@react-three/fiber';
|
|
3
4
|
import { GUI } from 'lil-gui';
|
|
4
5
|
import { useEffect, useState } from 'react';
|
|
5
6
|
import { GuiContext } from '../debug-context';
|
|
@@ -15,6 +16,8 @@ import { GuiContext } from '../debug-context';
|
|
|
15
16
|
* unmount/remount, the same element is re-attached and the scene's own effects
|
|
16
17
|
* re-add their folders. Rendered as an ancestor of the scene, so on unmount
|
|
17
18
|
* React tears the scene's folders down before this component destroys the panel.
|
|
19
|
+
* It also registers the panel's single change handler, which asks for a redraw. Every folder in the
|
|
20
|
+
* panel inherits it, so no controller carries one of its own.
|
|
18
21
|
* @param props - Component props; `children` is the scene subtree that consumes
|
|
19
22
|
* the GUI via `useGui`.
|
|
20
23
|
* @returns The children wrapped with the GUI provided through context.
|
|
@@ -22,11 +25,18 @@ import { GuiContext } from '../debug-context';
|
|
|
22
25
|
export function Gui(props) {
|
|
23
26
|
const { children } = props;
|
|
24
27
|
const [gui] = useState(() => new GUI({ autoPlace: false, closeFolders: true }));
|
|
28
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
25
29
|
useEffect(() => {
|
|
26
30
|
gui.domElement.classList.add('lil-auto-place', 'autoPlace');
|
|
27
31
|
document.body.appendChild(gui.domElement);
|
|
28
32
|
gui.close();
|
|
33
|
+
// One handler for the whole panel: lil-gui walks a changed controller up its parent chain, so
|
|
34
|
+
// this fires for every controller in every folder and no folder registers its own. It calls
|
|
35
|
+
// whichever `invalidate` the current mount installed — in the external embed `ExternalCanvas`
|
|
36
|
+
// replaces it with the host's `requestFrame`, which is how a GUI change reaches a host that owns
|
|
37
|
+
// the frame.
|
|
38
|
+
gui.onChange(() => invalidate());
|
|
29
39
|
return () => gui.destroy();
|
|
30
|
-
}, [gui]);
|
|
40
|
+
}, [gui, invalidate]);
|
|
31
41
|
return _jsx(GuiContext, { value: gui, children: children });
|
|
32
42
|
}
|
|
@@ -57,9 +57,13 @@ export interface LayoutTargets {
|
|
|
57
57
|
* Two passes, in the legacy `UpdatesSystem`'s shape: the stale defs first, except that a hidden
|
|
58
58
|
* def or one the frustum rejects is *deferred* rather than laid out; then the deferred ones, with
|
|
59
59
|
* whatever budget is left. Priority is all deferral buys — every def is still laid out before the
|
|
60
|
-
* sweep ends, because whether a def draws can be exactly what its rule decides.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* sweep ends, because whether a def draws can be exactly what its rule decides.
|
|
61
|
+
*
|
|
62
|
+
* **A drain always takes at least one def**, across the two passes together, however little
|
|
63
|
+
* budget is left — including none, which is what a worker descheduled before the first check
|
|
64
|
+
* leaves. That is what makes a sweep unable to stall: each drain shortens the backlog, so the
|
|
65
|
+
* frames the driver keeps asking for eventually end it. Without the guarantee a drain can dequeue
|
|
66
|
+
* nothing, return "more to do", and be asked again on the next frame with the same result.
|
|
63
67
|
* @param frustum - The camera's frustum in plan space, when the caller has one; without it only
|
|
64
68
|
* hidden defs are deferred.
|
|
65
69
|
* @param budgetMs - How much of the frame a call may spend. Non-positive means no limit.
|
|
@@ -208,14 +208,25 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
208
208
|
if (pending.size === 0 && deferred.size === 0)
|
|
209
209
|
return false;
|
|
210
210
|
const start = performance.now();
|
|
211
|
-
const overBudget = () => budgetMs > 0 && performance.now() - start >= budgetMs;
|
|
212
211
|
let wrote = 0;
|
|
212
|
+
/**
|
|
213
|
+
* Defs this drain has taken off a queue, laid out or deferred.
|
|
214
|
+
*
|
|
215
|
+
* It gates the budget so that the **first** def of a drain is always taken. Without that, a
|
|
216
|
+
* budget already spent before the loop began — the worker descheduled between `start` and the
|
|
217
|
+
* 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. Shared across both passes, so
|
|
219
|
+
* the guarantee is one def per drain rather than one per pass.
|
|
220
|
+
*/
|
|
221
|
+
let processed = 0;
|
|
222
|
+
const overBudget = () => processed > 0 && budgetMs > 0 && performance.now() - start >= budgetMs;
|
|
213
223
|
// Sets iterate in insertion order and tolerate deletion mid-walk, which is what makes the two
|
|
214
224
|
// passes below plain loops: the order moves arrived is the priority within a pass, and a
|
|
215
225
|
// processed def leaves its set before its rules run.
|
|
216
226
|
for (const def of pending) {
|
|
217
227
|
if (overBudget())
|
|
218
228
|
break;
|
|
229
|
+
processed++;
|
|
219
230
|
pending.delete(def);
|
|
220
231
|
// Deferral is priority, never a skip: a hidden def, or one the camera cannot see, joins the
|
|
221
232
|
// second pass rather than being laid out now. `def.hidden` needs no frustum — hidden is not
|
|
@@ -230,6 +241,7 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
230
241
|
for (const def of deferred) {
|
|
231
242
|
if (overBudget())
|
|
232
243
|
break;
|
|
244
|
+
processed++;
|
|
233
245
|
deferred.delete(def);
|
|
234
246
|
if (layOutStale(def))
|
|
235
247
|
wrote++;
|
|
@@ -82,8 +82,7 @@ function mountExternal(context, requestFrame, onFrame) {
|
|
|
82
82
|
camera.matrixAutoUpdate = false;
|
|
83
83
|
camera.matrixWorldAutoUpdate = false;
|
|
84
84
|
camera.manual = true;
|
|
85
|
-
// Constructed bare on purpose: the
|
|
86
|
-
// output — and anything the renderer would opine on is left unset.
|
|
85
|
+
// Constructed bare on purpose: anything the renderer would opine on is left unset.
|
|
87
86
|
const glRenderer = new WebGLRenderer({ canvas, context });
|
|
88
87
|
// Never clear: the host's own layers are already on the canvas when this renderer draws.
|
|
89
88
|
glRenderer.autoClear = false;
|
|
@@ -106,10 +105,10 @@ function mountExternal(context, requestFrame, onFrame) {
|
|
|
106
105
|
void root
|
|
107
106
|
.configure({
|
|
108
107
|
gl: glRenderer,
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
108
|
+
// R3F force-writes ACES filmic tone mapping onto a fresh renderer unless `flat` is set, which
|
|
109
|
+
// is the one value landing on three's own default, `NoToneMapping`. This renderer does not
|
|
110
|
+
// tone map: `Lighting` keeps the rig's light sum in range by construction, rather than
|
|
111
|
+
// compressing an unbounded one back into range afterward.
|
|
113
112
|
flat: true,
|
|
114
113
|
// The cast widens past R3F's frustum-camera union; `manual` above is what keeps R3F's own
|
|
115
114
|
// camera handling off it.
|
|
@@ -50,21 +50,34 @@ export function setExternalCameraProjection(camera, mainMatrix) {
|
|
|
50
50
|
_mainInverse.copy(M).invert();
|
|
51
51
|
_near.set(0, 0, 1).applyMatrix4(_mainInverse);
|
|
52
52
|
_far.set(0, 0, -1).applyMatrix4(_mainInverse);
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const nearZ = _near.distanceToSquared(_position) <= _far.distanceToSquared(_position) ? 1 : -1;
|
|
54
|
+
if (nearZ === 1) {
|
|
55
|
+
_direction.copy(_far).sub(_near).normalize();
|
|
55
56
|
}
|
|
56
57
|
else {
|
|
57
|
-
_direction.copy(
|
|
58
|
+
_direction.copy(_near).sub(_far).normalize();
|
|
58
59
|
}
|
|
59
|
-
// Step 3:
|
|
60
|
+
// Step 3: the camera's up axis, measured rather than chosen, and a rigid view matrix from it.
|
|
61
|
+
//
|
|
62
|
+
// Any up not parallel to the view direction gives a valid (P′, V′) pair for *drawing*, because P
|
|
63
|
+
// is recomputed from M · matrixWorld at the end and absorbs whatever roll V′ was given. The axis
|
|
64
|
+
// is also *read*, though: `Lighting` takes the plan's heading from the camera's up axis, so a
|
|
65
|
+
// chosen one aims the key light at an angle the host never had — a fixed world +z aimed it from
|
|
66
|
+
// the far side of the plan at every bearing, and every face turned toward the camera lost its
|
|
67
|
+
// direct light.
|
|
68
|
+
//
|
|
69
|
+
// Two points at one clip x and one clip z, one at the top of the screen and one at the bottom,
|
|
70
|
+
// differ by a vector along the camera's up axis alone: a constant clip z is a constant view-space
|
|
71
|
+
// z, so the forward components cancel and the difference is (0, Δy, 0) in view space. Clip y
|
|
72
|
+
// grows upward under every projection, maplibre's reverse-z included, so this reads none of the
|
|
73
|
+
// host's own conventions — and unlike a chosen axis it has no degenerate view to fall back from,
|
|
74
|
+
// since the axis it recovers is perpendicular to the view direction by construction.
|
|
60
75
|
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
_up.
|
|
66
|
-
if (Math.abs(_up.dot(_direction)) > 0.999)
|
|
67
|
-
_up.set(0, 1, 0);
|
|
76
|
+
// Unprojected at `nearZ`, the near plane's own clip z: a point there is finite under either z
|
|
77
|
+
// convention, which an interior z is not.
|
|
78
|
+
_screenTop.set(0, 1, nearZ).applyMatrix4(_mainInverse);
|
|
79
|
+
_screenBottom.set(0, -1, nearZ).applyMatrix4(_mainInverse);
|
|
80
|
+
_up.copy(_screenTop).sub(_screenBottom).normalize();
|
|
68
81
|
_target.copy(_position).add(_direction);
|
|
69
82
|
camera.matrixWorld.lookAt(_position, _target, _up);
|
|
70
83
|
camera.matrixWorld.setPosition(_position);
|
|
@@ -84,5 +97,7 @@ const _position = new Vector3();
|
|
|
84
97
|
const _near = new Vector3();
|
|
85
98
|
const _far = new Vector3();
|
|
86
99
|
const _direction = new Vector3();
|
|
100
|
+
const _screenTop = new Vector3();
|
|
101
|
+
const _screenBottom = new Vector3();
|
|
87
102
|
const _target = new Vector3();
|
|
88
103
|
const _up = new Vector3();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { type CameraAPI, type CameraAxes, type CameraState, type ControlsConfig, type InertiaConfig, type PanConfig, type RollConfig, type TransitionOptions, useCameraAPI, useCameraMoveStart, useCameraState, type ZoomToOptions, } from './controls';
|
|
2
2
|
export { prepareDebug } from './debug/debug-chunk';
|
|
3
3
|
export { useDebugPanel } from './debug/debug-context';
|
|
4
|
+
export { type LightingProps, type ShadowProps } from './objects/lighting';
|
|
4
5
|
export * from './Renderer';
|
|
5
6
|
export { type CameraProps, type SmoothTime } from './space/Camera';
|
|
6
7
|
export { type CanvasPoint, type CanvasPointLike, type PlanPoint, type PlanPointLike, } from './space/Space';
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { declareBeforeMain } from '../core/shader-source';
|
|
4
4
|
import { ColorMaterial } from './materials/ColorMaterial';
|
|
5
|
-
import { DIM_INHERIT, DIM_UNIFORM } from './materials/dim';
|
|
5
|
+
import { DIM_INHERIT, DIM_UNIFORM } from './materials/effects/color-dim';
|
|
6
6
|
/**
|
|
7
7
|
* The scene's background: a screen-space quad drawn below every layer, replacing what a clear
|
|
8
8
|
* color would paint — and unlike one, dimmed with the venue. The root `Layer` renders it inside
|
|
@@ -5,8 +5,8 @@ import { type RenderableDef } from '../../types/defs';
|
|
|
5
5
|
/**
|
|
6
6
|
* What a container needs from the mesh it writes into.
|
|
7
7
|
*
|
|
8
|
-
* A capability rather than a union of our mesh classes: `
|
|
9
|
-
* `
|
|
8
|
+
* A capability rather than a union of our mesh classes: `BatchedMeshImpl`, `InstancedMeshImpl`,
|
|
9
|
+
* `BatchedLinesImpl` and `BatchedTextImpl` share no base below `Object3D`, yet every one of them
|
|
10
10
|
* declares per-instance uniforms and hides an instance the same way — which is all a container
|
|
11
11
|
* touches generically. Everything else it reaches through its own mesh type, which the subclass
|
|
12
12
|
* names.
|
|
@@ -148,6 +148,8 @@ export declare abstract class SlotContainer<R, M extends SlotMesh = SlotMesh, T
|
|
|
148
148
|
private readonly sortDirection;
|
|
149
149
|
/** Set by {@link bind}, which runs before any effect can claim a slot. */
|
|
150
150
|
private boundMesh;
|
|
151
|
+
/** What to run when a member turns out to belong in a sibling container; see {@link reportKindMismatch}. */
|
|
152
|
+
private kindMismatchHandler;
|
|
151
153
|
/**
|
|
152
154
|
* @param options - How this container differs from the default.
|
|
153
155
|
*/
|
|
@@ -201,6 +203,29 @@ export declare abstract class SlotContainer<R, M extends SlotMesh = SlotMesh, T
|
|
|
201
203
|
* @returns The def, or `undefined` when the hit cannot be traced to one.
|
|
202
204
|
*/
|
|
203
205
|
defFromHit(hit: Intersection): RenderableDef | undefined;
|
|
206
|
+
/**
|
|
207
|
+
* Registers what to do when a member turns out to belong in a sibling container; `undefined`
|
|
208
|
+
* clears it.
|
|
209
|
+
*
|
|
210
|
+
* Here rather than on each subclass because the situation is one situation: a layer is split
|
|
211
|
+
* across several containers by some property of the def, and that property can move after the def
|
|
212
|
+
* was placed. What splits them differs — a shape's color crossing alpha 1, a model's `opacity`,
|
|
213
|
+
* an image's orientation — but what a container does about it does not, and the component that
|
|
214
|
+
* owns the split is always the one that can repair it.
|
|
215
|
+
*
|
|
216
|
+
* A method rather than an assignable field because the container is held in `useState`, and the
|
|
217
|
+
* React Compiler rejects writing to a property of a state value ("This value cannot be modified").
|
|
218
|
+
* Calling a method is fine, which is how {@link reserve} already mutates.
|
|
219
|
+
* @param handler - The re-split to run, or `undefined` to clear it.
|
|
220
|
+
*/
|
|
221
|
+
setKindMismatchHandler(handler: (() => void) | undefined): void;
|
|
222
|
+
/**
|
|
223
|
+
* Reports that a member no longer belongs in this container, so it is drawing from the wrong mesh.
|
|
224
|
+
* Runs the registered handler, if any; a container mounted without one — in a spec, or bare —
|
|
225
|
+
* drops the report rather than failing, since nothing it could do would be more correct than
|
|
226
|
+
* carrying on.
|
|
227
|
+
*/
|
|
228
|
+
reportKindMismatch(): void;
|
|
204
229
|
/**
|
|
205
230
|
* Shows or hides one instance. Every def type carries `hidden`, so `useDefSync` drives this for
|
|
206
231
|
* all of them.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DIM_SCHEMA, DIM_UNIFORM, dimOrdered, instanceDim, isDimmed } from '../materials/dim';
|
|
1
|
+
import { DIM_SCHEMA, DIM_UNIFORM, dimOrdered, instanceDim, isDimmed, } from '../materials/effects/color-dim';
|
|
2
2
|
/**
|
|
3
3
|
* One layer's mesh bookkeeping: the bound mesh, which slot each def holds, and the paint order
|
|
4
4
|
* across them.
|
|
@@ -79,6 +79,8 @@ export class SlotContainer {
|
|
|
79
79
|
sortDirection;
|
|
80
80
|
/** Set by {@link bind}, which runs before any effect can claim a slot. */
|
|
81
81
|
boundMesh;
|
|
82
|
+
/** What to run when a member turns out to belong in a sibling container; see {@link reportKindMismatch}. */
|
|
83
|
+
kindMismatchHandler;
|
|
82
84
|
/**
|
|
83
85
|
* @param options - How this container differs from the default.
|
|
84
86
|
*/
|
|
@@ -133,6 +135,12 @@ export class SlotContainer {
|
|
|
133
135
|
// `setPaintOrder` defaults to 0 and the component's order pass relabels it in the same effect
|
|
134
136
|
// flush, before anything draws.
|
|
135
137
|
this.orderBySlot[slot] = this.orderByDef.get(def) ?? 0;
|
|
138
|
+
// Seeded here as well, so the instance's first `setDimAt` — every instance writes its dim as it
|
|
139
|
+
// mounts — reads the split it already draws on rather than `undefined`, and leaves the order
|
|
140
|
+
// pass to the structural one. Without it, mounting a layer of n defs runs n full order passes.
|
|
141
|
+
// A dim that arrives with the sync still crosses the split and still re-orders, and a claim
|
|
142
|
+
// made before the layer's own dim is known is relabelled by `setPaintOrder` in the same flush.
|
|
143
|
+
this.dimmedBySlot[slot] = isDimmed(def.dim, this.layerDimmed);
|
|
136
144
|
return slot;
|
|
137
145
|
}
|
|
138
146
|
/**
|
|
@@ -169,6 +177,33 @@ export class SlotContainer {
|
|
|
169
177
|
defFromHit(hit) {
|
|
170
178
|
return this.defAt(hit.instanceId);
|
|
171
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Registers what to do when a member turns out to belong in a sibling container; `undefined`
|
|
182
|
+
* clears it.
|
|
183
|
+
*
|
|
184
|
+
* Here rather than on each subclass because the situation is one situation: a layer is split
|
|
185
|
+
* across several containers by some property of the def, and that property can move after the def
|
|
186
|
+
* was placed. What splits them differs — a shape's color crossing alpha 1, a model's `opacity`,
|
|
187
|
+
* an image's orientation — but what a container does about it does not, and the component that
|
|
188
|
+
* owns the split is always the one that can repair it.
|
|
189
|
+
*
|
|
190
|
+
* A method rather than an assignable field because the container is held in `useState`, and the
|
|
191
|
+
* React Compiler rejects writing to a property of a state value ("This value cannot be modified").
|
|
192
|
+
* Calling a method is fine, which is how {@link reserve} already mutates.
|
|
193
|
+
* @param handler - The re-split to run, or `undefined` to clear it.
|
|
194
|
+
*/
|
|
195
|
+
setKindMismatchHandler(handler) {
|
|
196
|
+
this.kindMismatchHandler = handler;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Reports that a member no longer belongs in this container, so it is drawing from the wrong mesh.
|
|
200
|
+
* Runs the registered handler, if any; a container mounted without one — in a spec, or bare —
|
|
201
|
+
* drops the report rather than failing, since nothing it could do would be more correct than
|
|
202
|
+
* carrying on.
|
|
203
|
+
*/
|
|
204
|
+
reportKindMismatch() {
|
|
205
|
+
this.kindMismatchHandler?.();
|
|
206
|
+
}
|
|
172
207
|
/**
|
|
173
208
|
* Shows or hides one instance. Every def type carries `hidden`, so `useDefSync` drives this for
|
|
174
209
|
* all of them.
|
|
@@ -2,7 +2,7 @@ import { useThree } from '@react-three/fiber';
|
|
|
2
2
|
import { use, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
3
|
import { registerHitResolver } from '../../events/pointer';
|
|
4
4
|
import { registerSlotWriter, subscribeDef, syncNow, versionOf } from '../../update';
|
|
5
|
-
import { LayerDimContext } from '../materials/dim';
|
|
5
|
+
import { LayerDimContext } from '../materials/effects/color-dim';
|
|
6
6
|
/**
|
|
7
7
|
* Creates one layer's container and keeps it wired to its layer for as long as the component is
|
|
8
8
|
* mounted: the membership's paint order, and how hits on its mesh resolve back to defs.
|
|
@@ -6,7 +6,7 @@ import { type Logger } from '../../utils/logging';
|
|
|
6
6
|
* A mesh whose instances carry a transform.
|
|
7
7
|
*
|
|
8
8
|
* Named as a capability rather than listed as a union of our mesh classes, because not every batch
|
|
9
|
-
* has one: `
|
|
9
|
+
* has one: `BatchedLinesImpl` places its members by their endpoints and has no per-instance matrix
|
|
10
10
|
* at all, yet still writes colors through the helper below. A class that satisfies only one of
|
|
11
11
|
* these two interfaces is the point — the alternative was no-op matrix methods on a class that has
|
|
12
12
|
* no transforms, which would read as a capability it does not have.
|
|
@@ -12,6 +12,16 @@ export interface DefsProps<T extends RenderableDef> {
|
|
|
12
12
|
* updated.
|
|
13
13
|
*/
|
|
14
14
|
defs: readonly T[];
|
|
15
|
+
/**
|
|
16
|
+
* How many of `defs` may mount their instances now, counted from the start of the list — the
|
|
17
|
+
* staged-mount slice, from `useStagedChildren`. Defaults to all of them, which is what a caller
|
|
18
|
+
* mounting a layer outside the scheduler wants.
|
|
19
|
+
*
|
|
20
|
+
* Only the instances are sliced. The container is still built, sized and paint-ordered from the
|
|
21
|
+
* whole of `defs`, so a growing prefix claims into buffers that were allocated once, and the
|
|
22
|
+
* order pass that labels them runs per membership change rather than per slice.
|
|
23
|
+
*/
|
|
24
|
+
mounted?: number;
|
|
15
25
|
}
|
|
16
26
|
/** Props for a component keeping one def's instance in sync. */
|
|
17
27
|
export interface InstanceProps<T extends RenderableDef, C> {
|
|
@@ -35,6 +35,11 @@ import { TextureBudgetContext, useResizeFactor } from './texture-budget';
|
|
|
35
35
|
*/
|
|
36
36
|
export function Images(props) {
|
|
37
37
|
const { defs, maxAtlasSize } = props;
|
|
38
|
+
// Handed to each atlas as a prefix of *its* placements rather than apportioned between them, and
|
|
39
|
+
// a split source has several placements per def — so a slice mounts somewhere between one and a
|
|
40
|
+
// few instances per granted child. The scheduler measures what a slice cost and sizes the next
|
|
41
|
+
// one from that, which is what keeps the budget whatever the ratio turns out to be here.
|
|
42
|
+
const mounted = props.mounted ?? defs.length;
|
|
38
43
|
const gl = useThree((state) => state.gl);
|
|
39
44
|
const maxTextureSize = maxAtlasSize ?? gl.capabilities.maxTextureSize;
|
|
40
45
|
// Repack when the *set* of images changes, via the "adjust state on prop change" pattern. The
|
|
@@ -48,7 +53,7 @@ export function Images(props) {
|
|
|
48
53
|
if (!sameSet(packed.defs, defs)) {
|
|
49
54
|
setPacked((current) => packImages(defs, maxTextureSize, current.generation + 1));
|
|
50
55
|
}
|
|
51
|
-
return (_jsx(_Fragment, { children: packed.atlases.map((atlas) => (_jsx(ImageAtlas, { atlas: atlas, defs: defs }, atlasKey(atlas, packed.generation)))) }));
|
|
56
|
+
return (_jsx(_Fragment, { children: packed.atlases.map((atlas) => (_jsx(ImageAtlas, { atlas: atlas, defs: defs, mounted: mounted }, atlasKey(atlas, packed.generation)))) }));
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
59
|
* Draws one atlas as an `InstancedMesh`: the R3F-owned mesh with the atlas texture as its
|
|
@@ -67,6 +72,8 @@ export function Images(props) {
|
|
|
67
72
|
*/
|
|
68
73
|
function ImageAtlas(props) {
|
|
69
74
|
const { atlas, defs } = props;
|
|
75
|
+
// Only the placements are staged; the container and the texture are built from the whole atlas.
|
|
76
|
+
const mounted = props.mounted ?? atlas.images.length;
|
|
70
77
|
const [texture] = useState(() => makeAtlasTexture(atlas.canvas));
|
|
71
78
|
const container = useContainer(() => new ImageContainer(atlas), defs);
|
|
72
79
|
const invalidate = useThree((state) => state.invalidate);
|
|
@@ -92,9 +99,9 @@ function ImageAtlas(props) {
|
|
|
92
99
|
// rejecting its render reads.
|
|
93
100
|
// The quad and the material are children so R3F creates, attaches, and disposes both — the args
|
|
94
101
|
// pair are placeholders the constructor demands (see `ImageMeshArgs`). The material has to be per
|
|
95
|
-
// mesh: `
|
|
102
|
+
// mesh: `InstancedMeshImpl` wraps whatever it draws with in a closure over its own textures, and two
|
|
96
103
|
// instanced meshes on one material would both render against the first one's.
|
|
97
|
-
return (_jsxs(_Fragment, { children: [_jsxs(InstancedMesh, { args: container.args, ref: (mesh) => container.bind(mesh), sortObjects: true, customSort: container.customSort, perObjectFrustumCulled: true, children: [_jsx("planeGeometry", {}), _jsx(TextureMaterial, { map: texture, uvOffset: UV_OFFSET_UNIFORM })] }), atlas.images.map((packed) => (_jsx(ImageInstance, { def: packed.def, packed: packed, container: container }, `${defKey(packed.def)}:${packed.uv.u0}:${packed.uv.v0}`)))] }));
|
|
104
|
+
return (_jsxs(_Fragment, { children: [_jsxs(InstancedMesh, { args: container.args, ref: (mesh) => container.bind(mesh), sortObjects: true, customSort: container.customSort, perObjectFrustumCulled: true, children: [_jsx("planeGeometry", {}), _jsx(TextureMaterial, { map: texture, uvOffset: UV_OFFSET_UNIFORM })] }), atlas.images.slice(0, mounted).map((packed) => (_jsx(ImageInstance, { def: packed.def, packed: packed, container: container }, `${defKey(packed.def)}:${packed.uv.u0}:${packed.uv.v0}`)))] }));
|
|
98
105
|
}
|
|
99
106
|
/**
|
|
100
107
|
* Whether two snapshots hold the same *set* of images, order aside — the question "does this need a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type InstancedMeshImpl, type InstancedMeshImplParams } from '../../core/instanced/InstancedMesh';
|
|
2
2
|
import { type ImageDef } from '../../types/defs';
|
|
3
3
|
import { SlotContainer } from '../containers';
|
|
4
4
|
import { type Atlas, type PackedImage } from './atlas-packer';
|
|
@@ -15,16 +15,16 @@ export declare const UV_OFFSET_UNIFORM = "uvOffset";
|
|
|
15
15
|
* R3F diffs `args` element-wise and rebuilds the object when any element changes, so this is
|
|
16
16
|
* snapshotted once and never mutated.
|
|
17
17
|
*
|
|
18
|
-
* Only the options: `
|
|
18
|
+
* Only the options: `InstancedMeshImpl` takes no geometry or material, seeding throwaways that the
|
|
19
19
|
* element's `<planeGeometry>` and `<TextureMaterial>` children replace (R3F creates, attaches, and
|
|
20
20
|
* disposes those).
|
|
21
21
|
*/
|
|
22
|
-
export type ImageMeshArgs = [params:
|
|
22
|
+
export type ImageMeshArgs = [params: InstancedMeshImplParams];
|
|
23
23
|
/**
|
|
24
24
|
* One atlas's mesh bookkeeping.
|
|
25
25
|
*
|
|
26
26
|
* Simpler than a shape container, and deliberately so: an atlas draws one shared quad, so there is
|
|
27
|
-
* no geometry cache and no geometry-id juggling, and `
|
|
27
|
+
* no geometry cache and no geometry-id juggling, and `InstancedMeshImpl` grows its own buffers, so
|
|
28
28
|
* there is no capacity pre-scan — the mesh is merely sized to the atlas's packed-image count and
|
|
29
29
|
* left to resize itself if a claim ever overflows it. What remains is a slot per packed image and
|
|
30
30
|
* the uv region each one samples, written once at claim since a def's source never changes.
|
|
@@ -34,7 +34,7 @@ export type ImageMeshArgs = [params: InstancedMeshExtParams];
|
|
|
34
34
|
* change to the *set* of images is the harder case and still repacks into a fresh container —
|
|
35
35
|
* packing has no incremental path for that — but a pure reorder does not.
|
|
36
36
|
*/
|
|
37
|
-
export declare class ImageContainer extends SlotContainer<PackedImage,
|
|
37
|
+
export declare class ImageContainer extends SlotContainer<PackedImage, InstancedMeshImpl, ImageDef> {
|
|
38
38
|
/** The constructor args R3F builds the mesh from; a frozen snapshot, never mutated. */
|
|
39
39
|
readonly args: ImageMeshArgs;
|
|
40
40
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Vector4 } from 'three';
|
|
2
2
|
import { SlotContainer } from '../containers';
|
|
3
|
-
import { DIM_SCHEMA } from '../materials/dim';
|
|
3
|
+
import { DIM_SCHEMA } from '../materials/effects/color-dim';
|
|
4
4
|
/**
|
|
5
5
|
* The per-instance uniform each atlas mesh reads its texture region from. The container owns the
|
|
6
6
|
* name: it writes each slot's region here (`setUniformAt`) and hands it to the mesh's
|
|
@@ -13,7 +13,7 @@ export const UV_OFFSET_UNIFORM = 'uvOffset';
|
|
|
13
13
|
* One atlas's mesh bookkeeping.
|
|
14
14
|
*
|
|
15
15
|
* Simpler than a shape container, and deliberately so: an atlas draws one shared quad, so there is
|
|
16
|
-
* no geometry cache and no geometry-id juggling, and `
|
|
16
|
+
* no geometry cache and no geometry-id juggling, and `InstancedMeshImpl` grows its own buffers, so
|
|
17
17
|
* there is no capacity pre-scan — the mesh is merely sized to the atlas's packed-image count and
|
|
18
18
|
* left to resize itself if a claim ever overflows it. What remains is a slot per packed image and
|
|
19
19
|
* the uv region each one samples, written once at claim since a def's source never changes.
|
|
@@ -5,7 +5,7 @@ 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/dim';
|
|
8
|
+
import { LayerDimContext } from '../materials/effects/color-dim';
|
|
9
9
|
import { Models } from '../models';
|
|
10
10
|
import { Shapes } from '../shapes';
|
|
11
11
|
import { Texts } from '../text';
|
|
@@ -64,7 +64,7 @@ function LayerNode(props) {
|
|
|
64
64
|
// visibility this is never a prop: nothing between here and the material has any use for it.
|
|
65
65
|
const inheritedDim = use(LayerDimContext);
|
|
66
66
|
const dim = layer.dim ?? inheritedDim;
|
|
67
|
-
const { children,
|
|
67
|
+
const { children, mounted } = useStagedChildren(layer);
|
|
68
68
|
useLayoutTargets(layer, children);
|
|
69
69
|
const pointerProps = usePointerProps(layer, visible);
|
|
70
70
|
let contents = null;
|
|
@@ -72,8 +72,8 @@ function LayerNode(props) {
|
|
|
72
72
|
const childStep = orderStep / children.length;
|
|
73
73
|
contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, parentVisible: visible, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
|
|
74
74
|
}
|
|
75
|
-
else if (
|
|
76
|
-
contents = _jsx(LeafContent, { defs: children });
|
|
75
|
+
else if (mounted > 0) {
|
|
76
|
+
contents = _jsx(LeafContent, { defs: children, mounted: mounted });
|
|
77
77
|
}
|
|
78
78
|
const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: orderBase, ...pointerProps, children: [prelude, contents] }));
|
|
79
79
|
// Always provided, never conditionally on the layer declaring a `dim`: making the provider appear
|
|
@@ -98,17 +98,17 @@ function LayerNode(props) {
|
|
|
98
98
|
* @returns The container for the defs' kind.
|
|
99
99
|
*/
|
|
100
100
|
function LeafContent(props) {
|
|
101
|
-
const { defs } = props;
|
|
101
|
+
const { defs, mounted } = props;
|
|
102
102
|
let content = null;
|
|
103
103
|
if (isShapeDefs(defs))
|
|
104
|
-
content = _jsx(Shapes, { defs: defs });
|
|
104
|
+
content = _jsx(Shapes, { defs: defs, mounted: mounted });
|
|
105
105
|
else if (isImageDefs(defs))
|
|
106
|
-
content = _jsx(Images, { defs: defs });
|
|
106
|
+
content = _jsx(Images, { defs: defs, mounted: mounted });
|
|
107
107
|
else if (isTextDefs(defs))
|
|
108
|
-
content = _jsx(Texts, { defs: defs });
|
|
108
|
+
content = _jsx(Texts, { defs: defs, mounted: mounted });
|
|
109
109
|
else if (isLineDefs(defs))
|
|
110
|
-
content = _jsx(Lines, { defs: defs });
|
|
110
|
+
content = _jsx(Lines, { defs: defs, mounted: mounted });
|
|
111
111
|
else if (isModelDefs(defs))
|
|
112
|
-
content = _jsx(Models, { defs: defs });
|
|
112
|
+
content = _jsx(Models, { defs: defs, mounted: mounted });
|
|
113
113
|
return _jsx(Suspense, { fallback: null, children: content });
|
|
114
114
|
}
|