@expofp/renderer 0.0.0-experimental.2d6c681

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 (280) hide show
  1. package/README.md +145 -0
  2. package/dist/Renderer.d.ts +102 -0
  3. package/dist/Renderer.js +96 -0
  4. package/dist/controls/CameraControls.d.ts +108 -0
  5. package/dist/controls/CameraControls.js +203 -0
  6. package/dist/controls/Controls.d.ts +53 -0
  7. package/dist/controls/Controls.js +54 -0
  8. package/dist/controls/camera-api.d.ts +226 -0
  9. package/dist/controls/camera-api.js +179 -0
  10. package/dist/controls/gestures.d.ts +71 -0
  11. package/dist/controls/gestures.js +98 -0
  12. package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
  13. package/dist/controls/handlers/DoubleTapZoom.js +82 -0
  14. package/dist/controls/handlers/Inertia.d.ts +60 -0
  15. package/dist/controls/handlers/Inertia.js +222 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +212 -0
  18. package/dist/controls/handlers/Pitch.d.ts +19 -0
  19. package/dist/controls/handlers/Pitch.js +200 -0
  20. package/dist/controls/handlers/Roll.d.ts +30 -0
  21. package/dist/controls/handlers/Roll.js +196 -0
  22. package/dist/controls/handlers/Zoom.d.ts +28 -0
  23. package/dist/controls/handlers/Zoom.js +34 -0
  24. package/dist/controls/hooks.d.ts +136 -0
  25. package/dist/controls/hooks.js +200 -0
  26. package/dist/controls/index.d.ts +9 -0
  27. package/dist/controls/index.js +7 -0
  28. package/dist/controls/zoom-to.d.ts +89 -0
  29. package/dist/controls/zoom-to.js +91 -0
  30. package/dist/core/InstanceUniforms.d.ts +108 -0
  31. package/dist/core/InstanceUniforms.js +298 -0
  32. package/dist/core/SquareDataTexture.d.ts +277 -0
  33. package/dist/core/SquareDataTexture.js +476 -0
  34. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  35. package/dist/core/batch/BatchedMesh.js +155 -0
  36. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  37. package/dist/core/instanced/FrustumCulling.js +246 -0
  38. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  39. package/dist/core/instanced/InstancedMesh.js +497 -0
  40. package/dist/core/instanced/Instances.d.ts +119 -0
  41. package/dist/core/instanced/Instances.js +230 -0
  42. package/dist/core/instanced/Raycasting.d.ts +118 -0
  43. package/dist/core/instanced/Raycasting.js +134 -0
  44. package/dist/core/instanced/instanced-material.d.ts +77 -0
  45. package/dist/core/instanced/instanced-material.js +228 -0
  46. package/dist/core/lines/BatchedLines.d.ts +137 -0
  47. package/dist/core/lines/BatchedLines.js +215 -0
  48. package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
  49. package/dist/core/lines/FrustumCullingLines.js +85 -0
  50. package/dist/core/lines/RaycastingLines.d.ts +45 -0
  51. package/dist/core/lines/RaycastingLines.js +91 -0
  52. package/dist/core/lines/lines-material.d.ts +47 -0
  53. package/dist/core/lines/lines-material.js +124 -0
  54. package/dist/core/shader-source.d.ts +70 -0
  55. package/dist/core/shader-source.js +110 -0
  56. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  57. package/dist/core/sprites/BatchedSprites.js +173 -0
  58. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  59. package/dist/core/sprites/RaycastingSprites.js +71 -0
  60. package/dist/core/sprites/sprite-material.d.ts +59 -0
  61. package/dist/core/sprites/sprite-material.js +192 -0
  62. package/dist/core/text/BatchedText.d.ts +233 -0
  63. package/dist/core/text/BatchedText.js +473 -0
  64. package/dist/core/text/MemberTexture.d.ts +90 -0
  65. package/dist/core/text/MemberTexture.js +289 -0
  66. package/dist/core/text/Text.d.ts +88 -0
  67. package/dist/core/text/Text.js +230 -0
  68. package/dist/core/text/batched-text-material.d.ts +56 -0
  69. package/dist/core/text/batched-text-material.js +123 -0
  70. package/dist/core/text/glyph-packing.d.ts +17 -0
  71. package/dist/core/text/glyph-packing.js +61 -0
  72. package/dist/core/typed-array.d.ts +16 -0
  73. package/dist/core/typed-array.js +20 -0
  74. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  75. package/dist/debug/components/DebugPanelProvider.js +50 -0
  76. package/dist/debug/components/DebugTools.d.ts +22 -0
  77. package/dist/debug/components/DebugTools.js +28 -0
  78. package/dist/debug/components/Gui.d.ts +22 -0
  79. package/dist/debug/components/Gui.js +42 -0
  80. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  81. package/dist/debug/components/MemoryInfo.js +42 -0
  82. package/dist/debug/components/Panel.d.ts +31 -0
  83. package/dist/debug/components/Panel.js +104 -0
  84. package/dist/debug/components/Stats.d.ts +9 -0
  85. package/dist/debug/components/Stats.js +24 -0
  86. package/dist/debug/debug-chunk.d.ts +26 -0
  87. package/dist/debug/debug-chunk.js +32 -0
  88. package/dist/debug/debug-context.d.ts +41 -0
  89. package/dist/debug/debug-context.js +59 -0
  90. package/dist/debug/index.d.ts +41 -0
  91. package/dist/debug/index.js +43 -0
  92. package/dist/events/LayoutDriver.d.ts +36 -0
  93. package/dist/events/LayoutDriver.js +192 -0
  94. package/dist/events/PointerMissed.d.ts +23 -0
  95. package/dist/events/PointerMissed.js +58 -0
  96. package/dist/events/def-culling.d.ts +46 -0
  97. package/dist/events/def-culling.js +69 -0
  98. package/dist/events/layout-targets.d.ts +151 -0
  99. package/dist/events/layout-targets.js +397 -0
  100. package/dist/events/placement/box.d.ts +59 -0
  101. package/dist/events/placement/box.js +96 -0
  102. package/dist/events/placement/grid.d.ts +161 -0
  103. package/dist/events/placement/grid.js +235 -0
  104. package/dist/events/placement/index.d.ts +2 -0
  105. package/dist/events/placement/index.js +1 -0
  106. package/dist/events/placement/pass.d.ts +70 -0
  107. package/dist/events/placement/pass.js +617 -0
  108. package/dist/events/pointer.d.ts +194 -0
  109. package/dist/events/pointer.js +356 -0
  110. package/dist/external/ExternalCanvas.d.ts +56 -0
  111. package/dist/external/ExternalCanvas.js +177 -0
  112. package/dist/external/external-camera.d.ts +18 -0
  113. package/dist/external/external-camera.js +103 -0
  114. package/dist/external/external-events.d.ts +14 -0
  115. package/dist/external/external-events.js +33 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/index.js +14 -0
  118. package/dist/objects/Background.d.ts +31 -0
  119. package/dist/objects/Background.js +37 -0
  120. package/dist/objects/containers/SlotContainer.d.ts +312 -0
  121. package/dist/objects/containers/SlotContainer.js +311 -0
  122. package/dist/objects/containers/hooks.d.ts +61 -0
  123. package/dist/objects/containers/hooks.js +196 -0
  124. package/dist/objects/containers/index.d.ts +6 -0
  125. package/dist/objects/containers/index.js +4 -0
  126. package/dist/objects/containers/keys.d.ts +10 -0
  127. package/dist/objects/containers/keys.js +18 -0
  128. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  129. package/dist/objects/containers/mesh-writes.js +97 -0
  130. package/dist/objects/containers/props.d.ts +33 -0
  131. package/dist/objects/containers/props.js +1 -0
  132. package/dist/objects/images/ImageInstance.d.ts +29 -0
  133. package/dist/objects/images/ImageInstance.js +152 -0
  134. package/dist/objects/images/Images.d.ts +49 -0
  135. package/dist/objects/images/Images.js +394 -0
  136. package/dist/objects/images/atlas-packer.d.ts +129 -0
  137. package/dist/objects/images/atlas-packer.js +287 -0
  138. package/dist/objects/images/image-container.d.ts +74 -0
  139. package/dist/objects/images/image-container.js +100 -0
  140. package/dist/objects/images/index.d.ts +2 -0
  141. package/dist/objects/images/index.js +1 -0
  142. package/dist/objects/images/pack-sources.d.ts +190 -0
  143. package/dist/objects/images/pack-sources.js +272 -0
  144. package/dist/objects/images/texture-budget.d.ts +120 -0
  145. package/dist/objects/images/texture-budget.js +215 -0
  146. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  147. package/dist/objects/images/worker/atlas-worker.js +123 -0
  148. package/dist/objects/images/worker/client.d.ts +62 -0
  149. package/dist/objects/images/worker/client.js +249 -0
  150. package/dist/objects/images/worker/index.d.ts +10 -0
  151. package/dist/objects/images/worker/index.js +1 -0
  152. package/dist/objects/layer/Layer.d.ts +28 -0
  153. package/dist/objects/layer/Layer.js +134 -0
  154. package/dist/objects/layer/index.d.ts +2 -0
  155. package/dist/objects/layer/index.js +1 -0
  156. package/dist/objects/layer/layer-context.d.ts +74 -0
  157. package/dist/objects/layer/layer-context.js +82 -0
  158. package/dist/objects/layer/mount-scheduler.d.ts +131 -0
  159. package/dist/objects/layer/mount-scheduler.js +289 -0
  160. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  161. package/dist/objects/layer/visibility-gui.js +93 -0
  162. package/dist/objects/lighting/Lighting.d.ts +92 -0
  163. package/dist/objects/lighting/Lighting.js +384 -0
  164. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  165. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  166. package/dist/objects/lighting/index.d.ts +11 -0
  167. package/dist/objects/lighting/index.js +10 -0
  168. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  169. package/dist/objects/lighting/lighting-gui.js +114 -0
  170. package/dist/objects/lighting/occluder.d.ts +34 -0
  171. package/dist/objects/lighting/occluder.js +62 -0
  172. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  173. package/dist/objects/lighting/occlusion-targets.js +276 -0
  174. package/dist/objects/lines/LineInstance.d.ts +21 -0
  175. package/dist/objects/lines/LineInstance.js +73 -0
  176. package/dist/objects/lines/Lines.d.ts +17 -0
  177. package/dist/objects/lines/Lines.js +31 -0
  178. package/dist/objects/lines/index.d.ts +2 -0
  179. package/dist/objects/lines/index.js +1 -0
  180. package/dist/objects/lines/line-container.d.ts +35 -0
  181. package/dist/objects/lines/line-container.js +40 -0
  182. package/dist/objects/materials/ColorMaterial.d.ts +92 -0
  183. package/dist/objects/materials/ColorMaterial.js +124 -0
  184. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  185. package/dist/objects/materials/LineMaterial.js +74 -0
  186. package/dist/objects/materials/ModelMaterial.d.ts +53 -0
  187. package/dist/objects/materials/ModelMaterial.js +111 -0
  188. package/dist/objects/materials/TextureMaterial.d.ts +33 -0
  189. package/dist/objects/materials/TextureMaterial.js +48 -0
  190. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  191. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  192. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  193. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  194. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  195. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  196. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  197. package/dist/objects/materials/effects/ao/index.js +72 -0
  198. package/dist/objects/materials/effects/color-dim.d.ts +115 -0
  199. package/dist/objects/materials/effects/color-dim.js +206 -0
  200. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  201. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  202. package/dist/objects/materials/effects/effects-context.d.ts +53 -0
  203. package/dist/objects/materials/effects/effects-context.js +24 -0
  204. package/dist/objects/materials/shared.d.ts +24 -0
  205. package/dist/objects/materials/shared.js +72 -0
  206. package/dist/objects/models/ModelInstance.d.ts +22 -0
  207. package/dist/objects/models/ModelInstance.js +33 -0
  208. package/dist/objects/models/Models.d.ts +22 -0
  209. package/dist/objects/models/Models.js +186 -0
  210. package/dist/objects/models/gltf-cache.d.ts +17 -0
  211. package/dist/objects/models/gltf-cache.js +50 -0
  212. package/dist/objects/models/index.d.ts +2 -0
  213. package/dist/objects/models/index.js +1 -0
  214. package/dist/objects/models/model-asset.d.ts +48 -0
  215. package/dist/objects/models/model-asset.js +77 -0
  216. package/dist/objects/models/model-container.d.ts +123 -0
  217. package/dist/objects/models/model-container.js +207 -0
  218. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  219. package/dist/objects/shapes/ShapeInstance.js +39 -0
  220. package/dist/objects/shapes/Shapes.d.ts +21 -0
  221. package/dist/objects/shapes/Shapes.js +131 -0
  222. package/dist/objects/shapes/index.d.ts +2 -0
  223. package/dist/objects/shapes/index.js +1 -0
  224. package/dist/objects/shapes/shape-container.d.ts +141 -0
  225. package/dist/objects/shapes/shape-container.js +242 -0
  226. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  227. package/dist/objects/shapes/shape-geometry.js +94 -0
  228. package/dist/objects/text/TextInstance.d.ts +22 -0
  229. package/dist/objects/text/TextInstance.js +95 -0
  230. package/dist/objects/text/Texts.d.ts +30 -0
  231. package/dist/objects/text/Texts.js +43 -0
  232. package/dist/objects/text/index.d.ts +2 -0
  233. package/dist/objects/text/index.js +1 -0
  234. package/dist/objects/text/text-container.d.ts +121 -0
  235. package/dist/objects/text/text-container.js +229 -0
  236. package/dist/objects/text/text-layout.d.ts +92 -0
  237. package/dist/objects/text/text-layout.js +180 -0
  238. package/dist/space/Camera.d.ts +103 -0
  239. package/dist/space/Camera.js +321 -0
  240. package/dist/space/Scene.d.ts +47 -0
  241. package/dist/space/Scene.js +127 -0
  242. package/dist/space/Space.d.ts +325 -0
  243. package/dist/space/Space.js +359 -0
  244. package/dist/space/VisibleRect.d.ts +45 -0
  245. package/dist/space/VisibleRect.js +129 -0
  246. package/dist/space/camera-handle.d.ts +67 -0
  247. package/dist/space/camera-handle.js +63 -0
  248. package/dist/space/camera-kind.d.ts +26 -0
  249. package/dist/space/camera-kind.js +30 -0
  250. package/dist/space/canvas-size.d.ts +24 -0
  251. package/dist/space/canvas-size.js +20 -0
  252. package/dist/space/clipping.d.ts +69 -0
  253. package/dist/space/clipping.js +130 -0
  254. package/dist/space/layout-measure.d.ts +67 -0
  255. package/dist/space/layout-measure.js +175 -0
  256. package/dist/space/viewport.d.ts +112 -0
  257. package/dist/space/viewport.js +67 -0
  258. package/dist/types/defs.d.ts +234 -0
  259. package/dist/types/defs.js +1 -0
  260. package/dist/types/events.d.ts +198 -0
  261. package/dist/types/events.js +1 -0
  262. package/dist/types/guards.d.ts +21 -0
  263. package/dist/types/guards.js +52 -0
  264. package/dist/update/index.d.ts +3 -0
  265. package/dist/update/index.js +2 -0
  266. package/dist/update/slot-writers.d.ts +30 -0
  267. package/dist/update/slot-writers.js +76 -0
  268. package/dist/update/store.d.ts +41 -0
  269. package/dist/update/store.js +104 -0
  270. package/dist/utils/arrays.d.ts +13 -0
  271. package/dist/utils/arrays.js +20 -0
  272. package/dist/utils/colors.d.ts +19 -0
  273. package/dist/utils/colors.js +27 -0
  274. package/dist/utils/hot-state.d.ts +41 -0
  275. package/dist/utils/hot-state.js +37 -0
  276. package/dist/utils/logging.d.ts +29 -0
  277. package/dist/utils/logging.js +55 -0
  278. package/dist/utils/math.d.ts +52 -0
  279. package/dist/utils/math.js +76 -0
  280. package/package.json +79 -0
@@ -0,0 +1,123 @@
1
+ import { openMain } from '../shader-source';
2
+ /**
3
+ * The material side of a text batch: what troika's derivation has to become before it can draw a
4
+ * batch's members, and where the batch's per-instance uniforms are declared.
5
+ *
6
+ * Troika builds that material in two moments with its own derivation between them, so the work
7
+ * splits the same way — {@link patchBaseMaterial} runs on the base material *before*
8
+ * `createDerivedMaterial`, {@link configureDerivedMaterial} on the result *after*. Each order is
9
+ * load-bearing; see the two.
10
+ *
11
+ * Free functions holding no state, as `instanced-material.ts` is. What is per-batch — which material
12
+ * has already been wrapped — stays on the batch.
13
+ */
14
+ /** The per-member index attribute troika's batch material declares, and the uniforms texture indexes by. */
15
+ export const MEMBER_INDEX_ATTRIBUTE = 'aTroikaTextBatchMemberIndex';
16
+ /**
17
+ * Wraps a base material's `onBeforeCompile` so a batch's per-instance uniforms are declared ahead of
18
+ * anything the material itself patches in.
19
+ *
20
+ * The prologue goes on the **base**, ahead of troika's rewrite: troika calls the base material's
21
+ * handler first and then rebuilds `main` around it, renaming the original `troikaOrigMain`.
22
+ * Injecting here puts the prologue and whatever the material patches in on the same side of that
23
+ * rename, so a material patch reading a per-instance uniform finds it in scope; injecting from the
24
+ * derived handler instead would strand the two in different functions.
25
+ *
26
+ * Call once per material — a second wrap would nest, and the caller holds the memo that prevents it.
27
+ * @param material - The base material troika derives from.
28
+ * @param uniforms - The batch's per-instance uniforms.
29
+ */
30
+ export function patchBaseMaterial(material, uniforms) {
31
+ const onBeforeCompileBase = material.onBeforeCompile.bind(material);
32
+ material.onBeforeCompile = (shader, renderer) => {
33
+ uniforms.patchShader(shader, MEMBER_INDEX_ATTRIBUTE);
34
+ onBeforeCompileBase(shader, renderer);
35
+ };
36
+ }
37
+ /**
38
+ * Finishes troika's derivation: reverts its two impositions on the base, and installs the one shader
39
+ * hook a batch needs.
40
+ * @param derived - What `createDerivedMaterial` returned.
41
+ * @param base - The material it was derived from.
42
+ * @param uniforms - The batch's per-instance uniforms, whose presence the program is cached by.
43
+ * @returns `derived`.
44
+ */
45
+ export function configureDerivedMaterial(derived, base, uniforms) {
46
+ // troika forces `transparent = true` on every derived text material, which moves the batch into
47
+ // three's *transparent* render list — drawn after every opaque object regardless of `renderOrder`,
48
+ // so text would always cover the layers above it. Our base materials get alpha blending from
49
+ // `CustomBlending` instead (objects/materials/shared.ts), so this restores what the base asked for.
50
+ // Everything else troika leaves alone already delegates to the base through the prototype chain.
51
+ derived.transparent = base.transparent;
52
+ // With `transparent` reverted the batch renders from three's *opaque* list, which
53
+ // `painterSortStable` orders by `material.id` — and troika builds its outline material with
54
+ // `id + 0.1`, so the preliminary outline pass would sort *after* the fill and paint over it. (Its
55
+ // own forced transparency hid this: the transparent list sorts by depth, with no `material.id`
56
+ // term.) Building it here, with a lower id, restores the intended order; troika's getter finds it
57
+ // already cached and uses it.
58
+ derived._outlineMtl = makeOutlineMaterial(derived);
59
+ // Recompile when the uniform schema changes, since that changes the generated GLSL. The whole
60
+ // schema, not just whether a texture exists: two batches declaring different uniforms compile
61
+ // different source, and three compares nothing but this key.
62
+ const cacheKeyBase = derived.customProgramCacheKey.bind(derived);
63
+ derived.customProgramCacheKey = () => `efpText_${uniforms.shaderKey}|${cacheKeyBase()}`;
64
+ // Assignment, *not* the read-bind-wrap pattern the sibling meshes use: troika defines
65
+ // `onBeforeCompile` as an accessor pair whose getter returns troika's own handler and whose setter
66
+ // stashes ours (troika-three-utils DerivedMaterial), so calling through the getter from inside our
67
+ // handler would re-enter troika's, which calls ours again. Troika invokes this one last, after its
68
+ // shader upgrade, which is the only thing that has to be true.
69
+ derived.onBeforeCompile = (shader) => {
70
+ // Late by necessity: troika's own `vertexTransform` *reads* this offset, and the transform runs
71
+ // from the main it builds, ahead of the base's. Initializing any earlier would leave the
72
+ // transform reading the garbage this exists to prevent.
73
+ shader.vertexShader = openMain(shader.vertexShader, POSITION_OFFSET_INIT);
74
+ };
75
+ return derived;
76
+ }
77
+ /**
78
+ * Whether a material draws through the batched text shader — the only thing a batch's per-draw
79
+ * texture fill asks of the material it is handed.
80
+ *
81
+ * `isTroikaTextMaterial`, which upstream tests instead, does not establish that. Troika sets it on
82
+ * the text derivation, which the batch's material only sits on top of — so a plain unbatched `Text`
83
+ * material carries the flag too, and filling one would write matrices its shader never reads. The
84
+ * uniforms are the honest test: a batch derives over a link that declares them, an unbatched `Text`
85
+ * never does, and every derivation merges its base's uniform map forward, so they survive into any
86
+ * sibling built by calling `material.constructor`.
87
+ * @param material - The material three is about to draw with.
88
+ * @returns Whether it reads its transforms from a matrix texture.
89
+ */
90
+ export function readsMemberMatrices(material) {
91
+ return !!material.uniforms?.uTroikaMatricesTexture;
92
+ }
93
+ /**
94
+ * Builds the outline pass's material: troika's own recipe, with the id inverted.
95
+ *
96
+ * Prototype-chained to the fill material so it shares every uniform and shader, exactly as troika
97
+ * does — the two differ only in `isTextOutlineMaterial`, which is what the per-draw fill branches on
98
+ * to write the outline row of the data texture.
99
+ * @param derived - The fill material to derive the outline pass from.
100
+ * @returns The outline material.
101
+ */
102
+ function makeOutlineMaterial(derived) {
103
+ const outline = Object.create(derived, {
104
+ // Below the fill, not above it: three's opaque sort is ascending by `material.id`, and the
105
+ // outline is a *preliminary* pass. A fraction keeps it clear of every integer material id.
106
+ id: { value: derived.id - 0.5 },
107
+ });
108
+ outline.isTextOutlineMaterial = true;
109
+ outline.depthWrite = false;
110
+ outline.map = null;
111
+ // troika disposes its own outline material with the fill; matched so replacing it leaks nothing.
112
+ derived.addEventListener('dispose', function onDispose() {
113
+ derived.removeEventListener('dispose', onDispose);
114
+ outline.dispose();
115
+ });
116
+ return outline;
117
+ }
118
+ /**
119
+ * Seeds the position-offset varying. An uninitialized varying reads as garbage on some Samsung/Mali
120
+ * drivers, and troika's `vertexTransform` only assigns this one in the outline branch — so the main
121
+ * pass would never write it at all.
122
+ */
123
+ const POSITION_OFFSET_INIT = /* glsl */ `uTroikaPositionOffset = vec2(0.0);`;
@@ -0,0 +1,17 @@
1
+ import { type GlyphsGeometry, type TextRenderInfo } from 'troika-three-text';
2
+ import { type TextImpl } from './Text';
3
+ /**
4
+ * Rewrites a batch's glyph attributes from its members' render info.
5
+ *
6
+ * Two orders meet here, which is the whole point of the pass: glyph runs are appended in *paint*
7
+ * order, since draw order is simply the order runs appear in the batched attributes, while the
8
+ * member index each run carries is the member’s stable *id*, which is the row it reads its matrix
9
+ * and uniforms from.
10
+ * @param geometry - The batch's glyph geometry, whose attributes are replaced.
11
+ * @param ordered - The ids to draw, in paint order.
12
+ * @param textAt - The member holding an id, or `undefined` when it holds none.
13
+ * @returns The render info the batch should adopt as its own — any member's will do, since a batch
14
+ * shares one SDF atlas — or `undefined` when no member has synced.
15
+ */
16
+ export declare function packGlyphs(geometry: GlyphsGeometry, ordered: readonly number[], textAt: (memberId: number) => TextImpl | undefined): TextRenderInfo | undefined;
17
+ //# sourceMappingURL=glyph-packing.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { DynamicDrawUsage } from 'three';
2
+ import { resizeTypedArray } from '../typed-array';
3
+ import { MEMBER_INDEX_ATTRIBUTE } from './batched-text-material';
4
+ /**
5
+ * Rewrites a batch's glyph attributes from its members' render info.
6
+ *
7
+ * Two orders meet here, which is the whole point of the pass: glyph runs are appended in *paint*
8
+ * order, since draw order is simply the order runs appear in the batched attributes, while the
9
+ * member index each run carries is the member’s stable *id*, which is the row it reads its matrix
10
+ * and uniforms from.
11
+ * @param geometry - The batch's glyph geometry, whose attributes are replaced.
12
+ * @param ordered - The ids to draw, in paint order.
13
+ * @param textAt - The member holding an id, or `undefined` when it holds none.
14
+ * @returns The render info the batch should adopt as its own — any member's will do, since a batch
15
+ * shares one SDF atlas — or `undefined` when no member has synced.
16
+ */
17
+ export function packGlyphs(geometry, ordered, textAt) {
18
+ const attributes = geometry.attributes;
19
+ let memberIndexes = attributes[MEMBER_INDEX_ATTRIBUTE]?.array ?? new Uint16Array(0);
20
+ let glyphIndexes = attributes[GLYPH_INDEX_ATTRIBUTE]?.array ?? new Float32Array(0);
21
+ let glyphBounds = attributes[GLYPH_BOUNDS_ATTRIBUTE]?.array ?? new Float32Array(0);
22
+ // Initial pass to collect total glyph count and resize the arrays if needed.
23
+ let totalGlyphCount = 0;
24
+ // The inherited `Text` code reads `textRenderInfo` for whole-batch state (the SDF atlas, its glyph
25
+ // size and exponent); any member's will do.
26
+ let batchRenderInfo;
27
+ for (const memberId of ordered) {
28
+ const renderInfo = textAt(memberId)?.textRenderInfo;
29
+ if (!renderInfo)
30
+ continue;
31
+ totalGlyphCount += renderInfo.glyphAtlasIndices.length;
32
+ batchRenderInfo = renderInfo;
33
+ }
34
+ if (totalGlyphCount !== memberIndexes.length) {
35
+ memberIndexes = resizeTypedArray(memberIndexes, totalGlyphCount);
36
+ glyphIndexes = resizeTypedArray(glyphIndexes, totalGlyphCount);
37
+ glyphBounds = resizeTypedArray(glyphBounds, totalGlyphCount * 4);
38
+ }
39
+ // Populate batch arrays.
40
+ let glyphIndex = 0;
41
+ for (const memberId of ordered) {
42
+ const renderInfo = textAt(memberId)?.textRenderInfo;
43
+ if (!renderInfo)
44
+ continue;
45
+ const glyphCount = renderInfo.glyphAtlasIndices.length;
46
+ memberIndexes.fill(memberId, glyphIndex, glyphIndex + glyphCount);
47
+ glyphIndexes.set(renderInfo.glyphAtlasIndices, glyphIndex);
48
+ glyphBounds.set(renderInfo.glyphBounds, glyphIndex * 4);
49
+ glyphIndex += glyphCount;
50
+ }
51
+ // Update the geometry attributes.
52
+ geometry.updateAttributeData(MEMBER_INDEX_ATTRIBUTE, memberIndexes, 1);
53
+ geometry.getAttribute(MEMBER_INDEX_ATTRIBUTE).setUsage(DynamicDrawUsage);
54
+ geometry.updateAttributeData(GLYPH_INDEX_ATTRIBUTE, glyphIndexes, 1);
55
+ geometry.updateAttributeData(GLYPH_BOUNDS_ATTRIBUTE, glyphBounds, 4);
56
+ return batchRenderInfo;
57
+ }
58
+ /** One SDF atlas index per glyph. */
59
+ const GLYPH_INDEX_ATTRIBUTE = 'aTroikaGlyphIndex';
60
+ /** Four floats per glyph, bounding the quad it is drawn on. */
61
+ const GLYPH_BOUNDS_ATTRIBUTE = 'aTroikaGlyphBounds';
@@ -0,0 +1,16 @@
1
+ import { type TypedArray } from 'three';
2
+ /**
3
+ * Copies a typed array into a fresh one of a different length, keeping as much of the contents as
4
+ * fits: growing zero-fills the tail, shrinking drops it.
5
+ *
6
+ * The element type is taken from the source's own constructor rather than a parameter, so a caller
7
+ * resizing a `Uint16Array` of member indexes and one resizing a `Float32Array` of glyph bounds share
8
+ * the same call. Always a new array — an in-place `ArrayBuffer.resize` would not do here, because
9
+ * both callers hand the result to something that captured the old buffer (three's `BufferAttribute`,
10
+ * or a `DataTexture`'s image) and must see a distinct one to notice the change.
11
+ * @param source - The array to copy.
12
+ * @param length - The new array's length, in elements.
13
+ * @returns The resized copy.
14
+ */
15
+ export declare function resizeTypedArray<T extends TypedArray>(source: T, length: number): T;
16
+ //# sourceMappingURL=typed-array.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copies a typed array into a fresh one of a different length, keeping as much of the contents as
3
+ * fits: growing zero-fills the tail, shrinking drops it.
4
+ *
5
+ * The element type is taken from the source's own constructor rather than a parameter, so a caller
6
+ * resizing a `Uint16Array` of member indexes and one resizing a `Float32Array` of glyph bounds share
7
+ * the same call. Always a new array — an in-place `ArrayBuffer.resize` would not do here, because
8
+ * both callers hand the result to something that captured the old buffer (three's `BufferAttribute`,
9
+ * or a `DataTexture`'s image) and must see a distinct one to notice the change.
10
+ * @param source - The array to copy.
11
+ * @param length - The new array's length, in elements.
12
+ * @returns The resized copy.
13
+ */
14
+ export function resizeTypedArray(source, length) {
15
+ const constructor = source.constructor;
16
+ const resized = new constructor(length);
17
+ // `subarray` clamps to the source's own length, so this is also the growing case.
18
+ resized.set(source.subarray(0, length));
19
+ return resized;
20
+ }
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Props for {@link DebugPanelProvider}. */
3
+ export interface DebugPanelProviderProps {
4
+ /** The scene to publish from. */
5
+ children: ReactNode;
6
+ }
7
+ /**
8
+ * Owns the debug panel's entries and the write API that fills them.
9
+ *
10
+ * Deliberately *inside* the `<Canvas>`. R3F re-runs its configuration on every render of the canvas
11
+ * element, and that path replaces the `size` object in its store even when the dimensions have not
12
+ * changed — so state held above the canvas and written once per frame, as a camera publishing its
13
+ * position is, hands every `useThree(state => state.size)` subscriber a new object every frame. Held
14
+ * here, a push re-renders this component and the panel below it and nothing else: `children` arrives
15
+ * as an unchanged element, so React skips the scene entirely.
16
+ * @param props - The scene to publish from.
17
+ * @returns The scene, under the panel's write API, with the panel beside it.
18
+ */
19
+ export declare function DebugPanelProvider(props: DebugPanelProviderProps): ReactNode;
20
+ //# sourceMappingURL=DebugPanelProvider.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { DebugPanelContext } from '../debug-context';
4
+ import { Panel } from './Panel';
5
+ /**
6
+ * Owns the debug panel's entries and the write API that fills them.
7
+ *
8
+ * Deliberately *inside* the `<Canvas>`. R3F re-runs its configuration on every render of the canvas
9
+ * element, and that path replaces the `size` object in its store even when the dimensions have not
10
+ * changed — so state held above the canvas and written once per frame, as a camera publishing its
11
+ * position is, hands every `useThree(state => state.size)` subscriber a new object every frame. Held
12
+ * here, a push re-renders this component and the panel below it and nothing else: `children` arrives
13
+ * as an unchanged element, so React skips the scene entirely.
14
+ * @param props - The scene to publish from.
15
+ * @returns The scene, under the panel's write API, with the panel beside it.
16
+ */
17
+ export function DebugPanelProvider(props) {
18
+ const { children } = props;
19
+ const [entries, setEntries] = useState({});
20
+ // Not memoized by hand: the React Compiler keeps this object stable, so DebugPanelContext
21
+ // consumers don't re-render when `entries` changes. Both updaters take the functional form, so
22
+ // nothing here closes over the entries themselves.
23
+ const panel = {
24
+ set(key, value) {
25
+ setEntries((prev) => {
26
+ // An existing key keeps the slot it already holds, because the spread writes it in place.
27
+ if (key in prev)
28
+ return Object.is(prev[key], value) ? prev : { ...prev, [key]: value };
29
+ // A key arrives for the first time, so rebuild the object in name order. Insertion order is
30
+ // what the readout prints, and a key that a publisher removes on unmount would otherwise
31
+ // come back at the bottom. `Object.fromEntries` needs Chrome 73, so build the object here.
32
+ const next = {};
33
+ for (const name of [...Object.keys(prev), key].sort()) {
34
+ next[name] = name === key ? value : prev[name];
35
+ }
36
+ return next;
37
+ });
38
+ },
39
+ remove(key) {
40
+ setEntries((prev) => {
41
+ if (!(key in prev))
42
+ return prev;
43
+ const next = { ...prev };
44
+ delete next[key];
45
+ return next;
46
+ });
47
+ },
48
+ };
49
+ return (_jsxs(DebugPanelContext, { value: panel, children: [children, _jsx(Panel, { corner: "right", label: "Debug", value: entries })] }));
50
+ }
@@ -0,0 +1,22 @@
1
+ import 'webgl-memory';
2
+ import { type ReactNode } from 'react';
3
+ /** Props for {@link DebugTools}. */
4
+ export interface DebugToolsProps {
5
+ /** The scene to instrument. */
6
+ children?: ReactNode;
7
+ }
8
+ /**
9
+ * The debug overlay, canvas-free — the heavy, lazily-loaded chunk. Wraps the scene in the debug
10
+ * GUI (shared through context so scene components can add folders) and the debug-panel write API
11
+ * (so scene components can publish values via `useDebugPanel`), plus the frame-stats, GPU-memory,
12
+ * and combined debug-values panels. All of it attaches to the document imperatively, outside the
13
+ * R3F reconciler, which is what lets the same component serve both mounts: inside the renderer's
14
+ * own `<Canvas>` and inside an external host's root alike.
15
+ *
16
+ * lil-gui, stats-gl and webgl-memory are reachable only through here, so this module is loaded
17
+ * exclusively via the debug entry's dynamic import and never lands in the main bundle.
18
+ * @param props - The scene to instrument.
19
+ * @returns The scene wrapped in the debug overlay.
20
+ */
21
+ export declare function DebugTools(props: DebugToolsProps): ReactNode;
22
+ //# sourceMappingURL=DebugTools.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Patches `HTMLCanvasElement.getContext` during chunk eval, so any context created after this
3
+ // chunk loads is born instrumented. The internal mount guarantees the ordering by suspending its
4
+ // Canvas behind this chunk (see `CanvasWithDebug`); an external host has to arrange it itself by
5
+ // awaiting `prepareDebug()` before creating its map — its context predates this code otherwise,
6
+ // and the memory panel stays empty.
7
+ import 'webgl-memory';
8
+ import { DebugPanelProvider } from './DebugPanelProvider';
9
+ import { Gui } from './Gui';
10
+ import { MemoryInfo } from './MemoryInfo';
11
+ import { Stats } from './Stats';
12
+ /**
13
+ * The debug overlay, canvas-free — the heavy, lazily-loaded chunk. Wraps the scene in the debug
14
+ * GUI (shared through context so scene components can add folders) and the debug-panel write API
15
+ * (so scene components can publish values via `useDebugPanel`), plus the frame-stats, GPU-memory,
16
+ * and combined debug-values panels. All of it attaches to the document imperatively, outside the
17
+ * R3F reconciler, which is what lets the same component serve both mounts: inside the renderer's
18
+ * own `<Canvas>` and inside an external host's root alike.
19
+ *
20
+ * lil-gui, stats-gl and webgl-memory are reachable only through here, so this module is loaded
21
+ * exclusively via the debug entry's dynamic import and never lands in the main bundle.
22
+ * @param props - The scene to instrument.
23
+ * @returns The scene wrapped in the debug overlay.
24
+ */
25
+ export function DebugTools(props) {
26
+ const { children } = props;
27
+ return (_jsx(Gui, { children: _jsxs(DebugPanelProvider, { children: [children, _jsx(Stats, {}), _jsx(MemoryInfo, {})] }) }));
28
+ }
@@ -0,0 +1,22 @@
1
+ import './Gui.module.css';
2
+ import { type PropsWithChildren } from 'react';
3
+ /**
4
+ * Initializes a GUI panel and shares it with the scene through context.
5
+ *
6
+ * The panel is created during render (with `autoPlace: false`, so the
7
+ * constructor has no DOM side effect), which makes it available to the scene
8
+ * synchronously on first render — no `null` phase to guard. The effect only
9
+ * attaches its `domElement` to the document and, on cleanup, calls `destroy()`,
10
+ * which detaches the element and clears child folders without invalidating the
11
+ * instance or its element. That keeps it StrictMode-safe: after the dev-only
12
+ * unmount/remount, the same element is re-attached and the scene's own effects
13
+ * re-add their folders. Rendered as an ancestor of the scene, so on unmount
14
+ * React tears the scene's folders down before this component destroys the panel.
15
+ * It also registers the panel's single change handler, which asks for a redraw. Every folder in the
16
+ * panel inherits it, so no controller carries one of its own.
17
+ * @param props - Component props; `children` is the scene subtree that consumes
18
+ * the GUI via `useGui`.
19
+ * @returns The children wrapped with the GUI provided through context.
20
+ */
21
+ export declare function Gui(props: PropsWithChildren): import("react").JSX.Element;
22
+ //# sourceMappingURL=Gui.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import './Gui.module.css'; // Global overrides of lil-gui's stock styling (checkbox row layout).
3
+ import { useThree } from '@react-three/fiber';
4
+ import { GUI } from 'lil-gui';
5
+ import { useEffect, useState } from 'react';
6
+ import { GuiContext } from '../debug-context';
7
+ /**
8
+ * Initializes a GUI panel and shares it with the scene through context.
9
+ *
10
+ * The panel is created during render (with `autoPlace: false`, so the
11
+ * constructor has no DOM side effect), which makes it available to the scene
12
+ * synchronously on first render — no `null` phase to guard. The effect only
13
+ * attaches its `domElement` to the document and, on cleanup, calls `destroy()`,
14
+ * which detaches the element and clears child folders without invalidating the
15
+ * instance or its element. That keeps it StrictMode-safe: after the dev-only
16
+ * unmount/remount, the same element is re-attached and the scene's own effects
17
+ * re-add their folders. Rendered as an ancestor of the scene, so on unmount
18
+ * React tears the scene's folders down before this component destroys the panel.
19
+ * It also registers the panel's single change handler, which asks for a redraw. Every folder in the
20
+ * panel inherits it, so no controller carries one of its own.
21
+ * @param props - Component props; `children` is the scene subtree that consumes
22
+ * the GUI via `useGui`.
23
+ * @returns The children wrapped with the GUI provided through context.
24
+ */
25
+ export function Gui(props) {
26
+ const { children } = props;
27
+ const [gui] = useState(() => new GUI({ autoPlace: false, closeFolders: true }));
28
+ const invalidate = useThree((state) => state.invalidate);
29
+ useEffect(() => {
30
+ gui.domElement.classList.add('lil-auto-place', 'autoPlace');
31
+ document.body.appendChild(gui.domElement);
32
+ gui.close();
33
+ // One handler for the whole panel: lil-gui walks a changed controller up its parent chain, so
34
+ // this fires for every controller in every folder and no folder registers its own. It calls
35
+ // whichever `invalidate` the current mount installed — in the external embed `ExternalCanvas`
36
+ // replaces it with the host's `requestFrame`, which is how a GUI change reaches a host that owns
37
+ // the frame.
38
+ gui.onChange(() => invalidate());
39
+ return () => gui.destroy();
40
+ }, [gui, invalidate]);
41
+ return _jsx(GuiContext, { value: gui, children: children });
42
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * GPU-memory debug panel backed by webgl-memory, shown bottom-left.
3
+ *
4
+ * The component lives inside the R3F `<Canvas>` tree so it can reach the
5
+ * renderer via `useThree`. The figures come from the `GMAN_webgl_memory` context
6
+ * extension that webgl-memory installs (see the debug entry, which loads it
7
+ * before the canvas context is created); this component only reads it, never
8
+ * imports the library.
9
+ *
10
+ * webgl-memory exposes no change event, so it samples the extension after every
11
+ * rendered frame and hands the snapshot to {@link Panel}. Without the
12
+ * extension — a context created before webgl-memory could instrument it —
13
+ * nothing ever fills the panel, and it stays visibly empty on purpose: a wrong
14
+ * readout is diagnosable where a missing one is not, and `prepareDebug` is what
15
+ * keeps that state out of practice. `getMemoryInfo` is
16
+ * cheap, and the panel skips its DOM update whenever the serialized readout is
17
+ * unchanged, so sampling unconditionally (rather than only while the panel is
18
+ * open) costs nothing visible.
19
+ * @returns The bottom-left GPU-memory panel.
20
+ */
21
+ export declare function MemoryInfo(): import("react").JSX.Element;
22
+ //# sourceMappingURL=MemoryInfo.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { addAfterEffect, useThree } from '@react-three/fiber';
3
+ import { useEffect, useState } from 'react';
4
+ import { Panel } from './Panel';
5
+ /**
6
+ * GPU-memory debug panel backed by webgl-memory, shown bottom-left.
7
+ *
8
+ * The component lives inside the R3F `<Canvas>` tree so it can reach the
9
+ * renderer via `useThree`. The figures come from the `GMAN_webgl_memory` context
10
+ * extension that webgl-memory installs (see the debug entry, which loads it
11
+ * before the canvas context is created); this component only reads it, never
12
+ * imports the library.
13
+ *
14
+ * webgl-memory exposes no change event, so it samples the extension after every
15
+ * rendered frame and hands the snapshot to {@link Panel}. Without the
16
+ * extension — a context created before webgl-memory could instrument it —
17
+ * nothing ever fills the panel, and it stays visibly empty on purpose: a wrong
18
+ * readout is diagnosable where a missing one is not, and `prepareDebug` is what
19
+ * keeps that state out of practice. `getMemoryInfo` is
20
+ * cheap, and the panel skips its DOM update whenever the serialized readout is
21
+ * unchanged, so sampling unconditionally (rather than only while the panel is
22
+ * open) costs nothing visible.
23
+ * @returns The bottom-left GPU-memory panel.
24
+ */
25
+ export function MemoryInfo() {
26
+ const gl = useThree((state) => state.gl);
27
+ const [memoryInfo, setMemoryInfo] = useState(null);
28
+ useEffect(() => {
29
+ const ext = gl.getContext().getExtension('GMAN_webgl_memory');
30
+ if (!ext)
31
+ return;
32
+ return addAfterEffect(() => {
33
+ setMemoryInfo({ ...ext.getMemoryInfo(), drawCalls: gl.info.render.calls });
34
+ });
35
+ }, [gl]);
36
+ return _jsx(Panel, { corner: "left", label: "Memory Info", value: memoryInfo, replacer: localizeNumbers });
37
+ }
38
+ // `JSON.stringify` replacer: render numbers with locale thousands separators
39
+ // (e.g. `1,048,576`).
40
+ function localizeNumbers(_key, value) {
41
+ return typeof value === 'number' ? value.toLocaleString() : value;
42
+ }
@@ -0,0 +1,31 @@
1
+ /** Props for {@link Panel}. */
2
+ export interface PanelProps {
3
+ /** Which bottom corner the panel is pinned to. */
4
+ corner: 'left' | 'right';
5
+ /** Shown on the toggle button after "Show"/"Hide". */
6
+ label: string;
7
+ /** Value serialized to JSON and shown while the panel is open. */
8
+ value: unknown;
9
+ /** Optional `JSON.stringify` replacer, for custom formatting of the value. */
10
+ replacer?: (key: string, value: unknown) => unknown;
11
+ }
12
+ /**
13
+ * A collapsible debug readout pinned to a bottom corner, rendered as regular DOM
14
+ * outside the R3F reconciler. The reusable chrome extracted from the GPU-memory
15
+ * panel: a caller only supplies a corner, a label, and the `value` to display.
16
+ *
17
+ * A component inside the `<Canvas>` tree cannot return DOM — R3F's reconciler
18
+ * only understands Three.js objects, so a returned `<div>` (even via
19
+ * `createPortal`) never becomes real DOM. So, like drei's `<Html>`, it renders
20
+ * into a plain element created with `document.createElement` using react-dom's
21
+ * own root, and returns `null` to R3F.
22
+ *
23
+ * The interactive open state and serialization live in {@link PanelView}, inside
24
+ * that react-dom root, so toggling, style HMR, and value changes reconcile there
25
+ * without depending on the parent (or R3F) re-running an effect. This component
26
+ * only pushes the latest props into the root whenever they change.
27
+ * @param props - Corner, toggle label, and the value to display.
28
+ * @returns `null`.
29
+ */
30
+ export declare function Panel(props: PanelProps): null;
31
+ //# sourceMappingURL=Panel.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import styles from './Panel.module.css';
5
+ /**
6
+ * The panel's DOM, rendered *inside* the detached react-dom root. The open state
7
+ * and the serialization live here — not on the parent {@link Panel} — so the
8
+ * toggle click, HMR of these styles, and value changes all reconcile as an
9
+ * ordinary react-dom subtree. If the parent diffed the markup by hand instead
10
+ * (keyed on a fixed dep list), anything outside that list — opening the panel,
11
+ * editing this CSS module — wouldn't show until the next value push happened to
12
+ * re-run it.
13
+ * @param props - Corner, toggle label, value to display, and optional replacer.
14
+ * @returns The panel element.
15
+ */
16
+ function PanelView(props) {
17
+ const { corner, label, value } = props;
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ function replacer(key, value) {
20
+ if (Array.isArray(value))
21
+ return `[${value.join(', ')}]`;
22
+ if (props.replacer)
23
+ return props.replacer(key, value);
24
+ return value;
25
+ }
26
+ function serialize() {
27
+ // Serialize only while open — the readout is hidden otherwise.
28
+ if (!isOpen)
29
+ return '';
30
+ // A single-key wrapper (e.g. `{ camera: {…} }`) is unwrapped to its value, so
31
+ // the readout shows the payload directly rather than one level of nesting.
32
+ let valueToSerialize = value;
33
+ if (typeof value === 'object' && value) {
34
+ const keys = Object.keys(value);
35
+ valueToSerialize = keys.length === 1 ? value[keys[0]] : value;
36
+ }
37
+ // `JSON.stringify` returns `undefined`, not a string, for a value that JSON cannot hold: the
38
+ // value `undefined` itself, or a function. The unwrap above exposes such a value, because a
39
+ // lone entry becomes the whole value. A wrapper of two entries hides it, because
40
+ // `JSON.stringify` drops the key. An empty string keeps the readout blank, so a publisher can
41
+ // push any value and the panel still renders.
42
+ const json = JSON.stringify(valueToSerialize, replacer, 2) ?? '';
43
+ return json.replaceAll('"', '');
44
+ }
45
+ return (_jsxs("div", { className: `${styles.container} ${styles[corner]}`, children: [_jsxs("button", { className: styles.toggle, onClick: () => setIsOpen((open) => !open), children: [isOpen ? 'Hide' : 'Show', " ", label] }), isOpen && _jsx("div", { className: styles.info, children: serialize() })] }));
46
+ }
47
+ /**
48
+ * A collapsible debug readout pinned to a bottom corner, rendered as regular DOM
49
+ * outside the R3F reconciler. The reusable chrome extracted from the GPU-memory
50
+ * panel: a caller only supplies a corner, a label, and the `value` to display.
51
+ *
52
+ * A component inside the `<Canvas>` tree cannot return DOM — R3F's reconciler
53
+ * only understands Three.js objects, so a returned `<div>` (even via
54
+ * `createPortal`) never becomes real DOM. So, like drei's `<Html>`, it renders
55
+ * into a plain element created with `document.createElement` using react-dom's
56
+ * own root, and returns `null` to R3F.
57
+ *
58
+ * The interactive open state and serialization live in {@link PanelView}, inside
59
+ * that react-dom root, so toggling, style HMR, and value changes reconcile there
60
+ * without depending on the parent (or R3F) re-running an effect. This component
61
+ * only pushes the latest props into the root whenever they change.
62
+ * @param props - Corner, toggle label, and the value to display.
63
+ * @returns `null`.
64
+ */
65
+ export function Panel(props) {
66
+ const { corner, label, value, replacer } = props;
67
+ // Created imperatively, so R3F's reconciler never sees these DOM elements.
68
+ const [container] = useState(() => document.createElement('div'));
69
+ const rootRef = useRef(null);
70
+ const pendingUnmountRef = useRef(false);
71
+ // Mount the container and spin up a react-dom root once.
72
+ //
73
+ // Unmounting a nested root synchronously during the parent's commit warns
74
+ // "unmount while rendering", so the unmount is deferred to a microtask
75
+ // (Promise, not queueMicrotask — old WebViews lack it). That deferral has to
76
+ // be cancellable: under StrictMode the effect runs mount → unmount → mount,
77
+ // and the queued unmount can fire *after* the remount has already reused this
78
+ // root (react-dom flushes the microtask late, R3F's reconciler earlier). Left
79
+ // unguarded, the stale microtask would tear down the live panel. Every
80
+ // (re)mount clears `pendingUnmountRef`, so a superseded unmount skips itself;
81
+ // only a real unmount — with no remount to clear the flag — destroys the root.
82
+ useEffect(() => {
83
+ pendingUnmountRef.current = false;
84
+ document.body.appendChild(container);
85
+ const root = rootRef.current ?? createRoot(container);
86
+ rootRef.current = root;
87
+ return () => {
88
+ container.remove();
89
+ pendingUnmountRef.current = true;
90
+ void Promise.resolve().then(() => {
91
+ if (!pendingUnmountRef.current)
92
+ return;
93
+ rootRef.current?.unmount();
94
+ rootRef.current = null;
95
+ });
96
+ };
97
+ }, [container]);
98
+ // Push the latest props into the detached root. PanelView reconciles as its own
99
+ // react-dom subtree, keeping its open state across these updates.
100
+ useEffect(() => {
101
+ rootRef.current?.render(_jsx(PanelView, { corner: corner, label: label, value: value, replacer: replacer }));
102
+ }, [corner, label, value, replacer]);
103
+ return null;
104
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Performance-stats overlay (frame rate, GPU timing, refresh rate) attached to
3
+ * the document body and refreshed after every rendered frame.
4
+ *
5
+ * The overlay is mounted imperatively, so the component renders nothing.
6
+ * @returns `null`.
7
+ */
8
+ export declare function Stats(): null;
9
+ //# sourceMappingURL=Stats.d.ts.map