@expofp/renderer 3.2.5 → 3.27.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 (228) hide show
  1. package/README.md +111 -0
  2. package/dist/Renderer.d.ts +88 -0
  3. package/dist/Renderer.js +53 -0
  4. package/dist/controls/CameraControls.d.ts +102 -0
  5. package/dist/controls/CameraControls.js +239 -0
  6. package/dist/controls/camera-api.d.ts +206 -0
  7. package/dist/controls/camera-api.js +181 -0
  8. package/dist/controls/camera-move.d.ts +13 -0
  9. package/dist/controls/camera-move.js +18 -0
  10. package/dist/controls/camera-state.d.ts +40 -0
  11. package/dist/controls/camera-state.js +65 -0
  12. package/dist/controls/gestures.d.ts +70 -0
  13. package/dist/controls/gestures.js +84 -0
  14. package/dist/controls/handlers/Inertia.d.ts +111 -0
  15. package/dist/controls/handlers/Inertia.js +215 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +32 -0
  18. package/dist/controls/handlers/Pitch.d.ts +94 -0
  19. package/dist/controls/handlers/Pitch.js +191 -0
  20. package/dist/controls/handlers/Roll.d.ts +103 -0
  21. package/dist/controls/handlers/Roll.js +182 -0
  22. package/dist/controls/handlers/Zoom.d.ts +15 -0
  23. package/dist/controls/handlers/Zoom.js +26 -0
  24. package/dist/controls/index.d.ts +56 -0
  25. package/dist/controls/index.js +58 -0
  26. package/dist/controls/zoom-to.d.ts +89 -0
  27. package/dist/controls/zoom-to.js +91 -0
  28. package/dist/core/InstanceUniforms.d.ts +93 -0
  29. package/dist/core/InstanceUniforms.js +266 -0
  30. package/dist/core/SquareDataTexture.d.ts +277 -0
  31. package/dist/core/SquareDataTexture.js +476 -0
  32. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  33. package/dist/core/batch/BatchedMesh.js +149 -0
  34. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  35. package/dist/core/instanced/FrustumCulling.js +246 -0
  36. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  37. package/dist/core/instanced/InstancedMesh.js +497 -0
  38. package/dist/core/instanced/Instances.d.ts +119 -0
  39. package/dist/core/instanced/Instances.js +230 -0
  40. package/dist/core/instanced/Raycasting.d.ts +118 -0
  41. package/dist/core/instanced/Raycasting.js +134 -0
  42. package/dist/core/instanced/instanced-material.d.ts +77 -0
  43. package/dist/core/instanced/instanced-material.js +224 -0
  44. package/dist/core/lines/BatchedLines.d.ts +135 -0
  45. package/dist/core/lines/BatchedLines.js +213 -0
  46. package/dist/core/lines/FrustumCulling.d.ts +43 -0
  47. package/dist/core/lines/FrustumCulling.js +85 -0
  48. package/dist/core/lines/Raycasting.d.ts +44 -0
  49. package/dist/core/lines/Raycasting.js +91 -0
  50. package/dist/core/lines/lines-material.d.ts +47 -0
  51. package/dist/core/lines/lines-material.js +115 -0
  52. package/dist/core/shader-source.d.ts +50 -0
  53. package/dist/core/shader-source.js +84 -0
  54. package/dist/core/text/BatchedText.d.ts +227 -0
  55. package/dist/core/text/BatchedText.js +467 -0
  56. package/dist/core/text/MemberTexture.d.ts +90 -0
  57. package/dist/core/text/MemberTexture.js +194 -0
  58. package/dist/core/text/Text.d.ts +73 -0
  59. package/dist/core/text/Text.js +215 -0
  60. package/dist/core/text/batched-text-material.d.ts +56 -0
  61. package/dist/core/text/batched-text-material.js +121 -0
  62. package/dist/core/text/glyph-packing.d.ts +17 -0
  63. package/dist/core/text/glyph-packing.js +61 -0
  64. package/dist/core/typed-array.d.ts +16 -0
  65. package/dist/core/typed-array.js +20 -0
  66. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  67. package/dist/debug/components/DebugPanelProvider.js +38 -0
  68. package/dist/debug/components/DebugTools.d.ts +22 -0
  69. package/dist/debug/components/DebugTools.js +28 -0
  70. package/dist/debug/components/Gui.d.ts +20 -0
  71. package/dist/debug/components/Gui.js +32 -0
  72. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  73. package/dist/debug/components/MemoryInfo.js +42 -0
  74. package/dist/debug/components/Panel.d.ts +31 -0
  75. package/dist/debug/components/Panel.js +98 -0
  76. package/dist/debug/components/Stats.d.ts +9 -0
  77. package/dist/debug/components/Stats.js +24 -0
  78. package/dist/debug/debug-context.d.ts +41 -0
  79. package/dist/debug/debug-context.js +59 -0
  80. package/dist/debug/index.d.ts +51 -0
  81. package/dist/debug/index.js +64 -0
  82. package/dist/events/LayoutDriver.d.ts +31 -0
  83. package/dist/events/LayoutDriver.js +88 -0
  84. package/dist/events/PointerMissed.d.ts +23 -0
  85. package/dist/events/PointerMissed.js +58 -0
  86. package/dist/events/def-culling.d.ts +46 -0
  87. package/dist/events/def-culling.js +69 -0
  88. package/dist/events/layout-targets.d.ts +104 -0
  89. package/dist/events/layout-targets.js +245 -0
  90. package/dist/events/pointer.d.ts +118 -0
  91. package/dist/events/pointer.js +168 -0
  92. package/dist/external/ExternalCanvas.d.ts +56 -0
  93. package/dist/external/ExternalCanvas.js +178 -0
  94. package/dist/external/external-camera.d.ts +17 -0
  95. package/dist/external/external-camera.js +82 -0
  96. package/dist/external/external-events.d.ts +14 -0
  97. package/dist/external/external-events.js +33 -0
  98. package/dist/index.d.ts +11 -783
  99. package/dist/index.js +12 -8031
  100. package/dist/objects/Background.d.ts +31 -0
  101. package/dist/objects/Background.js +37 -0
  102. package/dist/objects/Lighting.d.ts +20 -0
  103. package/dist/objects/Lighting.js +85 -0
  104. package/dist/objects/containers/SlotContainer.d.ts +266 -0
  105. package/dist/objects/containers/SlotContainer.js +251 -0
  106. package/dist/objects/containers/hooks.d.ts +46 -0
  107. package/dist/objects/containers/hooks.js +118 -0
  108. package/dist/objects/containers/index.d.ts +6 -0
  109. package/dist/objects/containers/index.js +4 -0
  110. package/dist/objects/containers/keys.d.ts +10 -0
  111. package/dist/objects/containers/keys.js +18 -0
  112. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  113. package/dist/objects/containers/mesh-writes.js +97 -0
  114. package/dist/objects/containers/props.d.ts +23 -0
  115. package/dist/objects/containers/props.js +1 -0
  116. package/dist/objects/images/ImageInstance.d.ts +29 -0
  117. package/dist/objects/images/ImageInstance.js +83 -0
  118. package/dist/objects/images/Images.d.ts +37 -0
  119. package/dist/objects/images/Images.js +204 -0
  120. package/dist/objects/images/atlas-packer.d.ts +128 -0
  121. package/dist/objects/images/atlas-packer.js +315 -0
  122. package/dist/objects/images/image-container.d.ts +51 -0
  123. package/dist/objects/images/image-container.js +70 -0
  124. package/dist/objects/images/index.d.ts +2 -0
  125. package/dist/objects/images/index.js +1 -0
  126. package/dist/objects/images/texture-budget.d.ts +120 -0
  127. package/dist/objects/images/texture-budget.js +215 -0
  128. package/dist/objects/layer/Layer.d.ts +27 -0
  129. package/dist/objects/layer/Layer.js +114 -0
  130. package/dist/objects/layer/index.d.ts +2 -0
  131. package/dist/objects/layer/index.js +1 -0
  132. package/dist/objects/layer/mount-scheduler.d.ts +102 -0
  133. package/dist/objects/layer/mount-scheduler.js +145 -0
  134. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  135. package/dist/objects/layer/visibility-gui.js +93 -0
  136. package/dist/objects/lines/LineInstance.d.ts +21 -0
  137. package/dist/objects/lines/LineInstance.js +73 -0
  138. package/dist/objects/lines/Lines.d.ts +17 -0
  139. package/dist/objects/lines/Lines.js +29 -0
  140. package/dist/objects/lines/index.d.ts +2 -0
  141. package/dist/objects/lines/index.js +1 -0
  142. package/dist/objects/lines/line-container.d.ts +35 -0
  143. package/dist/objects/lines/line-container.js +40 -0
  144. package/dist/objects/materials/ColorMaterial.d.ts +85 -0
  145. package/dist/objects/materials/ColorMaterial.js +89 -0
  146. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  147. package/dist/objects/materials/LineMaterial.js +72 -0
  148. package/dist/objects/materials/ModelMaterial.d.ts +41 -0
  149. package/dist/objects/materials/ModelMaterial.js +84 -0
  150. package/dist/objects/materials/TextureMaterial.d.ts +30 -0
  151. package/dist/objects/materials/TextureMaterial.js +29 -0
  152. package/dist/objects/materials/dim.d.ts +123 -0
  153. package/dist/objects/materials/dim.js +213 -0
  154. package/dist/objects/materials/shared.d.ts +6 -0
  155. package/dist/objects/materials/shared.js +51 -0
  156. package/dist/objects/models/ModelInstance.d.ts +22 -0
  157. package/dist/objects/models/ModelInstance.js +33 -0
  158. package/dist/objects/models/Models.d.ts +22 -0
  159. package/dist/objects/models/Models.js +177 -0
  160. package/dist/objects/models/gltf-cache.d.ts +17 -0
  161. package/dist/objects/models/gltf-cache.js +50 -0
  162. package/dist/objects/models/index.d.ts +2 -0
  163. package/dist/objects/models/index.js +1 -0
  164. package/dist/objects/models/model-asset.d.ts +48 -0
  165. package/dist/objects/models/model-asset.js +77 -0
  166. package/dist/objects/models/model-container.d.ts +139 -0
  167. package/dist/objects/models/model-container.js +227 -0
  168. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  169. package/dist/objects/shapes/ShapeInstance.js +40 -0
  170. package/dist/objects/shapes/Shapes.d.ts +21 -0
  171. package/dist/objects/shapes/Shapes.js +120 -0
  172. package/dist/objects/shapes/index.d.ts +2 -0
  173. package/dist/objects/shapes/index.js +1 -0
  174. package/dist/objects/shapes/shape-container.d.ts +165 -0
  175. package/dist/objects/shapes/shape-container.js +272 -0
  176. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  177. package/dist/objects/shapes/shape-geometry.js +94 -0
  178. package/dist/objects/text/TextInstance.d.ts +22 -0
  179. package/dist/objects/text/TextInstance.js +95 -0
  180. package/dist/objects/text/Texts.d.ts +30 -0
  181. package/dist/objects/text/Texts.js +41 -0
  182. package/dist/objects/text/index.d.ts +2 -0
  183. package/dist/objects/text/index.js +1 -0
  184. package/dist/objects/text/text-container.d.ts +105 -0
  185. package/dist/objects/text/text-container.js +206 -0
  186. package/dist/objects/text/text-layout.d.ts +83 -0
  187. package/dist/objects/text/text-layout.js +154 -0
  188. package/dist/space/Camera.d.ts +103 -0
  189. package/dist/space/Camera.js +321 -0
  190. package/dist/space/Scene.d.ts +44 -0
  191. package/dist/space/Scene.js +112 -0
  192. package/dist/space/Space.d.ts +288 -0
  193. package/dist/space/Space.js +329 -0
  194. package/dist/space/VisibleRect.d.ts +72 -0
  195. package/dist/space/VisibleRect.js +129 -0
  196. package/dist/space/camera-handle.d.ts +67 -0
  197. package/dist/space/camera-handle.js +63 -0
  198. package/dist/space/camera-kind.d.ts +26 -0
  199. package/dist/space/camera-kind.js +30 -0
  200. package/dist/space/canvas-size.d.ts +24 -0
  201. package/dist/space/canvas-size.js +20 -0
  202. package/dist/space/clipping.d.ts +55 -0
  203. package/dist/space/clipping.js +108 -0
  204. package/dist/space/layout-measure.d.ts +65 -0
  205. package/dist/space/layout-measure.js +173 -0
  206. package/dist/space/viewport.d.ts +112 -0
  207. package/dist/space/viewport.js +67 -0
  208. package/dist/types/defs.d.ts +169 -0
  209. package/dist/types/defs.js +1 -0
  210. package/dist/types/events.d.ts +183 -0
  211. package/dist/types/events.js +1 -0
  212. package/dist/types/guards.d.ts +14 -0
  213. package/dist/types/guards.js +42 -0
  214. package/dist/update/index.d.ts +3 -0
  215. package/dist/update/index.js +2 -0
  216. package/dist/update/slot-writers.d.ts +30 -0
  217. package/dist/update/slot-writers.js +72 -0
  218. package/dist/update/store.d.ts +41 -0
  219. package/dist/update/store.js +98 -0
  220. package/dist/utils/arrays.d.ts +13 -0
  221. package/dist/utils/arrays.js +20 -0
  222. package/dist/utils/colors.d.ts +19 -0
  223. package/dist/utils/colors.js +27 -0
  224. package/dist/utils/logging.d.ts +26 -0
  225. package/dist/utils/logging.js +47 -0
  226. package/dist/utils/math.d.ts +24 -0
  227. package/dist/utils/math.js +27 -0
  228. package/package.json +67 -19
@@ -0,0 +1,41 @@
1
+ import { type Material } from 'three';
2
+ /** Props for {@link ModelMaterial}. */
3
+ export interface ModelMaterialProps {
4
+ /**
5
+ * The material the GLB authored. Shared across every layer drawing the same model, so it is never
6
+ * mutated — this clones it and adjusts the clone.
7
+ */
8
+ source: Material;
9
+ /**
10
+ * The name of a per-instance `float` uniform to multiply alpha by, so one mesh can hold instances
11
+ * of differing opacity. The mesh owns the name and declares it; the material only patches its
12
+ * shader to read it.
13
+ */
14
+ opacityUniform: string;
15
+ /**
16
+ * Whether any instance drawing this material is translucent.
17
+ *
18
+ * Per-instance opacity is a *fragment* multiplier, but whether alpha blends at all is a **material**
19
+ * decision: three drops a `NormalBlending` material with `transparent: false` to `NoBlending`
20
+ * (`WebGLState.setMaterial`), which discards the alpha entirely — the instance then composites
21
+ * against the page rather than against the scene. So the mesh has to tell the material that at
22
+ * least one of its instances needs blending.
23
+ */
24
+ translucent?: boolean;
25
+ }
26
+ /**
27
+ * The material a model draws with: the GLB's own, cloned and adjusted for the plan's depth rules and
28
+ * for per-instance opacity.
29
+ *
30
+ * A model is one of the two kinds of content that **write depth**, translucent ones included — that
31
+ * is what makes it occlude itself, other models, and any 2D content drawn after it. The rest of the
32
+ * scene draws with `depthWrite: false` and a small polygon offset precisely so it can be biased
33
+ * against surfaces like these, so a model carries no offset of its own.
34
+ *
35
+ * Everything else about the material is left as the GLB authored it — maps, roughness, metalness,
36
+ * emissive, side. A model's appearance is the asset's business, not the renderer's.
37
+ * @param props - The source material and the per-instance opacity uniform's name.
38
+ * @returns The material element.
39
+ */
40
+ export declare function ModelMaterial(props: ModelMaterialProps): import("react").JSX.Element;
41
+ //# sourceMappingURL=ModelMaterial.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { LessEqualDepth, } from 'three';
4
+ import { useDimPatch } from './dim';
5
+ /**
6
+ * The material a model draws with: the GLB's own, cloned and adjusted for the plan's depth rules and
7
+ * for per-instance opacity.
8
+ *
9
+ * A model is one of the two kinds of content that **write depth**, translucent ones included — that
10
+ * is what makes it occlude itself, other models, and any 2D content drawn after it. The rest of the
11
+ * scene draws with `depthWrite: false` and a small polygon offset precisely so it can be biased
12
+ * against surfaces like these, so a model carries no offset of its own.
13
+ *
14
+ * Everything else about the material is left as the GLB authored it — maps, roughness, metalness,
15
+ * emissive, side. A model's appearance is the asset's business, not the renderer's.
16
+ * @param props - The source material and the per-instance opacity uniform's name.
17
+ * @returns The material element.
18
+ */
19
+ export function ModelMaterial(props) {
20
+ const { source, opacityUniform } = props;
21
+ const dimPatch = useDimPatch();
22
+ const translucent = props.translucent ?? false;
23
+ // Cloned once and held in state: R3F would dispose a material it created on unmount, and the
24
+ // source belongs to the module-level model cache, which may still be serving other layers. The
25
+ // dim patch is installed with it, once — the patch is stable and reads the current amount through
26
+ // the uniform it owns.
27
+ const [material] = useState(() => adjust(source.clone(), opacityUniform, dimPatch, translucent));
28
+ useEffect(() => () => material.dispose(), [material]);
29
+ return _jsx("primitive", { object: material, attach: "material" });
30
+ }
31
+ /**
32
+ * Applies the plan's depth rules and the per-instance opacity patch to a cloned GLB material.
33
+ * @param material - The clone to adjust.
34
+ * @param opacityUniform - The per-instance uniform's name.
35
+ * @param dimPatch - Installs dimming on the compiled shader.
36
+ * @param translucent - Whether any instance drawing it needs its alpha blended.
37
+ * @returns The same material.
38
+ */
39
+ function adjust(material, opacityUniform, dimPatch, translucent) {
40
+ material.depthFunc = LessEqualDepth;
41
+ material.polygonOffset = false;
42
+ // `transparent` moves a material into three's transparent list, drawn after *every* opaque object
43
+ // in the scene whatever the layer order says — which is what lets a translucent model blend with
44
+ // the content behind it instead of with an empty frame. It is a **material** property and never a
45
+ // per-instance one, so which list a def belongs in is a partition rather than a flag: `Models`
46
+ // keeps a layer's translucent copies in a batch of their own and re-splits when a def's opacity
47
+ // crosses 1, exactly as `Shapes` splits its volumes. `ColorMaterial` makes the same trade and
48
+ // records what it costs — a planar def sitting *on* a translucent one is drawn before it.
49
+ material.transparent =
50
+ material.transparent || translucent || material.opacity < 1;
51
+ // Depth writes stay on even when translucent, which is not three's usual convention and is
52
+ // deliberate. Two of the plan's rules depend on it: a translucent model must still be occluded by
53
+ // anything nearer the camera, and a planar def drawn after it — a label or logo on a booth roof —
54
+ // must be occluded by it. Both are depth-buffer questions, and switching writes off answers them
55
+ // wrongly: a *farther* solid drawn later would have nothing to reject it and would paint straight
56
+ // over the translucent one in front of it.
57
+ //
58
+ // What it costs is seeing *through* a translucent surface to content drawn after it, which no
59
+ // painter's-order renderer can offer — blending needs depth order, and array order is authoring
60
+ // order. MapLibre makes the same trade for `fill-extrusion` (`DepthMode.ReadWrite` in both of its
61
+ // passes), degrading to a color error rather than a structural one. The obligation it puts on
62
+ // callers — order a translucent model's layer after whatever should show through it — is spelled
63
+ // out in the design spec's Decision 2, along with the two alternatives and why each loses more
64
+ // than it gains.
65
+ material.depthWrite = true;
66
+ const chained = material.onBeforeCompile.bind(material);
67
+ material.onBeforeCompile = (parameters, ...rest) => {
68
+ chained(parameters, ...rest);
69
+ // Applied at the very end of the fragment chain, after `<dithering_fragment>`, so it multiplies
70
+ // the final composed alpha rather than an intermediate one. The instanced mesh injects the
71
+ // uniform fetch at the top of `main()`, so the name is in scope by the time this runs.
72
+ parameters.fragmentShader = parameters.fragmentShader.replace('#include <dithering_fragment>',
73
+ /*glsl*/ `
74
+ #include <dithering_fragment>
75
+ gl_FragColor.a *= ${opacityUniform};
76
+ `.trim());
77
+ // Dim touches rgb and opacity touches alpha, so the two patches are independent however they
78
+ // are ordered — and dim lands ahead of the colorspace conversion either way, which is where a
79
+ // gamma-corrected darken has to be.
80
+ dimPatch(parameters);
81
+ };
82
+ material.needsUpdate = true;
83
+ return material;
84
+ }
@@ -0,0 +1,30 @@
1
+ import { type Texture } from 'three';
2
+ /** Props for {@link TextureMaterial}. */
3
+ export interface TextureMaterialProps {
4
+ /**
5
+ * Texture map. Atlas textures are uploaded with `flipY = false`, so v runs down the canvas and
6
+ * the region coordinates need no vertical inversion anywhere.
7
+ */
8
+ map: Texture;
9
+ /**
10
+ * The name of a per-instance `vec4` uniform to source the texture region from — for a texture
11
+ * atlas on an instanced/batched mesh that declares and writes it. The mesh owns the name; the
12
+ * material only patches its shader to sample `map` through it. Omit it for a plain textured
13
+ * material: the shader is left unpatched, so the material draws its whole `map` and works in any
14
+ * context, not only an instanced one.
15
+ */
16
+ uvOffset?: string;
17
+ }
18
+ /**
19
+ * The material used to render a texture, optionally through a per-instance uv-offset uniform so an
20
+ * instanced/batched mesh can pack many regions into one atlas.
21
+ *
22
+ * When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
23
+ * `<uv_vertex>` has run, so the instance's atlas region replaces whatever the stock chunk computed.
24
+ * Instanced meshes wrap this `onBeforeCompile` rather than replacing it, and inject their uniform
25
+ * fetch at the top of `main()`, so the uniform is in scope by the time this line runs.
26
+ * @param props - The material's texture and optional uv-offset uniform name.
27
+ * @returns The material element.
28
+ */
29
+ export declare function TextureMaterial(props: TextureMaterialProps): import("react").JSX.Element;
30
+ //# sourceMappingURL=TextureMaterial.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useDimPatch } from './dim';
3
+ import { sharedParams } from './shared';
4
+ /**
5
+ * The material used to render a texture, optionally through a per-instance uv-offset uniform so an
6
+ * instanced/batched mesh can pack many regions into one atlas.
7
+ *
8
+ * When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
9
+ * `<uv_vertex>` has run, so the instance's atlas region replaces whatever the stock chunk computed.
10
+ * Instanced meshes wrap this `onBeforeCompile` rather than replacing it, and inject their uniform
11
+ * fetch at the top of `main()`, so the uniform is in scope by the time this line runs.
12
+ * @param props - The material's texture and optional uv-offset uniform name.
13
+ * @returns The material element.
14
+ */
15
+ export function TextureMaterial(props) {
16
+ const { map, uvOffset } = props;
17
+ const applyDim = useDimPatch();
18
+ function onBeforeCompile(parameters) {
19
+ applyDim(parameters);
20
+ if (!uvOffset)
21
+ return;
22
+ parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>',
23
+ /*glsl*/ `
24
+ #include <uv_vertex>
25
+ vMapUv = uv * ${uvOffset}.zw + ${uvOffset}.xy;
26
+ `.trim());
27
+ }
28
+ return _jsx("meshBasicMaterial", { ...sharedParams, map: map, onBeforeCompile: onBeforeCompile });
29
+ }
@@ -0,0 +1,123 @@
1
+ import { type IUniform, type WebGLProgramParametersWithUniforms } from 'three';
2
+ import { type UniformSchemaShader } from '../../core/InstanceUniforms';
3
+ import { type RenderableDef } from '../../types/defs';
4
+ /**
5
+ * Patches a compiled shader to dim what it draws. Returned by {@link useDimPatch} and called from a
6
+ * material's own `onBeforeCompile`, so a material that already patches its shader composes the two
7
+ * rather than choosing between them.
8
+ */
9
+ export type DimPatch = (parameters: WebGLProgramParametersWithUniforms) => void;
10
+ /**
11
+ * The dim patch every material installs, reading the layer amount from {@link LayerDimContext}.
12
+ *
13
+ * Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
14
+ * so threading it as a prop would put a `dim` on each material, each container, and each of their
15
+ * inner components, and any new one would have to remember to forward it. What a material needs to
16
+ * know is only *how* to be dimmed; what it is dimmed *by* belongs to the layer it draws under.
17
+ *
18
+ * The amount is held in one `IUniform` object per material and mutated in place, never replaced, so
19
+ * an animated dim — floorplan eases the venue's dim over 300ms — costs a number per material per
20
+ * frame with no recompile.
21
+ * @returns The patch to call from the material's `onBeforeCompile`.
22
+ */
23
+ export declare function useDimPatch(): DimPatch;
24
+ /**
25
+ * Installs dimming into a shader about to be compiled: the layer's uniform, the declarations, and
26
+ * the application.
27
+ *
28
+ * **Applies only once the mesh's uniform prologue is in the shader**, and that guard is what makes
29
+ * installation order stop mattering. The applied line reads the per-instance amount the prologue
30
+ * declares as a *local of `main`*, so it has to land in the same `main`. Most meshes inject their
31
+ * prologue and then call the material's handler, so a material-installed patch is already last. Text
32
+ * is the exception: troika calls the base material's handler first and then rebuilds `main` around
33
+ * it — renaming the original `troikaOrigMain` and calling it from a new one — so a patch installed
34
+ * there would land in a scope the prologue's local never reaches. With this guard that early call
35
+ * simply does nothing, and `BatchedTextExt` runs the same patch again once its prologue is in.
36
+ * @param parameters - The shader being compiled.
37
+ * @param uniform - The layer's dim amount.
38
+ * @returns Whether the shader was patched.
39
+ * @throws {Error} If the prologue is present but the shader converts no color, which would leave
40
+ * dimming silently absent from a material that asked for it.
41
+ */
42
+ export declare function applyDim(parameters: WebGLProgramParametersWithUniforms, uniform: IUniform<number>): boolean;
43
+ /**
44
+ * Orders a layer's defs so the dimmed ones draw first and the undimmed ones last, on top.
45
+ *
46
+ * What this is for: dimming the venue behind a search leaves the few undimmed defs as the subject of
47
+ * the frame, and a subject that a dimmed neighbor happens to overlap would be covered by it. Paint
48
+ * order is the only thing that resolves overlap in a 2D layer — 2D content never writes depth,
49
+ * leaving depth testing nothing of its own to compare against — so being undimmed has to lift a def
50
+ * above the defs it is being distinguished from.
51
+ *
52
+ * **Stable**, and that is load-bearing rather than a nicety: within each group the defs keep their
53
+ * array order, which is the order the caller authored and the only thing that decides overlap
54
+ * between two defs that are both dimmed or both lit. A dim is allowed to lift a def over the dimmed
55
+ * ones; it is not allowed to reshuffle anything else.
56
+ *
57
+ * Returns the input array itself when the rule changes nothing — every def in one group, which is
58
+ * every layer that is uniformly dimmed or uniformly not. Callers key their order pass on the
59
+ * identity of what comes back, so an unchanged layer costs neither an allocation nor a re-label.
60
+ * @param defs - The layer's defs, in authored order.
61
+ * @param layerDimmed - Whether the layer is dimmed, which is what a def without a `dim` of its own
62
+ * inherits. Only the fact is needed, never the amount: this is a two-way split, and every amount
63
+ * above zero lands on the same side of it. That is also what keeps an eased layer dim cheap — the
64
+ * order pass re-runs when the value crosses zero, not on each of the frames between.
65
+ * @returns The defs in draw order.
66
+ */
67
+ export declare function dimOrdered<T extends RenderableDef>(defs: readonly T[], layerDimmed: boolean): readonly T[];
68
+ /**
69
+ * Whether a def draws dimmed at all — the one question paint order asks about dimming, as opposed
70
+ * to the amount, which only the shader reads.
71
+ * @param dim - The def's own `dim`; `undefined` inherits.
72
+ * @param layerDimmed - Whether the layer it draws under is dimmed.
73
+ * @returns Whether it draws dimmed.
74
+ */
75
+ export declare function isDimmed(dim: number | undefined, layerDimmed: boolean): boolean;
76
+ /**
77
+ * Encodes a def's own `dim` for its per-instance uniform.
78
+ * @param dim - The def's `dim`; `undefined` defers to the layer.
79
+ * @returns The value to write.
80
+ */
81
+ export declare function instanceDim(dim: number | undefined): number;
82
+ /**
83
+ * Clamps a dim amount into the range the shader interpolates over, so `mix` never extrapolates into
84
+ * a color that is not a dim of the original.
85
+ * @param dim - The amount to clamp.
86
+ * @returns The amount, within `[0, 1]`.
87
+ */
88
+ 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
+ /**
99
+ * The per-instance uniform a def's own dim is written to. Declared by every container that draws
100
+ * dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
101
+ * declare it before any slot writes one.
102
+ */
103
+ export declare const DIM_UNIFORM = "instanceDim";
104
+ /**
105
+ * What an instance writes to defer to its layer.
106
+ *
107
+ * Any negative value would do; the sentinel only has to fall outside `[0, 1]`, which is why the
108
+ * amount itself can be stored as-is rather than packed around a zero that means "inherit". The
109
+ * imperative renderer needed that packing because its texture zero-filled and nothing wrote an
110
+ * instance on mount — here every instance writes its own value through `useDefSync`, so the fill is
111
+ * never what a draw reads.
112
+ */
113
+ export declare const DIM_INHERIT = -1;
114
+ /**
115
+ * The schema a container declares to give its instances a dim of their own.
116
+ *
117
+ * Declared in the **vertex** stage although only the fragment shader reads it: that is what makes
118
+ * `InstanceUniforms` fetch it once per vertex and deliver it as a varying, instead of a texel fetch
119
+ * per fragment. It also moves any sibling uniform the same way, which costs nothing — images, lines
120
+ * and models already declare vertex uniforms, and shapes and text declare none besides this.
121
+ */
122
+ export declare const DIM_SCHEMA: UniformSchemaShader;
123
+ //# sourceMappingURL=dim.d.ts.map
@@ -0,0 +1,213 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ import { createContext, use, useEffect, useRef, useState } from 'react';
3
+ import { declareBeforeMain } from '../../core/shader-source';
4
+ /**
5
+ * The dim patch every material installs, reading the layer amount from {@link LayerDimContext}.
6
+ *
7
+ * Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
8
+ * so threading it as a prop would put a `dim` on each material, each container, and each of their
9
+ * inner components, and any new one would have to remember to forward it. What a material needs to
10
+ * know is only *how* to be dimmed; what it is dimmed *by* belongs to the layer it draws under.
11
+ *
12
+ * The amount is held in one `IUniform` object per material and mutated in place, never replaced, so
13
+ * an animated dim — floorplan eases the venue's dim over 300ms — costs a number per material per
14
+ * frame with no recompile.
15
+ * @returns The patch to call from the material's `onBeforeCompile`.
16
+ */
17
+ export function useDimPatch() {
18
+ const dim = use(LayerDimContext);
19
+ const invalidate = useThree((state) => state.invalidate);
20
+ // A ref, not state: this object is written after render, every frame of an eased dim, and a ref is
21
+ // the container React sanctions that for. Its identity is what three holds onto, so the value can
22
+ // move underneath a program that is already built.
23
+ const uniformRef = useRef({ value: 0 });
24
+ // Created once and kept: R3F writes every changed prop onto the material, so handing it a fresh
25
+ // handler each render risks a recompile per frame — the one thing an animated dim must not cost.
26
+ const [patch] = useState(() => (parameters) => applyDim(parameters, uniformRef.current));
27
+ const amount = clampDim(dim);
28
+ useEffect(() => {
29
+ uniformRef.current.value = amount;
30
+ invalidate();
31
+ }, [amount, invalidate]);
32
+ return patch;
33
+ }
34
+ /**
35
+ * Installs dimming into a shader about to be compiled: the layer's uniform, the declarations, and
36
+ * the application.
37
+ *
38
+ * **Applies only once the mesh's uniform prologue is in the shader**, and that guard is what makes
39
+ * installation order stop mattering. The applied line reads the per-instance amount the prologue
40
+ * declares as a *local of `main`*, so it has to land in the same `main`. Most meshes inject their
41
+ * prologue and then call the material's handler, so a material-installed patch is already last. Text
42
+ * is the exception: troika calls the base material's handler first and then rebuilds `main` around
43
+ * it — renaming the original `troikaOrigMain` and calling it from a new one — so a patch installed
44
+ * there would land in a scope the prologue's local never reaches. With this guard that early call
45
+ * simply does nothing, and `BatchedTextExt` runs the same patch again once its prologue is in.
46
+ * @param parameters - The shader being compiled.
47
+ * @param uniform - The layer's dim amount.
48
+ * @returns Whether the shader was patched.
49
+ * @throws {Error} If the prologue is present but the shader converts no color, which would leave
50
+ * dimming silently absent from a material that asked for it.
51
+ */
52
+ export function applyDim(parameters, uniform) {
53
+ const fragment = parameters.fragmentShader;
54
+ // Nothing to read, so nothing to do — either the mesh declared no per-instance dim, or its
55
+ // prologue has not been injected yet.
56
+ if (!fragment.includes(DIM_UNIFORM))
57
+ return false;
58
+ // Already installed, which is how a second call after the prologue arrives stays harmless.
59
+ if (fragment.includes(DIM_FUNCTION))
60
+ return false;
61
+ const anchor = colorspaceAnchor(fragment);
62
+ if (!anchor)
63
+ throw new Error('applyDim: the shader has no colorspace conversion to dim before.');
64
+ parameters.uniforms[LAYER_DIM] = uniform;
65
+ // Ahead of `main`, because these are file-scope declarations — and `dimColor` takes the amount as a
66
+ // parameter precisely so it needs nothing `main` declares.
67
+ parameters.fragmentShader = declareBeforeMain(fragment, DIM_DEFS).replace(anchor, `${DIM_APPLY}\n${anchor}`);
68
+ return true;
69
+ }
70
+ /**
71
+ * Orders a layer's defs so the dimmed ones draw first and the undimmed ones last, on top.
72
+ *
73
+ * What this is for: dimming the venue behind a search leaves the few undimmed defs as the subject of
74
+ * the frame, and a subject that a dimmed neighbor happens to overlap would be covered by it. Paint
75
+ * order is the only thing that resolves overlap in a 2D layer — 2D content never writes depth,
76
+ * leaving depth testing nothing of its own to compare against — so being undimmed has to lift a def
77
+ * above the defs it is being distinguished from.
78
+ *
79
+ * **Stable**, and that is load-bearing rather than a nicety: within each group the defs keep their
80
+ * array order, which is the order the caller authored and the only thing that decides overlap
81
+ * between two defs that are both dimmed or both lit. A dim is allowed to lift a def over the dimmed
82
+ * ones; it is not allowed to reshuffle anything else.
83
+ *
84
+ * Returns the input array itself when the rule changes nothing — every def in one group, which is
85
+ * every layer that is uniformly dimmed or uniformly not. Callers key their order pass on the
86
+ * identity of what comes back, so an unchanged layer costs neither an allocation nor a re-label.
87
+ * @param defs - The layer's defs, in authored order.
88
+ * @param layerDimmed - Whether the layer is dimmed, which is what a def without a `dim` of its own
89
+ * inherits. Only the fact is needed, never the amount: this is a two-way split, and every amount
90
+ * above zero lands on the same side of it. That is also what keeps an eased layer dim cheap — the
91
+ * order pass re-runs when the value crosses zero, not on each of the frames between.
92
+ * @returns The defs in draw order.
93
+ */
94
+ export function dimOrdered(defs, layerDimmed) {
95
+ const dimmed = [];
96
+ const lit = [];
97
+ for (const def of defs)
98
+ (isDimmed(def.dim, layerDimmed) ? dimmed : lit).push(def);
99
+ return dimmed.length === 0 || lit.length === 0 ? defs : [...dimmed, ...lit];
100
+ }
101
+ /**
102
+ * Whether a def draws dimmed at all — the one question paint order asks about dimming, as opposed
103
+ * to the amount, which only the shader reads.
104
+ * @param dim - The def's own `dim`; `undefined` inherits.
105
+ * @param layerDimmed - Whether the layer it draws under is dimmed.
106
+ * @returns Whether it draws dimmed.
107
+ */
108
+ export function isDimmed(dim, layerDimmed) {
109
+ return dim === undefined ? layerDimmed : dim > 0;
110
+ }
111
+ /**
112
+ * Encodes a def's own `dim` for its per-instance uniform.
113
+ * @param dim - The def's `dim`; `undefined` defers to the layer.
114
+ * @returns The value to write.
115
+ */
116
+ export function instanceDim(dim) {
117
+ return dim === undefined ? DIM_INHERIT : clampDim(dim);
118
+ }
119
+ /**
120
+ * Clamps a dim amount into the range the shader interpolates over, so `mix` never extrapolates into
121
+ * a color that is not a dim of the original.
122
+ * @param dim - The amount to clamp.
123
+ * @returns The amount, within `[0, 1]`.
124
+ */
125
+ export function clampDim(dim) {
126
+ return Math.min(Math.max(dim, 0), 1);
127
+ }
128
+ /**
129
+ * Where the fragment's color is final and still linear — the point dimming has to land in front of,
130
+ * spelled whichever way the shader currently spells it.
131
+ *
132
+ * Two spellings because three resolves `#include` late, after every `onBeforeCompile`, while troika
133
+ * resolves them itself while rebuilding `main`. A patch running after troika's rebuild therefore
134
+ * finds the conversion already expanded to its chunk body. Both name the same line; neither is a
135
+ * private marker of troika's.
136
+ * @param fragmentShader - The shader to search.
137
+ * @returns The anchor found, or `undefined` if the shader converts no color.
138
+ */
139
+ function colorspaceAnchor(fragmentShader) {
140
+ const include = /*glsl*/ `#include <colorspace_fragment>`;
141
+ const expanded = /*glsl*/ `gl_FragColor = linearToOutputTexel( gl_FragColor );`;
142
+ if (fragmentShader.includes(include))
143
+ return include;
144
+ return fragmentShader.includes(expanded) ? expanded : undefined;
145
+ }
146
+ /**
147
+ * The dim amount a layer draws at, which every material beneath it reads.
148
+ *
149
+ * Context rather than a prop because the value crosses the whole subtree and concerns none of the
150
+ * components between: a container hands its material nothing, and a material that never asked about
151
+ * layers still dims correctly. `Layer` provides it, and only when it declares a `dim` of its own —
152
+ * a layer without one is transparent to this, which is exactly what inheritance means.
153
+ */
154
+ export const LayerDimContext = createContext(0);
155
+ /**
156
+ * The per-instance uniform a def's own dim is written to. Declared by every container that draws
157
+ * dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
158
+ * declare it before any slot writes one.
159
+ */
160
+ export const DIM_UNIFORM = 'instanceDim';
161
+ /**
162
+ * What an instance writes to defer to its layer.
163
+ *
164
+ * Any negative value would do; the sentinel only has to fall outside `[0, 1]`, which is why the
165
+ * amount itself can be stored as-is rather than packed around a zero that means "inherit". The
166
+ * imperative renderer needed that packing because its texture zero-filled and nothing wrote an
167
+ * instance on mount — here every instance writes its own value through `useDefSync`, so the fill is
168
+ * never what a draw reads.
169
+ */
170
+ export const DIM_INHERIT = -1;
171
+ /**
172
+ * The schema a container declares to give its instances a dim of their own.
173
+ *
174
+ * Declared in the **vertex** stage although only the fragment shader reads it: that is what makes
175
+ * `InstanceUniforms` fetch it once per vertex and deliver it as a varying, instead of a texel fetch
176
+ * per fragment. It also moves any sibling uniform the same way, which costs nothing — images, lines
177
+ * and models already declare vertex uniforms, and shapes and text declare none besides this.
178
+ */
179
+ export const DIM_SCHEMA = { vertex: { [DIM_UNIFORM]: 'float' } };
180
+ /** The uniform carrying the layer-level amount an instance without one of its own falls back to. */
181
+ const LAYER_DIM = 'uDim';
182
+ /** The signature {@link applyDim} looks for to tell an already-patched shader from a fresh one. */
183
+ const DIM_FUNCTION = 'vec4 dimColor(';
184
+ /**
185
+ * Dimming's file-scope declarations.
186
+ *
187
+ * Dim is a desaturation toward gray, not a darkening — which is why it cannot be baked into a
188
+ * per-instance tint: a texel sampled per fragment has no tint to bake into.
189
+ *
190
+ * `dimColor` takes straight (non-premultiplied) `gl_FragColor` and returns it the same way. The
191
+ * imperative renderer divided rgb by alpha here and multiplied it back at the end; the round-trip
192
+ * cancels — `mix(c, dot(w, c) / k, t)` is linear and homogeneous in `c`, so `f(c / a) * a == f(c)`
193
+ * for every alpha — and it was the one thing that could produce a NaN, at `a == 0`, which SDF text
194
+ * edges generate in quantity. Dropping it changes no pixel.
195
+ */
196
+ const DIM_DEFS = /* glsl */ `
197
+ uniform float ${LAYER_DIM};
198
+
199
+ const vec3 grayWeights = vec3(0.299, 0.587, 0.114);
200
+ const float darkenFactor = pow(2., 2.2); // Gamma corrected
201
+
202
+ ${DIM_FUNCTION}vec4 col, float amount) {
203
+ vec3 gray = vec3(dot(grayWeights, col.rgb));
204
+ return vec4(mix(col.rgb, gray / darkenFactor, amount), col.a);
205
+ }
206
+ `;
207
+ /**
208
+ * Dimming applied to the fragment, while the color is still linear — which is what
209
+ * `darkenFactor`'s gamma correction assumes.
210
+ */
211
+ const DIM_APPLY = /* glsl */ `
212
+ gl_FragColor = dimColor(gl_FragColor, ${DIM_UNIFORM} < 0. ? ${LAYER_DIM} : ${DIM_UNIFORM});
213
+ `;
@@ -0,0 +1,6 @@
1
+ import { type MaterialParameters } from 'three';
2
+ /**
3
+ * Material parameter defaults, shared between all types
4
+ */
5
+ export declare const sharedParams: MaterialParameters;
6
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { CustomBlending, DoubleSide, LessEqualDepth, OneFactor, OneMinusSrcAlphaFactor, SrcAlphaFactor, } from 'three';
2
+ /**
3
+ * Setting transparent: true on material changes render order, these parameters imitate transparent blending
4
+ * without reordering layers
5
+ */
6
+ const blendingParams = {
7
+ blending: CustomBlending,
8
+ blendSrc: SrcAlphaFactor,
9
+ blendSrcAlpha: OneFactor,
10
+ blendDst: OneMinusSrcAlphaFactor,
11
+ blendDstAlpha: OneMinusSrcAlphaFactor,
12
+ };
13
+ /**
14
+ * Resolvable depth steps of camera-ward bias on every 2D fragment — `polygonOffsetUnits`, negated.
15
+ *
16
+ * The flat, face-on term of the decal offset: with no tilt there is no depth slope for `factor` to
17
+ * scale, and all that separates a label from the roof it sits on is how two different transform
18
+ * chains rounded the same plane. Each chain lands within a float ULP or two of the exact depth, and
19
+ * one ULP of view depth is worth up to two resolvable steps behind a near plane keeping 98%
20
+ * headroom — so a couple of ULPs of disagreement is about four steps, which is what this covers.
21
+ */
22
+ const FLAT_BIAS_UNITS = 4;
23
+ /**
24
+ * Material parameter defaults, shared between all types
25
+ */
26
+ export const sharedParams = {
27
+ ...blendingParams,
28
+ side: DoubleSide,
29
+ depthFunc: LessEqualDepth,
30
+ depthWrite: false,
31
+ // 2D content never writes depth, so the buffer only ever holds what 3D content put there — and a
32
+ // 2D fragment coplanar with a solid surface (a label on a booth roof) is compared against a depth
33
+ // some other triangle interpolated. A small bias toward the camera wins that comparison; it is
34
+ // inert while nothing writes depth.
35
+ //
36
+ // `factor` covers camera tilt: the hardware scales it by the polygon's own window-space depth
37
+ // slope, making the bias one pixel's worth of that slope at any pitch — clear of every
38
+ // slope-proportional interpolation difference, yet unable to climb through anything that occludes
39
+ // the surface by more than a pixel. The imperative renderer had to carry this term as a constant,
40
+ // `⌈tan(maxPolarAngle)⌉ = 12`: with every layer writing depth, coplanar layers were kept apart by
41
+ // per-layer `units` steps, and surfaces sharing a plane share a slope, so `factor` cancelled
42
+ // between them and only a worst-case constant could size the step. With a single bias level there
43
+ // is no step, the real slope replaces the ceiling, and nothing here derives from `MAX_PITCH`.
44
+ //
45
+ // `units` covers the flat, top-down case, where the slope is zero and `factor` contributes
46
+ // nothing — sized by `FLAT_BIAS_UNITS`.
47
+ polygonOffset: true,
48
+ polygonOffsetFactor: -1,
49
+ polygonOffsetUnits: -FLAT_BIAS_UNITS,
50
+ forceSinglePass: true,
51
+ };
@@ -0,0 +1,22 @@
1
+ import { type ModelDef } from '../../types/defs';
2
+ import { type InstanceProps } from '../containers';
3
+ import { type ModelContainer } from './model-container';
4
+ /** Props for {@link ModelInstance}. */
5
+ export type ModelInstanceProps = InstanceProps<ModelDef, ModelContainer>;
6
+ /**
7
+ * Keeps one model's instance current: claims a slot on mount, writes the def's placement, opacity
8
+ * and visibility on mount and after every `update(def)`, and releases the slot on unmount.
9
+ *
10
+ * Renders no scene node — the mesh belongs to the container. Nothing here can force a re-claim: the
11
+ * geometry is the model part's, shared by every def drawing it, so moving a model is an ordinary
12
+ * write and its slot survives. `url` is not updatable by contract, which is what makes that true: a
13
+ * different model is a different def, and a different def a different instance.
14
+ *
15
+ * Memoized for the same reason as `ShapeInstance`: a structural change hands the layer a fresh
16
+ * `defs` snapshot, so element-identity bailout never applies and only prop equality keeps unchanged
17
+ * siblings from re-rendering. Correctness never depends on it.
18
+ * @param props - The def and its container.
19
+ * @returns `null`; the instance lives in the mesh, not the scene graph.
20
+ */
21
+ export declare const ModelInstance: import("react").MemoExoticComponent<(props: ModelInstanceProps) => null>;
22
+ //# sourceMappingURL=ModelInstance.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { memo } from 'react';
2
+ import { useDefSync } from '../containers';
3
+ import { modelKindOf } from './model-container';
4
+ /**
5
+ * Keeps one model's instance current: claims a slot on mount, writes the def's placement, opacity
6
+ * and visibility on mount and after every `update(def)`, and releases the slot on unmount.
7
+ *
8
+ * Renders no scene node — the mesh belongs to the container. Nothing here can force a re-claim: the
9
+ * geometry is the model part's, shared by every def drawing it, so moving a model is an ordinary
10
+ * write and its slot survives. `url` is not updatable by contract, which is what makes that true: a
11
+ * different model is a different def, and a different def a different instance.
12
+ *
13
+ * Memoized for the same reason as `ShapeInstance`: a structural change hands the layer a fresh
14
+ * `defs` snapshot, so element-identity bailout never applies and only prop equality keeps unchanged
15
+ * siblings from re-rendering. Correctness never depends on it.
16
+ * @param props - The def and its container.
17
+ * @returns `null`; the instance lives in the mesh, not the scene graph.
18
+ */
19
+ export const ModelInstance = memo(function ModelInstance(props) {
20
+ const { def, container } = props;
21
+ // No per-instance resource: every def on this mesh draws the same merged geometry, so the slot is
22
+ // backed by nothing and nothing can invalidate it.
23
+ useDefSync(container, def, null, (slot) => {
24
+ container.place(slot, def);
25
+ container.setOpacity(slot, def);
26
+ // Checked on every sync, because a def leaves its batch's kind without re-rendering this layer:
27
+ // `opacity` crossing 1 reaches the instance through `update()` and stops there, so this is where
28
+ // a batch learns it is holding something whose material describes the wrong render list.
29
+ if (modelKindOf(def) !== container.kind)
30
+ container.reportKindMismatch();
31
+ });
32
+ return null;
33
+ });