@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,242 @@
1
+ import { isMesh } from '@expofp/geometry';
2
+ import { isTranslucentColor } from '../../utils/colors';
3
+ import { SlotContainer } from '../containers';
4
+ import { shapeGeometry } from './shape-geometry';
5
+ /**
6
+ * Every kind of shape batch, **in the order they draw within a layer** — which is what `Shapes`
7
+ * takes each batch's `renderOrder` from, so this list is the one place that order is stated.
8
+ *
9
+ * Solids draw after the flat shapes, so a volume occludes the flat content of its own layer rather
10
+ * than being painted over by it — the same rule that governs whole layers, applied within one.
11
+ */
12
+ export const SHAPE_KINDS = ['planar', 'solid', 'translucent'];
13
+ /**
14
+ * The batch a def belongs in.
15
+ *
16
+ * A shape is **solid** exactly when it is a `Mesh` whose vertices span more than one z — elevation is
17
+ * placement, not volume, so a quad or a lifted-but-flat mesh stays planar and is placed correctly by
18
+ * draw order alone. A solid is **translucent** when its color carries an alpha below 1. That has to
19
+ * be a partition rather than a per-instance flag because `transparent` is a *material* property:
20
+ * three decides which render list an object belongs to, never which instance, so two solids of
21
+ * differing transparency cannot share a mesh.
22
+ *
23
+ * **Only volumes split on translucency.** A flat shape with an alpha below 1 stays `planar`, because
24
+ * flat content already blends without leaving the opaque list: `sharedParams` reaches alpha blending
25
+ * through `CustomBlending` rather than through `transparent: true`, which three honors either way,
26
+ * and nothing planar writes depth. So array order is the whole of it — a translucent flat shape
27
+ * blends over whatever precedes it, and an opaque one authored later paints straight over it. That
28
+ * last part is a capability, not an accident: ordering an opaque fill after a translucent one is how
29
+ * a caller occludes it, and a batch of its own would take that away by forcing it after every opaque
30
+ * object in the scene, where nothing in its layer could cover it again.
31
+ *
32
+ * A volume has no such freedom. It writes depth and is depth-tested, so what ends up on top is the
33
+ * depth buffer's answer rather than the author's, and blending correctly means being drawn after the
34
+ * opaque content it blends with — which is a render pass, and therefore a batch.
35
+ *
36
+ * One function rather than a predicate in `Shapes` that places a def and another in `ShapeInstance`
37
+ * that notices it no longer belongs. Those are the same question, and two expressions of it can
38
+ * drift — a def placed by one rule and checked by the other would either sit in the wrong batch or
39
+ * ask to be re-split forever. A planar shape returns before the color is looked at, so the parse
40
+ * costs nothing on the many layers holding only flat content.
41
+ * @param def - The def to classify.
42
+ * @returns The kind of batch it draws from.
43
+ */
44
+ export function shapeKindOf(def) {
45
+ if (!isMesh(def.shape) || def.shape.isPlanar)
46
+ return 'planar';
47
+ return isTranslucentColor(def.color) ? 'translucent' : 'solid';
48
+ }
49
+ /**
50
+ * One shape layer's mesh bookkeeping: reserved buffer space, slot allocation, which geometries the
51
+ * mesh has registered, and the paint-order table.
52
+ *
53
+ * Slot allocation is the mesh's own — `addInstance`/`deleteInstance` maintain its free list — so
54
+ * what this adds is what the mesh will not track: how much vertex and index space is reserved, and
55
+ * which of its geometry ids each template was given. Building the templates is not its work either;
56
+ * they come from {@link shapeGeometry}, which caches them across every container.
57
+ */
58
+ export class ShapeContainer extends SlotContainer {
59
+ /**
60
+ * The mesh's constructor args, sized to hold the defs the container was created with exactly.
61
+ * Passed as `<batchedMesh args={container.capacity}>`; R3F diffs `args` element-wise and a change
62
+ * disposes and rebuilds the mesh, orphaning every claimed slot — so this is a snapshot taken at
63
+ * creation, which growth never mutates.
64
+ */
65
+ capacity;
66
+ /**
67
+ * Which kind of member this container holds, and so what its mesh is laid out for: a volume batch
68
+ * carries creased normals for the lit material and is non-indexed because that is what
69
+ * `toCreasedNormals` produces, and a translucent one draws in three's transparent pass. A member
70
+ * whose color stops matching is in the wrong pass, which is what {@link reportKindMismatch} exists
71
+ * to correct.
72
+ */
73
+ kind;
74
+ /**
75
+ * Which id the mesh gave each geometry it has registered. Per container even though the templates
76
+ * behind them are shared, because an id is the mesh's own numbering into its own buffers.
77
+ */
78
+ geometryIds = new Map();
79
+ /** What is allocated now, which growth mutates — unlike the {@link capacity} snapshot. */
80
+ allocated;
81
+ /**
82
+ * What the next growth jumps to, refreshed by {@link reserve}. Kept apart from {@link allocated}:
83
+ * a reservation must not resize anything by itself, only size the resize an overflowing claim was
84
+ * going to do anyway.
85
+ */
86
+ target;
87
+ /**
88
+ * Builds a shape layer's container, sized to hold `defs` exactly so a layer that mounts full never
89
+ * reallocates. The mesh itself is R3F's: the component renders `<batchedMesh>` from
90
+ * {@link capacity} and hands it over through {@link SlotContainer.bind}, and R3F disposes it on
91
+ * unmount. Nothing else needs disposal — `addGeometry` copies the template geometries' data into
92
+ * the mesh, so the templates are never uploaded to the GPU. They are not this container's to free
93
+ * in any case: {@link shapeGeometry} shares them across every container, and each is collected
94
+ * with the shape that keyed it.
95
+ * @param defs - The layer's shapes, used only to size the initial capacity.
96
+ * @param kind - Which of the layer's batches this is. A batched mesh mirrors its first geometry's
97
+ * attributes *and* its index-ness, and three rejects any later geometry that disagrees — so this
98
+ * decides the layout for every geometry in it, the shared unit plane included. It also decides
99
+ * the render pass its members belong to, and therefore which colors are at home in it.
100
+ */
101
+ constructor(defs, kind = 'planar') {
102
+ super();
103
+ this.kind = kind;
104
+ this.allocated = measureCapacity(defs, (shape) => this.geometryFor(shape));
105
+ // A snapshot, not a live view: growth mutates `allocated.vertices`/`indices`, and leaking that
106
+ // into the mounted element's `args` would make R3F rebuild the mesh.
107
+ this.capacity = [this.allocated.instances, this.allocated.vertices, this.allocated.indices];
108
+ this.target = {
109
+ instances: this.allocated.instances,
110
+ vertices: this.allocated.vertices,
111
+ indices: this.allocated.indices,
112
+ };
113
+ }
114
+ /**
115
+ * The geometry a shape draws with, in this container's layout. Resolved through the shared cache,
116
+ * so a shape drawn by two containers — the same volume in the opaque and the translucent batch —
117
+ * is built once and handed to both.
118
+ * @param shape - The shape to resolve.
119
+ * @returns The geometry, built on first use.
120
+ */
121
+ geometryFor(shape) {
122
+ // Both volume kinds share one layout, which is what lets the cache serve a def crossing between
123
+ // them without rebuilding anything.
124
+ return shapeGeometry(shape, this.kind !== 'planar');
125
+ }
126
+ /**
127
+ * Declares the membership this container is about to hold, setting the size the next growth jumps
128
+ * to. The component calls this from render, before React mounts the instances that will claim in
129
+ * their effects — so a membership change that outgrows the buffers costs one resize per buffer,
130
+ * sized exactly, instead of one per claim. Bookkeeping only: nothing resizes until a claim or
131
+ * geometry registration actually overflows, so a reservation from a discarded or repeated render
132
+ * is harmless.
133
+ * @param defs - The shapes this container is about to hold.
134
+ */
135
+ reserve(defs) {
136
+ this.target.instances = defs.length;
137
+ // Geometry space is a high-water mark like the instance count — registered geometries are never
138
+ // removed — so the target is what is used now plus every geometry the new membership will
139
+ // register on its first claim.
140
+ let vertices = this.allocated.usedVertices;
141
+ let indices = this.allocated.usedIndices;
142
+ const seen = new Set();
143
+ for (const def of defs) {
144
+ const geometry = this.geometryFor(def.shape);
145
+ if (this.geometryIds.has(geometry) || seen.has(geometry))
146
+ continue;
147
+ seen.add(geometry);
148
+ vertices += vertexCountOf(geometry);
149
+ indices += indexCountOf(geometry);
150
+ }
151
+ this.target.vertices = vertices;
152
+ this.target.indices = indices;
153
+ }
154
+ addInstance(geometry) {
155
+ const geometryId = this.geometryIdFor(geometry);
156
+ // Full exactly when no slot is free and no room remains to append one; `addInstance` would
157
+ // throw. Growth jumps to the reserved membership's size — releases run before claims within a
158
+ // commit, so that size is enough for every claim the change brings and the resize happens once —
159
+ // with one appended slot as the floor when the reservation is stale or smaller. Never shrunk on
160
+ // release: freed ids keep later ids live, so the mesh forbids shrinking past them, and capacity
161
+ // kept as a high-water mark lets membership oscillate without rebuilding the instance textures.
162
+ if (this.mesh.instanceCount === this.mesh.maxInstanceCount) {
163
+ this.mesh.setInstanceCount(Math.max(this.target.instances, this.mesh.maxInstanceCount + 1));
164
+ }
165
+ return this.mesh.addInstance(geometryId);
166
+ }
167
+ removeInstance(slot) {
168
+ // In act environments R3F disposes an unmounting mesh synchronously, before this passive cleanup
169
+ // runs (in the browser disposal is deferred to idle priority, after it). Safe today: r185's
170
+ // `deleteInstance` touches only bookkeeping arrays, never the nulled textures. A three upgrade
171
+ // that changes that will fail the unmount specs.
172
+ this.mesh.deleteInstance(slot);
173
+ }
174
+ /**
175
+ * Registers a geometry with the mesh if it is not already, growing the buffers first.
176
+ * @param geometry - The geometry to register.
177
+ * @returns The mesh's geometry id.
178
+ */
179
+ geometryIdFor(geometry) {
180
+ const existing = this.geometryIds.get(geometry);
181
+ if (existing !== undefined)
182
+ return existing;
183
+ // Sizing has to happen before `addGeometry`: the mesh throws once its reserved vertex or index
184
+ // space runs out. Growth jumps to the reserved membership's size, so a change that brings
185
+ // several new geometries resizes the buffers once, on the first registration, and exactly.
186
+ const vertices = this.allocated.usedVertices + vertexCountOf(geometry);
187
+ const indices = this.allocated.usedIndices + indexCountOf(geometry);
188
+ if (vertices > this.allocated.vertices || indices > this.allocated.indices) {
189
+ this.allocated.vertices = Math.max(vertices, this.target.vertices);
190
+ this.allocated.indices = Math.max(indices, this.target.indices);
191
+ this.mesh.setGeometrySize(this.allocated.vertices, this.allocated.indices);
192
+ }
193
+ this.allocated.usedVertices = vertices;
194
+ this.allocated.usedIndices = indices;
195
+ const id = this.mesh.addGeometry(geometry);
196
+ this.geometryIds.set(geometry, id);
197
+ return id;
198
+ }
199
+ }
200
+ /**
201
+ * Sizes a container to hold the given shapes exactly, floored at 1 so a layer that mounts empty is
202
+ * not built at a degenerate zero size.
203
+ * @param defs - The layer's shapes.
204
+ * @param geometryFor - Resolves a shape's geometry.
205
+ * @returns The initial capacity, with nothing used yet.
206
+ */
207
+ function measureCapacity(defs, geometryFor) {
208
+ let vertices = 0;
209
+ let indices = 0;
210
+ const seen = new Set();
211
+ for (const def of defs) {
212
+ const geometry = geometryFor(def.shape);
213
+ if (seen.has(geometry))
214
+ continue;
215
+ seen.add(geometry);
216
+ vertices += vertexCountOf(geometry);
217
+ indices += indexCountOf(geometry);
218
+ }
219
+ return {
220
+ instances: Math.max(defs.length, 1),
221
+ vertices: Math.max(vertices, 1),
222
+ indices: Math.max(indices, 1),
223
+ usedVertices: 0,
224
+ usedIndices: 0,
225
+ };
226
+ }
227
+ /**
228
+ * Vertices a geometry reserves.
229
+ * @param geometry - The geometry.
230
+ * @returns The vertex count.
231
+ */
232
+ function vertexCountOf(geometry) {
233
+ return geometry.getAttribute('position').count;
234
+ }
235
+ /**
236
+ * Indices a geometry reserves, or `0` when it is non-indexed.
237
+ * @param geometry - The geometry.
238
+ * @returns The index count.
239
+ */
240
+ function indexCountOf(geometry) {
241
+ return geometry.getIndex()?.count ?? 0;
242
+ }
@@ -0,0 +1,27 @@
1
+ import { type Shape } from '@expofp/geometry';
2
+ import { BufferGeometry } from 'three';
3
+ /**
4
+ * The template geometry a shape draws with, in the layout its batch locks itself into. Boxes and
5
+ * rects share one unit plane and are placed by their instance transform; meshes and polygons carry
6
+ * absolute coordinates in their own geometry.
7
+ *
8
+ * The cache is process-wide rather than per container, because the two solid batches — opaque and
9
+ * translucent — build byte-identical geometry for the same shape. Without sharing, a volume moving
10
+ * between them as its alpha crosses 1 re-runs `toNonIndexed`, `computeVertexNormals` and
11
+ * `toCreasedNormals` over every vertex, and does so for the batch's *whole* membership, since a
12
+ * container resolves every member's geometry to measure its capacity before anything claims a slot.
13
+ *
14
+ * Sharing one template across several meshes is safe because `BatchedMesh.addGeometry` **copies** its
15
+ * data in: a template is never uploaded to the GPU, and nothing mutates one after it is built. What
16
+ * it costs is that a template now outlives the container that first asked for it — the mesh-like
17
+ * ones until their shape is collected, the two planes for the life of the process, which is a few
18
+ * hundred bytes of vertex data against a plane rebuilt per shape layer of every plan.
19
+ * @param shape - The shape to resolve.
20
+ * @param solid - Whether the container drawing it holds volumes. A batched mesh mirrors its first
21
+ * geometry's attributes *and* its index-ness and three rejects any later geometry that disagrees,
22
+ * so the container's own flag decides the layout rather than anything derived here — a container
23
+ * holding a shape it should not is then caught by `addGeometry` rather than served quietly.
24
+ * @returns The geometry, built on first use.
25
+ */
26
+ export declare function shapeGeometry(shape: Shape, solid: boolean): BufferGeometry;
27
+ //# sourceMappingURL=shape-geometry.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { isBox, isRect } from '@expofp/geometry';
2
+ import { BufferGeometry, PlaneGeometry } from 'three';
3
+ import { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4
+ /**
5
+ * The template geometry a shape draws with, in the layout its batch locks itself into. Boxes and
6
+ * rects share one unit plane and are placed by their instance transform; meshes and polygons carry
7
+ * absolute coordinates in their own geometry.
8
+ *
9
+ * The cache is process-wide rather than per container, because the two solid batches — opaque and
10
+ * translucent — build byte-identical geometry for the same shape. Without sharing, a volume moving
11
+ * between them as its alpha crosses 1 re-runs `toNonIndexed`, `computeVertexNormals` and
12
+ * `toCreasedNormals` over every vertex, and does so for the batch's *whole* membership, since a
13
+ * container resolves every member's geometry to measure its capacity before anything claims a slot.
14
+ *
15
+ * Sharing one template across several meshes is safe because `BatchedMesh.addGeometry` **copies** its
16
+ * data in: a template is never uploaded to the GPU, and nothing mutates one after it is built. What
17
+ * it costs is that a template now outlives the container that first asked for it — the mesh-like
18
+ * ones until their shape is collected, the two planes for the life of the process, which is a few
19
+ * hundred bytes of vertex data against a plane rebuilt per shape layer of every plan.
20
+ * @param shape - The shape to resolve.
21
+ * @param solid - Whether the container drawing it holds volumes. A batched mesh mirrors its first
22
+ * geometry's attributes *and* its index-ness and three rejects any later geometry that disagrees,
23
+ * so the container's own flag decides the layout rather than anything derived here — a container
24
+ * holding a shape it should not is then caught by `addGeometry` rather than served quietly.
25
+ * @returns The geometry, built on first use.
26
+ */
27
+ export function shapeGeometry(shape, solid) {
28
+ const layout = solid ? 'solid' : 'planar';
29
+ if (isBox(shape) || isRect(shape))
30
+ return (planes[layout] ??= makePlaneGeometry(solid));
31
+ let layouts = shapeGeometries.get(shape);
32
+ if (!layouts) {
33
+ layouts = {};
34
+ shapeGeometries.set(shape, layouts);
35
+ }
36
+ return (layouts[layout] ??= makeMeshGeometry(shape, solid));
37
+ }
38
+ /**
39
+ * The unit plane every box and rect draws with, scaled and placed by its instance transform.
40
+ *
41
+ * A planar container strips it to positions: `MeshBasicMaterial` is unlit, so normals and uvs go
42
+ * unused, and stripping them matches the position-only mesh geometry beside it — a batched mesh
43
+ * locks its attribute layout to the first geometry it sees, so every geometry in it must agree. A
44
+ * solid container keeps the normals for the lit material and drops the index, because creased mesh
45
+ * geometry is non-indexed and three refuses to mix the two.
46
+ * @param solid - Whether the container draws volumes.
47
+ * @returns The unit plane, in the container's layout.
48
+ */
49
+ function makePlaneGeometry(solid) {
50
+ const plane = new PlaneGeometry().deleteAttribute('uv');
51
+ return solid ? plane.toNonIndexed() : plane.deleteAttribute('normal');
52
+ }
53
+ /**
54
+ * Builds geometry from a mesh-like shape: indexed and position-only for a planar container,
55
+ * non-indexed with creased normals for a solid one.
56
+ *
57
+ * The creasing is what makes an extruded shape read as architecture rather than as a smoothed blob —
58
+ * faces meeting at more than 30° keep a hard edge, so walls stay flat and corners stay sharp.
59
+ * @param mesh - The shape supplying vertices and triangle indices.
60
+ * @param solid - Whether the container draws volumes.
61
+ * @returns The geometry.
62
+ */
63
+ function makeMeshGeometry(mesh, solid) {
64
+ const geometry = new BufferGeometry()
65
+ .setFromPoints(mesh.vertices)
66
+ .setIndex(mesh.indices.flat());
67
+ if (!solid)
68
+ return geometry;
69
+ // Not chained: `computeVertexNormals` returns void rather than `this`, and `toCreasedNormals`
70
+ // needs the seed normals it writes.
71
+ const expanded = geometry.toNonIndexed();
72
+ expanded.computeVertexNormals();
73
+ return toCreasedNormals(expanded, CREASE_ANGLE);
74
+ }
75
+ /**
76
+ * Template geometry per mesh-like shape, weak so it is collected with the shape that keyed it.
77
+ *
78
+ * Both layouts hang off one entry rather than the key carrying the layout, because a shape can be
79
+ * asked for either. Its own kind fixes which one it is *placed* in, but a def whose `shape` is
80
+ * replaced with one of another kind draws from its old batch for the commit between the mismatch
81
+ * report and the re-split — long enough to resolve a geometry there. Keyed by shape alone, that
82
+ * first ask would decide the layout for every later one, and a lit batch would end up drawing a
83
+ * position-only geometry with no normals to shade: dark, and no error to say why.
84
+ *
85
+ * Identity is the right key for the same reason it is the right key for a slot's resource: a shape
86
+ * is replaced rather than mutated when a def's geometry changes, so a new shape arrives as a new
87
+ * key and the entry it supersedes is collected with the old one. An in-place topology mutation is
88
+ * not a supported update either way.
89
+ */
90
+ const shapeGeometries = new WeakMap();
91
+ /** The unit plane in each layout, built on the first box or rect a container of that kind draws. */
92
+ const planes = {};
93
+ /** Above this angle between neighboring faces, a shared edge stays hard instead of being smoothed. */
94
+ const CREASE_ANGLE = Math.PI / 6;
@@ -0,0 +1,22 @@
1
+ import { type TextDef } from '../../types/defs';
2
+ import { type InstanceProps } from '../containers';
3
+ import { type TextContainer } from './text-container';
4
+ /** Props for {@link TextInstance}. */
5
+ export type TextInstanceProps = InstanceProps<TextDef, TextContainer>;
6
+ /**
7
+ * Keeps one text def's lines current: claims a group of members on mount, writes each line's
8
+ * content, placement and visibility on mount and after every `update(def)`, and releases the group
9
+ * on unmount.
10
+ *
11
+ * Renders no scene node — the batch belongs to the container. The claim keys on `def.lines.length`,
12
+ * so adding or removing a line re-claims the group with the new size; changing a line's *content*
13
+ * is an ordinary write.
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 lines live in the batch, not the scene graph.
20
+ */
21
+ export declare const TextInstance: import("react").MemoExoticComponent<(props: TextInstanceProps) => null>;
22
+ //# sourceMappingURL=TextInstance.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { memo } from 'react';
2
+ import { Color, LinearSRGBColorSpace, SRGBColorSpace } from 'three';
3
+ import { normalizeColor } from '../../utils/colors';
4
+ import { createLogger } from '../../utils/logging';
5
+ import { useDefSync } from '../containers';
6
+ import { applyPlacement, layoutText } from './text-layout';
7
+ const logger = createLogger('text-instance');
8
+ /**
9
+ * Keeps one text def's lines current: claims a group of members on mount, writes each line's
10
+ * content, placement and visibility on mount and after every `update(def)`, and releases the group
11
+ * on unmount.
12
+ *
13
+ * Renders no scene node — the batch belongs to the container. The claim keys on `def.lines.length`,
14
+ * so adding or removing a line re-claims the group with the new size; changing a line's *content*
15
+ * is an ordinary write.
16
+ *
17
+ * Memoized for the same reason as `ShapeInstance`: a structural change hands the layer a fresh
18
+ * `defs` snapshot, so element-identity bailout never applies and only prop equality keeps unchanged
19
+ * siblings from re-rendering. Correctness never depends on it.
20
+ * @param props - The def and its container.
21
+ * @returns `null`; the lines live in the batch, not the scene graph.
22
+ */
23
+ export const TextInstance = memo(function TextInstance(props) {
24
+ const { def, container } = props;
25
+ useDefSync(container, def, () => def.lines.length, (slot) => {
26
+ writeText(container, slot, def);
27
+ });
28
+ return null;
29
+ });
30
+ /**
31
+ * Writes a def's whole line stack into its group.
32
+ * @param container - The container holding the group.
33
+ * @param slot - The def's group.
34
+ * @param def - The text def supplying content and layout.
35
+ */
36
+ function writeText(container, slot, def) {
37
+ const { texts, slots } = container.linesAt(slot);
38
+ const placements = layoutText(def);
39
+ for (let line = 0; line < texts.length; line++) {
40
+ writeLine(container.mesh, slots[line], texts[line], def.lines[line], placements[line], def);
41
+ }
42
+ // Content writes flag the batch as needing a sync; this is what actually runs one, coalesced
43
+ // across every instance that wrote in the same tick.
44
+ container.scheduleSync();
45
+ }
46
+ /**
47
+ * Writes one line's content and placement into its member.
48
+ * @param mesh - The batch, for the visibility write that also invalidates its bounds.
49
+ * @param lineSlot - The member's batch slot.
50
+ * @param text - The member.
51
+ * @param line - The line def.
52
+ * @param placed - The line's laid-out placement.
53
+ * @param def - The owning def, supplying the bounds the placement is relative to.
54
+ */
55
+ function writeLine(mesh, lineSlot, text, line, placed, def) {
56
+ // A zero font size is the floorplan's "too small to read at this zoom" sentinel — its scale tables
57
+ // fall through to zero rather than carrying a separate hidden flag. Routed through the batch, not
58
+ // `text.visible`, so the bounds that enclose only visible members are invalidated with it.
59
+ mesh.setVisibleAt(lineSlot, line.fontSize > 0);
60
+ if (line.fontSize <= 0)
61
+ return;
62
+ // All syncable; troika's own setters skip an unchanged write, so an update that only moved a
63
+ // label re-lays-out nothing.
64
+ text.text = line.text;
65
+ text.font = line.fontUrl;
66
+ text.anchorX = placed.anchorX;
67
+ text.anchorY = placed.anchorY;
68
+ text.textAlign = placed.textAlign;
69
+ writeColorAndOpacity(text, line.color, 'fill');
70
+ // `stroke` is a halo drawn *around* the glyphs, so it maps to troika's outline rather than its
71
+ // stroke, which is drawn inside them.
72
+ //
73
+ // Divided by the font size, because the outline is measured in the member's own units and the
74
+ // member lays out at one em while the def's width is in the same units as its `fontSize`. Without
75
+ // the conversion a two-unit halo becomes two *ems* and swallows the glyphs entirely.
76
+ if (line.stroke) {
77
+ writeColorAndOpacity(text, line.stroke.color, 'outline');
78
+ text.outlineWidth = line.fontSize == 0 ? 0 : line.stroke.width / line.fontSize;
79
+ }
80
+ applyPlacement(text, placed, def.bounds, def.screenSpace === true);
81
+ }
82
+ function writeColorAndOpacity(text, color, field) {
83
+ const colorFieldName = field == 'fill' ? 'color' : 'outlineColor';
84
+ const opacityFieldName = `${field}Opacity`;
85
+ const rgb = normalizeColor(color);
86
+ if (!rgb) {
87
+ logger.warn(`Invalid color: ${color} %O`, text);
88
+ text[colorFieldName] = color;
89
+ return;
90
+ }
91
+ _color.setRGB(rgb.r / 255, rgb.g / 255, rgb.b / 255, SRGBColorSpace);
92
+ text[colorFieldName] = _color.getHex(LinearSRGBColorSpace);
93
+ text[opacityFieldName] = rgb.a;
94
+ }
95
+ const _color = new Color();
@@ -0,0 +1,30 @@
1
+ import { type TextDef } from '../../types/defs';
2
+ import { type DefsProps } from '../containers';
3
+ /** Props for {@link Texts}. */
4
+ export type TextsProps = DefsProps<TextDef>;
5
+ /**
6
+ * Draws a collection of texts as members of one `BatchedText`, so a layer of labels renders in a
7
+ * single draw call.
8
+ *
9
+ * Each def contributes one member per line, laid out as a block inside the def's bounds. Membership
10
+ * is reconciled by React exactly as it is for shapes: a new `defs` array re-renders this component
11
+ * and React mounts or unmounts only the delta, with no differ of our own.
12
+ *
13
+ * Two things differ from a shape layer, both because a text batch draws through packed glyph
14
+ * attributes rather than an indirect draw list:
15
+ *
16
+ * - **Paint order is the pack order**, so there is no `customSort`; the container hands the batch an
17
+ * ordered member list and a change repacks. The order pass skips an unchanged order to keep an
18
+ * `update()` that moved nothing from repacking.
19
+ * - **Syncing is asynchronous** — troika fetches and parses the font — so the container drives it
20
+ * eagerly with R3F's `invalidate` as the completion callback, rather than leaving it to the next
21
+ * `onBeforeRender`, which would have nothing to request the frame that shows the result.
22
+ *
23
+ * The material is an ordinary `<ColorMaterial>` child: troika derives its text material from
24
+ * whatever base it is given, and the base is what supplies the layer blending — which is also what
25
+ * keeps the batch out of three's transparent render list.
26
+ * @param props - The texts to draw.
27
+ * @returns The batch and one instance component per text.
28
+ */
29
+ export declare function Texts(props: TextsProps): import("react").JSX.Element;
30
+ //# sourceMappingURL=Texts.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useThree } from '@react-three/fiber';
3
+ import { BatchedText } from '../../core/text/BatchedText';
4
+ import { defKey, useContainer } from '../containers';
5
+ import { ColorMaterial } from '../materials/ColorMaterial';
6
+ import { TextContainer } from './text-container';
7
+ import { TextInstance } from './TextInstance';
8
+ /**
9
+ * Draws a collection of texts as members of one `BatchedText`, so a layer of labels renders in a
10
+ * single draw call.
11
+ *
12
+ * Each def contributes one member per line, laid out as a block inside the def's bounds. Membership
13
+ * is reconciled by React exactly as it is for shapes: a new `defs` array re-renders this component
14
+ * and React mounts or unmounts only the delta, with no differ of our own.
15
+ *
16
+ * Two things differ from a shape layer, both because a text batch draws through packed glyph
17
+ * attributes rather than an indirect draw list:
18
+ *
19
+ * - **Paint order is the pack order**, so there is no `customSort`; the container hands the batch an
20
+ * ordered member list and a change repacks. The order pass skips an unchanged order to keep an
21
+ * `update()` that moved nothing from repacking.
22
+ * - **Syncing is asynchronous** — troika fetches and parses the font — so the container drives it
23
+ * eagerly with R3F's `invalidate` as the completion callback, rather than leaving it to the next
24
+ * `onBeforeRender`, which would have nothing to request the frame that shows the result.
25
+ *
26
+ * The material is an ordinary `<ColorMaterial>` child: troika derives its text material from
27
+ * whatever base it is given, and the base is what supplies the layer blending — which is also what
28
+ * keeps the batch out of three's transparent render list.
29
+ * @param props - The texts to draw.
30
+ * @returns The batch and one instance component per text.
31
+ */
32
+ export function Texts(props) {
33
+ const { defs } = props;
34
+ // Only the instances are staged; the container is built and ordered from the whole membership.
35
+ const mounted = props.mounted ?? defs.length;
36
+ const invalidate = useThree((state) => state.invalidate);
37
+ // The container is bookkeeping only — the members and the mesh are created against R3F's object —
38
+ // so the copy StrictMode's double-invoked initializer discards is plain garbage.
39
+ const container = useContainer(() => new TextContainer(invalidate), defs);
40
+ // The `ref` arrow (not `ref={container.bind}`) keeps the React Compiler from inferring `container`
41
+ // itself as a ref and rejecting its render-time reads.
42
+ return (_jsxs(_Fragment, { children: [_jsx(BatchedText, { ref: (mesh) => container.bind(mesh), children: _jsx(ColorMaterial, {}) }), defs.slice(0, mounted).map((def) => (_jsx(TextInstance, { def: def, container: container }, defKey(def))))] }));
43
+ }
@@ -0,0 +1,2 @@
1
+ export { Texts, type TextsProps } from './Texts';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { Texts } from './Texts';