@expofp/renderer 3.2.5 → 3.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +111 -0
  2. package/dist/Renderer.d.ts +88 -0
  3. package/dist/Renderer.js +53 -0
  4. package/dist/controls/CameraControls.d.ts +102 -0
  5. package/dist/controls/CameraControls.js +239 -0
  6. package/dist/controls/camera-api.d.ts +206 -0
  7. package/dist/controls/camera-api.js +181 -0
  8. package/dist/controls/camera-move.d.ts +13 -0
  9. package/dist/controls/camera-move.js +18 -0
  10. package/dist/controls/camera-state.d.ts +40 -0
  11. package/dist/controls/camera-state.js +65 -0
  12. package/dist/controls/gestures.d.ts +70 -0
  13. package/dist/controls/gestures.js +84 -0
  14. package/dist/controls/handlers/Inertia.d.ts +111 -0
  15. package/dist/controls/handlers/Inertia.js +215 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +32 -0
  18. package/dist/controls/handlers/Pitch.d.ts +94 -0
  19. package/dist/controls/handlers/Pitch.js +191 -0
  20. package/dist/controls/handlers/Roll.d.ts +103 -0
  21. package/dist/controls/handlers/Roll.js +182 -0
  22. package/dist/controls/handlers/Zoom.d.ts +15 -0
  23. package/dist/controls/handlers/Zoom.js +26 -0
  24. package/dist/controls/index.d.ts +56 -0
  25. package/dist/controls/index.js +58 -0
  26. package/dist/controls/zoom-to.d.ts +89 -0
  27. package/dist/controls/zoom-to.js +91 -0
  28. package/dist/core/InstanceUniforms.d.ts +93 -0
  29. package/dist/core/InstanceUniforms.js +266 -0
  30. package/dist/core/SquareDataTexture.d.ts +277 -0
  31. package/dist/core/SquareDataTexture.js +476 -0
  32. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  33. package/dist/core/batch/BatchedMesh.js +149 -0
  34. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  35. package/dist/core/instanced/FrustumCulling.js +246 -0
  36. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  37. package/dist/core/instanced/InstancedMesh.js +497 -0
  38. package/dist/core/instanced/Instances.d.ts +119 -0
  39. package/dist/core/instanced/Instances.js +230 -0
  40. package/dist/core/instanced/Raycasting.d.ts +118 -0
  41. package/dist/core/instanced/Raycasting.js +134 -0
  42. package/dist/core/instanced/instanced-material.d.ts +77 -0
  43. package/dist/core/instanced/instanced-material.js +224 -0
  44. package/dist/core/lines/BatchedLines.d.ts +135 -0
  45. package/dist/core/lines/BatchedLines.js +213 -0
  46. package/dist/core/lines/FrustumCulling.d.ts +43 -0
  47. package/dist/core/lines/FrustumCulling.js +85 -0
  48. package/dist/core/lines/Raycasting.d.ts +44 -0
  49. package/dist/core/lines/Raycasting.js +91 -0
  50. package/dist/core/lines/lines-material.d.ts +47 -0
  51. package/dist/core/lines/lines-material.js +115 -0
  52. package/dist/core/shader-source.d.ts +50 -0
  53. package/dist/core/shader-source.js +84 -0
  54. package/dist/core/text/BatchedText.d.ts +227 -0
  55. package/dist/core/text/BatchedText.js +467 -0
  56. package/dist/core/text/MemberTexture.d.ts +90 -0
  57. package/dist/core/text/MemberTexture.js +194 -0
  58. package/dist/core/text/Text.d.ts +73 -0
  59. package/dist/core/text/Text.js +215 -0
  60. package/dist/core/text/batched-text-material.d.ts +56 -0
  61. package/dist/core/text/batched-text-material.js +121 -0
  62. package/dist/core/text/glyph-packing.d.ts +17 -0
  63. package/dist/core/text/glyph-packing.js +61 -0
  64. package/dist/core/typed-array.d.ts +16 -0
  65. package/dist/core/typed-array.js +20 -0
  66. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  67. package/dist/debug/components/DebugPanelProvider.js +38 -0
  68. package/dist/debug/components/DebugTools.d.ts +22 -0
  69. package/dist/debug/components/DebugTools.js +28 -0
  70. package/dist/debug/components/Gui.d.ts +20 -0
  71. package/dist/debug/components/Gui.js +32 -0
  72. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  73. package/dist/debug/components/MemoryInfo.js +42 -0
  74. package/dist/debug/components/Panel.d.ts +31 -0
  75. package/dist/debug/components/Panel.js +98 -0
  76. package/dist/debug/components/Stats.d.ts +9 -0
  77. package/dist/debug/components/Stats.js +24 -0
  78. package/dist/debug/debug-context.d.ts +41 -0
  79. package/dist/debug/debug-context.js +59 -0
  80. package/dist/debug/index.d.ts +51 -0
  81. package/dist/debug/index.js +64 -0
  82. package/dist/events/LayoutDriver.d.ts +31 -0
  83. package/dist/events/LayoutDriver.js +88 -0
  84. package/dist/events/PointerMissed.d.ts +23 -0
  85. package/dist/events/PointerMissed.js +58 -0
  86. package/dist/events/def-culling.d.ts +46 -0
  87. package/dist/events/def-culling.js +69 -0
  88. package/dist/events/layout-targets.d.ts +104 -0
  89. package/dist/events/layout-targets.js +245 -0
  90. package/dist/events/pointer.d.ts +118 -0
  91. package/dist/events/pointer.js +168 -0
  92. package/dist/external/ExternalCanvas.d.ts +56 -0
  93. package/dist/external/ExternalCanvas.js +178 -0
  94. package/dist/external/external-camera.d.ts +17 -0
  95. package/dist/external/external-camera.js +82 -0
  96. package/dist/external/external-events.d.ts +14 -0
  97. package/dist/external/external-events.js +33 -0
  98. package/dist/index.d.ts +11 -783
  99. package/dist/index.js +12 -8031
  100. package/dist/objects/Background.d.ts +31 -0
  101. package/dist/objects/Background.js +37 -0
  102. package/dist/objects/Lighting.d.ts +20 -0
  103. package/dist/objects/Lighting.js +85 -0
  104. package/dist/objects/containers/SlotContainer.d.ts +266 -0
  105. package/dist/objects/containers/SlotContainer.js +251 -0
  106. package/dist/objects/containers/hooks.d.ts +46 -0
  107. package/dist/objects/containers/hooks.js +118 -0
  108. package/dist/objects/containers/index.d.ts +6 -0
  109. package/dist/objects/containers/index.js +4 -0
  110. package/dist/objects/containers/keys.d.ts +10 -0
  111. package/dist/objects/containers/keys.js +18 -0
  112. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  113. package/dist/objects/containers/mesh-writes.js +97 -0
  114. package/dist/objects/containers/props.d.ts +23 -0
  115. package/dist/objects/containers/props.js +1 -0
  116. package/dist/objects/images/ImageInstance.d.ts +29 -0
  117. package/dist/objects/images/ImageInstance.js +83 -0
  118. package/dist/objects/images/Images.d.ts +37 -0
  119. package/dist/objects/images/Images.js +204 -0
  120. package/dist/objects/images/atlas-packer.d.ts +128 -0
  121. package/dist/objects/images/atlas-packer.js +315 -0
  122. package/dist/objects/images/image-container.d.ts +51 -0
  123. package/dist/objects/images/image-container.js +70 -0
  124. package/dist/objects/images/index.d.ts +2 -0
  125. package/dist/objects/images/index.js +1 -0
  126. package/dist/objects/images/texture-budget.d.ts +120 -0
  127. package/dist/objects/images/texture-budget.js +215 -0
  128. package/dist/objects/layer/Layer.d.ts +27 -0
  129. package/dist/objects/layer/Layer.js +114 -0
  130. package/dist/objects/layer/index.d.ts +2 -0
  131. package/dist/objects/layer/index.js +1 -0
  132. package/dist/objects/layer/mount-scheduler.d.ts +102 -0
  133. package/dist/objects/layer/mount-scheduler.js +145 -0
  134. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  135. package/dist/objects/layer/visibility-gui.js +93 -0
  136. package/dist/objects/lines/LineInstance.d.ts +21 -0
  137. package/dist/objects/lines/LineInstance.js +73 -0
  138. package/dist/objects/lines/Lines.d.ts +17 -0
  139. package/dist/objects/lines/Lines.js +29 -0
  140. package/dist/objects/lines/index.d.ts +2 -0
  141. package/dist/objects/lines/index.js +1 -0
  142. package/dist/objects/lines/line-container.d.ts +35 -0
  143. package/dist/objects/lines/line-container.js +40 -0
  144. package/dist/objects/materials/ColorMaterial.d.ts +85 -0
  145. package/dist/objects/materials/ColorMaterial.js +89 -0
  146. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  147. package/dist/objects/materials/LineMaterial.js +72 -0
  148. package/dist/objects/materials/ModelMaterial.d.ts +41 -0
  149. package/dist/objects/materials/ModelMaterial.js +84 -0
  150. package/dist/objects/materials/TextureMaterial.d.ts +30 -0
  151. package/dist/objects/materials/TextureMaterial.js +29 -0
  152. package/dist/objects/materials/dim.d.ts +123 -0
  153. package/dist/objects/materials/dim.js +213 -0
  154. package/dist/objects/materials/shared.d.ts +6 -0
  155. package/dist/objects/materials/shared.js +51 -0
  156. package/dist/objects/models/ModelInstance.d.ts +22 -0
  157. package/dist/objects/models/ModelInstance.js +33 -0
  158. package/dist/objects/models/Models.d.ts +22 -0
  159. package/dist/objects/models/Models.js +177 -0
  160. package/dist/objects/models/gltf-cache.d.ts +17 -0
  161. package/dist/objects/models/gltf-cache.js +50 -0
  162. package/dist/objects/models/index.d.ts +2 -0
  163. package/dist/objects/models/index.js +1 -0
  164. package/dist/objects/models/model-asset.d.ts +48 -0
  165. package/dist/objects/models/model-asset.js +77 -0
  166. package/dist/objects/models/model-container.d.ts +139 -0
  167. package/dist/objects/models/model-container.js +227 -0
  168. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  169. package/dist/objects/shapes/ShapeInstance.js +40 -0
  170. package/dist/objects/shapes/Shapes.d.ts +21 -0
  171. package/dist/objects/shapes/Shapes.js +120 -0
  172. package/dist/objects/shapes/index.d.ts +2 -0
  173. package/dist/objects/shapes/index.js +1 -0
  174. package/dist/objects/shapes/shape-container.d.ts +165 -0
  175. package/dist/objects/shapes/shape-container.js +272 -0
  176. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  177. package/dist/objects/shapes/shape-geometry.js +94 -0
  178. package/dist/objects/text/TextInstance.d.ts +22 -0
  179. package/dist/objects/text/TextInstance.js +95 -0
  180. package/dist/objects/text/Texts.d.ts +30 -0
  181. package/dist/objects/text/Texts.js +41 -0
  182. package/dist/objects/text/index.d.ts +2 -0
  183. package/dist/objects/text/index.js +1 -0
  184. package/dist/objects/text/text-container.d.ts +105 -0
  185. package/dist/objects/text/text-container.js +206 -0
  186. package/dist/objects/text/text-layout.d.ts +83 -0
  187. package/dist/objects/text/text-layout.js +154 -0
  188. package/dist/space/Camera.d.ts +103 -0
  189. package/dist/space/Camera.js +321 -0
  190. package/dist/space/Scene.d.ts +44 -0
  191. package/dist/space/Scene.js +112 -0
  192. package/dist/space/Space.d.ts +288 -0
  193. package/dist/space/Space.js +329 -0
  194. package/dist/space/VisibleRect.d.ts +72 -0
  195. package/dist/space/VisibleRect.js +129 -0
  196. package/dist/space/camera-handle.d.ts +67 -0
  197. package/dist/space/camera-handle.js +63 -0
  198. package/dist/space/camera-kind.d.ts +26 -0
  199. package/dist/space/camera-kind.js +30 -0
  200. package/dist/space/canvas-size.d.ts +24 -0
  201. package/dist/space/canvas-size.js +20 -0
  202. package/dist/space/clipping.d.ts +55 -0
  203. package/dist/space/clipping.js +108 -0
  204. package/dist/space/layout-measure.d.ts +65 -0
  205. package/dist/space/layout-measure.js +173 -0
  206. package/dist/space/viewport.d.ts +112 -0
  207. package/dist/space/viewport.js +67 -0
  208. package/dist/types/defs.d.ts +169 -0
  209. package/dist/types/defs.js +1 -0
  210. package/dist/types/events.d.ts +183 -0
  211. package/dist/types/events.js +1 -0
  212. package/dist/types/guards.d.ts +14 -0
  213. package/dist/types/guards.js +42 -0
  214. package/dist/update/index.d.ts +3 -0
  215. package/dist/update/index.js +2 -0
  216. package/dist/update/slot-writers.d.ts +30 -0
  217. package/dist/update/slot-writers.js +72 -0
  218. package/dist/update/store.d.ts +41 -0
  219. package/dist/update/store.js +98 -0
  220. package/dist/utils/arrays.d.ts +13 -0
  221. package/dist/utils/arrays.js +20 -0
  222. package/dist/utils/colors.d.ts +19 -0
  223. package/dist/utils/colors.js +27 -0
  224. package/dist/utils/logging.d.ts +26 -0
  225. package/dist/utils/logging.js +47 -0
  226. package/dist/utils/math.d.ts +24 -0
  227. package/dist/utils/math.js +27 -0
  228. package/package.json +67 -19
@@ -0,0 +1,215 @@
1
+ import { createContext, useEffect, useSyncExternalStore } from 'react';
2
+ import { createLogger } from '../../utils/logging';
3
+ const logger = createLogger('texture-budget');
4
+ /** Bytes one RGBA texel occupies. */
5
+ const BYTES_PER_PIXEL = 4;
6
+ /**
7
+ * How much a full mipmap chain adds over the base level: the 1/4 + 1/16 + … geometric series sums to
8
+ * 1/3, so a mipmapped texture costs ~4/3 its base size. The atlas textures upload with mipmaps on
9
+ * (see `makeAtlasTexture`), so the budget counts them, or it would under-report GPU use by a third.
10
+ */
11
+ const MIPMAP_FACTOR = 4 / 3;
12
+ /** A megabyte, the unit `memoryLimit` is given in. */
13
+ const BYTES_PER_MB = 1024 * 1024;
14
+ /**
15
+ * How much the resize factor must move before subscribers are told. A change this small shifts an
16
+ * atlas edge by well under a pixel, so re-deriving every texture for it would be churn with no
17
+ * visible effect — the point of the gate is to keep a burst of atlas mounts, each nudging the total,
18
+ * from re-scaling the whole scene once per mount.
19
+ */
20
+ const FACTOR_EPSILON = 1e-3;
21
+ /**
22
+ * The GPU footprint of an atlas of the given pixel size: RGBA, with the mipmap chain counted. Reads
23
+ * the atlas's full (packed) dimensions — the resize factor scales this down, so budgeting against
24
+ * the full size is what the factor is solved from.
25
+ * @param width - Atlas width in pixels.
26
+ * @param height - Atlas height in pixels.
27
+ * @returns The size in bytes.
28
+ */
29
+ export function getAtlasSizeBytes(width, height) {
30
+ return width * height * BYTES_PER_PIXEL * MIPMAP_FACTOR;
31
+ }
32
+ /**
33
+ * The scene's shared texture-memory budget. Every image atlas registers its full size here, and the
34
+ * store solves for one linear resize factor that brings the resizable atlases within `memoryLimit` —
35
+ * shared by all of them, so their relative sizes are preserved with no priority.
36
+ *
37
+ * The factor is exposed through `useSyncExternalStore`: an atlas that mounts, unmounts, or a limit
38
+ * that changes recomputes it, and a material move re-derives only the resizable atlases' textures —
39
+ * never a repack, since uv regions are normalized and so invariant to the canvas's pixel size.
40
+ * Recompute runs synchronously per registration; the render churn that would cause is left to
41
+ * React's automatic batching to coalesce, and the `FACTOR_EPSILON` gate keeps sub-pixel nudges from
42
+ * notifying at all. When no cap is set the whole apparatus stands down: {@link TextureBudget.isActive}
43
+ * is false, atlases skip registration entirely (see {@link useResizeFactor}), and the factor is 1.
44
+ */
45
+ export class TextureBudget {
46
+ /** The cap in megabytes, or undefined/`0` for no cap — a falsy value scales nothing. */
47
+ memoryLimitMb;
48
+ /** Every registered atlas; the entry object is its own handle. */
49
+ atlases = new Set();
50
+ /** Subscribers to the resize factor — the mounted resizable atlases. */
51
+ listeners = new Set();
52
+ /** The current shared linear resize factor; `1` means no scaling. */
53
+ factor = 1;
54
+ /**
55
+ * @param memoryLimitMb - The cap in megabytes, or undefined/`0` for no cap.
56
+ */
57
+ constructor(memoryLimitMb) {
58
+ this.memoryLimitMb = memoryLimitMb;
59
+ }
60
+ /**
61
+ * Whether a cap is in force. False for an absent or zero limit — nothing can be compressed, so
62
+ * consumers skip the budget entirely rather than do work that resolves to "no scaling".
63
+ * @returns Whether a positive limit is set.
64
+ */
65
+ isActive() {
66
+ return !!this.memoryLimitMb;
67
+ }
68
+ /**
69
+ * Changes the cap. Called when the renderer's `memoryLimit` prop changes, so a caller can raise,
70
+ * lower, or clear it at runtime and the resizable atlases re-derive their textures live.
71
+ * @param memoryLimitMb - The new cap, or undefined/`0` for no cap.
72
+ */
73
+ setMemoryLimit(memoryLimitMb) {
74
+ if (memoryLimitMb === this.memoryLimitMb)
75
+ return;
76
+ const wasActive = this.isActive();
77
+ this.memoryLimitMb = memoryLimitMb;
78
+ if (this.isActive() !== wasActive) {
79
+ // The cap turned on or off: recompute and always notify, so every atlas hook re-runs to
80
+ // register or drop itself and re-reads the factor, even when its numeric value did not move.
81
+ this.factor = this.computeResizeFactor();
82
+ this.notify();
83
+ }
84
+ else {
85
+ this.recompute();
86
+ }
87
+ }
88
+ /**
89
+ * Registers one atlas's full size, recomputing the factor. Only called while {@link isActive}, so
90
+ * an uncapped scene never populates this set.
91
+ * @param entry - The atlas's dimensions and kind; pass the same object back to unregister.
92
+ */
93
+ register(entry) {
94
+ this.atlases.add(entry);
95
+ this.recompute();
96
+ }
97
+ /**
98
+ * Drops an atlas's contribution, recomputing the factor.
99
+ * @param entry - The object {@link register} was given.
100
+ */
101
+ unregister(entry) {
102
+ if (this.atlases.delete(entry))
103
+ this.recompute();
104
+ }
105
+ /**
106
+ * The current shared resize factor.
107
+ * @returns The factor in `(0, 1]`.
108
+ */
109
+ getResizeFactor() {
110
+ return this.factor;
111
+ }
112
+ /**
113
+ * Subscribes to factor and active-state changes.
114
+ * @param listener - Called when the factor moves past {@link FACTOR_EPSILON} or the cap toggles.
115
+ * @returns The unsubscribe function.
116
+ */
117
+ subscribe(listener) {
118
+ this.listeners.add(listener);
119
+ return () => {
120
+ this.listeners.delete(listener);
121
+ };
122
+ }
123
+ /** Recomputes the factor and, if it moved materially, notifies subscribers. */
124
+ recompute() {
125
+ const next = this.computeResizeFactor();
126
+ if (Math.abs(next - this.factor) < FACTOR_EPSILON)
127
+ return;
128
+ this.factor = next;
129
+ this.notify();
130
+ }
131
+ /** Notifies every subscriber. */
132
+ notify() {
133
+ for (const listener of this.listeners)
134
+ listener();
135
+ }
136
+ /**
137
+ * Solves for the linear factor that fits the resizable atlases in the budget the fixed ones leave.
138
+ *
139
+ * Every degenerate case falls back to `1` — no scaling — rather than forcing a fit: no cap, nothing
140
+ * resizable to scale, or a cap the fixed atlases alone already exceed (canvases cannot be
141
+ * compressed, so there is nothing to give). The factor is only ever clamped up to `1`, never down
142
+ * to a floor: an image is scaled by however much the budget demands.
143
+ * @returns The factor in `(0, 1]`.
144
+ */
145
+ computeResizeFactor() {
146
+ // Silent: reached only when a cap is cleared at runtime, since an atlas never registers without
147
+ // one (see useResizeFactor, which logs the no-cap case at the point it skips the work).
148
+ if (!this.memoryLimitMb)
149
+ return 1;
150
+ let totalResizable = 0;
151
+ let totalFixed = 0;
152
+ for (const entry of this.atlases) {
153
+ const bytes = getAtlasSizeBytes(entry.width, entry.height);
154
+ if (entry.resizable)
155
+ totalResizable += bytes;
156
+ else
157
+ totalFixed += bytes;
158
+ }
159
+ if (totalResizable === 0) {
160
+ logger.debug('no resizable atlases, atlases will not be scaled');
161
+ return 1;
162
+ }
163
+ const budget = this.memoryLimitMb * BYTES_PER_MB - totalFixed;
164
+ if (budget <= 0) {
165
+ logger.debug('memory limit too low to resize textures, atlases will not be scaled');
166
+ return 1;
167
+ }
168
+ const factor = Math.sqrt(budget / totalResizable);
169
+ if (factor >= 1)
170
+ return 1;
171
+ logger.debug('resize factor: %s (budget %d bytes, resizable %d bytes)', factor.toFixed(3), budget, totalResizable);
172
+ return factor;
173
+ }
174
+ }
175
+ /**
176
+ * The scene's texture budget, provided by `Renderer`. The default is an uncapped budget, so an image
177
+ * layer rendered outside a `Renderer` (a test, a story) behaves exactly as before this existed —
178
+ * everything uploads at full size.
179
+ */
180
+ export const TextureBudgetContext = createContext(new TextureBudget());
181
+ /**
182
+ * Registers an atlas with the scene budget for its mounted lifetime and returns the resize factor it
183
+ * should upload at: the shared factor for a resizable atlas, always `1` for a fixed one (its pixels
184
+ * still count toward the budget, they just never scale).
185
+ *
186
+ * When the budget is inactive — no `memoryLimit` — the atlas is not registered at all: there is
187
+ * nothing to solve, so an uncapped scene does no per-atlas budget work. Turning a cap on at runtime
188
+ * flips `isActive`, which re-runs the registration effect.
189
+ * @param budget - The scene budget from context.
190
+ * @param atlas - The atlas to register and read a factor for.
191
+ * @returns The linear factor this atlas's canvas should be scaled by before upload, in `(0, 1]`.
192
+ */
193
+ export function useResizeFactor(budget, atlas) {
194
+ // The inline getters/subscribe close over `budget`; the React Compiler memoizes them on it, so
195
+ // `useSyncExternalStore` sees stable references and does not re-subscribe every render. They must
196
+ // be arrows (not `budget.subscribe`) so the call keeps its `this`.
197
+ const active = useSyncExternalStore((listener) => budget.subscribe(listener), () => budget.isActive());
198
+ const factor = useSyncExternalStore((listener) => budget.subscribe(listener), () => budget.getResizeFactor());
199
+ useEffect(() => {
200
+ // No cap in force: registering would only feed a computation that resolves to "no scaling", so
201
+ // an uncapped scene does no per-atlas budget work. A later `setMemoryLimit` flips `active` and
202
+ // re-runs this to register. The no-cap case is logged once by `Renderer`, not here per atlas.
203
+ if (!active)
204
+ return;
205
+ const entry = {
206
+ width: atlas.canvas.width,
207
+ height: atlas.canvas.height,
208
+ resizable: atlas.resizable,
209
+ };
210
+ budget.register(entry);
211
+ return () => budget.unregister(entry);
212
+ }, [budget, atlas, active]);
213
+ // A fixed atlas never scales; an uncapped budget leaves the factor at 1, so this is 1 then too.
214
+ return atlas.resizable ? factor : 1;
215
+ }
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type LayerDef } from '../../types/defs';
3
+ /** Props for {@link Layer}. */
4
+ export interface LayerProps {
5
+ /** The layer definition to render, together with its descendants. */
6
+ layer: LayerDef;
7
+ /**
8
+ * Rendered inside the root group, before the tree's own content — under the root layer's dim
9
+ * provider, which is what the scene's background quad mounts here for: the root's dim reaches it
10
+ * like any layer's, and its material exists before every layer's, which its draw order relies
11
+ * on.
12
+ */
13
+ children?: ReactNode;
14
+ }
15
+ /**
16
+ * Renders a {@link LayerDef} tree, mapping defs onto the scene-graph hierarchy.
17
+ *
18
+ * This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
19
+ * "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
20
+ * scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
21
+ * carries: nothing above the root can hide it, and the whole tree's draw order lives inside
22
+ * `[0, 1)`.
23
+ * @param props - The layer props.
24
+ * @returns The tree's root group.
25
+ */
26
+ export declare function Layer(props: LayerProps): import("react").JSX.Element;
27
+ //# sourceMappingURL=Layer.d.ts.map
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Suspense, use } from 'react';
3
+ import { useLayoutTargets } from '../../events/layout-targets';
4
+ import { usePointerProps } from '../../events/pointer';
5
+ import { isImageDefs, isLayerDefs, isLineDefs, isModelDefs, isShapeDefs, isTextDefs, } from '../../types/guards';
6
+ import { Images } from '../images';
7
+ import { Lines } from '../lines';
8
+ import { LayerDimContext } from '../materials/dim';
9
+ import { Models } from '../models';
10
+ import { Shapes } from '../shapes';
11
+ import { Texts } from '../text';
12
+ import { useStagedChildren } from './mount-scheduler';
13
+ import { useVisibilityGui } from './visibility-gui';
14
+ /**
15
+ * Renders a {@link LayerDef} tree, mapping defs onto the scene-graph hierarchy.
16
+ *
17
+ * This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
18
+ * "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
19
+ * scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
20
+ * carries: nothing above the root can hide it, and the whole tree's draw order lives inside
21
+ * `[0, 1)`.
22
+ * @param props - The layer props.
23
+ * @returns The tree's root group.
24
+ */
25
+ export function Layer(props) {
26
+ const { layer, children } = props;
27
+ useVisibilityGui(layer);
28
+ return (_jsx(LayerNode, { layer: layer, parentVisible: true, orderBase: 0, orderStep: 1, children: children }));
29
+ }
30
+ /**
31
+ * One layer of the tree as a three.js `<group>`, recursing into its child layers.
32
+ *
33
+ * Everything layer-shaped is handled here as group state — the name, visibility from
34
+ * `layer.hidden`, the dim the subtree inherits, and the pointer props that make the group
35
+ * interactive and picked (see `usePointerProps`) — so the containers below stay free of layer
36
+ * semantics and receive only their child defs. The children arrive through `useStagedChildren`,
37
+ * which is what makes `layer.hidden = true; update(layer)` flip a group prop instead of rebuilding
38
+ * anything, defers a membership change's instance-list sweep to transition priority, and stages
39
+ * leaf mounting one commit per frame — see the hook for all three.
40
+ *
41
+ * **Re-layout is registered, not rendered.** A layer registers the defs that declare an `onScale`
42
+ * with the renderer's re-layout targets — the children it holds, and itself, which is how a layer
43
+ * whose membership depends on the camera re-derives it. From then on a pixel-size change reaches
44
+ * those defs through the frame loop, with no render of this component involved.
45
+ *
46
+ * **Draw order is a group prop.** three propagates a group's `renderOrder` down its subtree as
47
+ * `groupOrder`, which `painterSortStable` compares before `renderOrder` and `material.id` — so
48
+ * numbering the groups is all it takes to make array order the draw order. A nested group
49
+ * overwrites `groupOrder` for its own subtree, so the value that decides a mesh's place is its
50
+ * nearest enclosing layer's; the numbers on layers above it are overwritten and simply do not
51
+ * matter. Without this the order falls out of `material.id`, which ascends in mount order and
52
+ * therefore puts any material built after first paint — every model's, since they exist only once
53
+ * its GLB has loaded — last in the scene regardless of where its layer sits.
54
+ * @param props - The node props.
55
+ * @returns The `<group>` for a layer def.
56
+ */
57
+ function LayerNode(props) {
58
+ const { layer, children: prelude, parentVisible, orderBase, orderStep } = props;
59
+ // On screen only if nothing above hides it either — the value picking hangs off, and what the
60
+ // children inherit in turn.
61
+ const visible = parentVisible && !layer.hidden;
62
+ // Nearest declaration wins and reaches every material beneath through context — so only
63
+ // `undefined` inherits, and a layer's own `0` un-dims it inside a dimmed ancestor. Unlike
64
+ // visibility this is never a prop: nothing between here and the material has any use for it.
65
+ const inheritedDim = use(LayerDimContext);
66
+ const dim = layer.dim ?? inheritedDim;
67
+ const { children, admitted } = useStagedChildren(layer);
68
+ useLayoutTargets(layer, children);
69
+ const pointerProps = usePointerProps(layer, visible);
70
+ let contents = null;
71
+ if (isLayerDefs(children)) {
72
+ const childStep = orderStep / children.length;
73
+ contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, parentVisible: visible, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
74
+ }
75
+ else if (admitted) {
76
+ contents = _jsx(LeafContent, { defs: children });
77
+ }
78
+ const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: orderBase, ...pointerProps, children: [prelude, contents] }));
79
+ // Always provided, never conditionally on the layer declaring a `dim`: making the provider appear
80
+ // when one is first set would change this component's element type, and React would remount the
81
+ // subtree — releasing and re-claiming every GPU slot beneath it, exactly what an eased dim must
82
+ // not do. A layer that declares none provides what it inherited, which no consumer can tell apart
83
+ // from not providing at all.
84
+ return _jsx(LayerDimContext, { value: dim, children: group });
85
+ }
86
+ /**
87
+ * The content of one leaf layer: the container for its defs' kind, inside a Suspense boundary of
88
+ * the leaf's own.
89
+ *
90
+ * The boundary means a content component that suspends — a loader hook, a lazily imported one —
91
+ * costs this leaf alone. Without it the nearest boundary is the one R3F's `<Canvas>` wraps
92
+ * everything in, and falling back to it hides *every* object in the scene (R3F's `hideInstance`
93
+ * writes `visible = false`) while the admission queue stalls forever, since the leaf never commits
94
+ * and so never reports mounted. With it, a suspended leaf behaves exactly like an empty one: the
95
+ * layer still commits, still reports, the next leaf is admitted on schedule, and the content mounts
96
+ * when it resolves.
97
+ * @param props - The leaf's defs.
98
+ * @returns The container for the defs' kind.
99
+ */
100
+ function LeafContent(props) {
101
+ const { defs } = props;
102
+ let content = null;
103
+ if (isShapeDefs(defs))
104
+ content = _jsx(Shapes, { defs: defs });
105
+ else if (isImageDefs(defs))
106
+ content = _jsx(Images, { defs: defs });
107
+ else if (isTextDefs(defs))
108
+ content = _jsx(Texts, { defs: defs });
109
+ else if (isLineDefs(defs))
110
+ content = _jsx(Lines, { defs: defs });
111
+ else if (isModelDefs(defs))
112
+ content = _jsx(Models, { defs: defs });
113
+ return _jsx(Suspense, { fallback: null, children: content });
114
+ }
@@ -0,0 +1,2 @@
1
+ export { Layer, type LayerProps } from './Layer';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { Layer } from './Layer';
@@ -0,0 +1,102 @@
1
+ import { type LayerDef, type RenderableDef, type RenderableDefCollection } from '../../types/defs';
2
+ /**
3
+ * Admits shape layers into the scene one per frame, in registration order.
4
+ *
5
+ * Mounting a large layer's instances is one unbreakable commit — `useDeferredValue` can slice a
6
+ * render phase, but nothing can slice a commit — so the unit of progressive mounting is the leaf
7
+ * layer: a scene's first (small) layers paint without waiting for every later layer's commit.
8
+ * Priority is registration order, which is the **flattened leaf order** of the tree: leaves
9
+ * register from a passive mount effect, and React runs those children-first, in sibling order.
10
+ * Pacing is one admission per animation frame, granted after the previously admitted leaf reports
11
+ * its content committed — so each layer's frame actually presents before the next layer's commit
12
+ * begins.
13
+ *
14
+ * An **empty** leaf is exempt: it has no instances to commit, so it is admitted on registration and
15
+ * never paced. Without that, a layer filled after mount — wayfinding's route, markers, traffic —
16
+ * would have to wait out the staging of every leaf ahead of it (a real plan runs to ~90 leaves,
17
+ * ~20s) before it were allowed to draw at all.
18
+ */
19
+ export interface MountScheduler {
20
+ /**
21
+ * Queues a leaf for admission. Call from a mount effect so call order is the flattened leaf
22
+ * order. Admission is sticky: a leaf that re-registers (StrictMode effect cycling, a remount)
23
+ * stays admitted.
24
+ * @param leaf - The shape layer to queue.
25
+ * @returns An unregister function.
26
+ */
27
+ register(leaf: LayerDef): () => void;
28
+ /**
29
+ * The queued leaves, in registration order — the flattened leaf order of the mounted tree. The
30
+ * queue doubles as the tree's leaf census, which is what the layer-visibility GUI lists.
31
+ * @returns A snapshot of the queue.
32
+ */
33
+ leaves(): readonly LayerDef[];
34
+ /**
35
+ * Whether a leaf may mount its content — the `useSyncExternalStore` snapshot.
36
+ * @param leaf - The shape layer to check.
37
+ * @returns `true` once admitted.
38
+ */
39
+ isAdmitted(leaf: LayerDef): boolean;
40
+ /**
41
+ * Subscribes to scheduler changes: admissions, registrations, and unregistrations.
42
+ * @param listener - Called after every change.
43
+ * @returns An unsubscribe function.
44
+ */
45
+ subscribe(listener: () => void): () => void;
46
+ /**
47
+ * Marks the admitted leaf's content as committed, releasing the next admission on the following
48
+ * animation frame.
49
+ * @param leaf - The shape layer whose content committed.
50
+ */
51
+ reportMounted(leaf: LayerDef): void;
52
+ }
53
+ /** What {@link useStagedChildren} hands a layer: the children to render, and their admission. */
54
+ export interface StagedChildren<T extends RenderableDef> {
55
+ /** The layer's children — the deferred, version-keyed snapshot described on the hook. */
56
+ children: Readonly<RenderableDefCollection<T>>;
57
+ /** Whether a leaf layer may mount its content; always `true` for a layer of layers. */
58
+ admitted: boolean;
59
+ }
60
+ /**
61
+ * Builds the scheduler a `Layer` tree's root provides.
62
+ * @returns The scheduler.
63
+ */
64
+ export declare function createMountScheduler(): MountScheduler;
65
+ /**
66
+ * A layer's children as they should render now, together with the staged-mount admission for their
67
+ * content. Three behaviors fold together here, because their consumer needs all of them and the
68
+ * intermediate values are footguns — rendering the un-deferred snapshot would defeat the deferral:
69
+ *
70
+ * - **Subscription.** The store's version-keyed snapshot of `layer.children`: a new identity
71
+ * exactly when the layer is updated, the cached one otherwise. The layer's own array is mutated
72
+ * in place, so passing it down would leave a memoized container stale; the snapshot is what makes
73
+ * `layer.hidden = true; update(layer)` re-render the layer while child identities survive, so
74
+ * keyed reconciliation mounts and unmounts only the membership delta.
75
+ * - **Deferral.** Children flow through `useDeferredValue`, so a membership change's urgent render
76
+ * is the layer component alone; the container's instance-list sweep re-renders at transition
77
+ * priority, interruptible, keeping frames flowing. `update(def)` writes on leaf defs are
78
+ * unaffected — they never pass through `children`.
79
+ * - **Admission.** Whether a leaf may mount its content, per {@link useStagedMount}. A leaf that is
80
+ * not yet admitted renders `null` rather than an empty container, so each batch is still created
81
+ * at its full size and never reallocates on mount. Defs stay the source of truth: `update()`
82
+ * against a not-yet-admitted layer is harmless, because admission mounts whatever the defs hold
83
+ * at that moment.
84
+ * @param layer - The layer to observe.
85
+ * @returns The current children snapshot and the admission flag.
86
+ */
87
+ export declare function useStagedChildren<T extends RenderableDef>(layer: LayerDef<T>): StagedChildren<T>;
88
+ /**
89
+ * Whether a layer may mount its content, per the tree's scheduler.
90
+ *
91
+ * Consumes the scheduler {@link MountSchedulerContext} provides. Without one — a plain `<Layer>`
92
+ * mount, or staging disabled — everything is admitted immediately and no queueing happens. Under a
93
+ * scheduler, a leaf is admitted one per frame in flattened leaf order, and reports back from the
94
+ * admission commit so the next leaf is released only after this one's content committed.
95
+ * @param layer - The layer def.
96
+ * @param leaf - Whether the layer is a shape layer; only leaves are staged.
97
+ * @returns Whether the layer's content may mount.
98
+ */
99
+ export declare function useStagedMount(layer: LayerDef, leaf: boolean): boolean;
100
+ /** The tree's scheduler, provided by `Scene` and inherited by every `Layer` beneath it. */
101
+ export declare const MountSchedulerContext: import("react").Context<MountScheduler | null>;
102
+ //# sourceMappingURL=mount-scheduler.d.ts.map
@@ -0,0 +1,145 @@
1
+ import { createContext, use, useDeferredValue, useEffect, useSyncExternalStore } from 'react';
2
+ import { isLayerDefs } from '../../types/guards';
3
+ import { childrenOf, subscribeDef } from '../../update';
4
+ /**
5
+ * Builds the scheduler a `Layer` tree's root provides.
6
+ * @returns The scheduler.
7
+ */
8
+ export function createMountScheduler() {
9
+ const queue = [];
10
+ // Weak and sticky: admission survives unregister (a StrictMode effect cycle must not unmount
11
+ // content already on screen), and an entry is collected with its def.
12
+ const admitted = new WeakSet();
13
+ const listeners = new Set();
14
+ let awaiting = null;
15
+ let pacing = false;
16
+ // Frame pacing in the browser; a macrotask in headless spec environments without rAF.
17
+ const nextFrame = typeof requestAnimationFrame === 'function'
18
+ ? (callback) => void requestAnimationFrame(callback)
19
+ : (callback) => void setTimeout(callback, 0);
20
+ const notifyAll = () => {
21
+ for (const listener of [...listeners])
22
+ listener();
23
+ };
24
+ const pump = () => {
25
+ if (awaiting || pacing)
26
+ return;
27
+ const next = queue.find((leaf) => !admitted.has(leaf));
28
+ if (!next)
29
+ return;
30
+ admitted.add(next);
31
+ awaiting = next;
32
+ notifyAll();
33
+ };
34
+ return {
35
+ register: (leaf) => {
36
+ // An empty leaf has no instances to commit, so it costs nothing to admit and nothing to
37
+ // pace: it takes no queue slot and never becomes `awaiting`. This is what keeps a layer
38
+ // filled *after* mount — wayfinding's route, markers, traffic — from waiting out the whole
39
+ // plan's staging before it may draw. Admission is sticky, so the later fill mounts at once,
40
+ // as the one small commit it is.
41
+ if (leaf.children.length === 0 && !admitted.has(leaf)) {
42
+ admitted.add(leaf);
43
+ notifyAll();
44
+ }
45
+ if (!queue.includes(leaf)) {
46
+ queue.push(leaf);
47
+ notifyAll();
48
+ }
49
+ pump();
50
+ return () => {
51
+ const index = queue.indexOf(leaf);
52
+ if (index >= 0) {
53
+ queue.splice(index, 1);
54
+ notifyAll();
55
+ }
56
+ // An admitted leaf that unmounts before reporting must not stall the queue forever.
57
+ if (awaiting === leaf) {
58
+ awaiting = null;
59
+ pump();
60
+ }
61
+ };
62
+ },
63
+ leaves: () => [...queue],
64
+ isAdmitted: (leaf) => admitted.has(leaf),
65
+ subscribe: (listener) => {
66
+ listeners.add(listener);
67
+ return () => {
68
+ listeners.delete(listener);
69
+ };
70
+ },
71
+ reportMounted: (leaf) => {
72
+ if (awaiting !== leaf)
73
+ return;
74
+ awaiting = null;
75
+ // The pacing flag keeps a registration arriving before the next frame from admitting
76
+ // synchronously and skipping the one-per-frame cadence.
77
+ pacing = true;
78
+ nextFrame(() => {
79
+ pacing = false;
80
+ pump();
81
+ });
82
+ },
83
+ };
84
+ }
85
+ /**
86
+ * A layer's children as they should render now, together with the staged-mount admission for their
87
+ * content. Three behaviors fold together here, because their consumer needs all of them and the
88
+ * intermediate values are footguns — rendering the un-deferred snapshot would defeat the deferral:
89
+ *
90
+ * - **Subscription.** The store's version-keyed snapshot of `layer.children`: a new identity
91
+ * exactly when the layer is updated, the cached one otherwise. The layer's own array is mutated
92
+ * in place, so passing it down would leave a memoized container stale; the snapshot is what makes
93
+ * `layer.hidden = true; update(layer)` re-render the layer while child identities survive, so
94
+ * keyed reconciliation mounts and unmounts only the membership delta.
95
+ * - **Deferral.** Children flow through `useDeferredValue`, so a membership change's urgent render
96
+ * is the layer component alone; the container's instance-list sweep re-renders at transition
97
+ * priority, interruptible, keeping frames flowing. `update(def)` writes on leaf defs are
98
+ * unaffected — they never pass through `children`.
99
+ * - **Admission.** Whether a leaf may mount its content, per {@link useStagedMount}. A leaf that is
100
+ * not yet admitted renders `null` rather than an empty container, so each batch is still created
101
+ * at its full size and never reallocates on mount. Defs stay the source of truth: `update()`
102
+ * against a not-yet-admitted layer is harmless, because admission mounts whatever the defs hold
103
+ * at that moment.
104
+ * @param layer - The layer to observe.
105
+ * @returns The current children snapshot and the admission flag.
106
+ */
107
+ export function useStagedChildren(layer) {
108
+ const snapshot = useSyncExternalStore((listener) => subscribeDef(layer, listener), () => childrenOf(layer));
109
+ const children = useDeferredValue(snapshot);
110
+ const admitted = useStagedMount(layer, !isLayerDefs(children));
111
+ return { children, admitted };
112
+ }
113
+ /**
114
+ * Whether a layer may mount its content, per the tree's scheduler.
115
+ *
116
+ * Consumes the scheduler {@link MountSchedulerContext} provides. Without one — a plain `<Layer>`
117
+ * mount, or staging disabled — everything is admitted immediately and no queueing happens. Under a
118
+ * scheduler, a leaf is admitted one per frame in flattened leaf order, and reports back from the
119
+ * admission commit so the next leaf is released only after this one's content committed.
120
+ * @param layer - The layer def.
121
+ * @param leaf - Whether the layer is a shape layer; only leaves are staged.
122
+ * @returns Whether the layer's content may mount.
123
+ */
124
+ export function useStagedMount(layer, leaf) {
125
+ const scheduler = use(MountSchedulerContext);
126
+ const admitted = useSyncExternalStore((onStoreChange) => (scheduler ? scheduler.subscribe(onStoreChange) : noopUnsubscribe), () => !leaf || !scheduler || scheduler.isAdmitted(layer));
127
+ // Registration from a passive effect, deliberately: effect order across the tree is the
128
+ // flattened leaf order, which is the admission priority.
129
+ useEffect(() => {
130
+ if (!scheduler || !leaf)
131
+ return undefined;
132
+ return scheduler.register(layer);
133
+ }, [scheduler, layer, leaf]);
134
+ // Runs in the admission commit, after the children's effects — the content's slots are claimed
135
+ // by the time the next leaf is released.
136
+ useEffect(() => {
137
+ if (scheduler && leaf && admitted)
138
+ scheduler.reportMounted(layer);
139
+ }, [scheduler, layer, leaf, admitted]);
140
+ return admitted;
141
+ }
142
+ /** The tree's scheduler, provided by `Scene` and inherited by every `Layer` beneath it. */
143
+ export const MountSchedulerContext = createContext(null);
144
+ // What subscribing without a scheduler returns: nothing to unsubscribe from.
145
+ const noopUnsubscribe = () => undefined;
@@ -0,0 +1,19 @@
1
+ import { type LayerDef } from '../../types/defs';
2
+ /**
3
+ * Builds the layer-visibility debug GUI: called by the tree's root `Layer` alone, a no-op without
4
+ * a debug GUI in context. Two folders, one checkbox per layer:
5
+ *
6
+ * - **Main layers** — the root's direct child layers, in child order, kept current through the
7
+ * same store subscription `update()` drives (a root membership change rebuilds the rows).
8
+ * - **Content layers** — every leaf (shape) layer in the tree, in the mount scheduler's
9
+ * registration order, which is the flattened leaf order. No new bookkeeping tracks the leaves:
10
+ * `useStagedMount` already registers each one with the scheduler, so this folder mirrors the
11
+ * scheduler's queue and follows leaves as they mount and unmount. Without a scheduler in
12
+ * context (a plain `<Layer>` mount outside `Scene`) the folder stays empty.
13
+ *
14
+ * The effect runs entirely against the defs and the GUI — toggles never re-render the layer tree
15
+ * beyond what `update()` itself triggers.
16
+ * @param layer - The tree's root layer def.
17
+ */
18
+ export declare function useVisibilityGui(layer: LayerDef): void;
19
+ //# sourceMappingURL=visibility-gui.d.ts.map