@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.
Files changed (88) hide show
  1. package/dist/Renderer.d.ts +14 -0
  2. package/dist/Renderer.js +16 -4
  3. package/dist/core/SquareDataTexture.d.ts +2 -2
  4. package/dist/core/SquareDataTexture.js +2 -2
  5. package/dist/core/batch/BatchedMesh.d.ts +3 -3
  6. package/dist/core/batch/BatchedMesh.js +3 -3
  7. package/dist/core/instanced/InstancedMesh.d.ts +7 -7
  8. package/dist/core/instanced/InstancedMesh.js +4 -4
  9. package/dist/core/instanced/instanced-material.js +1 -1
  10. package/dist/core/lines/BatchedLines.d.ts +4 -4
  11. package/dist/core/lines/BatchedLines.js +4 -4
  12. package/dist/core/shader-source.d.ts +20 -0
  13. package/dist/core/shader-source.js +26 -0
  14. package/dist/core/text/BatchedText.d.ts +14 -14
  15. package/dist/core/text/BatchedText.js +9 -9
  16. package/dist/core/text/MemberTexture.d.ts +2 -2
  17. package/dist/core/text/Text.d.ts +5 -5
  18. package/dist/core/text/Text.js +4 -4
  19. package/dist/core/text/glyph-packing.d.ts +2 -2
  20. package/dist/debug/components/Gui.d.ts +2 -0
  21. package/dist/debug/components/Gui.js +11 -1
  22. package/dist/events/layout-targets.d.ts +7 -3
  23. package/dist/events/layout-targets.js +13 -1
  24. package/dist/external/ExternalCanvas.js +5 -6
  25. package/dist/external/external-camera.js +26 -11
  26. package/dist/index.d.ts +1 -0
  27. package/dist/objects/Background.js +1 -1
  28. package/dist/objects/containers/SlotContainer.d.ts +27 -2
  29. package/dist/objects/containers/SlotContainer.js +36 -1
  30. package/dist/objects/containers/hooks.js +1 -1
  31. package/dist/objects/containers/mesh-writes.d.ts +1 -1
  32. package/dist/objects/containers/props.d.ts +10 -0
  33. package/dist/objects/images/Images.js +10 -3
  34. package/dist/objects/images/image-container.d.ts +5 -5
  35. package/dist/objects/images/image-container.js +2 -2
  36. package/dist/objects/layer/Layer.js +10 -10
  37. package/dist/objects/layer/mount-scheduler.d.ts +83 -54
  38. package/dist/objects/layer/mount-scheduler.js +204 -60
  39. package/dist/objects/lighting/Lighting.d.ts +92 -0
  40. package/dist/objects/lighting/Lighting.js +385 -0
  41. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  42. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  43. package/dist/objects/lighting/index.d.ts +11 -0
  44. package/dist/objects/lighting/index.js +10 -0
  45. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  46. package/dist/objects/lighting/lighting-gui.js +114 -0
  47. package/dist/objects/lighting/occluder.d.ts +34 -0
  48. package/dist/objects/lighting/occluder.js +62 -0
  49. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  50. package/dist/objects/lighting/occlusion-targets.js +276 -0
  51. package/dist/objects/lines/Lines.js +3 -1
  52. package/dist/objects/lines/line-container.d.ts +4 -4
  53. package/dist/objects/materials/ColorMaterial.d.ts +58 -51
  54. package/dist/objects/materials/ColorMaterial.js +74 -52
  55. package/dist/objects/materials/LineMaterial.d.ts +1 -1
  56. package/dist/objects/materials/LineMaterial.js +1 -1
  57. package/dist/objects/materials/ModelMaterial.d.ts +30 -18
  58. package/dist/objects/materials/ModelMaterial.js +68 -41
  59. package/dist/objects/materials/TextureMaterial.js +1 -1
  60. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  61. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  62. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  63. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  64. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  65. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  66. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  67. package/dist/objects/materials/effects/ao/index.js +72 -0
  68. package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +5 -5
  69. package/dist/objects/materials/{dim.js → effects/color-dim.js} +7 -25
  70. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  71. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  72. package/dist/objects/materials/effects/effects-context.d.ts +52 -0
  73. package/dist/objects/materials/effects/effects-context.js +24 -0
  74. package/dist/objects/models/Models.js +20 -4
  75. package/dist/objects/models/model-container.d.ts +4 -20
  76. package/dist/objects/models/model-container.js +2 -22
  77. package/dist/objects/shapes/Shapes.js +20 -2
  78. package/dist/objects/shapes/shape-container.d.ts +3 -20
  79. package/dist/objects/shapes/shape-container.js +0 -21
  80. package/dist/objects/text/Texts.js +3 -1
  81. package/dist/objects/text/text-container.d.ts +6 -6
  82. package/dist/objects/text/text-container.js +4 -4
  83. package/dist/objects/text/text-layout.d.ts +2 -2
  84. package/dist/space/Scene.d.ts +3 -0
  85. package/dist/space/Scene.js +17 -5
  86. package/package.json +2 -2
  87. package/dist/objects/Lighting.d.ts +0 -20
  88. package/dist/objects/Lighting.js +0 -85
@@ -5,6 +5,7 @@ import { InstancedMesh } from '../../core/instanced/InstancedMesh';
5
5
  import { sameSequence } from '../../utils/arrays';
6
6
  import { createLogger } from '../../utils/logging';
7
7
  import { defKey, useContainer } from '../containers';
8
+ import { useOccluder } from '../lighting/occluder';
8
9
  import { ModelMaterial } from '../materials/ModelMaterial';
9
10
  import { loadModel } from './gltf-cache';
10
11
  import { MODEL_KINDS, ModelContainer, modelKindOf, OPACITY_UNIFORM, } from './model-container';
@@ -28,6 +29,10 @@ const logger = createLogger('models');
28
29
  */
29
30
  export function Models(props) {
30
31
  const { defs } = props;
32
+ // Handed to each part's mesh as a prefix of *its* members, the way `Shapes` hands one to each of
33
+ // its batches: a model layer is small and its meshes are few, and the scheduler sizes the next
34
+ // slice from what this one actually cost.
35
+ const mounted = props.mounted ?? defs.length;
31
36
  // The layer split two ways at once: by url, because that is what a mesh's geometry comes from,
32
37
  // and by kind, because `transparent` is a material property and a translucent copy of a model
33
38
  // therefore cannot share a mesh with an opaque one (see `modelKindOf`).
@@ -84,7 +89,7 @@ export function Models(props) {
84
89
  const members = partition.batches[kind].get(asset.url);
85
90
  if (!members)
86
91
  return null;
87
- return (_jsx(Fragment, { children: asset.parts.map((part) => (_jsx(ModelPartMesh, { asset: asset, part: part, defs: members, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, part.id))) }, kind));
92
+ return (_jsx(Fragment, { children: asset.parts.map((part) => (_jsx(ModelPartMesh, { asset: asset, part: part, defs: members, mounted: mounted, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, part.id))) }, kind));
88
93
  }) }, asset.url))) }));
89
94
  }
90
95
  /**
@@ -128,11 +133,14 @@ function emptied(previous) {
128
133
  */
129
134
  function ModelPartMesh(props) {
130
135
  const { asset, part, defs, kind, renderOrder, onKindMismatch } = props;
136
+ // Only the instances are staged; the container is sized and ordered from every member.
137
+ const mounted = props.mounted ?? defs.length;
131
138
  // Ordering is a no-op for models — their container declares it so, and the mesh depth-sorts itself
132
- // instead — but the hit resolution is not: `InstancedMeshExt.raycast` walks the live slots,
139
+ // instead — but the hit resolution is not: `InstancedMeshImpl.raycast` walks the live slots,
133
140
  // skipping deleted and hidden ones, and raycasts the real merged geometry, so a hit names the
134
141
  // model's actual triangles and the slot that drew them.
135
142
  const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs);
143
+ const occluder = useOccluder();
136
144
  // Assigned after mount rather than passed to the constructor: the container is built once and
137
145
  // outlives every render, so it cannot close over a current callback. Nothing can fire before this
138
146
  // runs — the split that placed these defs was computed from the same opacities they mount with.
@@ -142,7 +150,7 @@ function ModelPartMesh(props) {
142
150
  }, [container, onKindMismatch]);
143
151
  // The geometry belongs to the module-level cache and may be drawing in another layer, so it is
144
152
  // mounted as a `primitive` with `dispose={null}` — R3F must not free it on unmount. Sharing it is
145
- // safe because `InstancedMeshExt` never writes to the geometry it is handed: its draw list lives in
153
+ // safe because `InstancedMeshImpl` never writes to the geometry it is handed: its draw list lives in
146
154
  // a texture of its own, addressed by `gl_InstanceID`. The material cannot be shared the same way —
147
155
  // the mesh wraps it in a closure over its own textures — so `ModelMaterial` clones it per mesh.
148
156
  //
@@ -150,7 +158,15 @@ function ModelPartMesh(props) {
150
158
  // needs (see `ModelContainer.meshMatrix`); `matrixAutoUpdate` off because it is composed, not
151
159
  // decomposed from position/rotation/scale. `sortObjects` is what a depth-writing draw wants in
152
160
  // place of paint order — nearest first while opaque, farthest first once the material blends.
153
- return (_jsxs(_Fragment, { children: [_jsxs(InstancedMesh, { args: container.args, ref: (mesh) => container.bind(mesh), matrixAutoUpdate: false, matrix: container.meshMatrix, sortObjects: true, renderOrder: renderOrder, children: [_jsx("primitive", { object: part.geometry, attach: "geometry", dispose: null }), _jsx(ModelMaterial, { source: part.material, opacityUniform: OPACITY_UNIFORM, translucent: kind === 'translucent' })] }), defs.map((def) => (_jsx(ModelInstance, { def: def, container: container }, defKey(def))))] }));
161
+ //
162
+ // The opaque mesh asks for occlusion, or a model would neither occlude nor be occluded; the
163
+ // translucent one stays out, because a surface you can see through must not cast full occlusion,
164
+ // and asks with `null` — which is also what a detaching `ref` says, so the layer number and the
165
+ // scene's registry are `useOccluder`'s alone.
166
+ return (_jsxs(_Fragment, { children: [_jsxs(InstancedMesh, { args: container.args, ref: (mesh) => {
167
+ container.bind(mesh);
168
+ occluder(kind === 'opaque' ? mesh : null);
169
+ }, matrixAutoUpdate: false, matrix: container.meshMatrix, sortObjects: true, renderOrder: renderOrder, children: [_jsx("primitive", { object: part.geometry, attach: "geometry", dispose: null }), _jsx(ModelMaterial, { source: part.material, opacityUniform: OPACITY_UNIFORM, translucent: kind === 'translucent' })] }), defs.slice(0, mounted).map((def) => (_jsx(ModelInstance, { def: def, container: container }, defKey(def))))] }));
154
170
  }
155
171
  /**
156
172
  * Loads one model, reporting a failure rather than rejecting — one missing model should not cost a
@@ -1,6 +1,6 @@
1
1
  import { type Rect } from '@expofp/geometry';
2
2
  import { type Box3, Matrix4 } from 'three';
3
- import { type InstancedMeshExt, type InstancedMeshExtParams } from '../../core/instanced/InstancedMesh';
3
+ import { type InstancedMeshImpl, type InstancedMeshImplParams } from '../../core/instanced/InstancedMesh';
4
4
  import { type ModelDef } from '../../types/defs';
5
5
  import { SlotContainer } from '../containers';
6
6
  /**
@@ -10,7 +10,7 @@ import { SlotContainer } from '../containers';
10
10
  */
11
11
  export declare const OPACITY_UNIFORM = "instanceOpacity";
12
12
  /** The constructor tuple R3F builds one model mesh from. Frozen, so R3F never rebuilds the mesh. */
13
- export type ModelMeshArgs = [params: InstancedMeshExtParams];
13
+ export type ModelMeshArgs = [params: InstancedMeshImplParams];
14
14
  /**
15
15
  * Every kind of model batch, **in the order they draw within a layer** — which is what `Models`
16
16
  * numbers them by.
@@ -34,11 +34,11 @@ export declare function modelKindOf(def: ModelDef): ModelKind;
34
34
  * twenty copies of a chair are one draw call rather than twenty.
35
35
  *
36
36
  * Much simpler than a shape container. The geometry is the part's — shared, never per instance — so
37
- * there is no geometry cache and no capacity pre-scan, and `InstancedMeshExt` grows its own buffers,
37
+ * there is no geometry cache and no capacity pre-scan, and `InstancedMeshImpl` grows its own buffers,
38
38
  * so nothing here has to size anything. What is left is a slot per def and the transform each one
39
39
  * draws with.
40
40
  */
41
- export declare class ModelContainer extends SlotContainer<null, InstancedMeshExt, ModelDef> {
41
+ export declare class ModelContainer extends SlotContainer<null, InstancedMeshImpl, ModelDef> {
42
42
  /** The constructor args R3F builds the mesh from; a frozen snapshot, never mutated. */
43
43
  readonly args: ModelMeshArgs;
44
44
  /** Which kind of member this container holds, and so which render list its mesh draws from. */
@@ -50,7 +50,6 @@ export declare class ModelContainer extends SlotContainer<null, InstancedMeshExt
50
50
  readonly meshMatrix: Matrix4;
51
51
  /** The model's bounds in plan units; every instance is fitted against them. */
52
52
  private readonly extents;
53
- private kindMismatchHandler;
54
53
  /**
55
54
  * Builds the bookkeeping for one model part.
56
55
  * @param extents - The model's own bounds, y-up, as `bakeGltf` left them.
@@ -74,21 +73,6 @@ export declare class ModelContainer extends SlotContainer<null, InstancedMeshExt
74
73
  * quietly appear to order this one.
75
74
  */
76
75
  setPaintOrder(): void;
77
- /**
78
- * Registers what to do when a member turns out to be the wrong kind. `Models` supplies a re-split;
79
- * `undefined` clears it.
80
- *
81
- * A method rather than an assignable field because the container is held in `useState`, and the
82
- * React Compiler rejects writing to a property of a state value.
83
- * @param handler - The re-split to run, or `undefined` to clear it.
84
- */
85
- setKindMismatchHandler(handler: (() => void) | undefined): void;
86
- /**
87
- * Reports that a member no longer matches {@link kind} — its `opacity` crossed 1 — so it is
88
- * drawing from a mesh whose material describes the wrong render list. Runs the registered handler,
89
- * if any; a container mounted outside `Models` has none and the report is simply dropped.
90
- */
91
- reportKindMismatch(): void;
92
76
  /**
93
77
  * Writes a def's placement into its slot: the model's extents fitted into `bounds`.
94
78
  *
@@ -1,6 +1,6 @@
1
1
  import { Matrix4, Quaternion, Vector3 } 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 a model mesh reads its opacity from. The container owns the name: it
6
6
  * declares the schema on bind and writes each slot's value, and hands the name to `ModelMaterial`,
@@ -30,7 +30,7 @@ export function modelKindOf(def) {
30
30
  * twenty copies of a chair are one draw call rather than twenty.
31
31
  *
32
32
  * Much simpler than a shape container. The geometry is the part's — shared, never per instance — so
33
- * there is no geometry cache and no capacity pre-scan, and `InstancedMeshExt` grows its own buffers,
33
+ * there is no geometry cache and no capacity pre-scan, and `InstancedMeshImpl` grows its own buffers,
34
34
  * so nothing here has to size anything. What is left is a slot per def and the transform each one
35
35
  * draws with.
36
36
  */
@@ -46,7 +46,6 @@ export class ModelContainer extends SlotContainer {
46
46
  meshMatrix = PLAN_FROM_GLTF;
47
47
  /** The model's bounds in plan units; every instance is fitted against them. */
48
48
  extents;
49
- kindMismatchHandler;
50
49
  /**
51
50
  * Builds the bookkeeping for one model part.
52
51
  * @param extents - The model's own bounds, y-up, as `bakeGltf` left them.
@@ -78,25 +77,6 @@ export class ModelContainer extends SlotContainer {
78
77
  setPaintOrder() {
79
78
  // Nothing: the depth buffer orders models, and a layer's parts are separate meshes regardless.
80
79
  }
81
- /**
82
- * Registers what to do when a member turns out to be the wrong kind. `Models` supplies a re-split;
83
- * `undefined` clears it.
84
- *
85
- * A method rather than an assignable field because the container is held in `useState`, and the
86
- * React Compiler rejects writing to a property of a state value.
87
- * @param handler - The re-split to run, or `undefined` to clear it.
88
- */
89
- setKindMismatchHandler(handler) {
90
- this.kindMismatchHandler = handler;
91
- }
92
- /**
93
- * Reports that a member no longer matches {@link kind} — its `opacity` crossed 1 — so it is
94
- * drawing from a mesh whose material describes the wrong render list. Runs the registered handler,
95
- * if any; a container mounted outside `Models` has none and the report is simply dropped.
96
- */
97
- reportKindMismatch() {
98
- this.kindMismatchHandler?.();
99
- }
100
80
  /**
101
81
  * Writes a def's placement into its slot: the model's extents fitted into `bounds`.
102
82
  *
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useEffect, useState } from 'react';
3
3
  import { BatchedMesh } from '../../core/batch/BatchedMesh';
4
4
  import { defKey, useContainer } from '../containers';
5
+ import { useOccluder } from '../lighting/occluder';
5
6
  import { ColorMaterial } from '../materials/ColorMaterial';
6
7
  import { SHAPE_KINDS, ShapeContainer, shapeKindOf } from './shape-container';
7
8
  import { ShapeInstance } from './ShapeInstance';
@@ -22,6 +23,11 @@ import { ShapeInstance } from './ShapeInstance';
22
23
  */
23
24
  export function Shapes(props) {
24
25
  const { defs } = props;
26
+ // Handed to each batch as a prefix of *its* members rather than apportioned between them: the
27
+ // batches are three at most, so a slice can overshoot the scheduler's budget by that factor at
28
+ // worst, and the scheduler measures what a slice actually cost and sizes the next one from it.
29
+ // Apportioning would cost a walk of the layer's defs per render to find where the prefix falls.
30
+ const mounted = props.mounted ?? defs.length;
25
31
  // Held in state rather than derived each render, the same "adjust on prop change" shape `Images`
26
32
  // uses for its pack — because the split has a second trigger besides `defs`. A def's color and its
27
33
  // shape are ordinary slot writes that never re-render this component, so either one crossing a
@@ -64,7 +70,7 @@ export function Shapes(props) {
64
70
  const members = batches[kind];
65
71
  if (!members)
66
72
  return null;
67
- return (_jsx(ShapeBatch, { defs: members, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, kind));
73
+ return (_jsx(ShapeBatch, { defs: members, mounted: mounted, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, kind));
68
74
  }) }));
69
75
  }
70
76
  /**
@@ -92,6 +98,8 @@ function partitionShapes(defs, mounted) {
92
98
  */
93
99
  function ShapeBatch(props) {
94
100
  const { defs, kind, renderOrder, onKindMismatch } = props;
101
+ // Only the instances are staged; the container is sized, reserved and ordered from every member.
102
+ const mounted = props.mounted ?? defs.length;
95
103
  // Sized from the first render's defs; membership changes grow the mesh in place, never rebuild
96
104
  // it. The container is bookkeeping plus CPU-side template geometries, so the copy StrictMode's
97
105
  // double-invoked initializer discards and the one an unmount strands are both plain garbage.
@@ -99,6 +107,7 @@ function ShapeBatch(props) {
99
107
  // Stickiness costs this nothing: a batch mounts on the render that first fills it, so the defs
100
108
  // sizing the container are a real membership rather than the empty one it may later stand at.
101
109
  const container = useContainer(() => new ShapeContainer(defs, kind), defs);
110
+ const occluder = useOccluder();
102
111
  // Assigned after mount rather than passed to the constructor: the container is built once and
103
112
  // outlives every render, so it cannot close over a current callback. Nothing can fire before this
104
113
  // runs — the split that placed these defs was computed from the same colors they mount with.
@@ -116,5 +125,14 @@ function ShapeBatch(props) {
116
125
  // claimed slot — `capacity` is a frozen snapshot, so that never happens. The `ref` arrow is
117
126
  // deliberate: `ref={container.bind}` would make the React Compiler infer `container` itself as a
118
127
  // ref and reject its render-time reads.
119
- return (_jsxs(_Fragment, { children: [_jsx(BatchedMesh, { args: container.capacity, ref: (mesh) => container.bind(mesh), customSort: container.customSort, renderOrder: renderOrder, children: _jsx(ColorMaterial, { solid: kind !== 'planar', translucent: kind === 'translucent' }) }), defs.map((def) => (_jsx(ShapeInstance, { def: def, container: container }, defKey(def))))] }));
128
+ //
129
+ // The opaque volumes are what the depth prepass draws, so their batch asks for occlusion: the
130
+ // translucent ones stay out because a surface you can see through must not cast full occlusion,
131
+ // and the planar ones stay out because that is what keeps ambient occlusion and the contact
132
+ // shadow disjoint. A batch of either of those kinds asks with `null`, which is also what a
133
+ // detaching `ref` says — so the layer number and the scene's registry are `useOccluder`'s alone.
134
+ return (_jsxs(_Fragment, { children: [_jsx(BatchedMesh, { args: container.capacity, ref: (mesh) => {
135
+ container.bind(mesh);
136
+ occluder(kind === 'solid' ? mesh : null);
137
+ }, customSort: container.customSort, renderOrder: renderOrder, children: _jsx(ColorMaterial, { solid: kind !== 'planar', translucent: kind === 'translucent' }) }), defs.slice(0, mounted).map((def) => (_jsx(ShapeInstance, { def: def, container: container }, defKey(def))))] }));
120
138
  }
@@ -1,6 +1,6 @@
1
1
  import { type Shape } from '@expofp/geometry';
2
2
  import { type BufferGeometry, type Intersection } from 'three';
3
- import { type BatchedMeshExt } from '../../core/batch/BatchedMesh';
3
+ import { type BatchedMeshImpl } from '../../core/batch/BatchedMesh';
4
4
  import { type RenderableDef, type ShapeDef } from '../../types/defs';
5
5
  import { SlotContainer } from '../containers';
6
6
  /**
@@ -21,7 +21,7 @@ export declare const SHAPE_KINDS: readonly ['planar', 'solid', 'translucent'];
21
21
  * One value rather than the `solid`/`translucent` pair it replaces. The pair could express a
22
22
  * translucent-but-flat batch, which nothing partitions for and no material describes, and it split
23
23
  * one fact across a field each reader needed and a field none did. This is also the "kind" that
24
- * {@link ShapeContainer.reportKindMismatch} and its handler have always named.
24
+ * {@link SlotContainer.reportKindMismatch} and its handler have always named.
25
25
  *
26
26
  * Derived from {@link SHAPE_KINDS} rather than listed again, so the two cannot drift.
27
27
  */
@@ -67,7 +67,7 @@ export declare function shapeKindOf(def: ShapeDef): ShapeKind;
67
67
  * which of its geometry ids each template was given. Building the templates is not its work either;
68
68
  * they come from {@link shapeGeometry}, which caches them across every container.
69
69
  */
70
- export declare class ShapeContainer extends SlotContainer<BufferGeometry, BatchedMeshExt, ShapeDef> {
70
+ export declare class ShapeContainer extends SlotContainer<BufferGeometry, BatchedMeshImpl, ShapeDef> {
71
71
  /**
72
72
  * The mesh's constructor args, sized to hold the defs the container was created with exactly.
73
73
  * Passed as `<batchedMesh args={container.capacity}>`; R3F diffs `args` element-wise and a change
@@ -96,7 +96,6 @@ export declare class ShapeContainer extends SlotContainer<BufferGeometry, Batche
96
96
  * going to do anyway.
97
97
  */
98
98
  private readonly target;
99
- private kindMismatchHandler;
100
99
  /**
101
100
  * Builds a shape layer's container, sized to hold `defs` exactly so a layer that mounts full never
102
101
  * reallocates. The mesh itself is R3F's: the component renders `<batchedMesh>` from
@@ -130,22 +129,6 @@ export declare class ShapeContainer extends SlotContainer<BufferGeometry, Batche
130
129
  * @param defs - The shapes this container is about to hold.
131
130
  */
132
131
  reserve(defs: readonly ShapeDef[]): void;
133
- /**
134
- * Registers what to do when a member turns out to be the wrong kind. `Shapes` supplies a re-split;
135
- * `undefined` clears it.
136
- *
137
- * A method rather than an assignable field because the container is held in `useState`, and the
138
- * React Compiler rejects writing to a property of a state value ("This value cannot be modified").
139
- * Calling a method is fine, which is how {@link reserve} already mutates.
140
- * @param handler - The re-split to run, or `undefined` to clear it.
141
- */
142
- setKindMismatchHandler(handler: (() => void) | undefined): void;
143
- /**
144
- * Reports that a member no longer matches {@link kind} — its color crossed alpha 1, or its shape
145
- * was replaced with one of another kind — so it is drawing from the wrong batch. Runs the registered handler, if any;
146
- * a container mounted outside `Shapes` has none and the report is simply dropped.
147
- */
148
- reportKindMismatch(): void;
149
132
  /**
150
133
  * A batched mesh reports the instance it hit as `batchId`, which is the slot this container
151
134
  * allocated.
@@ -84,7 +84,6 @@ export class ShapeContainer extends SlotContainer {
84
84
  * going to do anyway.
85
85
  */
86
86
  target;
87
- kindMismatchHandler;
88
87
  /**
89
88
  * Builds a shape layer's container, sized to hold `defs` exactly so a layer that mounts full never
90
89
  * reallocates. The mesh itself is R3F's: the component renders `<batchedMesh>` from
@@ -152,26 +151,6 @@ export class ShapeContainer extends SlotContainer {
152
151
  this.target.vertices = vertices;
153
152
  this.target.indices = indices;
154
153
  }
155
- /**
156
- * Registers what to do when a member turns out to be the wrong kind. `Shapes` supplies a re-split;
157
- * `undefined` clears it.
158
- *
159
- * A method rather than an assignable field because the container is held in `useState`, and the
160
- * React Compiler rejects writing to a property of a state value ("This value cannot be modified").
161
- * Calling a method is fine, which is how {@link reserve} already mutates.
162
- * @param handler - The re-split to run, or `undefined` to clear it.
163
- */
164
- setKindMismatchHandler(handler) {
165
- this.kindMismatchHandler = handler;
166
- }
167
- /**
168
- * Reports that a member no longer matches {@link kind} — its color crossed alpha 1, or its shape
169
- * was replaced with one of another kind — so it is drawing from the wrong batch. Runs the registered handler, if any;
170
- * a container mounted outside `Shapes` has none and the report is simply dropped.
171
- */
172
- reportKindMismatch() {
173
- this.kindMismatchHandler?.();
174
- }
175
154
  /**
176
155
  * A batched mesh reports the instance it hit as `batchId`, which is the slot this container
177
156
  * allocated.
@@ -31,11 +31,13 @@ import { TextInstance } from './TextInstance';
31
31
  */
32
32
  export function Texts(props) {
33
33
  const { defs } = props;
34
+ // Only the instances are staged; the container is built and ordered from the whole membership.
35
+ const mounted = props.mounted ?? defs.length;
34
36
  const invalidate = useThree((state) => state.invalidate);
35
37
  // The container is bookkeeping only — the members and the mesh are created against R3F's object —
36
38
  // so the copy StrictMode's double-invoked initializer discards is plain garbage.
37
39
  const container = useContainer(() => new TextContainer(invalidate), defs);
38
40
  // The `ref` arrow (not `ref={container.bind}`) keeps the React Compiler from inferring `container`
39
41
  // itself as a ref and rejecting its render-time reads.
40
- return (_jsxs(_Fragment, { children: [_jsx(BatchedText, { ref: (mesh) => container.bind(mesh), children: _jsx(ColorMaterial, {}) }), defs.map((def) => (_jsx(TextInstance, { def: def, container: container }, defKey(def))))] }));
42
+ return (_jsxs(_Fragment, { children: [_jsx(BatchedText, { ref: (mesh) => container.bind(mesh), children: _jsx(ColorMaterial, {}) }), defs.slice(0, mounted).map((def) => (_jsx(TextInstance, { def: def, container: container }, defKey(def))))] }));
41
43
  }
@@ -1,12 +1,12 @@
1
1
  import { type Intersection } from 'three';
2
- import { type BatchedTextExt } from '../../core/text/BatchedText';
3
- import { TextExt } from '../../core/text/Text';
2
+ import { type BatchedTextImpl } from '../../core/text/BatchedText';
3
+ import { TextImpl } from '../../core/text/Text';
4
4
  import { type RenderableDef, type TextDef } from '../../types/defs';
5
5
  import { SlotContainer } from '../containers';
6
- /** One def's members: a `TextExt` per line, and the batch slot each of them occupies. */
6
+ /** One def's members: a `TextImpl` per line, and the batch slot each of them occupies. */
7
7
  export interface LineGroup {
8
8
  /** The members, in the def's line order. */
9
- readonly texts: readonly TextExt[];
9
+ readonly texts: readonly TextImpl[];
10
10
  /** Each member's batch slot, index-parallel to {@link texts}. */
11
11
  readonly slots: readonly number[];
12
12
  }
@@ -14,7 +14,7 @@ export interface LineGroup {
14
14
  * One text layer's bookkeeping: a group of members per def, and the paint order across them.
15
15
  *
16
16
  * The slot a def claims here is a **group**, not a batch member — a `TextDef` holds an array of
17
- * lines and each line is its own `TextExt`. That is the one place this differs from the shape and
17
+ * lines and each line is its own `TextImpl`. That is the one place this differs from the shape and
18
18
  * image containers, and it falls out of the def type rather than being a choice: the lines of one
19
19
  * def are laid out together (a centered block needs every line's height before it can place the
20
20
  * first), so they cannot be independent instances.
@@ -27,7 +27,7 @@ export interface LineGroup {
27
27
  * draw list, so {@link applyOrder} hands the batch a rebuilt member list instead of labeling slots,
28
28
  * and the inherited `customSort` goes unused.
29
29
  */
30
- export declare class TextContainer extends SlotContainer<number, BatchedTextExt, TextDef> {
30
+ export declare class TextContainer extends SlotContainer<number, BatchedTextImpl, TextDef> {
31
31
  /** Groups by slot, with holes for released ones. */
32
32
  private readonly groups;
33
33
  private readonly freeGroups;
@@ -1,13 +1,13 @@
1
- import { TextExt } from '../../core/text/Text';
1
+ import { TextImpl } from '../../core/text/Text';
2
2
  import { sameSequence } from '../../utils/arrays';
3
3
  import { SlotContainer } from '../containers';
4
- import { dimOrdered, isDimmed } from '../materials/dim';
4
+ import { dimOrdered, isDimmed } from '../materials/effects/color-dim';
5
5
  import { LAYOUT_FONT_SIZE, LINE_HEIGHT } from './text-layout';
6
6
  /**
7
7
  * One text layer's bookkeeping: a group of members per def, and the paint order across them.
8
8
  *
9
9
  * The slot a def claims here is a **group**, not a batch member — a `TextDef` holds an array of
10
- * lines and each line is its own `TextExt`. That is the one place this differs from the shape and
10
+ * lines and each line is its own `TextImpl`. That is the one place this differs from the shape and
11
11
  * image containers, and it falls out of the def type rather than being a choice: the lines of one
12
12
  * def are laid out together (a centered block needs every line's height before it can place the
13
13
  * first), so they cannot be independent instances.
@@ -194,7 +194,7 @@ export class TextContainer extends SlotContainer {
194
194
  * @returns The member.
195
195
  */
196
196
  function makeMember() {
197
- const text = new TextExt();
197
+ const text = new TextImpl();
198
198
  text.fontSize = LAYOUT_FONT_SIZE;
199
199
  text.lineHeight = LINE_HEIGHT;
200
200
  // Wrapping is the floorplan's job: it measures and breaks lines itself, then hands over the
@@ -1,5 +1,5 @@
1
1
  import { type Rect } from '@expofp/geometry';
2
- import { type TextExt } from '../../core/text/Text';
2
+ import { type TextImpl } from '../../core/text/Text';
3
3
  import { type TextDef, type TextLineDef } from '../../types/defs';
4
4
  /**
5
5
  * The em size every member lays out at, fixed for its lifetime.
@@ -70,7 +70,7 @@ export declare function layoutText(def: TextDef): LaidOutLine[];
70
70
  * @param line - The line's local placement.
71
71
  * @param bounds - The def's bounds, supplying the rotation, center and elevation.
72
72
  */
73
- export declare function applyPlacement(text: TextExt, line: LaidOutLine, bounds: Rect): void;
73
+ export declare function applyPlacement(text: TextImpl, line: LaidOutLine, bounds: Rect): void;
74
74
  /**
75
75
  * The height one line's box occupies.
76
76
  *
@@ -1,5 +1,6 @@
1
1
  import { type Box } from '@expofp/geometry';
2
2
  import { type ReactNode } from 'react';
3
+ import { type LightingProps } from '../objects/lighting';
3
4
  import { type SceneDef } from '../types/defs';
4
5
  import { type CameraProps } from './Camera';
5
6
  /** Props for {@link Scene}: the scene to draw, plus the camera limits it passes straight down. */
@@ -15,6 +16,8 @@ export interface SceneProps extends CameraProps {
15
16
  * The part of the canvas the plan should be framed inside; see {@link RendererProps.visibleRect}.
16
17
  */
17
18
  visibleRect?: Box;
19
+ /** The lighting and darkening values a caller set; see {@link RendererProps.lighting}. */
20
+ lighting?: LightingProps;
18
21
  /**
19
22
  * Whether an external host owns the camera. When set, the scene mounts no {@link Camera} — and
20
23
  * with it no fit, no `visibleRect` framing and no camera commands: the view arrives from the
@@ -6,7 +6,8 @@ import { Background } from '../objects/Background';
6
6
  import { TextureBudget, TextureBudgetContext } from '../objects/images/texture-budget';
7
7
  import { Layer } from '../objects/layer/index';
8
8
  import { createMountScheduler, MountSchedulerContext } from '../objects/layer/mount-scheduler';
9
- import { Lighting } from '../objects/Lighting';
9
+ import { Lighting, OcclusionPasses } from '../objects/lighting';
10
+ import { createSceneEffects, SceneEffectsContext, } from '../objects/materials/effects/effects-context';
10
11
  import { createLogger, printScene } from '../utils/logging';
11
12
  import { Camera } from './Camera';
12
13
  import { useCanvasSize } from './canvas-size';
@@ -26,14 +27,14 @@ const logger = createLogger('scene');
26
27
  * @returns The scene.
27
28
  */
28
29
  export function Scene(props) {
29
- const { sceneDef, memoryLimit, visibleRect, external, children, ...cameraProps } = props;
30
+ const { sceneDef, memoryLimit, lighting, visibleRect, external, children, ...cameraProps } = props;
30
31
  const { viewbox, staticTransform } = sceneDef;
31
32
  useEffect(() => {
32
33
  if (visibleRect && staticTransform) {
33
34
  logger.warn('visibleRect is ignored for a scene with a staticTransform');
34
35
  }
35
36
  }, [visibleRect, staticTransform]);
36
- const graph = (_jsx(SceneGraph, { sceneDef: sceneDef, memoryLimit: memoryLimit, children: children }));
37
+ const graph = (_jsx(SceneGraph, { sceneDef: sceneDef, memoryLimit: memoryLimit, lighting: lighting, children: children }));
37
38
  return (_jsx(PlanSpace, { sceneDef: sceneDef, children: external ? (graph) : (_jsxs(Camera, { ...cameraProps, children: [visibleRect && !staticTransform && _jsx(VisibleRect, { rect: visibleRect, viewbox: viewbox }), graph] })) }));
38
39
  }
39
40
  /**
@@ -78,7 +79,7 @@ function PlanSpace(props) {
78
79
  * @returns The graph.
79
80
  */
80
81
  function SceneGraph(props) {
81
- const { sceneDef, memoryLimit, children } = props;
82
+ const { sceneDef, memoryLimit, lighting, children } = props;
82
83
  const { viewbox, staticTransform, rootLayer } = sceneDef;
83
84
  const { width, height, dpr } = useCanvasSize();
84
85
  const invalidate = useThree((state) => state.invalidate);
@@ -87,6 +88,17 @@ function SceneGraph(props) {
87
88
  // One budget for the scene's lifetime; the limit is synced rather than the budget rebuilt, so an
88
89
  // atlas's registration survives a `memoryLimit` change.
89
90
  const [budget] = useState(() => new TextureBudget(memoryLimit));
91
+ // One object for the scene's lifetime, published above both halves of the tree: the passes write
92
+ // into it and the lit materials inside the world group read it, and neither may import the other.
93
+ // The plan matrix uniform holds the space's own `matrixInverse`. The space recomputes that matrix
94
+ // in place on every refit and never replaces it, so a lit material reads the current fit with no
95
+ // copy, from its very first commit. Never write through `planFromWorld.value`, because the write
96
+ // lands in the space itself.
97
+ const [effects] = useState(() => {
98
+ const effects = createSceneEffects();
99
+ effects.planFromWorld.value = space.matrixInverse;
100
+ return effects;
101
+ });
90
102
  const worldRef = useRef(null);
91
103
  useLayoutEffect(() => {
92
104
  space.refit({ viewbox, width, height, dpr, staticTransform });
@@ -108,5 +120,5 @@ function SceneGraph(props) {
108
120
  useEffect(() => {
109
121
  printScene(sceneDef, logger.debug);
110
122
  }, [sceneDef]);
111
- return (_jsxs(LayoutDriver, { children: [_jsxs("group", { name: "world", ref: worldRef, matrixAutoUpdate: false, matrix: space.matrix, children: [_jsx(Lighting, {}), _jsx(TextureBudgetContext, { value: budget, children: _jsx(MountSchedulerContext, { value: scheduler, children: _jsx(Layer, { layer: rootLayer, children: sceneDef.background !== undefined && _jsx(Background, { color: sceneDef.background }) }) }) })] }), children] }));
123
+ return (_jsx(SceneEffectsContext, { value: effects, children: _jsxs(LayoutDriver, { children: [_jsxs("group", { name: "world", ref: worldRef, matrixAutoUpdate: false, matrix: space.matrix, children: [_jsx(TextureBudgetContext, { value: budget, children: _jsx(MountSchedulerContext, { value: scheduler, children: _jsx(Layer, { layer: rootLayer, children: sceneDef.background !== undefined && _jsx(Background, { color: sceneDef.background }) }) }) }), _jsx(Lighting, { ...lighting })] }), _jsx(OcclusionPasses, {}), children] }) }));
112
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "3.34.2",
3
+ "version": "3.35.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/expofp/efp-app.git",
@@ -42,7 +42,7 @@
42
42
  "troika-three-text": "^0.52.5",
43
43
  "tslib": "^2.3.0",
44
44
  "webgl-memory": "^1.1.3",
45
- "@expofp/geometry": "3.34.2"
45
+ "@expofp/geometry": "3.35.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@react-three/test-renderer": "^9.1.0",
@@ -1,20 +0,0 @@
1
- /**
2
- * Scene lighting: a hemisphere fill from above the plan plus a directional key that follows the
3
- * camera, so a surface square-on to the viewer is shaded by exactly its own color (see
4
- * {@link HALF_ALBEDO}).
5
- *
6
- * Only lit materials see any of this — solid shape batches and models. Every 2D primitive draws
7
- * through a `MeshBasicMaterial`, so lines, text and images are unaffected by a light no matter what
8
- * the scene holds. That is deliberate: a route line or a booth label has no meaningful surface
9
- * normal, and shading one would make its color depend on where the camera is.
10
- *
11
- * **These sit inside the scene's world matrix**, so every direction below is expressed in the plan's
12
- * own space and taken there through {@link Space.matrixInverse} — one inverse, shared, rather than
13
- * one per light. Three reads a light's direction from where it ends up in the *world*, so the two
14
- * transforms cancel and the light points where it was aimed under any fit. The key light needs a
15
- * target inside the same space for that to hold: its direction is the difference of two world
16
- * positions, and only a target under the same matrix cancels the fit's translation.
17
- * @returns The fill and key lights, and the key's target.
18
- */
19
- export declare function Lighting(): import("react").JSX.Element;
20
- //# sourceMappingURL=Lighting.d.ts.map
@@ -1,85 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useFrame } from '@react-three/fiber';
3
- import { useLayoutEffect, useRef } from 'react';
4
- import { Color } from 'three';
5
- import { useSpace } from '../space/Space';
6
- /**
7
- * Scene lighting: a hemisphere fill from above the plan plus a directional key that follows the
8
- * camera, so a surface square-on to the viewer is shaded by exactly its own color (see
9
- * {@link HALF_ALBEDO}).
10
- *
11
- * Only lit materials see any of this — solid shape batches and models. Every 2D primitive draws
12
- * through a `MeshBasicMaterial`, so lines, text and images are unaffected by a light no matter what
13
- * the scene holds. That is deliberate: a route line or a booth label has no meaningful surface
14
- * normal, and shading one would make its color depend on where the camera is.
15
- *
16
- * **These sit inside the scene's world matrix**, so every direction below is expressed in the plan's
17
- * own space and taken there through {@link Space.matrixInverse} — one inverse, shared, rather than
18
- * one per light. Three reads a light's direction from where it ends up in the *world*, so the two
19
- * transforms cancel and the light points where it was aimed under any fit. The key light needs a
20
- * target inside the same space for that to hold: its direction is the difference of two world
21
- * positions, and only a target under the same matrix cancels the fit's translation.
22
- * @returns The fill and key lights, and the key's target.
23
- */
24
- export function Lighting() {
25
- const fillRef = useRef(null);
26
- const keyRef = useRef(null);
27
- const targetRef = useRef(null);
28
- const space = useSpace();
29
- useLayoutEffect(() => {
30
- keyRef.current.target = targetRef.current;
31
- }, []);
32
- // Taken from the camera's own backward axis rather than from the controls' target: only the
33
- // direction matters for a directional light — the target below pins the other end — so this needs
34
- // no access to `controls`, and unlike normalizing the camera's *position* it stays correct when
35
- // the plan's coordinates are far from the origin, which SVG viewboxes routinely are.
36
- //
37
- // Column 2 of `matrixWorld` is that backward axis: where the light belongs, shining from the
38
- // camera toward what it looks at. Read with `setFromMatrixColumn` rather than through
39
- // `Camera.getWorldDirection`, which negates the axis and, worse, re-runs `updateWorldMatrix` —
40
- // leaving the camera's `matrixWorldNeedsUpdate` set on every single call, which kept the scene
41
- // perpetually dirty and no screenshot ever settled.
42
- //
43
- // Under the on-demand loop this runs only on frames something already asked for, and camera
44
- // movement is what asks for them, so a still plan costs nothing.
45
- useFrame((state) => {
46
- keyRef.current.position
47
- .setFromMatrixColumn(state.camera.matrixWorld, 2)
48
- .transformDirection(space.matrixInverse);
49
- // The plan's own up axis, as a point whose world position lands back on it — which is what three
50
- // reads a hemisphere light's sky axis from. It is world -z either way, the side the camera is
51
- // on: every path into world space ends with handedness flip.
52
- //
53
- // Computed here rather than memoized against the space because the space is refitted in place
54
- // and so never changes identity — and it is four vector operations on a frame that was going to
55
- // aim the key light anyway.
56
- fillRef.current.position
57
- .setFromMatrixColumn(space.matrix, 2)
58
- .normalize()
59
- .applyMatrix4(space.matrixInverse);
60
- });
61
- return (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: fillRef, color: LIGHT_COLOR, groundColor: GROUND_COLOR, intensity: HALF_ALBEDO }), _jsx("directionalLight", { ref: keyRef, color: LIGHT_COLOR, intensity: HALF_ALBEDO }), _jsx("object3D", { ref: targetRef })] }));
62
- }
63
- /**
64
- * Each light's intensity, so the two together sum to `π`.
65
- *
66
- * three's diffuse BRDF is `albedo / π`, so a light of intensity `I` reaching a surface head-on
67
- * contributes `I · albedo / π`. At `0.5π` each contributes half the albedo, and the pair sums to
68
- * exactly the albedo — a surface facing the camera therefore renders at precisely the color the plan
69
- * author picked, and only faces turning away darken. The key light tracks the camera and the fill's
70
- * sky axis points at it too, so "facing the camera" is the same set of surfaces for both.
71
- *
72
- * Two things elsewhere are load-bearing for that identity rather than cosmetic: the lit material sets
73
- * `ior = 1` and `specularIntensity = 0`, which drive `F0` to zero so no specular lobe pushes a facing
74
- * surface *above* its albedo, and `<Canvas flat>` keeps tone mapping off, which would remap the
75
- * result. Change either and the identity quietly stops holding.
76
- */
77
- const HALF_ALBEDO = 0.5 * Math.PI;
78
- /**
79
- * Both lights' color — one shared constant, because the {@link HALF_ALBEDO} identity is a property
80
- * of the pair: two halves sum back to the author's color only when they are halves of the *same*
81
- * color. Each light copies it on mount, so writing to a light's own color never reaches the other.
82
- */
83
- const LIGHT_COLOR = new Color('white');
84
- /** The fill's ground half: pure black, so the side facing away from the sky axis adds nothing. */
85
- const GROUND_COLOR = new Color('black');