@expofp/renderer 3.38.0 → 3.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/controls/camera-api.d.ts +28 -0
  2. package/dist/controls/camera-api.js +33 -10
  3. package/dist/controls/hooks.js +2 -0
  4. package/dist/core/InstanceUniforms.d.ts +15 -0
  5. package/dist/core/InstanceUniforms.js +32 -0
  6. package/dist/core/batch/BatchedMesh.js +6 -0
  7. package/dist/core/instanced/instanced-material.js +5 -1
  8. package/dist/core/lines/BatchedLines.d.ts +9 -7
  9. package/dist/core/lines/BatchedLines.js +9 -7
  10. package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
  11. package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
  12. package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
  13. package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
  14. package/dist/core/lines/lines-material.js +10 -1
  15. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  16. package/dist/core/sprites/BatchedSprites.js +205 -0
  17. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  18. package/dist/core/sprites/RaycastingSprites.js +71 -0
  19. package/dist/core/sprites/sprite-material.d.ts +59 -0
  20. package/dist/core/sprites/sprite-material.js +192 -0
  21. package/dist/core/text/BatchedText.d.ts +6 -0
  22. package/dist/core/text/BatchedText.js +6 -0
  23. package/dist/core/text/MemberTexture.js +55 -1
  24. package/dist/core/text/Text.d.ts +16 -1
  25. package/dist/core/text/Text.js +15 -0
  26. package/dist/core/text/batched-text-material.js +4 -2
  27. package/dist/events/LayoutDriver.d.ts +6 -6
  28. package/dist/events/LayoutDriver.js +9 -12
  29. package/dist/events/layout-targets.d.ts +35 -34
  30. package/dist/events/layout-targets.js +127 -107
  31. package/dist/objects/containers/hooks.d.ts +19 -4
  32. package/dist/objects/containers/hooks.js +101 -23
  33. package/dist/objects/containers/mesh-writes.js +3 -3
  34. package/dist/objects/images/ImageInstance.js +104 -36
  35. package/dist/objects/images/Images.d.ts +8 -7
  36. package/dist/objects/images/Images.js +94 -28
  37. package/dist/objects/images/image-container.d.ts +14 -3
  38. package/dist/objects/images/image-container.js +19 -5
  39. package/dist/objects/layer/Layer.d.ts +7 -6
  40. package/dist/objects/layer/Layer.js +77 -57
  41. package/dist/objects/layer/layer-context.d.ts +74 -0
  42. package/dist/objects/layer/layer-context.js +82 -0
  43. package/dist/objects/materials/ColorMaterial.js +16 -3
  44. package/dist/objects/materials/LineMaterial.js +4 -2
  45. package/dist/objects/materials/TextureMaterial.js +4 -2
  46. package/dist/objects/materials/effects/color-dim.d.ts +3 -11
  47. package/dist/objects/materials/effects/color-dim.js +17 -17
  48. package/dist/objects/materials/effects/effects-context.d.ts +6 -5
  49. package/dist/objects/materials/shared.d.ts +18 -0
  50. package/dist/objects/materials/shared.js +21 -0
  51. package/dist/objects/models/Models.js +1 -8
  52. package/dist/objects/shapes/ShapeInstance.js +1 -2
  53. package/dist/objects/shapes/Shapes.js +2 -9
  54. package/dist/objects/text/TextInstance.js +2 -2
  55. package/dist/objects/text/text-layout.d.ts +13 -4
  56. package/dist/objects/text/text-layout.js +30 -4
  57. package/dist/space/Space.d.ts +15 -0
  58. package/dist/space/Space.js +16 -10
  59. package/dist/types/defs.d.ts +37 -0
  60. package/dist/utils/logging.d.ts +6 -3
  61. package/dist/utils/logging.js +15 -7
  62. package/dist/utils/math.d.ts +11 -0
  63. package/dist/utils/math.js +14 -0
  64. package/package.json +2 -2
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, use } from 'react';
2
+ import { memo, Suspense } from 'react';
3
3
  import { useLayoutTargets } from '../../events/layout-targets';
4
4
  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/effects/color-dim';
9
8
  import { Models } from '../models';
10
9
  import { Shapes } from '../shapes';
11
10
  import { Texts } from '../text';
11
+ import { LayerDimContext, LayerOverlayContext, useDimChain, useLayerOverlay, } from './layer-context';
12
12
  import { useStagedChildren } from './mount-scheduler';
13
13
  import { useVisibilityGui } from './visibility-gui';
14
14
  /**
@@ -17,15 +17,45 @@ import { useVisibilityGui } from './visibility-gui';
17
17
  * This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
18
18
  * "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
19
19
  * scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
20
- * carries: nothing above the root can hide it, and the whole tree's draw order lives inside
21
- * `[0, 1)`.
20
+ * carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
21
+ * `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
22
+ * me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
22
23
  * @param props - The layer props.
23
24
  * @returns The tree's root group.
24
25
  */
25
26
  export function Layer(props) {
26
27
  const { layer, children } = props;
27
28
  useVisibilityGui(layer);
28
- return (_jsx(LayerNode, { layer: layer, parentVisible: true, orderBase: 0, orderStep: 1, children: children }));
29
+ return (_jsx(MemoLayerNode, { layer: layer, hidden: false, orderBase: 0, orderStep: 1, children: children }));
30
+ }
31
+ /**
32
+ * The content of one leaf layer: the container for its defs' kind, inside a Suspense boundary of
33
+ * the leaf's own.
34
+ *
35
+ * The boundary means a content component that suspends — a loader hook, a lazily imported one —
36
+ * costs this leaf alone. Without it the nearest boundary is the one R3F's `<Canvas>` wraps
37
+ * everything in, and falling back to it hides *every* object in the scene (R3F's `hideInstance`
38
+ * writes `visible = false`) while the admission queue stalls forever, since the leaf never commits
39
+ * and so never reports mounted. With it, a suspended leaf behaves exactly like an empty one: the
40
+ * layer still commits, still reports, the next leaf is admitted on schedule, and the content mounts
41
+ * when it resolves.
42
+ * @param props - The leaf's defs.
43
+ * @returns The container for the defs' kind.
44
+ */
45
+ function LeafContent(props) {
46
+ const { defs, mounted } = props;
47
+ let content = null;
48
+ if (isShapeDefs(defs))
49
+ content = _jsx(Shapes, { defs: defs, mounted: mounted });
50
+ else if (isImageDefs(defs))
51
+ content = _jsx(Images, { defs: defs, mounted: mounted });
52
+ else if (isTextDefs(defs))
53
+ content = _jsx(Texts, { defs: defs, mounted: mounted });
54
+ else if (isLineDefs(defs))
55
+ content = _jsx(Lines, { defs: defs, mounted: mounted });
56
+ else if (isModelDefs(defs))
57
+ content = _jsx(Models, { defs: defs, mounted: mounted });
58
+ return _jsx(Suspense, { fallback: null, children: content });
29
59
  }
30
60
  /**
31
61
  * One layer of the tree as a three.js `<group>`, recursing into its child layers.
@@ -43,72 +73,62 @@ export function Layer(props) {
43
73
  * whose membership depends on the camera re-derives it. From then on a pixel-size change reaches
44
74
  * those defs through the frame loop, with no render of this component involved.
45
75
  *
46
- * **Draw order is a group prop.** three propagates a group's `renderOrder` down its subtree as
47
- * `groupOrder`, which `painterSortStable` compares before `renderOrder` and `material.id` — so
48
- * numbering the groups is all it takes to make array order the draw order. A nested group
49
- * overwrites `groupOrder` for its own subtree, so the value that decides a mesh's place is its
50
- * nearest enclosing layer's; the numbers on layers above it are overwritten and simply do not
51
- * matter. Without this the order falls out of `material.id`, which ascends in mount order and
52
- * therefore puts any material built after first paint — every model's, since they exist only once
53
- * its GLB has loaded — last in the scene regardless of where its layer sits.
76
+ * Draw order is a pair: the tier, then the layer's own place. `overlay` adds a whole band above
77
+ * every plan-tier value. Array order still decides strictly within each band.
78
+ *
79
+ * **Dim is handed down as a source, and the node is rendered memoized.** An `update(layer)` renders this node
80
+ * alone: its child layers receive identical props, and the dim they inherit reaches its consumers
81
+ * through subscriptions to the layers themselves rather than a context value, so an eased dim — an update per frame
82
+ * on the root — renders one node instead of the whole tree.
54
83
  * @param props - The node props.
55
84
  * @returns The `<group>` for a layer def.
56
85
  */
57
86
  function LayerNode(props) {
58
- const { layer, children: prelude, parentVisible, orderBase, orderStep } = props;
59
- // On screen only if nothing above hides it either — the value picking hangs off, and what the
60
- // children inherit in turn.
61
- const visible = parentVisible && !layer.hidden;
62
- // Nearest declaration wins and reaches every material beneath through context — so only
63
- // `undefined` inherits, and a layer's own `0` un-dims it inside a dimmed ancestor. Unlike
64
- // visibility this is never a prop: nothing between here and the material has any use for it.
65
- const inheritedDim = use(LayerDimContext);
66
- const dim = layer.dim ?? inheritedDim;
87
+ const { layer, children: prelude, hidden: parentHidden, orderBase, orderStep } = props;
88
+ // What the nearest ancestor already resolved for dim and overlay. A layer cannot see its
89
+ // ancestors any other way.
90
+ const parentChain = useDimChain();
91
+ const parentOverlay = useLayerOverlay();
92
+ // Once hidden or once in the overlay tier, a subtree stays that way. A descendant that dropped
93
+ // back into the plan tier would invert the nesting, and a descendant of a hidden layer is not on
94
+ // screen whatever it says about itself.
95
+ const hidden = parentHidden || layer.hidden === true;
96
+ const overlay = parentOverlay || layer.overlay === true;
97
+ // Nearest first, so the chain resolves to this layer's own dim when it declares one. Stable for a
98
+ // mounted layer, so the consumers below follow its dim by subscription, never by render.
99
+ const dimChain = [layer, ...parentChain];
100
+ // On screen only if nothing above hides it either. This is the value picking hangs off.
101
+ const visible = !hidden;
67
102
  const { children, mounted } = useStagedChildren(layer);
68
103
  useLayoutTargets(layer, children);
69
104
  const pointerProps = usePointerProps(layer, visible);
70
105
  let contents = null;
71
106
  if (isLayerDefs(children)) {
72
107
  const childStep = orderStep / children.length;
73
- contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, parentVisible: visible, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
108
+ contents = children.map((child, index) => (_jsx(MemoLayerNode, { layer: child, hidden: hidden, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
74
109
  }
75
110
  else if (mounted > 0) {
76
111
  contents = _jsx(LeafContent, { defs: children, mounted: mounted });
77
112
  }
78
- const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: orderBase, ...pointerProps, children: [prelude, contents] }));
79
- // Always provided, never conditionally on the layer declaring a `dim`: making the provider appear
80
- // when one is first set would change this component's element type, and React would remount the
81
- // subtree — releasing and re-claiming every GPU slot beneath it, exactly what an eased dim must
82
- // not do. A layer that declares none provides what it inherited, which no consumer can tell apart
83
- // from not providing at all.
84
- return _jsx(LayerDimContext, { value: dim, children: group });
113
+ const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: (overlay ? OVERLAY_TIER : PLAN_TIER) + orderBase, ...pointerProps, children: [prelude, contents] }));
114
+ // Always provided, never only when the layer declares one. Making a provider appear on first use
115
+ // would change this component's element type and remount the subtree, which releases and re-claims
116
+ // every GPU slot beneath it.
117
+ return (_jsx(LayerDimContext, { value: dimChain, children: _jsx(LayerOverlayContext, { value: overlay, children: group }) }));
85
118
  }
86
119
  /**
87
- * The content of one leaf layer: the container for its defs' kind, inside a Suspense boundary of
88
- * the leaf's own.
89
- *
90
- * The boundary means a content component that suspends — a loader hook, a lazily imported one —
91
- * costs this leaf alone. Without it the nearest boundary is the one R3F's `<Canvas>` wraps
92
- * everything in, and falling back to it hides *every* object in the scene (R3F's `hideInstance`
93
- * writes `visible = false`) while the admission queue stalls forever, since the leaf never commits
94
- * and so never reports mounted. With it, a suspended leaf behaves exactly like an empty one: the
95
- * layer still commits, still reports, the next leaf is admitted on schedule, and the content mounts
96
- * when it resolves.
97
- * @param props - The leaf's defs.
98
- * @returns The container for the defs' kind.
120
+ * {@link LayerNode}, memoized — what both the root and the recursion render, so an update renders
121
+ * the one node it names and its child layers bail out on equal props.
99
122
  */
100
- function LeafContent(props) {
101
- const { defs, mounted } = props;
102
- let content = null;
103
- if (isShapeDefs(defs))
104
- content = _jsx(Shapes, { defs: defs, mounted: mounted });
105
- else if (isImageDefs(defs))
106
- content = _jsx(Images, { defs: defs, mounted: mounted });
107
- else if (isTextDefs(defs))
108
- content = _jsx(Texts, { defs: defs, mounted: mounted });
109
- else if (isLineDefs(defs))
110
- content = _jsx(Lines, { defs: defs, mounted: mounted });
111
- else if (isModelDefs(defs))
112
- content = _jsx(Models, { defs: defs, mounted: mounted });
113
- return _jsx(Suspense, { fallback: null, children: content });
114
- }
123
+ const MemoLayerNode = memo(LayerNode);
124
+ /**
125
+ * Where the plan's own layers are numbered from. The tree subdivides `[0, 1)` depth first, so the
126
+ * mantissa stays free for nesting.
127
+ */
128
+ const PLAN_TIER = 0;
129
+ /**
130
+ * Where an overlay subtree is numbered from, which puts it in `[1, 2)`. That is after every
131
+ * plan-tier layer, whatever renderOrder those layers hold. The whole cost is one bit of mantissa for
132
+ * subdivision.
133
+ */
134
+ const OVERLAY_TIER = 1;
@@ -0,0 +1,74 @@
1
+ import { type LayerDef } from '../../types/defs';
2
+ /**
3
+ * The layers whose `dim` decides a subtree's, nearest first: the enclosing layer, then each of its
4
+ * ancestors. Nearest declaration wins, and a layer's own `0` un-dims it inside a dimmed ancestor.
5
+ *
6
+ * The chain rather than the resolved amount, because the amount animates — floorplan eases the
7
+ * venue's dim over 300ms, an `update(layer)` per frame — and a number in context re-rendered every
8
+ * consumer and every layer between on each of those frames. A chain never changes for a mounted
9
+ * layer, so the tree stays still: consumers read it with {@link resolveDim} and follow it with
10
+ * {@link subscribeDim}, which is the def store's own notification.
11
+ */
12
+ export type DimChain = readonly LayerDef[];
13
+ /**
14
+ * The dim amount a layer draws at, as a value, so the caller re-renders whenever it changes. A
15
+ * consumer that only writes the amount somewhere reads {@link useDimChain} and subscribes instead.
16
+ * @returns The nearest declared amount, or `0` with no layer above.
17
+ */
18
+ export declare function useLayerDim(): number;
19
+ /**
20
+ * The enclosing layer's {@link DimChain}, for consumers that follow the dim without rendering: a
21
+ * material writing it into a uniform, a container re-running paint order when it crosses zero.
22
+ * @returns The chain, empty outside any layer.
23
+ */
24
+ export declare function useDimChain(): DimChain;
25
+ /**
26
+ * Whether the current layer draws in the overlay tier.
27
+ * @returns `true` if this layer or an ancestor opted into the overlay tier, `false` otherwise.
28
+ */
29
+ export declare function useLayerOverlay(): boolean;
30
+ /**
31
+ * Resolves a chain's dim amount.
32
+ * @param chain - The layers, nearest first.
33
+ * @returns The nearest declared dim, or `0` when none declares one.
34
+ */
35
+ export declare function resolveDim(chain: DimChain): number;
36
+ /**
37
+ * Follows every `update()` of a chain's layers — each one a possible dim change. The listener
38
+ * compares for itself; a membership update reaches it too and changes nothing.
39
+ * @param chain - The layers, nearest first.
40
+ * @param listener - Called after an update of any of them.
41
+ * @returns The unsubscribe function.
42
+ */
43
+ export declare function subscribeDim(chain: DimChain, listener: () => void): () => void;
44
+ /**
45
+ * The {@link DimChain} the current layer hands down to its descendants.
46
+ *
47
+ * Context rather than a prop, because the value crosses the whole subtree and concerns none of the
48
+ * components between. A container hands its material nothing about layers, and a material that
49
+ * never asked about them still dims correctly. Threading it as a prop would put a `dim` on every
50
+ * material, every container, and every one of their inner components.
51
+ *
52
+ * Read through {@link useLayerDim} or {@link useDimChain} instead of directly. A `LayerNode` provides it, always, never
53
+ * only when it declares a `dim` of its own. See `Layer.tsx` for why.
54
+ *
55
+ * Defaults to an empty chain, which reads `0`, undimmed, which is what "no layer above me" means for a component rendered
56
+ * outside a `Layer` tree. A fourth inherited field belongs in a context and a hook of its own here,
57
+ * not folded into this one.
58
+ */
59
+ export declare const LayerDimContext: import("react").Context<DimChain>;
60
+ /**
61
+ * Whether the current layer hands its overlay tier down to its descendants.
62
+ *
63
+ * Context rather than a prop, for the reason {@link LayerDimContext} is. It is also not a shading
64
+ * term, which is why `overlayParams`, the material parameters this tier unlocks, stays in
65
+ * `objects/materials/shared.ts` instead of moving here.
66
+ *
67
+ * Read through {@link useLayerOverlay} instead of directly. A `LayerNode` provides it, always,
68
+ * never only when it declares an `overlay` of its own. See `Layer.tsx` for why.
69
+ *
70
+ * Defaults to `false`, the plan tier, which is what "no layer above me" means for a component
71
+ * rendered outside a `Layer` tree.
72
+ */
73
+ export declare const LayerOverlayContext: import("react").Context<boolean>;
74
+ //# sourceMappingURL=layer-context.d.ts.map
@@ -0,0 +1,82 @@
1
+ import { createContext, use, useSyncExternalStore } from 'react';
2
+ import { subscribeDef } from '../../update';
3
+ /**
4
+ * The dim amount a layer draws at, as a value, so the caller re-renders whenever it changes. A
5
+ * consumer that only writes the amount somewhere reads {@link useDimChain} and subscribes instead.
6
+ * @returns The nearest declared amount, or `0` with no layer above.
7
+ */
8
+ export function useLayerDim() {
9
+ const chain = use(LayerDimContext);
10
+ return useSyncExternalStore((listener) => subscribeDim(chain, listener), () => resolveDim(chain));
11
+ }
12
+ /**
13
+ * The enclosing layer's {@link DimChain}, for consumers that follow the dim without rendering: a
14
+ * material writing it into a uniform, a container re-running paint order when it crosses zero.
15
+ * @returns The chain, empty outside any layer.
16
+ */
17
+ export function useDimChain() {
18
+ return use(LayerDimContext);
19
+ }
20
+ /**
21
+ * Whether the current layer draws in the overlay tier.
22
+ * @returns `true` if this layer or an ancestor opted into the overlay tier, `false` otherwise.
23
+ */
24
+ export function useLayerOverlay() {
25
+ return use(LayerOverlayContext);
26
+ }
27
+ /**
28
+ * Resolves a chain's dim amount.
29
+ * @param chain - The layers, nearest first.
30
+ * @returns The nearest declared dim, or `0` when none declares one.
31
+ */
32
+ export function resolveDim(chain) {
33
+ for (const layer of chain) {
34
+ if (layer.dim !== undefined)
35
+ return layer.dim;
36
+ }
37
+ return 0;
38
+ }
39
+ /**
40
+ * Follows every `update()` of a chain's layers — each one a possible dim change. The listener
41
+ * compares for itself; a membership update reaches it too and changes nothing.
42
+ * @param chain - The layers, nearest first.
43
+ * @param listener - Called after an update of any of them.
44
+ * @returns The unsubscribe function.
45
+ */
46
+ export function subscribeDim(chain, listener) {
47
+ const unsubscribes = chain.map((layer) => subscribeDef(layer, listener));
48
+ return () => {
49
+ for (const unsubscribe of unsubscribes)
50
+ unsubscribe();
51
+ };
52
+ }
53
+ /**
54
+ * The {@link DimChain} the current layer hands down to its descendants.
55
+ *
56
+ * Context rather than a prop, because the value crosses the whole subtree and concerns none of the
57
+ * components between. A container hands its material nothing about layers, and a material that
58
+ * never asked about them still dims correctly. Threading it as a prop would put a `dim` on every
59
+ * material, every container, and every one of their inner components.
60
+ *
61
+ * Read through {@link useLayerDim} or {@link useDimChain} instead of directly. A `LayerNode` provides it, always, never
62
+ * only when it declares a `dim` of its own. See `Layer.tsx` for why.
63
+ *
64
+ * Defaults to an empty chain, which reads `0`, undimmed, which is what "no layer above me" means for a component rendered
65
+ * outside a `Layer` tree. A fourth inherited field belongs in a context and a hook of its own here,
66
+ * not folded into this one.
67
+ */
68
+ export const LayerDimContext = createContext([]);
69
+ /**
70
+ * Whether the current layer hands its overlay tier down to its descendants.
71
+ *
72
+ * Context rather than a prop, for the reason {@link LayerDimContext} is. It is also not a shading
73
+ * term, which is why `overlayParams`, the material parameters this tier unlocks, stays in
74
+ * `objects/materials/shared.ts` instead of moving here.
75
+ *
76
+ * Read through {@link useLayerOverlay} instead of directly. A `LayerNode` provides it, always,
77
+ * never only when it declares an `overlay` of its own. See `Layer.tsx` for why.
78
+ *
79
+ * Defaults to `false`, the plan tier, which is what "no layer above me" means for a component
80
+ * rendered outside a `Layer` tree.
81
+ */
82
+ export const LayerOverlayContext = createContext(false);
@@ -1,10 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
+ import { createLogger } from '../../utils/logging';
4
+ import { useLayerOverlay } from '../layer/layer-context';
3
5
  import { applyAmbientOcclusion } from './effects/ao';
4
6
  import { useDimPatch } from './effects/color-dim';
5
7
  import { applyContactShadow } from './effects/contact-shadow';
6
8
  import { useSceneEffects } from './effects/effects-context';
7
- import { sharedParams } from './shared';
9
+ import { overlayParams, sharedParams } from './shared';
10
+ const logger = createLogger('color-material');
8
11
  /**
9
12
  * The material a shape batch draws with. It renders as a child of the mesh, so R3F attaches, updates
10
13
  * and disposes it. It is also troika's base material for text, which never asks for `solid`. See
@@ -76,6 +79,16 @@ export function ColorMaterial(props) {
76
79
  // skips the component in silence, so only the running app shows the failure.
77
80
  const color = props.color ?? 'white';
78
81
  const solid = props.solid === true;
82
+ const overlay = useLayerOverlay();
83
+ // Overlay is a contract for planar content. A volume that stops depth testing loses its own self
84
+ // occlusion, because its far faces then paint over its near ones in pack order. Making it work
85
+ // needs a depth clear between the tiers, so a second render pass, which external mode does not
86
+ // own. So a volume ignores the flag completely and says once that it did.
87
+ useEffect(() => {
88
+ if (solid && overlay) {
89
+ logger.warn('overlay has no effect on a solid batch, so it is ignored.');
90
+ }
91
+ }, [solid, overlay]);
79
92
  // Handed to both branches, because a volume dims the way flat content does. The patch runs on
80
93
  // whatever `gl_FragColor` the material arrived at, which for a lit material is the shaded color.
81
94
  const dimPatch = useDimPatch();
@@ -107,5 +120,5 @@ export function ColorMaterial(props) {
107
120
  const translucent = props.translucent ?? false;
108
121
  return (_jsx("meshLambertMaterial", { ...sharedParams, color: color, depthWrite: true, polygonOffset: false, transparent: translucent, forceSinglePass: !translucent, onBeforeCompile: onBeforeCompile }));
109
122
  }
110
- return _jsx("meshBasicMaterial", { ...sharedParams, color: color, onBeforeCompile: onBeforeCompile });
123
+ return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), color: color, onBeforeCompile: onBeforeCompile }));
111
124
  }
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { extend } from '@react-three/fiber';
3
3
  import { NotEqualStencilFunc, ReplaceStencilOp, } from 'three';
4
4
  import { LineMaterial as LineMaterialBase } from 'three/addons/lines/LineMaterial.js';
5
+ import { useLayerOverlay } from '../layer/layer-context';
5
6
  import { useDimPatch } from './effects/color-dim';
6
- import { sharedParams } from './shared';
7
+ import { overlayParams, sharedParams } from './shared';
7
8
  /**
8
9
  * The stencil value a line layer stamps on every pixel it covers.
9
10
  *
@@ -55,6 +56,7 @@ export function LineMaterial(props) {
55
56
  const color = props.color ?? 'white';
56
57
  const width = props.width ?? 1;
57
58
  const onBeforeCompile = useDimPatch();
59
+ const overlay = useLayerOverlay();
58
60
  /**
59
61
  * Hands the batch about to draw a clean stencil to stamp.
60
62
  *
@@ -68,5 +70,5 @@ export function LineMaterial(props) {
68
70
  function onBeforeRender(renderer) {
69
71
  renderer.clearStencil();
70
72
  }
71
- return (_jsx(LineMaterialElement, { ...sharedParams, color: color, linewidth: width, stencilWrite: true, stencilFunc: NotEqualStencilFunc, stencilZPass: ReplaceStencilOp, stencilRef: LINE_STENCIL_REF, onBeforeRender: onBeforeRender, onBeforeCompile: onBeforeCompile }));
73
+ return (_jsx(LineMaterialElement, { ...sharedParams, ...(overlay ? overlayParams : null), color: color, linewidth: width, stencilWrite: true, stencilFunc: NotEqualStencilFunc, stencilZPass: ReplaceStencilOp, stencilRef: LINE_STENCIL_REF, onBeforeRender: onBeforeRender, onBeforeCompile: onBeforeCompile }));
72
74
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { OneFactor } from 'three';
3
+ import { useLayerOverlay } from '../layer/layer-context';
3
4
  import { useDimPatch } from './effects/color-dim';
4
- import { sharedParams } from './shared';
5
+ import { overlayParams, sharedParams } from './shared';
5
6
  /**
6
7
  * The material used to render a texture, optionally through a per-instance uv-offset uniform so an
7
8
  * instanced/batched mesh can pack many regions into one atlas.
@@ -16,6 +17,7 @@ import { sharedParams } from './shared';
16
17
  export function TextureMaterial(props) {
17
18
  const { map, uvOffset } = props;
18
19
  const applyDim = useDimPatch();
20
+ const overlay = useLayerOverlay();
19
21
  function onBeforeCompile(parameters) {
20
22
  applyDim(parameters);
21
23
  if (!uvOffset)
@@ -35,5 +37,5 @@ export function TextureMaterial(props) {
35
37
  //
36
38
  // The override lives here, not in `sharedParams`, because `ColorMaterial` and `LineMaterial`
37
39
  // share that object and their colors are never premultiplied.
38
- return (_jsx("meshBasicMaterial", { ...sharedParams, blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
40
+ return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
39
41
  }
@@ -8,7 +8,7 @@ import { type RenderableDef } from '../../../types/defs';
8
8
  */
9
9
  export type DimPatch = (parameters: WebGLProgramParametersWithUniforms) => void;
10
10
  /**
11
- * The dim patch every material installs, reading the layer amount from {@link LayerDimContext}.
11
+ * The dim patch every material installs, following the layer amount from {@link useDimChain}.
12
12
  *
13
13
  * Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
14
14
  * so threading it as a prop would put a `dim` on each material, each container, and each of their
@@ -17,7 +17,8 @@ export type DimPatch = (parameters: WebGLProgramParametersWithUniforms) => void;
17
17
  *
18
18
  * The amount is held in one `IUniform` object per material and mutated in place, never replaced, so
19
19
  * an animated dim — floorplan eases the venue's dim over 300ms — costs a number per material per
20
- * frame with no recompile.
20
+ * frame with no recompile. The amount is followed by subscription, not read in render, so that frame
21
+ * renders nothing either.
21
22
  * @returns The patch to call from the material's `onBeforeCompile`.
22
23
  */
23
24
  export declare function useDimPatch(): DimPatch;
@@ -86,15 +87,6 @@ export declare function instanceDim(dim: number | undefined): number;
86
87
  * @returns The amount, within `[0, 1]`.
87
88
  */
88
89
  export declare function clampDim(dim: number): number;
89
- /**
90
- * The dim amount a layer draws at, which every material beneath it reads.
91
- *
92
- * Context rather than a prop because the value crosses the whole subtree and concerns none of the
93
- * components between: a container hands its material nothing, and a material that never asked about
94
- * layers still dims correctly. `Layer` provides it, and only when it declares a `dim` of its own —
95
- * a layer without one is transparent to this, which is exactly what inheritance means.
96
- */
97
- export declare const LayerDimContext: import("react").Context<number>;
98
90
  /**
99
91
  * The per-instance uniform a def's own dim is written to. Declared by every container that draws
100
92
  * dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
@@ -1,8 +1,9 @@
1
1
  import { useThree } from '@react-three/fiber';
2
- import { createContext, use, useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { declareBeforeMain, sceneColorAnchor } from '../../../core/shader-source';
4
+ import { resolveDim, subscribeDim, useDimChain } from '../../layer/layer-context';
4
5
  /**
5
- * The dim patch every material installs, reading the layer amount from {@link LayerDimContext}.
6
+ * The dim patch every material installs, following the layer amount from {@link useDimChain}.
6
7
  *
7
8
  * Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
8
9
  * so threading it as a prop would put a `dim` on each material, each container, and each of their
@@ -11,11 +12,12 @@ import { declareBeforeMain, sceneColorAnchor } from '../../../core/shader-source
11
12
  *
12
13
  * The amount is held in one `IUniform` object per material and mutated in place, never replaced, so
13
14
  * an animated dim — floorplan eases the venue's dim over 300ms — costs a number per material per
14
- * frame with no recompile.
15
+ * frame with no recompile. The amount is followed by subscription, not read in render, so that frame
16
+ * renders nothing either.
15
17
  * @returns The patch to call from the material's `onBeforeCompile`.
16
18
  */
17
19
  export function useDimPatch() {
18
- const dim = use(LayerDimContext);
20
+ const chain = useDimChain();
19
21
  const invalidate = useThree((state) => state.invalidate);
20
22
  // A ref, not state: this object is written after render, every frame of an eased dim, and a ref is
21
23
  // the container React sanctions that for. Its identity is what three holds onto, so the value can
@@ -24,11 +26,18 @@ export function useDimPatch() {
24
26
  // Created once and kept: R3F writes every changed prop onto the material, so handing it a fresh
25
27
  // handler each render risks a recompile per frame — the one thing an animated dim must not cost.
26
28
  const [patch] = useState(() => (parameters) => applyDim(parameters, uniformRef.current));
27
- const amount = clampDim(dim);
28
29
  useEffect(() => {
29
- uniformRef.current.value = amount;
30
- invalidate();
31
- }, [amount, invalidate]);
30
+ /** Writes the layer's current amount into the uniform, and asks for a frame when it moved. */
31
+ function follow() {
32
+ const amount = clampDim(resolveDim(chain));
33
+ if (uniformRef.current.value === amount)
34
+ return;
35
+ uniformRef.current.value = amount;
36
+ invalidate();
37
+ }
38
+ follow();
39
+ return subscribeDim(chain, follow);
40
+ }, [chain, invalidate]);
32
41
  return patch;
33
42
  }
34
43
  /**
@@ -125,15 +134,6 @@ export function instanceDim(dim) {
125
134
  export function clampDim(dim) {
126
135
  return Math.min(Math.max(dim, 0), 1);
127
136
  }
128
- /**
129
- * The dim amount a layer draws at, which every material beneath it reads.
130
- *
131
- * Context rather than a prop because the value crosses the whole subtree and concerns none of the
132
- * components between: a container hands its material nothing, and a material that never asked about
133
- * layers still dims correctly. `Layer` provides it, and only when it declares a `dim` of its own —
134
- * a layer without one is transparent to this, which is exactly what inheritance means.
135
- */
136
- export const LayerDimContext = createContext(0);
137
137
  /**
138
138
  * The per-instance uniform a def's own dim is written to. Declared by every container that draws
139
139
  * dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
@@ -25,11 +25,12 @@ import { type ContactShadow } from './contact-shadow';
25
25
  * `contact-shadow.ts` uses for its three uniforms, published the way `SpaceContext` publishes a
26
26
  * `Space` that is refitted in place.
27
27
  *
28
- * **`LayerDimContext` stays where it is and is not merged in.** It carries a single number, it is
29
- * re-provided by every `Layer`, and it nests. This bundle is one object per renderer mount whose
30
- * identity must never change, which is what lets a material capture it in `onBeforeCompile` and a
31
- * frame write `.value` with no recompile. Merging the two would either re-provide this object at
32
- * every layer boundary or flatten dim's nesting.
28
+ * **`LayerDimContext` and `LayerOverlayContext` stay where they are and are not merged in.** Each
29
+ * carries one primitive of a layer's inherited state, is re-provided by every layer, and nests.
30
+ * This bundle is one object per renderer mount whose identity must never change, which is what
31
+ * lets a material capture it in `onBeforeCompile` and a frame write `.value` with no recompile.
32
+ * Merging either one in would either re-provide this object at every layer boundary or flatten
33
+ * that nesting.
33
34
  */
34
35
  export type SceneEffects = AmbientOcclusion & ContactShadow;
35
36
  /**
@@ -3,4 +3,22 @@ import { type MaterialParameters } from 'three';
3
3
  * Material parameter defaults, shared between all types
4
4
  */
5
5
  export declare const sharedParams: MaterialParameters;
6
+ /**
7
+ * What an overlay layer's planar materials add on top of {@link sharedParams}.
8
+ *
9
+ * Two obstacles, and render order clears neither.
10
+ *
11
+ * Depth. A solid shape sets `depthWrite: true`, so booth volumes fill the depth buffer. A planar def
12
+ * drawn later is still depth tested against them and loses. Turning the test off leaves order inside
13
+ * the tier as pure painter's order, because nothing in the tier writes depth.
14
+ *
15
+ * Pass. A translucent solid carries `transparent: true`, so it lives in three's transparent list.
16
+ * three draws that list after every opaque object, whatever render order says. An overlay that
17
+ * stayed in the opaque list would lose to a glass roof. So it opts back in. Inside the transparent
18
+ * list `reversePainterSortStable` compares `groupOrder` first, and the overlay tier's value of at
19
+ * least 1 beats every plan-tier value.
20
+ *
21
+ * `forceSinglePass` is not touched, so a double-sided overlay quad still blends once per pixel.
22
+ */
23
+ export declare const overlayParams: MaterialParameters;
6
24
  //# sourceMappingURL=shared.d.ts.map
@@ -49,3 +49,24 @@ export const sharedParams = {
49
49
  polygonOffsetUnits: -FLAT_BIAS_UNITS,
50
50
  forceSinglePass: true,
51
51
  };
52
+ /**
53
+ * What an overlay layer's planar materials add on top of {@link sharedParams}.
54
+ *
55
+ * Two obstacles, and render order clears neither.
56
+ *
57
+ * Depth. A solid shape sets `depthWrite: true`, so booth volumes fill the depth buffer. A planar def
58
+ * drawn later is still depth tested against them and loses. Turning the test off leaves order inside
59
+ * the tier as pure painter's order, because nothing in the tier writes depth.
60
+ *
61
+ * Pass. A translucent solid carries `transparent: true`, so it lives in three's transparent list.
62
+ * three draws that list after every opaque object, whatever render order says. An overlay that
63
+ * stayed in the opaque list would lose to a glass roof. So it opts back in. Inside the transparent
64
+ * list `reversePainterSortStable` compares `groupOrder` first, and the overlay tier's value of at
65
+ * least 1 beats every plan-tier value.
66
+ *
67
+ * `forceSinglePass` is not touched, so a double-sided overlay quad still blends once per pixel.
68
+ */
69
+ export const overlayParams = {
70
+ depthTest: false,
71
+ transparent: true,
72
+ };
@@ -139,15 +139,8 @@ function ModelPartMesh(props) {
139
139
  // instead — but the hit resolution is not: `InstancedMeshImpl.raycast` walks the live slots,
140
140
  // skipping deleted and hidden ones, and raycasts the real merged geometry, so a hit names the
141
141
  // model's actual triangles and the slot that drew them.
142
- const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs);
142
+ const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs, onKindMismatch);
143
143
  const occluder = useOccluder();
144
- // Assigned after mount rather than passed to the constructor: the container is built once and
145
- // outlives every render, so it cannot close over a current callback. Nothing can fire before this
146
- // runs — the split that placed these defs was computed from the same opacities they mount with.
147
- useEffect(() => {
148
- container.setKindMismatchHandler(onKindMismatch);
149
- return () => container.setKindMismatchHandler(undefined);
150
- }, [container, onKindMismatch]);
151
144
  // The geometry belongs to the module-level cache and may be drawing in another layer, so it is
152
145
  // mounted as a `primitive` with `dispose={null}` — R3F must not free it on unmount. Sharing it is
153
146
  // safe because `InstancedMeshImpl` never writes to the geometry it is handed: its draw list lives in
@@ -24,8 +24,7 @@ const logger = createLogger('shape-instance');
24
24
  */
25
25
  export const ShapeInstance = memo(function ShapeInstance(props) {
26
26
  const { def, container } = props;
27
- const geometry = container.geometryFor(def.shape);
28
- useDefSync(container, def, geometry, (slot) => {
27
+ useDefSync(container, def, () => container.geometryFor(def.shape), (slot) => {
29
28
  writeMatrixIfChanged(container.mesh, slot, shapeToMatrix(def.shape));
30
29
  writeColorIfChanged(container.mesh, slot, def, logger);
31
30
  // Checked on every sync rather than only behind a color write, because a def leaves its batch's