@expofp/renderer 3.2.4 → 3.27.0

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 -8028
  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,72 @@
1
+ import { type Box } from '@expofp/geometry';
2
+ /** Props for {@link VisibleRect}. */
3
+ export interface VisibleRectProps {
4
+ /**
5
+ * The part of the canvas the plan should be framed inside, in css pixels from the canvas's
6
+ * top-left corner.
7
+ *
8
+ * Required, because there is no such thing as this component with nothing to do: a caller who
9
+ * named no rect does not render it at all, which is what keeps the default path free of a
10
+ * component whose every effect would return early.
11
+ */
12
+ rect: Box;
13
+ /** The scene's viewbox, in plan units: what "the plan fits" is measured against. */
14
+ viewbox: Box;
15
+ }
16
+ /**
17
+ * Frames the plan inside a sub-region of the canvas, for a host whose own UI covers the rest.
18
+ *
19
+ * **The plan is still drawn across the whole canvas.** Nothing is clipped and nothing is cropped:
20
+ * the frustum is *expanded*, so the strips behind a header or a bottom sheet show more of the plan
21
+ * than they would otherwise, while the viewbox itself is guaranteed to land inside the clear
22
+ * region. That is the point of doing this in the projection rather than with `gl.viewport`.
23
+ *
24
+ * **Two calls, and nothing else in the renderer knows.** The transform is a uniform scale and a
25
+ * translate in clip space, which is exactly what {@link PerspectiveCamera.setViewOffset} expresses
26
+ * when its virtual screen is *smaller* than the rendered window — the reverse of the tiled-render
27
+ * case it is usually reached for. Because it lands in `projectionMatrix`, picking, R3F's pointer
28
+ * events, frustum culling, `Space`'s coordinate conversions and the pixel-size measurement all follow it with
29
+ * no seam: three re-derives `view` inside every `updateProjectionMatrix()`, so it also survives
30
+ * R3F's per-resize `aspect` write and `Camera`'s near and far updates.
31
+ *
32
+ * The second call is for `camera-controls`, which is the one thing that does *not* read the
33
+ * projection: it converts a pixel drag to a world offset through `fov` and the element's height,
34
+ * and finds the cursor's world position for `dollyToCursor` the same way. `setViewport` exists for
35
+ * exactly that — it tells the controls which part of the element the image occupies — and handed
36
+ * the landing box it makes every one of those conversions correct, with no fork patch and no
37
+ * speed fudge. The box keeps the canvas's aspect by construction, which is what leaves
38
+ * `camera.aspect` right inside the `dollyToCursor` arithmetic.
39
+ *
40
+ * **The scale is measured against the plan, not the canvas** — see {@link framingScale}.
41
+ * @param props - Where to frame the plan, and what to measure the framing against.
42
+ * @returns Nothing; this renders no scene node.
43
+ */
44
+ export declare function VisibleRect(props: VisibleRectProps): null;
45
+ /**
46
+ * How far the plan has to shrink to fit inside the rect: `1` when it already does.
47
+ *
48
+ * **Measured against the plan's own footprint, not the canvas's.** Fitting the canvas into the rect
49
+ * would compose two contain-fits — plan into canvas, then canvas into rect — and a product of two
50
+ * independent minima is not the minimum the imperative renderer took over the same pair. The two
51
+ * agree only when the same axis binds both, and disagree in the commonest configuration there is: a
52
+ * wide hall on a portrait phone, where the plan already fits the clear band and fitting the canvas
53
+ * into it would shrink the plan by two thirds for nothing.
54
+ *
55
+ * Measured against the plan, the factorization telescopes back to what the imperative renderer
56
+ * computed in one step:
57
+ *
58
+ * ```
59
+ * fitScale · framingScale = min(W/a, H/b) · min(w/(a·fit), h/(b·fit)) = min(w/a, h/b)
60
+ * ```
61
+ *
62
+ * The clamp only guards a rect larger than the canvas; for a rect inside it the result is already
63
+ * at most `1`, because the fit leaves the plan touching at least one edge.
64
+ * @param rect - The region to frame the plan inside, in css pixels.
65
+ * @param viewbox - The scene's viewbox, in plan units.
66
+ * @param width - The canvas width in css pixels.
67
+ * @param height - The canvas height in css pixels.
68
+ * @param dpr - Device pixels per css pixel.
69
+ * @returns The scale, in `(0, 1]`.
70
+ */
71
+ export declare function framingScale(rect: Box, viewbox: Box, width: number, height: number, dpr: number): number;
72
+ //# sourceMappingURL=VisibleRect.d.ts.map
@@ -0,0 +1,129 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ import { useLayoutEffect } from 'react';
3
+ import { useCameraControls } from '../controls';
4
+ import { isPerspective } from './camera-kind';
5
+ import { useCanvasSize } from './canvas-size';
6
+ import { fitScale } from './Space';
7
+ /**
8
+ * Frames the plan inside a sub-region of the canvas, for a host whose own UI covers the rest.
9
+ *
10
+ * **The plan is still drawn across the whole canvas.** Nothing is clipped and nothing is cropped:
11
+ * the frustum is *expanded*, so the strips behind a header or a bottom sheet show more of the plan
12
+ * than they would otherwise, while the viewbox itself is guaranteed to land inside the clear
13
+ * region. That is the point of doing this in the projection rather than with `gl.viewport`.
14
+ *
15
+ * **Two calls, and nothing else in the renderer knows.** The transform is a uniform scale and a
16
+ * translate in clip space, which is exactly what {@link PerspectiveCamera.setViewOffset} expresses
17
+ * when its virtual screen is *smaller* than the rendered window — the reverse of the tiled-render
18
+ * case it is usually reached for. Because it lands in `projectionMatrix`, picking, R3F's pointer
19
+ * events, frustum culling, `Space`'s coordinate conversions and the pixel-size measurement all follow it with
20
+ * no seam: three re-derives `view` inside every `updateProjectionMatrix()`, so it also survives
21
+ * R3F's per-resize `aspect` write and `Camera`'s near and far updates.
22
+ *
23
+ * The second call is for `camera-controls`, which is the one thing that does *not* read the
24
+ * projection: it converts a pixel drag to a world offset through `fov` and the element's height,
25
+ * and finds the cursor's world position for `dollyToCursor` the same way. `setViewport` exists for
26
+ * exactly that — it tells the controls which part of the element the image occupies — and handed
27
+ * the landing box it makes every one of those conversions correct, with no fork patch and no
28
+ * speed fudge. The box keeps the canvas's aspect by construction, which is what leaves
29
+ * `camera.aspect` right inside the `dollyToCursor` arithmetic.
30
+ *
31
+ * **The scale is measured against the plan, not the canvas** — see {@link framingScale}.
32
+ * @param props - Where to frame the plan, and what to measure the framing against.
33
+ * @returns Nothing; this renders no scene node.
34
+ */
35
+ export function VisibleRect(props) {
36
+ const { rect, viewbox } = props;
37
+ const invalidate = useThree((state) => state.invalidate);
38
+ // Held rather than identified. `Camera` keeps only its camera's uuid because it *assigns* to
39
+ // fields of it and the React Compiler refuses to lower that; everything here is a method call,
40
+ // which the compiler is content with — so the dependency can be the camera itself and the effect
41
+ // needs no second read to find it.
42
+ const camera = useThree((state) => state.camera);
43
+ const controls = useCameraControls();
44
+ const { width, height, dpr } = useCanvasSize();
45
+ // Derived during render and depended on as three numbers, so a caller handing over a fresh `Box`
46
+ // each render — a MobX computed, say — does not re-place the framing for an unchanged rect.
47
+ const scale = framingScale(rect, viewbox, width, height, dpr);
48
+ const centerX = rect.center.x;
49
+ const centerY = rect.center.y;
50
+ useLayoutEffect(() => {
51
+ if (!isPerspective(camera))
52
+ return undefined;
53
+ if (width <= 0 || height <= 0)
54
+ return undefined;
55
+ // Nothing to place: a rect the plan already fills, sat in the middle of the canvas, works out to
56
+ // the identity, and leaving the camera untouched says so.
57
+ if (scale === 1 && centerX === width / 2 && centerY === height / 2)
58
+ return undefined;
59
+ placeFraming(camera, controls, scale, centerX, centerY, width, height);
60
+ invalidate();
61
+ return () => {
62
+ camera.clearViewOffset();
63
+ controls?.setViewport(null, 0, 0, 0);
64
+ invalidate();
65
+ };
66
+ }, [scale, centerX, centerY, width, height, camera, controls, invalidate]);
67
+ return null;
68
+ }
69
+ /**
70
+ * How far the plan has to shrink to fit inside the rect: `1` when it already does.
71
+ *
72
+ * **Measured against the plan's own footprint, not the canvas's.** Fitting the canvas into the rect
73
+ * would compose two contain-fits — plan into canvas, then canvas into rect — and a product of two
74
+ * independent minima is not the minimum the imperative renderer took over the same pair. The two
75
+ * agree only when the same axis binds both, and disagree in the commonest configuration there is: a
76
+ * wide hall on a portrait phone, where the plan already fits the clear band and fitting the canvas
77
+ * into it would shrink the plan by two thirds for nothing.
78
+ *
79
+ * Measured against the plan, the factorization telescopes back to what the imperative renderer
80
+ * computed in one step:
81
+ *
82
+ * ```
83
+ * fitScale · framingScale = min(W/a, H/b) · min(w/(a·fit), h/(b·fit)) = min(w/a, h/b)
84
+ * ```
85
+ *
86
+ * The clamp only guards a rect larger than the canvas; for a rect inside it the result is already
87
+ * at most `1`, because the fit leaves the plan touching at least one edge.
88
+ * @param rect - The region to frame the plan inside, in css pixels.
89
+ * @param viewbox - The scene's viewbox, in plan units.
90
+ * @param width - The canvas width in css pixels.
91
+ * @param height - The canvas height in css pixels.
92
+ * @param dpr - Device pixels per css pixel.
93
+ * @returns The scale, in `(0, 1]`.
94
+ */
95
+ export function framingScale(rect, viewbox, width, height, dpr) {
96
+ const fit = fitScale(viewbox, width * dpr, height * dpr);
97
+ // The plan's footprint on the canvas at zoom 1, in device pixels — the viewbox fit sizes one
98
+ // world unit to one device pixel, so this is the viewbox scaled and nothing more.
99
+ const planWidth = viewbox.size.width * fit;
100
+ const planHeight = viewbox.size.height * fit;
101
+ if (!(planWidth > 0) || !(planHeight > 0))
102
+ return 1;
103
+ return Math.min((rect.size.width * dpr) / planWidth, (rect.size.height * dpr) / planHeight, 1);
104
+ }
105
+ /**
106
+ * Writes the framing onto the camera and the controls.
107
+ *
108
+ * A function taking both rather than two statements in the effect, which is what lets the caller
109
+ * hold the controls from a hook: the React Compiler rejects a write to a *field of* a hook's return
110
+ * value and is content once that value is an argument someone else was passed.
111
+ *
112
+ * The camera call reads: the natural frustum maps to a virtual screen of `scale · canvas` css
113
+ * pixels, positioned so its centre lands on the rect's centre, and the canvas is the window onto
114
+ * it. The controls call names the same box in the element's own coordinates, `y` measured from the
115
+ * bottom as `setViewport` takes it.
116
+ * @param camera - The camera to place the framing on.
117
+ * @param controls - The scene's controls, when they have mounted.
118
+ * @param scale - How far the plan shrinks; see {@link framingScale}.
119
+ * @param centerX - The rect's centre, in css pixels from the canvas's left edge.
120
+ * @param centerY - The rect's centre, in css pixels from the canvas's top edge.
121
+ * @param width - The canvas width in css pixels.
122
+ * @param height - The canvas height in css pixels.
123
+ */
124
+ function placeFraming(camera, controls, scale, centerX, centerY, width, height) {
125
+ const boxWidth = scale * width;
126
+ const boxHeight = scale * height;
127
+ camera.setViewOffset(boxWidth, boxHeight, boxWidth / 2 - centerX, boxHeight / 2 - centerY, width, height);
128
+ controls?.setViewport(centerX - boxWidth / 2, height - boxHeight / 2 - centerY, boxWidth, boxHeight);
129
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The camera's zoom, as the rest of the renderer asks about it.
3
+ *
4
+ * Zoom is a factor, not a distance: `1` frames the scene's viewbox exactly, `2` shows half of it.
5
+ * The camera stores a distance, and the two convert through the buffer height — which is why this
6
+ * is a handle published by {@link Camera} rather than arithmetic a caller could do itself.
7
+ */
8
+ export interface CameraHandle {
9
+ /** The zoom factor the camera is currently at. */
10
+ readonly zoomFactor: number;
11
+ /**
12
+ * The camera distance that shows the plan at a zoom factor.
13
+ * @param zoomFactor - The factor to convert.
14
+ * @returns The distance in world units.
15
+ */
16
+ zoomFactorToDistance(zoomFactor: number): number;
17
+ /**
18
+ * The zoom factor a camera distance shows the plan at.
19
+ * @param distance - The distance in world units.
20
+ * @returns The factor.
21
+ */
22
+ distanceToZoomFactor(distance: number): number;
23
+ }
24
+ /** The scene's camera handle, published by {@link Camera}. */
25
+ export declare const CameraContext: import("react").Context<CameraHandle | undefined>;
26
+ /**
27
+ * The scene's camera handle, once {@link Camera} has published it.
28
+ * @returns The handle, or `undefined` when rendered outside a scene.
29
+ */
30
+ export declare function useCamera(): CameraHandle | undefined;
31
+ /**
32
+ * The zoom handle {@link Camera} publishes.
33
+ *
34
+ * A plain object rather than anything reactive: every member is derived from the buffer height it
35
+ * was built against and whatever the camera holds when it is asked, so there is no state here to go
36
+ * stale — only a factor and the two conversions that turn it into a distance and back.
37
+ *
38
+ * Takes a reading rather than the canvas state: the one live number it needs is how far the camera
39
+ * is from the plan, and asking for that instead of for the store keeps this file pure arithmetic —
40
+ * with no view on where a distance comes from, and nothing to import from `controls`.
41
+ * @param bufferHeight - The drawing buffer's height in device pixels.
42
+ * @param getDistance - How far the camera currently is from the plan, in world units, read at the
43
+ * moment of asking.
44
+ * @returns The handle.
45
+ */
46
+ export declare function createCameraHandle(bufferHeight: number, getDistance: () => number): CameraHandle;
47
+ /**
48
+ * The camera distance at which the drawing buffer's height covers exactly that many world units —
49
+ * zoom factor `1`.
50
+ *
51
+ * Paired with the viewbox fit, which sizes a world unit to a device pixel, this is what "the plan
52
+ * fills the canvas" means as a number.
53
+ * @param bufferHeight - The drawing buffer's height in device pixels.
54
+ * @param fov - The camera's vertical field of view in degrees.
55
+ * @returns The distance in world units, or `0` while the buffer has no height.
56
+ */
57
+ export declare function zoomIdentityDistance(bufferHeight: number, fov: number): number;
58
+ /**
59
+ * The camera's vertical field of view, in degrees — `2·atan(1/3)`, the value the imperative renderer
60
+ * took from Mapbox.
61
+ *
62
+ * Narrow on purpose. The plan is laid out against a single pixel-to-plan factor per frame (see
63
+ * `readPixelSize`), which under perspective is only ever an approximation across a tilted view; the
64
+ * narrower the field, the closer that approximation holds.
65
+ */
66
+ export declare const CAMERA_FOV = 36.87;
67
+ //# sourceMappingURL=camera-handle.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { degToRad } from '@expofp/geometry';
2
+ import { createContext, use } from 'react';
3
+ /** The scene's camera handle, published by {@link Camera}. */
4
+ export const CameraContext = createContext(undefined);
5
+ /**
6
+ * The scene's camera handle, once {@link Camera} has published it.
7
+ * @returns The handle, or `undefined` when rendered outside a scene.
8
+ */
9
+ export function useCamera() {
10
+ return use(CameraContext);
11
+ }
12
+ /**
13
+ * The zoom handle {@link Camera} publishes.
14
+ *
15
+ * A plain object rather than anything reactive: every member is derived from the buffer height it
16
+ * was built against and whatever the camera holds when it is asked, so there is no state here to go
17
+ * stale — only a factor and the two conversions that turn it into a distance and back.
18
+ *
19
+ * Takes a reading rather than the canvas state: the one live number it needs is how far the camera
20
+ * is from the plan, and asking for that instead of for the store keeps this file pure arithmetic —
21
+ * with no view on where a distance comes from, and nothing to import from `controls`.
22
+ * @param bufferHeight - The drawing buffer's height in device pixels.
23
+ * @param getDistance - How far the camera currently is from the plan, in world units, read at the
24
+ * moment of asking.
25
+ * @returns The handle.
26
+ */
27
+ export function createCameraHandle(bufferHeight, getDistance) {
28
+ const identity = zoomIdentityDistance(bufferHeight, CAMERA_FOV);
29
+ const toZoomFactor = (distance) => identity > 0 && distance > 0 ? identity / distance : 1;
30
+ return {
31
+ get zoomFactor() {
32
+ return toZoomFactor(getDistance());
33
+ },
34
+ zoomFactorToDistance(zoomFactor) {
35
+ return zoomFactor > 0 ? identity / zoomFactor : identity;
36
+ },
37
+ distanceToZoomFactor: toZoomFactor,
38
+ };
39
+ }
40
+ /**
41
+ * The camera distance at which the drawing buffer's height covers exactly that many world units —
42
+ * zoom factor `1`.
43
+ *
44
+ * Paired with the viewbox fit, which sizes a world unit to a device pixel, this is what "the plan
45
+ * fills the canvas" means as a number.
46
+ * @param bufferHeight - The drawing buffer's height in device pixels.
47
+ * @param fov - The camera's vertical field of view in degrees.
48
+ * @returns The distance in world units, or `0` while the buffer has no height.
49
+ */
50
+ export function zoomIdentityDistance(bufferHeight, fov) {
51
+ if (bufferHeight <= 0)
52
+ return 0;
53
+ return bufferHeight / 2 / Math.tan(degToRad(fov) / 2);
54
+ }
55
+ /**
56
+ * The camera's vertical field of view, in degrees — `2·atan(1/3)`, the value the imperative renderer
57
+ * took from Mapbox.
58
+ *
59
+ * Narrow on purpose. The plan is laid out against a single pixel-to-plan factor per frame (see
60
+ * `readPixelSize`), which under perspective is only ever an approximation across a tilted view; the
61
+ * narrower the field, the closer that approximation holds.
62
+ */
63
+ export const CAMERA_FOV = 36.87;
@@ -0,0 +1,26 @@
1
+ import { type Camera, type OrthographicCamera, type PerspectiveCamera } from 'three';
2
+ /**
3
+ * Whether a camera projects perspectively.
4
+ *
5
+ * Three's own `isPerspectiveCamera` brand rather than `instanceof`, so a camera from a second copy
6
+ * of three — a host's, under external mode — answers the same as one of ours.
7
+ * @param camera - The camera to test.
8
+ * @returns Whether it is a perspective camera.
9
+ */
10
+ export declare function isPerspective(camera: Camera): camera is PerspectiveCamera;
11
+ /**
12
+ * Whether a camera projects orthographically.
13
+ * @param camera - The camera to test.
14
+ * @returns Whether it is an orthographic camera.
15
+ */
16
+ export declare function isOrthographic(camera: Camera): camera is OrthographicCamera;
17
+ /**
18
+ * Whether a camera derives its own projection from a frustum, rather than carrying one handed to it.
19
+ *
20
+ * The question three's `Raycaster.setFromCamera` asks before it refuses a camera, and the one
21
+ * `Camera.tsx` and `VisibleRect.tsx` ask before writing frustum fields nothing else has.
22
+ * @param camera - The camera to test.
23
+ * @returns Whether it projects either standard way.
24
+ */
25
+ export declare function projectsFromFrustum(camera: Camera): boolean;
26
+ //# sourceMappingURL=camera-kind.d.ts.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Whether a camera projects perspectively.
3
+ *
4
+ * Three's own `isPerspectiveCamera` brand rather than `instanceof`, so a camera from a second copy
5
+ * of three — a host's, under external mode — answers the same as one of ours.
6
+ * @param camera - The camera to test.
7
+ * @returns Whether it is a perspective camera.
8
+ */
9
+ export function isPerspective(camera) {
10
+ return camera.isPerspectiveCamera === true;
11
+ }
12
+ /**
13
+ * Whether a camera projects orthographically.
14
+ * @param camera - The camera to test.
15
+ * @returns Whether it is an orthographic camera.
16
+ */
17
+ export function isOrthographic(camera) {
18
+ return camera.isOrthographicCamera === true;
19
+ }
20
+ /**
21
+ * Whether a camera derives its own projection from a frustum, rather than carrying one handed to it.
22
+ *
23
+ * The question three's `Raycaster.setFromCamera` asks before it refuses a camera, and the one
24
+ * `Camera.tsx` and `VisibleRect.tsx` ask before writing frustum fields nothing else has.
25
+ * @param camera - The camera to test.
26
+ * @returns Whether it projects either standard way.
27
+ */
28
+ export function projectsFromFrustum(camera) {
29
+ return isPerspective(camera) || isOrthographic(camera);
30
+ }
@@ -0,0 +1,24 @@
1
+ /** How large the canvas is, and how densely it draws. */
2
+ export interface CanvasSize {
3
+ /** Width in css pixels. */
4
+ readonly width: number;
5
+ /** Height in css pixels. */
6
+ readonly height: number;
7
+ /** Device pixels per css pixel. Multiply by it for drawing-buffer sizes. */
8
+ readonly dpr: number;
9
+ }
10
+ /**
11
+ * The canvas's size, subscribed to one number at a time.
12
+ *
13
+ * Never read `state.size` as an object. R3F re-runs its canvas configuration on every render of the
14
+ * `<Canvas>` element and replaces that object as it goes, dimensions unchanged or not — so a
15
+ * component selecting it re-renders on renders that changed nothing, and anything memoized against
16
+ * it is rebuilt. Selecting the numbers is immune, because the numbers only move when the canvas
17
+ * really does.
18
+ *
19
+ * The returned object is a fresh literal, which costs nothing: it is not what the subscription
20
+ * compares, and the React Compiler keeps its identity stable across renders where the numbers hold.
21
+ * @returns The canvas's css size and device pixel ratio.
22
+ */
23
+ export declare function useCanvasSize(): CanvasSize;
24
+ //# sourceMappingURL=canvas-size.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ /**
3
+ * The canvas's size, subscribed to one number at a time.
4
+ *
5
+ * Never read `state.size` as an object. R3F re-runs its canvas configuration on every render of the
6
+ * `<Canvas>` element and replaces that object as it goes, dimensions unchanged or not — so a
7
+ * component selecting it re-renders on renders that changed nothing, and anything memoized against
8
+ * it is rebuilt. Selecting the numbers is immune, because the numbers only move when the canvas
9
+ * really does.
10
+ *
11
+ * The returned object is a fresh literal, which costs nothing: it is not what the subscription
12
+ * compares, and the React Compiler keeps its identity stable across renders where the numbers hold.
13
+ * @returns The canvas's css size and device pixel ratio.
14
+ */
15
+ export function useCanvasSize() {
16
+ const width = useThree((state) => state.size.width);
17
+ const height = useThree((state) => state.size.height);
18
+ const dpr = useThree((state) => state.viewport.dpr);
19
+ return { width, height, dpr };
20
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The steepest the plan may **ever** be pitched away from face-on, in degrees.
3
+ *
4
+ * A hard cap, not a setting. The near plane below is sized against this angle: pitching the camera
5
+ * foreshortens an elevation along the view axis, and this is the steepest view the depth guarantee
6
+ * must survive, so a steeper view is one it never allowed for. `Camera` clamps `maxPitch` to it,
7
+ * and defaults to it.
8
+ *
9
+ * Deliberately **not** what the polygon offsets in `objects/materials` are sized against, though the
10
+ * imperative renderer's were (`⌈tan(maxPolarAngle)⌉`): a slope-scaled `polygonOffsetFactor` reads
11
+ * the actual slope per draw, so no worst-case angle enters that calculation — see `sharedParams`.
12
+ *
13
+ * In degrees, like every pitch the renderer's API talks about; the cosine the arithmetic actually
14
+ * wants is {@link MIN_PITCH_COSINE}, so the conversion happens once, here, and nowhere else.
15
+ *
16
+ * The imperative renderer's own limit, and the angle its depth guarantee was stated against.
17
+ */
18
+ export declare const MAX_PITCH = 85;
19
+ /**
20
+ * The near clipping plane that keeps depth precision at the plan constant as the camera dollies.
21
+ *
22
+ * Two constraints set it, and the larger wins so precision is never traded away for headroom:
23
+ *
24
+ * - **Headroom.** `distance × (1 − HEADROOM_RATIO)`. Dominates at close and default zoom, where it
25
+ * keeps the near plane a fixed fraction of the way to the plan and precision is already better
26
+ * than asked for.
27
+ * - **Precision.** `distance² / (levels × precision)`, the distance a depth buffer of that many
28
+ * levels can still resolve at the plan. Dominates far out, where it buys the guarantee back at
29
+ * the cost of some headroom.
30
+ *
31
+ * The guarantee is stated in **plan** units — see {@link ELEVATION_PRECISION} — so it is scaled
32
+ * into world units before the depth buffer is asked about it. The imperative renderer did not do
33
+ * that: it applied the plan-unit constant to world distances, which quietly loosened the guarantee
34
+ * on any plan drawn smaller than world scale. On a 60 000-unit plan in a 1600-pixel buffer that is a
35
+ * factor of 2.7 at default zoom, enough for two surfaces separated by exactly the documented minimum
36
+ * to z-fight.
37
+ *
38
+ * Sized against {@link MAX_PITCH} rather than against whatever pitch a scene currently allows, which
39
+ * is the conservative choice on purpose: the worst case is the only one that is always true, and a
40
+ * near plane computed from the pitch *in force* would have to be recomputed whenever that moved and
41
+ * would silently weaken the guarantee wherever nothing was enforcing a limit at all. What it costs
42
+ * is headroom, and only in the regime where {@link MIN_HEADROOM_RATIO} has already capped it.
43
+ * @param distance - Camera distance to the plan, in world units.
44
+ * @param scale - World units per plan unit, from the scene's {@link Space}.
45
+ * @returns The near plane in world units, or `undefined` while the camera sits on the plan.
46
+ */
47
+ export declare function nearPlane(distance: number, scale: number): number | undefined;
48
+ /**
49
+ * The far plane for a camera that may pull back that far, with a factor of two spare so content
50
+ * behind the plan — anything at a negative elevation — is not clipped at full zoom-out.
51
+ * @param maxDistance - The furthest the camera may get from the plan, in world units.
52
+ * @returns The far plane in world units.
53
+ */
54
+ export declare function farPlane(maxDistance: number): number;
55
+ //# sourceMappingURL=clipping.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Where the camera's frustum begins and ends.
3
+ *
4
+ * Both planes answer the same question from opposite sides — what the camera can still draw — and
5
+ * they are set on different clocks: the near plane follows the camera's distance, every frame it
6
+ * moves, because that is what depth precision at the plan depends on; the far plane is fixed by how
7
+ * far the zoom bounds let the camera retreat, so it changes only when those do.
8
+ */
9
+ import { degToRad } from '@expofp/geometry';
10
+ /**
11
+ * The steepest the plan may **ever** be pitched away from face-on, in degrees.
12
+ *
13
+ * A hard cap, not a setting. The near plane below is sized against this angle: pitching the camera
14
+ * foreshortens an elevation along the view axis, and this is the steepest view the depth guarantee
15
+ * must survive, so a steeper view is one it never allowed for. `Camera` clamps `maxPitch` to it,
16
+ * and defaults to it.
17
+ *
18
+ * Deliberately **not** what the polygon offsets in `objects/materials` are sized against, though the
19
+ * imperative renderer's were (`⌈tan(maxPolarAngle)⌉`): a slope-scaled `polygonOffsetFactor` reads
20
+ * the actual slope per draw, so no worst-case angle enters that calculation — see `sharedParams`.
21
+ *
22
+ * In degrees, like every pitch the renderer's API talks about; the cosine the arithmetic actually
23
+ * wants is {@link MIN_PITCH_COSINE}, so the conversion happens once, here, and nowhere else.
24
+ *
25
+ * The imperative renderer's own limit, and the angle its depth guarantee was stated against.
26
+ */
27
+ export const MAX_PITCH = 85;
28
+ /**
29
+ * The near clipping plane that keeps depth precision at the plan constant as the camera dollies.
30
+ *
31
+ * Two constraints set it, and the larger wins so precision is never traded away for headroom:
32
+ *
33
+ * - **Headroom.** `distance × (1 − HEADROOM_RATIO)`. Dominates at close and default zoom, where it
34
+ * keeps the near plane a fixed fraction of the way to the plan and precision is already better
35
+ * than asked for.
36
+ * - **Precision.** `distance² / (levels × precision)`, the distance a depth buffer of that many
37
+ * levels can still resolve at the plan. Dominates far out, where it buys the guarantee back at
38
+ * the cost of some headroom.
39
+ *
40
+ * The guarantee is stated in **plan** units — see {@link ELEVATION_PRECISION} — so it is scaled
41
+ * into world units before the depth buffer is asked about it. The imperative renderer did not do
42
+ * that: it applied the plan-unit constant to world distances, which quietly loosened the guarantee
43
+ * on any plan drawn smaller than world scale. On a 60 000-unit plan in a 1600-pixel buffer that is a
44
+ * factor of 2.7 at default zoom, enough for two surfaces separated by exactly the documented minimum
45
+ * to z-fight.
46
+ *
47
+ * Sized against {@link MAX_PITCH} rather than against whatever pitch a scene currently allows, which
48
+ * is the conservative choice on purpose: the worst case is the only one that is always true, and a
49
+ * near plane computed from the pitch *in force* would have to be recomputed whenever that moved and
50
+ * would silently weaken the guarantee wherever nothing was enforcing a limit at all. What it costs
51
+ * is headroom, and only in the regime where {@link MIN_HEADROOM_RATIO} has already capped it.
52
+ * @param distance - Camera distance to the plan, in world units.
53
+ * @param scale - World units per plan unit, from the scene's {@link Space}.
54
+ * @returns The near plane in world units, or `undefined` while the camera sits on the plan.
55
+ */
56
+ export function nearPlane(distance, scale) {
57
+ if (distance <= 0)
58
+ return undefined;
59
+ // Pitching the camera foreshortens an elevation along the view axis, so the separation the depth
60
+ // buffer actually sees is the plan one times the cosine.
61
+ const precision = ELEVATION_PRECISION * scale * MIN_PITCH_COSINE;
62
+ const near = (distance * distance) / (DEPTH_BUFFER_LEVELS * precision);
63
+ return Math.min(Math.max(near, distance * (1 - HEADROOM_RATIO)), distance * (1 - MIN_HEADROOM_RATIO));
64
+ }
65
+ /**
66
+ * The far plane for a camera that may pull back that far, with a factor of two spare so content
67
+ * behind the plan — anything at a negative elevation — is not clipped at full zoom-out.
68
+ * @param maxDistance - The furthest the camera may get from the plan, in world units.
69
+ * @returns The far plane in world units.
70
+ */
71
+ export function farPlane(maxDistance) {
72
+ return Math.max(maxDistance, 0.1) * 2;
73
+ }
74
+ /**
75
+ * Minimum z-axis separation between overlapping surfaces, in **plan** units.
76
+ *
77
+ * **For plan data providers:** shapes sharing an x/y footprint — an elevated platform over a floor
78
+ * area, layered decorations at one position — must be separated by at least this much on z to be
79
+ * ordered correctly. No minimum horizontal separation is required: screen-pixel density and depth
80
+ * precision both scale with camera distance and with the scene's scale, so any separation visible on
81
+ * screen already has depth margin, at any viewport size, zoom, scale or camera rotation.
82
+ *
83
+ * Deliberately the renderer's own constant rather than `@expofp/geometry`'s `ELEVATION_TOLERANCE`,
84
+ * which happens to hold the same number. They answer different questions — geometry's is "are these
85
+ * two elevations the same point?", this one is "must these two elevations still draw in the right
86
+ * order?" — and importing it would let a tuning of the geometry predicate silently re-tune the depth
87
+ * buffer.
88
+ */
89
+ const ELEVATION_PRECISION = 0.1;
90
+ /**
91
+ * Fraction of the camera's distance kept clear of the near plane at close and default zoom, so
92
+ * depth-buffer levels are not spent on the empty space between the camera and the plan.
93
+ */
94
+ const HEADROOM_RATIO = 0.98;
95
+ /**
96
+ * Least headroom the precision constraint may leave. Without it a plan drawn very small — a viewbox
97
+ * hundreds of times the buffer — asks for a near plane beyond the camera's own target and blanks the
98
+ * canvas. Past this point precision degrades instead, which is visible only where two surfaces are
99
+ * within the documented minimum of each other.
100
+ */
101
+ const MIN_HEADROOM_RATIO = 0.5;
102
+ /** 24-bit depth buffer: 2²⁴ discrete levels, which covers 99%+ of devices. */
103
+ const DEPTH_BUFFER_LEVELS = 2 ** 24;
104
+ /**
105
+ * How much of an elevation survives being viewed at {@link MAX_PITCH} — the least of it the depth
106
+ * buffer ever has to resolve, and so the figure the near plane is sized against.
107
+ */
108
+ const MIN_PITCH_COSINE = Math.cos(degToRad(MAX_PITCH));