@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.
Files changed (61) hide show
  1. package/dist/Renderer.d.ts +14 -0
  2. package/dist/Renderer.js +16 -4
  3. package/dist/core/shader-source.d.ts +20 -0
  4. package/dist/core/shader-source.js +26 -0
  5. package/dist/debug/components/Gui.d.ts +2 -0
  6. package/dist/debug/components/Gui.js +11 -1
  7. package/dist/external/ExternalCanvas.js +5 -6
  8. package/dist/external/external-camera.js +26 -11
  9. package/dist/index.d.ts +1 -0
  10. package/dist/objects/Background.js +1 -1
  11. package/dist/objects/containers/SlotContainer.js +7 -1
  12. package/dist/objects/containers/hooks.js +1 -1
  13. package/dist/objects/containers/props.d.ts +10 -0
  14. package/dist/objects/images/Images.js +9 -2
  15. package/dist/objects/images/image-container.js +1 -1
  16. package/dist/objects/layer/Layer.js +10 -10
  17. package/dist/objects/layer/mount-scheduler.d.ts +83 -54
  18. package/dist/objects/layer/mount-scheduler.js +204 -60
  19. package/dist/objects/lighting/Lighting.d.ts +92 -0
  20. package/dist/objects/lighting/Lighting.js +385 -0
  21. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  22. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  23. package/dist/objects/lighting/index.d.ts +11 -0
  24. package/dist/objects/lighting/index.js +10 -0
  25. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  26. package/dist/objects/lighting/lighting-gui.js +114 -0
  27. package/dist/objects/lighting/occluder.d.ts +34 -0
  28. package/dist/objects/lighting/occluder.js +62 -0
  29. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  30. package/dist/objects/lighting/occlusion-targets.js +276 -0
  31. package/dist/objects/lines/Lines.js +3 -1
  32. package/dist/objects/materials/ColorMaterial.d.ts +58 -51
  33. package/dist/objects/materials/ColorMaterial.js +74 -52
  34. package/dist/objects/materials/LineMaterial.js +1 -1
  35. package/dist/objects/materials/ModelMaterial.d.ts +30 -18
  36. package/dist/objects/materials/ModelMaterial.js +68 -41
  37. package/dist/objects/materials/TextureMaterial.js +1 -1
  38. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  39. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  40. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  41. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  42. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  43. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  44. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  45. package/dist/objects/materials/effects/ao/index.js +72 -0
  46. package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +4 -4
  47. package/dist/objects/materials/{dim.js → effects/color-dim.js} +6 -24
  48. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  49. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  50. package/dist/objects/materials/effects/effects-context.d.ts +52 -0
  51. package/dist/objects/materials/effects/effects-context.js +24 -0
  52. package/dist/objects/models/Models.js +18 -2
  53. package/dist/objects/models/model-container.js +1 -1
  54. package/dist/objects/shapes/Shapes.js +20 -2
  55. package/dist/objects/text/Texts.js +3 -1
  56. package/dist/objects/text/text-container.js +1 -1
  57. package/dist/space/Scene.d.ts +3 -0
  58. package/dist/space/Scene.js +17 -5
  59. package/package.json +2 -2
  60. package/dist/objects/Lighting.d.ts +0 -20
  61. package/dist/objects/Lighting.js +0 -85
@@ -2,6 +2,7 @@ import { type Box } from '@expofp/geometry';
2
2
  import { type HTMLAttributes, type ReactNode, type Ref } from 'react';
3
3
  import { type ControlsConfig } from './controls';
4
4
  import { type ExternalCanvasProps } from './external/ExternalCanvas';
5
+ import { type LightingProps } from './objects/lighting';
5
6
  import { type CameraProps } from './space/Camera';
6
7
  import { type SceneDef } from './types/defs';
7
8
  import { type PointerMissedEventData } from './types/events';
@@ -60,6 +61,19 @@ export interface RendererProps extends HTMLAttributes<HTMLDivElement>, CameraPro
60
61
  * without taking a gesture away.
61
62
  */
62
63
  controls?: ControlsConfig;
64
+ /**
65
+ * The scene's lighting, and the darkening that rides with it: the rig's contrast and three
66
+ * colors, and a `shadow` object holding the shade both darkening terms blend toward and each
67
+ * term's own numbers. See {@link LightingProps}.
68
+ *
69
+ * Every value is optional and every omitted one keeps the design's own default, so a caller sets
70
+ * the one number a plan needs and nothing else. Where the key light points is not here: that is
71
+ * the design's decision, and the debug GUI keeps it.
72
+ *
73
+ * Changing a value moves a mounted scene with no remount and no shader recompile. It lands on a
74
+ * uniform or on a field the frame reads, and asks for a redraw.
75
+ */
76
+ lighting?: LightingProps;
63
77
  /**
64
78
  * The part of the canvas the plan should be framed inside, in css pixels from its top-left
65
79
  * corner — for a host whose own header, sheet or panel covers the rest.
package/dist/Renderer.js CHANGED
@@ -24,12 +24,12 @@ export function Renderer(props) {
24
24
  // attributes — which is what React warns about, and the reason every member of `CameraProps` has
25
25
  // to be named here as well as reassembled below. They are passed on untouched: what an omitted
26
26
  // one means is `Camera`'s to say, and it is the only place that says it.
27
- const { debug, scene, memoryLimit, minZoom, maxZoom, minPitch, maxPitch, bounds, smoothTime, controls, visibleRect, onPointerMissed, gl, requestFrame, onFrame, children, ...canvasProps } = props;
27
+ const { debug, scene, memoryLimit, minZoom, maxZoom, minPitch, maxPitch, bounds, smoothTime, controls, lighting, visibleRect, onPointerMissed, gl, requestFrame, onFrame, children, ...canvasProps } = props;
28
28
  const cameraProps = { minZoom, maxZoom, minPitch, maxPitch, bounds, smoothTime };
29
29
  const external = gl !== undefined;
30
30
  // The one tree, whichever canvas it lands on. The mount decides nothing about it; the tree
31
31
  // leaves out what external mode has no object for — the controls here, the camera inside Scene.
32
- const tree = (_jsx(DebugOverlay, { enabled: debug, children: _jsxs(Scene, { sceneDef: scene, external: external, memoryLimit: memoryLimit, visibleRect: visibleRect, ...cameraProps, children: [!external && _jsx(Controls, { config: controls }), _jsx(PointerMissed, { onPointerMissed: onPointerMissed }), children] }) }));
32
+ const tree = (_jsx(DebugOverlay, { enabled: debug, children: _jsxs(Scene, { sceneDef: scene, external: external, memoryLimit: memoryLimit, lighting: lighting, visibleRect: visibleRect, ...cameraProps, children: [!external && _jsx(Controls, { config: controls }), _jsx(PointerMissed, { onPointerMissed: onPointerMissed }), children] }) }));
33
33
  // The host's context arriving is what selects external mode. The mount swap is an element-type
34
34
  // change, so flipping modes at runtime remounts — like `debug`, both are fixed for a renderer's
35
35
  // lifetime in practice.
@@ -43,11 +43,23 @@ export function Renderer(props) {
43
43
  return (_jsx(ExternalCanvas, { gl: gl, requestFrame: requestFrame, onFrame: onFrame, children: tree }));
44
44
  }
45
45
  return (_jsx(StrictMode, { children: _jsx(Suspense, { fallback: null, children: _jsx(CanvasWithDebug, { ...canvasProps, debugEnabled: debug,
46
- // Tonemapping off: plan colors are authored values, not scene-referred light.
46
+ // R3F force-writes ACES filmic tone mapping onto a fresh renderer unless `flat` is set,
47
+ // which lands it on three's own default, `NoToneMapping`, instead. This renderer does not
48
+ // tone map: `Lighting` keeps the rig's light sum in range by construction, rather than
49
+ // compressing an unbounded one back into range afterward.
47
50
  flat: true,
48
51
  // three leaves the stencil buffer off by default. Line layers need one: it is how they keep
49
52
  // translucent lines from darkening where they overlap (see `LineMaterial`), and without the
50
53
  // buffer the stencil settings silently do nothing. In the MapLibre embed the context is the
51
54
  // map's own, which already has one.
52
- gl: { stencil: true }, frameloop: debug ? 'always' : 'demand', resize: { debounce: 50 }, children: tree }) }) }));
55
+ gl: { stencil: true },
56
+ // three reads a program's info log straight after linking it, and that read blocks until
57
+ // the driver has finished linking. Left to itself the driver links in the background,
58
+ // because `KHR_parallel_shader_compile` is there, so the check is the whole of what makes
59
+ // a compile synchronous: 17 programs cost 64 ms of main thread on a large scene, in
60
+ // blocks of up to 12 ms, and they land in the frames where the layers are still mounting.
61
+ // Debug keeps the check, because a broken shader has to report itself somewhere.
62
+ onCreated: (state) => {
63
+ state.gl.debug.checkShaderErrors = debug ?? false;
64
+ }, frameloop: debug ? 'always' : 'demand', resize: { debounce: 50 }, children: tree }) }) }));
53
65
  }
@@ -11,6 +11,9 @@
11
11
  * Both helpers throw when they cannot find it, for the same reason: what reaches the driver
12
12
  * otherwise is a compile error naming an undeclared identifier, a long way from the patch that
13
13
  * failed to declare it.
14
+ *
15
+ * {@link sceneColorAnchor} is not about `main`, but it answers the same kind of question: where in a
16
+ * shader's own text a patch belongs.
14
17
  */
15
18
  /**
16
19
  * Declares GLSL above `main`, where file-scope declarations belong — samplers, varyings, functions.
@@ -47,4 +50,21 @@ export declare function openMain(source: string, glsl: string): string;
47
50
  * @throws {Error} If the source does not contain the anchor.
48
51
  */
49
52
  export declare function replaceAnchor(source: string, anchor: string, glsl: string): string;
53
+ /**
54
+ * Where the fragment's **scene** color is final: the line a patch that changes the scene must land
55
+ * in front of.
56
+ *
57
+ * `<opaque_fragment>` writes the scene color, and three's output chain follows it — tone mapping,
58
+ * the colorspace conversion, fog, dithering. This returns the first of those. A patch below any of
59
+ * them would edit a value the chain already transformed, so its strength would depend on how bright
60
+ * the pixel already was.
61
+ *
62
+ * Two spellings, because three resolves `#include` after every `onBeforeCompile` while troika
63
+ * resolves it earlier, in its own rebuild of `main`. The expanded spelling is the `#if` line rather
64
+ * than the tone-mapping line inside it: the renderer pins `NoToneMapping`, which leaves
65
+ * `TONE_MAPPING` undefined, and the preprocessor then strips that block and anything put in it.
66
+ * @param fragmentShader - The shader to search.
67
+ * @returns The anchor found, or `undefined` if the shader has no output chain.
68
+ */
69
+ export declare function sceneColorAnchor(fragmentShader: string): string | undefined;
50
70
  //# sourceMappingURL=shader-source.d.ts.map
@@ -11,6 +11,9 @@
11
11
  * Both helpers throw when they cannot find it, for the same reason: what reaches the driver
12
12
  * otherwise is a compile error naming an undeclared identifier, a long way from the patch that
13
13
  * failed to declare it.
14
+ *
15
+ * {@link sceneColorAnchor} is not about `main`, but it answers the same kind of question: where in a
16
+ * shader's own text a patch belongs.
14
17
  */
15
18
  /**
16
19
  * Declares GLSL above `main`, where file-scope declarations belong — samplers, varyings, functions.
@@ -56,6 +59,29 @@ export function replaceAnchor(source, anchor, glsl) {
56
59
  throw new Error(`shader-source: the shader has no "${anchor}" to rewrite.`);
57
60
  return source.slice(0, at) + glsl + source.slice(at + anchor.length);
58
61
  }
62
+ /**
63
+ * Where the fragment's **scene** color is final: the line a patch that changes the scene must land
64
+ * in front of.
65
+ *
66
+ * `<opaque_fragment>` writes the scene color, and three's output chain follows it — tone mapping,
67
+ * the colorspace conversion, fog, dithering. This returns the first of those. A patch below any of
68
+ * them would edit a value the chain already transformed, so its strength would depend on how bright
69
+ * the pixel already was.
70
+ *
71
+ * Two spellings, because three resolves `#include` after every `onBeforeCompile` while troika
72
+ * resolves it earlier, in its own rebuild of `main`. The expanded spelling is the `#if` line rather
73
+ * than the tone-mapping line inside it: the renderer pins `NoToneMapping`, which leaves
74
+ * `TONE_MAPPING` undefined, and the preprocessor then strips that block and anything put in it.
75
+ * @param fragmentShader - The shader to search.
76
+ * @returns The anchor found, or `undefined` if the shader has no output chain.
77
+ */
78
+ export function sceneColorAnchor(fragmentShader) {
79
+ const include = /*glsl*/ `#include <tonemapping_fragment>`;
80
+ const expanded = /*glsl*/ `#if defined( TONE_MAPPING )`;
81
+ if (fragmentShader.includes(include))
82
+ return include;
83
+ return fragmentShader.includes(expanded) ? expanded : undefined;
84
+ }
59
85
  /**
60
86
  * Rewrites the first opening of `main`, splicing rather than replacing — so a `$` in the injected
61
87
  * GLSL cannot be read as a replacement pattern.
@@ -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
  }
@@ -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 plan is drawn in three's own defaults — no tonemapping, sRGB
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
- // `flat` is not an opinion here but a shield: R3F's first configure force-writes
110
- // `toneMapping` onto any renderer it is handed — ACES filmic unless flat — and flat is the
111
- // one value that lands on three's own default, NoToneMapping. Everything else stays as the
112
- // bare constructor left it.
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
- if (_near.distanceToSquared(_position) > _far.distanceToSquared(_position)) {
54
- _direction.copy(_near).sub(_far).normalize();
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(_far).sub(_near).normalize();
58
+ _direction.copy(_near).sub(_far).normalize();
58
59
  }
59
- // Step 3: synthesize a rigid view matrix via look-at.
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
- // The choice of "up" is arbitrary as long as it is not parallel to the view direction: any
62
- // non-degenerate choice yields a valid (P′, V′) pair, because P is recomputed from
63
- // M · matrixWorld at the end. The fallback handles top-down or bottom-up views where world +z
64
- // aligns with the view ray.
65
- _up.set(0, 0, 1);
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
@@ -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.
@@ -133,6 +133,12 @@ export class SlotContainer {
133
133
  // `setPaintOrder` defaults to 0 and the component's order pass relabels it in the same effect
134
134
  // flush, before anything draws.
135
135
  this.orderBySlot[slot] = this.orderByDef.get(def) ?? 0;
136
+ // Seeded here as well, so the instance's first `setDimAt` — every instance writes its dim as it
137
+ // mounts — reads the split it already draws on rather than `undefined`, and leaves the order
138
+ // pass to the structural one. Without it, mounting a layer of n defs runs n full order passes.
139
+ // A dim that arrives with the sync still crosses the split and still re-orders, and a claim
140
+ // made before the layer's own dim is known is relabelled by `setPaintOrder` in the same flush.
141
+ this.dimmedBySlot[slot] = isDimmed(def.dim, this.layerDimmed);
136
142
  return slot;
137
143
  }
138
144
  /**
@@ -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.
@@ -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);
@@ -94,7 +101,7 @@ function ImageAtlas(props) {
94
101
  // pair are placeholders the constructor demands (see `ImageMeshArgs`). The material has to be per
95
102
  // mesh: `InstancedMeshExt` 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,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
@@ -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, admitted } = useStagedChildren(layer);
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 (admitted) {
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
  }