@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,321 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { degToRad } from '@expofp/geometry';
3
+ import { useFrame, useThree } from '@react-three/fiber';
4
+ import { useEffect, useLayoutEffect, useRef } from 'react';
5
+ import { Box3, Vector3 } from 'three';
6
+ import { useCameraControls } from '../controls';
7
+ import { CAMERA_FOV, CameraContext, createCameraHandle, zoomIdentityDistance, } from './camera-handle';
8
+ import { isPerspective } from './camera-kind';
9
+ import { useCanvasSize } from './canvas-size';
10
+ import { farPlane, MAX_PITCH, nearPlane } from './clipping';
11
+ import { useSpace } from './Space';
12
+ /**
13
+ * The camera the plan is drawn through, and the zoom that camera is read as.
14
+ *
15
+ * Everything the imperative renderer's camera system did, with one simplification the viewbox fit
16
+ * pays for: because {@link Space} sizes a world unit to a device pixel, the distance that frames the
17
+ * plan follows from the drawing buffer alone and none of the arithmetic here has to know what the
18
+ * scene holds. The near plane is the one exception, its guarantee being stated in plan units.
19
+ *
20
+ * **Zoom is the state and distance is derived.** The camera stores a distance, which the buffer
21
+ * height converts to a factor — so a resize would silently change the zoom if nothing held it. This
22
+ * remembers the factor and dollies back to it, which is what the imperative renderer kept a previous
23
+ * viewport height for; a user dolly writes the factor back on the controls' own update.
24
+ *
25
+ * **The one thing it needs from the scene is `space.scale`,** and only for the near plane. The
26
+ * imperative renderer kept its camera and scene independent because it applied its depth guarantee
27
+ * to world distances; that guarantee is stated in plan units, so converting it is what makes it hold
28
+ * on a plan drawn at any scale (see {@link nearPlane}). Nothing else here reads the scene.
29
+ *
30
+ * **All four limits are written here**, zoom and pitch alike, because they are the same kind of
31
+ * thing: a constraint on where the camera may go, holding for a programmatic move as much as for a
32
+ * dragged one. What the *user* may do is `Controls`'s business and nothing to do with these. Keeping
33
+ * them together also puts each limit beside the clipping plane derived from it — `maxPolarAngle`
34
+ * sizes the near plane, the maximum distance the far one.
35
+ *
36
+ * Configures R3F's camera rather than replacing it, which leaves `aspect` to R3F — it writes that on
37
+ * every resize, from the same size this reads. Mutations go through the store getter because the
38
+ * React Compiler refuses to lower a write to anything a hook returned.
39
+ * @param props - How far the plan may be zoomed and pitched, and what should reach the handle.
40
+ * @returns The children, under the camera handle.
41
+ */
42
+ export function Camera(props) {
43
+ // Read one by one rather than destructured with defaults: the React Compiler cannot yet lower a
44
+ // default inside a destructuring statement.
45
+ const minZoom = props.minZoom ?? DEFAULT_MIN_ZOOM;
46
+ const maxZoom = props.maxZoom ?? DEFAULT_MAX_ZOOM;
47
+ const minPitch = props.minPitch ?? DEFAULT_MIN_PITCH;
48
+ const maxPitch = props.maxPitch ?? DEFAULT_MAX_PITCH;
49
+ const bounds = props.bounds;
50
+ const smoothTime = props.smoothTime;
51
+ const children = props.children;
52
+ const get = useThree((state) => state.get);
53
+ const invalidate = useThree((state) => state.invalidate);
54
+ // The camera is *identified* rather than held, so the effects below re-run when R3F swaps it out
55
+ // and reach the new one through `get()` — which they must, since the React Compiler refuses to
56
+ // lower a write to anything a hook returned. The controls are held, because every write to them
57
+ // here goes through a function that takes them as an argument, which is a line the compiler is
58
+ // happy with and this file is short enough to keep to.
59
+ const cameraId = useThree((state) => state.camera.uuid);
60
+ const controls = useCameraControls();
61
+ const space = useSpace();
62
+ const { width, height, dpr } = useCanvasSize();
63
+ const bufferHeight = height * dpr;
64
+ // The zoom the camera is held at, so a resize can dolly back to it. A ref rather than state:
65
+ // nothing renders from it, and the frame loop writes it.
66
+ const zoomFactorRef = useRef(1);
67
+ // Not memoized by hand: the React Compiler caches the call against `bufferHeight`, `controls` and
68
+ // `get`, which is its whole dependency list.
69
+ const handle = createCameraHandle(bufferHeight, () => cameraDistance(controls, get().camera));
70
+ // What the imperative renderer did when it built its camera and its controls.
71
+ useLayoutEffect(() => {
72
+ const state = get();
73
+ const camera = state.camera;
74
+ if (!isPerspective(camera))
75
+ return;
76
+ camera.fov = CAMERA_FOV;
77
+ // The plan's own up axis, so the camera sits upright over a plan authored y-down.
78
+ camera.up.set(0, 0, -1);
79
+ camera.updateProjectionMatrix();
80
+ // camera-controls builds its orbit space from the up axis when it is *constructed*, which is
81
+ // during the render that mounts it and so before this ever runs. Nothing else tells it the axis
82
+ // moved, and left stale it orbits the plan from the wrong side — where every face is turned away
83
+ // and the plan draws as an empty canvas.
84
+ controls?.updateCameraUp();
85
+ }, [cameraId, controls, get]);
86
+ // How long a programmatic move takes. Its own effect for the same reason the pitch limits have
87
+ // one: it shares no input with the framing, and folding it in would re-dolly the camera whenever
88
+ // a caller changed a duration.
89
+ useLayoutEffect(() => {
90
+ if (controls)
91
+ setSmoothTime(controls, smoothTime);
92
+ }, [controls, smoothTime]);
93
+ // The camera's boundary. A **passive** effect rather than a layout one, which is what makes it
94
+ // read the right fit: `Scene` refits the space in a layout effect, layout effects run
95
+ // child-before-parent, and this component is `Scene`'s child — so a layout effect here would
96
+ // convert the region through the fit it is in the middle of replacing. Every passive effect in
97
+ // the tree runs after every layout effect, which is exactly late enough.
98
+ //
99
+ // It re-runs on a resize because the boundary is stated in world units and the fit that produces
100
+ // them moves with the canvas. `bounds` is compared by identity, as any prop is: a caller changing
101
+ // the region hands over a new `Box` rather than editing the one it already gave.
102
+ useEffect(() => {
103
+ if (!controls)
104
+ return;
105
+ setCameraBounds(controls, bounds, space);
106
+ invalidate();
107
+ }, [bounds, controls, dpr, height, invalidate, space, width]);
108
+ // How far the plan may be pitched. Its own effect: nothing here shares an input with the framing
109
+ // below, so folding the two together would rewrite the frustum and re-dolly the camera every time
110
+ // a caller changed a pitch limit — and would tie the limits to the canvas having a size, which a
111
+ // plan that has not been laid out yet still does not need.
112
+ useLayoutEffect(() => {
113
+ if (controls)
114
+ setPitchLimits(controls, minPitch, maxPitch);
115
+ }, [controls, maxPitch, minPitch]);
116
+ // The framing: how far the camera may go, where that puts the far plane, and the dolly back to
117
+ // the zoom it was already at. What the imperative renderer did on every resize, in the same order
118
+ // and for the same reasons — `aspect` is the one step missing, because R3F writes it itself on
119
+ // each resize, from the same size this reads.
120
+ //
121
+ // These three are one effect because they are one transaction: the distance limits must be in
122
+ // place before the dolly, or it is clamped by the bounds of the previous buffer height, and the
123
+ // far plane is derived from the same `maxDistance` the limits are.
124
+ useLayoutEffect(() => {
125
+ const camera = get().camera;
126
+ if (!isPerspective(camera))
127
+ return;
128
+ // Skip transient 0-sized canvases, which would make a frustum full of NaNs.
129
+ const identity = zoomIdentityDistance(bufferHeight, CAMERA_FOV);
130
+ if (identity <= 0)
131
+ return;
132
+ // Preserve zoom across the resize: the factor was measured against the previous buffer height,
133
+ // and is turned back into a distance through the new one.
134
+ const distance = handle.zoomFactorToDistance(zoomFactorRef.current);
135
+ const maxDistance = identity / minZoom;
136
+ const minDistance = identity / maxZoom;
137
+ camera.far = farPlane(maxDistance);
138
+ camera.updateProjectionMatrix();
139
+ if (controls) {
140
+ setDistanceLimits(controls, minDistance, maxDistance);
141
+ void controls.dollyTo(distance, false);
142
+ }
143
+ else {
144
+ // Nothing to dolly before the controls mount, so the camera is placed straight away. The
145
+ // world matrix is refreshed with it: three re-derives it at render time, but `useFrame`
146
+ // callbacks run before that and `Lighting` reads `camera.matrixWorld` directly, so without
147
+ // this the key light spends a frame aimed from wherever the camera used to be — and under the
148
+ // demand frameloop, possibly no second frame comes to correct it.
149
+ camera.position.set(0, 0, -distance);
150
+ camera.updateMatrixWorld();
151
+ }
152
+ invalidate();
153
+ }, [bufferHeight, cameraId, controls, get, handle, invalidate, maxZoom, minZoom]);
154
+ useEffect(() => {
155
+ if (!isPerspective(get().camera))
156
+ return undefined;
157
+ if (!controls)
158
+ return undefined;
159
+ /**
160
+ * Learns the zoom from a move the controls have made, so a user dolly is what the next resize
161
+ * is held at.
162
+ *
163
+ * Only from their `update`, never from the distance they hold when this mounts. A controls
164
+ * instance is constructed during render, from whichever camera R3F had at the time — and R3F
165
+ * may swap that camera out from under a committed effect — so the distance before the fit above
166
+ * has run is not a framing anything chose. Seeding the zoom from it made the fit dolly to it,
167
+ * and the plan settled at the closest zoom its bounds allowed.
168
+ */
169
+ function trackZoom() {
170
+ zoomFactorRef.current = handle.distanceToZoomFactor(controls.distance);
171
+ }
172
+ controls.addEventListener('update', trackZoom);
173
+ return () => controls.removeEventListener('update', trackZoom);
174
+ }, [cameraId, controls, get, handle]);
175
+ // The near plane, synced on every rendered frame rather than from the controls' update event: the
176
+ // dolly is only one of its inputs. The other is `space.scale`, and a refit moves that without any
177
+ // camera event at all — the space is stable and this component never re-renders for it, so a
178
+ // by-the-frame read is what keeps the guarantee under either cause. Runs at the default priority,
179
+ // after the controls have updated at `-1`, so the frame about to draw is the one measured; on a
180
+ // still frame the recomputation lands on `camera.near` unchanged and writes nothing.
181
+ useFrame((state) => {
182
+ const camera = state.camera;
183
+ if (!isPerspective(camera))
184
+ return;
185
+ // No pitch is passed: `nearPlane` is sized against the hard cap, so this holds whatever the
186
+ // controls currently allow — including for a caller who mounted controls of their own and
187
+ // enforces nothing.
188
+ const near = nearPlane(cameraDistance(controls, state.camera), space.scale);
189
+ if (near === undefined || camera.near === near)
190
+ return;
191
+ camera.near = near;
192
+ camera.updateProjectionMatrix();
193
+ });
194
+ return _jsx(CameraContext, { value: handle, children: children });
195
+ }
196
+ /**
197
+ * Holds the camera's pitch inside the bounds, moving it there if it is already outside.
198
+ *
199
+ * The second clamp — of the camera, not of the limit — is what makes a tightened limit take effect
200
+ * on a plan already tilted past it, rather than only constraining the next gesture; which is what
201
+ * the imperative renderer's pitch handler did whenever its options changed.
202
+ * @param controls - The controls to constrain.
203
+ * @param minPitch - The shallowest pitch, in degrees.
204
+ * @param maxPitch - The steepest pitch, in degrees, itself capped at {@link MAX_PITCH}.
205
+ */
206
+ function setPitchLimits(controls, minPitch, maxPitch) {
207
+ controls.minPolarAngle = degToRad(minPitch);
208
+ // Held to the hard cap however much more was asked for. Compared in degrees, which is the unit
209
+ // both are stated in, so the conversion happens once and after the clamp rather than either side
210
+ // of it.
211
+ controls.maxPolarAngle = degToRad(Math.min(maxPitch, MAX_PITCH));
212
+ if (controls.polarAngle < controls.minPolarAngle) {
213
+ void controls.rotatePolarTo(controls.minPolarAngle, false);
214
+ }
215
+ else if (controls.polarAngle > controls.maxPolarAngle) {
216
+ void controls.rotatePolarTo(controls.maxPolarAngle, false);
217
+ }
218
+ }
219
+ /**
220
+ * How far the camera is from the plan, which is what both the zoom factor and the near plane are
221
+ * read off.
222
+ *
223
+ * The controls own that distance once they mount. Before then — the frames between this component
224
+ * placing the camera and `Controls` publishing itself, and any scene rendered without controls at
225
+ * all — the camera has been put at `(0, 0, -distance)` looking straight down by the fit and nothing
226
+ * else has moved it, so its z offset *is* that distance. Not an approximation standing in for the
227
+ * real thing: the same number, still true by construction.
228
+ * @param controls - The scene's controls, when they have mounted.
229
+ * @param camera - The camera in use.
230
+ * @returns The distance in world units.
231
+ */
232
+ function cameraDistance(controls, camera) {
233
+ return controls?.distance ?? Math.abs(camera.position.z);
234
+ }
235
+ /**
236
+ * Holds the camera's dolly between the two distances the zoom bounds work out to.
237
+ *
238
+ * A function taking the controls rather than two assignments in the effect, which is what lets the
239
+ * caller hold them from a hook: the React Compiler rejects a write to a *field of* a hook's return
240
+ * value, and is content once that value is an argument someone else was passed.
241
+ * @param controls - The controls to constrain.
242
+ * @param minDistance - The closest the camera may come to the plan, in world units.
243
+ * @param maxDistance - The furthest it may retreat.
244
+ */
245
+ function setDistanceLimits(controls, minDistance, maxDistance) {
246
+ controls.minDistance = minDistance;
247
+ controls.maxDistance = maxDistance;
248
+ }
249
+ /**
250
+ * Holds the camera's target inside a region of the plan, or lets it go anywhere.
251
+ *
252
+ * The boundary camera-controls takes is in **world** units, and the plan-to-world fit changes on
253
+ * every resize, so the caller re-runs this whenever the canvas does.
254
+ * @param controls - The controls to constrain.
255
+ * @param bounds - The region, in plan units, or `undefined` to remove the boundary.
256
+ * @param space - The scene's space, which converts the region into world units.
257
+ */
258
+ function setCameraBounds(controls, bounds, space) {
259
+ if (!bounds) {
260
+ controls.setBoundary(undefined);
261
+ return;
262
+ }
263
+ // Two corners are the whole box: the plan-to-world mapping scales and translates, so opposite
264
+ // corners stay opposite.
265
+ const min = space.planToWorld({ ...bounds.min, z: 0 }, _min);
266
+ const max = space.planToWorld({ ...bounds.max, z: 0 }, _max);
267
+ controls.setBoundary(_boundary.set(min, max));
268
+ }
269
+ /**
270
+ * How long the controls take to settle a programmatic move.
271
+ *
272
+ * **Every duration is written on every call**, so the prop says the whole truth rather than
273
+ * accumulating: naming one sets it, and *not* naming one returns it to the default. Going from
274
+ * `{ zoomTime: 0.5 }` to `{ panTime: 2 }` therefore means "pan takes two seconds and zoom goes back
275
+ * to normal" — a caller who wanted both would have named both. That is what a prop should mean, and
276
+ * it is not what the library's own setter does: handed an object it *merges*, writing the durations
277
+ * named and leaving the rest at whatever they last were.
278
+ *
279
+ * The four names a caller uses map onto the four the library drives; `zoom` and `offset` are left
280
+ * alone, since nothing in this renderer moves them.
281
+ *
282
+ * A function taking the controls for the same reason {@link setPitchLimits} is one: the React
283
+ * Compiler rejects a write to a field of a hook's return value and is content once that value is an
284
+ * argument someone else was passed.
285
+ * @param controls - The controls to configure.
286
+ * @param smoothTime - The durations a caller asked for, if any.
287
+ */
288
+ function setSmoothTime(controls, smoothTime) {
289
+ controls.smoothTime = {
290
+ dolly: smoothTime?.zoomTime ?? DEFAULT_SMOOTH_TIME,
291
+ truck: smoothTime?.panTime ?? DEFAULT_SMOOTH_TIME,
292
+ rotateAzimuth: smoothTime?.rollTime ?? DEFAULT_SMOOTH_TIME,
293
+ rotatePolar: smoothTime?.pitchTime ?? DEFAULT_SMOOTH_TIME,
294
+ };
295
+ }
296
+ /** How far out the plan may be pushed when a caller names no limit: a tenth of the framing that fits it. */
297
+ const DEFAULT_MIN_ZOOM = 0.1;
298
+ /** How far in the plan may be brought when a caller names no limit. */
299
+ const DEFAULT_MAX_ZOOM = 35;
300
+ /** How far the plan may be pitched toward face-on when a caller names no limit: all the way. */
301
+ const DEFAULT_MIN_PITCH = 0;
302
+ /**
303
+ * How far the plan may be pitched away from face-on when a caller names no limit: as far as it is
304
+ * allowed to go.
305
+ *
306
+ * Written as its own constant rather than used directly, because the two answer different questions
307
+ * — "what do you get if you say nothing" and "what may you not exceed" — and only happen to agree.
308
+ * Lowering the default would be a judgement about how plans look; lowering the cap would be a
309
+ * judgement about what the depth buffer can resolve.
310
+ */
311
+ const DEFAULT_MAX_PITCH = MAX_PITCH;
312
+ /**
313
+ * How long a programmatic move takes to settle when a caller names no duration, in seconds.
314
+ *
315
+ * `camera-controls`' own default, restated because {@link setSmoothTime} writes every duration on
316
+ * every call rather than merging — see there for why.
317
+ */
318
+ const DEFAULT_SMOOTH_TIME = 0.25;
319
+ const _boundary = new Box3();
320
+ const _min = new Vector3();
321
+ const _max = new Vector3();
@@ -0,0 +1,44 @@
1
+ import { type Box } from '@expofp/geometry';
2
+ import { type ReactNode } from 'react';
3
+ import { type SceneDef } from '../types/defs';
4
+ import { type CameraProps } from './Camera';
5
+ /** Props for {@link Scene}: the scene to draw, plus the camera limits it passes straight down. */
6
+ export interface SceneProps extends CameraProps {
7
+ /** The scene definition to render. */
8
+ sceneDef: SceneDef;
9
+ /**
10
+ * Soft cap, in megabytes, on the GPU memory the image atlases may consume; see
11
+ * {@link RendererProps.memoryLimit}.
12
+ */
13
+ memoryLimit?: number;
14
+ /**
15
+ * The part of the canvas the plan should be framed inside; see {@link RendererProps.visibleRect}.
16
+ */
17
+ visibleRect?: Box;
18
+ /**
19
+ * Whether an external host owns the camera. When set, the scene mounts no {@link Camera} — and
20
+ * with it no fit, no `visibleRect` framing and no camera commands: the view arrives from the
21
+ * host, one matrix per frame, written by the external mount. The space and the graph are the
22
+ * same either way.
23
+ */
24
+ external?: boolean;
25
+ /**
26
+ * Components that need the scene's space, camera and re-layout registry — the renderer's own
27
+ * controls and missed-click bridge, and anything a caller renders inside `<Renderer>`.
28
+ */
29
+ children?: ReactNode;
30
+ }
31
+ /**
32
+ * A floor-plan scene: the plan's own space, the camera that frames it, and the layer tree drawn
33
+ * through both.
34
+ *
35
+ * Composed from two halves — {@link PlanSpace}, which owns the {@link Space}, and
36
+ * {@link SceneGraph}, which keeps it fitted and draws the layer tree under the re-layout
37
+ * registry. Between them sits the renderer's own {@link Camera}: the one part `external` leaves
38
+ * unmounted, since there the host owns the camera and the fit alike and the external mount writes
39
+ * the view straight onto its own bare camera.
40
+ * @param props - The scene to render, and what should reach its space.
41
+ * @returns The scene.
42
+ */
43
+ export declare function Scene(props: SceneProps): import("react").JSX.Element;
44
+ //# sourceMappingURL=Scene.d.ts.map
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useThree } from '@react-three/fiber';
3
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
4
+ import { LayoutDriver } from '../events/LayoutDriver';
5
+ import { Background } from '../objects/Background';
6
+ import { TextureBudget, TextureBudgetContext } from '../objects/images/texture-budget';
7
+ import { Layer } from '../objects/layer/index';
8
+ import { createMountScheduler, MountSchedulerContext } from '../objects/layer/mount-scheduler';
9
+ import { Lighting } from '../objects/Lighting';
10
+ import { createLogger, printScene } from '../utils/logging';
11
+ import { Camera } from './Camera';
12
+ import { useCanvasSize } from './canvas-size';
13
+ import { Space, SpaceContext, useSpace } from './Space';
14
+ import { VisibleRect } from './VisibleRect';
15
+ const logger = createLogger('scene');
16
+ /**
17
+ * A floor-plan scene: the plan's own space, the camera that frames it, and the layer tree drawn
18
+ * through both.
19
+ *
20
+ * Composed from two halves — {@link PlanSpace}, which owns the {@link Space}, and
21
+ * {@link SceneGraph}, which keeps it fitted and draws the layer tree under the re-layout
22
+ * registry. Between them sits the renderer's own {@link Camera}: the one part `external` leaves
23
+ * unmounted, since there the host owns the camera and the fit alike and the external mount writes
24
+ * the view straight onto its own bare camera.
25
+ * @param props - The scene to render, and what should reach its space.
26
+ * @returns The scene.
27
+ */
28
+ export function Scene(props) {
29
+ const { sceneDef, memoryLimit, visibleRect, external, children, ...cameraProps } = props;
30
+ const { viewbox, staticTransform } = sceneDef;
31
+ useEffect(() => {
32
+ if (visibleRect && staticTransform) {
33
+ logger.warn('visibleRect is ignored for a scene with a staticTransform');
34
+ }
35
+ }, [visibleRect, staticTransform]);
36
+ const graph = (_jsx(SceneGraph, { sceneDef: sceneDef, memoryLimit: memoryLimit, children: children }));
37
+ return (_jsx(PlanSpace, { sceneDef: sceneDef, children: external ? (graph) : (_jsxs(Camera, { ...cameraProps, children: [visibleRect && !staticTransform && _jsx(VisibleRect, { rect: visibleRect, viewbox: viewbox }), graph] })) }));
38
+ }
39
+ /**
40
+ * Owns the plan's {@link Space} and publishes it above everything else.
41
+ *
42
+ * This is the one mount point the rest hangs from, which is what lets the pieces reach each other
43
+ * without a provider apiece — a component that converts coordinates or measures the plan does not
44
+ * have to be inside the layer tree to do it. One space for the scene's lifetime is the point: its
45
+ * consumers read the current fit through a stable identity, so a resize re-renders none of them —
46
+ * see the contract on {@link Space}. Created already fitted rather than refit into shape by
47
+ * {@link SceneGraph}'s effect, so anything that measures the plan while mounting — the layout
48
+ * driver seeding its pixel size, defs laying out before the first frame — reads the real fit, not
49
+ * a placeholder.
50
+ * @param props - The scene whose space to own, and everything mounted under it.
51
+ * @returns The children, under the space.
52
+ */
53
+ function PlanSpace(props) {
54
+ const { sceneDef, children } = props;
55
+ const { viewbox, staticTransform } = sceneDef;
56
+ const { width, height, dpr } = useCanvasSize();
57
+ const [space] = useState(() => new Space({ viewbox, width, height, dpr, staticTransform }));
58
+ return _jsx(SpaceContext, { value: space, children: children });
59
+ }
60
+ /**
61
+ * The layer tree drawn through the scene's space, under the re-layout registry — and the keeper of
62
+ * the fit, which it re-applies whenever the canvas resizes.
63
+ *
64
+ * The refit is a layout effect rather than render work, because the mutation must land exactly once
65
+ * per commit — a render the concurrent renderer discards would otherwise move a fit no commit asked
66
+ * for. It runs before an enclosing {@link Camera}'s own layout effects (child before parent), and
67
+ * that is fine for the same reason the original ordering was: nothing reads the fit during commit —
68
+ * the camera's near plane, the one consumer that would care, syncs from the frame loop.
69
+ *
70
+ * The matrix goes onto a group with `matrixAutoUpdate` off, rather than decomposed into
71
+ * `position`/`scale` props, because a `staticTransform` may be any affine matrix and there is no
72
+ * decomposition worth the round trip. The mirrored z the fit carries is documented on
73
+ * {@link Space}: a def authored at elevation 10 lands nearer the camera, and floorplan never learns
74
+ * about the axis mapping. Three already handles the negative determinant — it flips `gl.frontFace`
75
+ * for a mirrored world matrix and carries normals through the normal matrix — so culling and
76
+ * lighting need no compensation.
77
+ * @param props - The scene to draw and what rides along inside the registry.
78
+ * @returns The graph.
79
+ */
80
+ function SceneGraph(props) {
81
+ const { sceneDef, memoryLimit, children } = props;
82
+ const { viewbox, staticTransform, rootLayer } = sceneDef;
83
+ const { width, height, dpr } = useCanvasSize();
84
+ const invalidate = useThree((state) => state.invalidate);
85
+ const space = useSpace();
86
+ const [scheduler] = useState(createMountScheduler);
87
+ // One budget for the scene's lifetime; the limit is synced rather than the budget rebuilt, so an
88
+ // atlas's registration survives a `memoryLimit` change.
89
+ const [budget] = useState(() => new TextureBudget(memoryLimit));
90
+ const worldRef = useRef(null);
91
+ useLayoutEffect(() => {
92
+ space.refit({ viewbox, width, height, dpr, staticTransform });
93
+ // Written by hand because the refit mutates in place: the group was given the matrix once at
94
+ // mount, and R3F re-applies a `matrix` prop only when its identity changes. The dirty flag makes
95
+ // three recompose the subtree's world matrices on the next pass, and the invalidate asks for it.
96
+ const world = worldRef.current;
97
+ if (world) {
98
+ world.matrix.copy(space.matrix);
99
+ world.matrixWorldNeedsUpdate = true;
100
+ }
101
+ invalidate();
102
+ }, [space, viewbox, width, height, dpr, staticTransform, invalidate]);
103
+ useEffect(() => {
104
+ budget.setMemoryLimit(memoryLimit);
105
+ if (!memoryLimit)
106
+ logger.debug('no memory limit set, image atlases upload at full size');
107
+ }, [budget, memoryLimit]);
108
+ useEffect(() => {
109
+ printScene(sceneDef, logger.debug);
110
+ }, [sceneDef]);
111
+ return (_jsxs(LayoutDriver, { children: [_jsxs("group", { name: "world", ref: worldRef, matrixAutoUpdate: false, matrix: space.matrix, children: [_jsx(Lighting, {}), _jsx(TextureBudgetContext, { value: budget, children: _jsx(MountSchedulerContext, { value: scheduler, children: _jsx(Layer, { layer: rootLayer, children: sceneDef.background !== undefined && _jsx(Background, { color: sceneDef.background }) }) }) })] }), children] }));
112
+ }