@expofp/renderer 0.0.0-experimental.2d6c681

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +145 -0
  2. package/dist/Renderer.d.ts +102 -0
  3. package/dist/Renderer.js +96 -0
  4. package/dist/controls/CameraControls.d.ts +108 -0
  5. package/dist/controls/CameraControls.js +203 -0
  6. package/dist/controls/Controls.d.ts +53 -0
  7. package/dist/controls/Controls.js +54 -0
  8. package/dist/controls/camera-api.d.ts +226 -0
  9. package/dist/controls/camera-api.js +179 -0
  10. package/dist/controls/gestures.d.ts +71 -0
  11. package/dist/controls/gestures.js +98 -0
  12. package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
  13. package/dist/controls/handlers/DoubleTapZoom.js +82 -0
  14. package/dist/controls/handlers/Inertia.d.ts +60 -0
  15. package/dist/controls/handlers/Inertia.js +222 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +212 -0
  18. package/dist/controls/handlers/Pitch.d.ts +19 -0
  19. package/dist/controls/handlers/Pitch.js +200 -0
  20. package/dist/controls/handlers/Roll.d.ts +30 -0
  21. package/dist/controls/handlers/Roll.js +196 -0
  22. package/dist/controls/handlers/Zoom.d.ts +28 -0
  23. package/dist/controls/handlers/Zoom.js +34 -0
  24. package/dist/controls/hooks.d.ts +136 -0
  25. package/dist/controls/hooks.js +200 -0
  26. package/dist/controls/index.d.ts +9 -0
  27. package/dist/controls/index.js +7 -0
  28. package/dist/controls/zoom-to.d.ts +89 -0
  29. package/dist/controls/zoom-to.js +91 -0
  30. package/dist/core/InstanceUniforms.d.ts +108 -0
  31. package/dist/core/InstanceUniforms.js +298 -0
  32. package/dist/core/SquareDataTexture.d.ts +277 -0
  33. package/dist/core/SquareDataTexture.js +476 -0
  34. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  35. package/dist/core/batch/BatchedMesh.js +155 -0
  36. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  37. package/dist/core/instanced/FrustumCulling.js +246 -0
  38. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  39. package/dist/core/instanced/InstancedMesh.js +497 -0
  40. package/dist/core/instanced/Instances.d.ts +119 -0
  41. package/dist/core/instanced/Instances.js +230 -0
  42. package/dist/core/instanced/Raycasting.d.ts +118 -0
  43. package/dist/core/instanced/Raycasting.js +134 -0
  44. package/dist/core/instanced/instanced-material.d.ts +77 -0
  45. package/dist/core/instanced/instanced-material.js +228 -0
  46. package/dist/core/lines/BatchedLines.d.ts +137 -0
  47. package/dist/core/lines/BatchedLines.js +215 -0
  48. package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
  49. package/dist/core/lines/FrustumCullingLines.js +85 -0
  50. package/dist/core/lines/RaycastingLines.d.ts +45 -0
  51. package/dist/core/lines/RaycastingLines.js +91 -0
  52. package/dist/core/lines/lines-material.d.ts +47 -0
  53. package/dist/core/lines/lines-material.js +124 -0
  54. package/dist/core/shader-source.d.ts +70 -0
  55. package/dist/core/shader-source.js +110 -0
  56. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  57. package/dist/core/sprites/BatchedSprites.js +173 -0
  58. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  59. package/dist/core/sprites/RaycastingSprites.js +71 -0
  60. package/dist/core/sprites/sprite-material.d.ts +59 -0
  61. package/dist/core/sprites/sprite-material.js +192 -0
  62. package/dist/core/text/BatchedText.d.ts +233 -0
  63. package/dist/core/text/BatchedText.js +473 -0
  64. package/dist/core/text/MemberTexture.d.ts +90 -0
  65. package/dist/core/text/MemberTexture.js +289 -0
  66. package/dist/core/text/Text.d.ts +88 -0
  67. package/dist/core/text/Text.js +230 -0
  68. package/dist/core/text/batched-text-material.d.ts +56 -0
  69. package/dist/core/text/batched-text-material.js +123 -0
  70. package/dist/core/text/glyph-packing.d.ts +17 -0
  71. package/dist/core/text/glyph-packing.js +61 -0
  72. package/dist/core/typed-array.d.ts +16 -0
  73. package/dist/core/typed-array.js +20 -0
  74. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  75. package/dist/debug/components/DebugPanelProvider.js +50 -0
  76. package/dist/debug/components/DebugTools.d.ts +22 -0
  77. package/dist/debug/components/DebugTools.js +28 -0
  78. package/dist/debug/components/Gui.d.ts +22 -0
  79. package/dist/debug/components/Gui.js +42 -0
  80. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  81. package/dist/debug/components/MemoryInfo.js +42 -0
  82. package/dist/debug/components/Panel.d.ts +31 -0
  83. package/dist/debug/components/Panel.js +104 -0
  84. package/dist/debug/components/Stats.d.ts +9 -0
  85. package/dist/debug/components/Stats.js +24 -0
  86. package/dist/debug/debug-chunk.d.ts +26 -0
  87. package/dist/debug/debug-chunk.js +32 -0
  88. package/dist/debug/debug-context.d.ts +41 -0
  89. package/dist/debug/debug-context.js +59 -0
  90. package/dist/debug/index.d.ts +41 -0
  91. package/dist/debug/index.js +43 -0
  92. package/dist/events/LayoutDriver.d.ts +36 -0
  93. package/dist/events/LayoutDriver.js +192 -0
  94. package/dist/events/PointerMissed.d.ts +23 -0
  95. package/dist/events/PointerMissed.js +58 -0
  96. package/dist/events/def-culling.d.ts +46 -0
  97. package/dist/events/def-culling.js +69 -0
  98. package/dist/events/layout-targets.d.ts +151 -0
  99. package/dist/events/layout-targets.js +397 -0
  100. package/dist/events/placement/box.d.ts +59 -0
  101. package/dist/events/placement/box.js +96 -0
  102. package/dist/events/placement/grid.d.ts +161 -0
  103. package/dist/events/placement/grid.js +235 -0
  104. package/dist/events/placement/index.d.ts +2 -0
  105. package/dist/events/placement/index.js +1 -0
  106. package/dist/events/placement/pass.d.ts +70 -0
  107. package/dist/events/placement/pass.js +617 -0
  108. package/dist/events/pointer.d.ts +194 -0
  109. package/dist/events/pointer.js +356 -0
  110. package/dist/external/ExternalCanvas.d.ts +56 -0
  111. package/dist/external/ExternalCanvas.js +177 -0
  112. package/dist/external/external-camera.d.ts +18 -0
  113. package/dist/external/external-camera.js +103 -0
  114. package/dist/external/external-events.d.ts +14 -0
  115. package/dist/external/external-events.js +33 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/index.js +14 -0
  118. package/dist/objects/Background.d.ts +31 -0
  119. package/dist/objects/Background.js +37 -0
  120. package/dist/objects/containers/SlotContainer.d.ts +312 -0
  121. package/dist/objects/containers/SlotContainer.js +311 -0
  122. package/dist/objects/containers/hooks.d.ts +61 -0
  123. package/dist/objects/containers/hooks.js +196 -0
  124. package/dist/objects/containers/index.d.ts +6 -0
  125. package/dist/objects/containers/index.js +4 -0
  126. package/dist/objects/containers/keys.d.ts +10 -0
  127. package/dist/objects/containers/keys.js +18 -0
  128. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  129. package/dist/objects/containers/mesh-writes.js +97 -0
  130. package/dist/objects/containers/props.d.ts +33 -0
  131. package/dist/objects/containers/props.js +1 -0
  132. package/dist/objects/images/ImageInstance.d.ts +29 -0
  133. package/dist/objects/images/ImageInstance.js +152 -0
  134. package/dist/objects/images/Images.d.ts +49 -0
  135. package/dist/objects/images/Images.js +394 -0
  136. package/dist/objects/images/atlas-packer.d.ts +129 -0
  137. package/dist/objects/images/atlas-packer.js +287 -0
  138. package/dist/objects/images/image-container.d.ts +74 -0
  139. package/dist/objects/images/image-container.js +100 -0
  140. package/dist/objects/images/index.d.ts +2 -0
  141. package/dist/objects/images/index.js +1 -0
  142. package/dist/objects/images/pack-sources.d.ts +190 -0
  143. package/dist/objects/images/pack-sources.js +272 -0
  144. package/dist/objects/images/texture-budget.d.ts +120 -0
  145. package/dist/objects/images/texture-budget.js +215 -0
  146. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  147. package/dist/objects/images/worker/atlas-worker.js +123 -0
  148. package/dist/objects/images/worker/client.d.ts +62 -0
  149. package/dist/objects/images/worker/client.js +249 -0
  150. package/dist/objects/images/worker/index.d.ts +10 -0
  151. package/dist/objects/images/worker/index.js +1 -0
  152. package/dist/objects/layer/Layer.d.ts +28 -0
  153. package/dist/objects/layer/Layer.js +134 -0
  154. package/dist/objects/layer/index.d.ts +2 -0
  155. package/dist/objects/layer/index.js +1 -0
  156. package/dist/objects/layer/layer-context.d.ts +74 -0
  157. package/dist/objects/layer/layer-context.js +82 -0
  158. package/dist/objects/layer/mount-scheduler.d.ts +131 -0
  159. package/dist/objects/layer/mount-scheduler.js +289 -0
  160. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  161. package/dist/objects/layer/visibility-gui.js +93 -0
  162. package/dist/objects/lighting/Lighting.d.ts +92 -0
  163. package/dist/objects/lighting/Lighting.js +384 -0
  164. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  165. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  166. package/dist/objects/lighting/index.d.ts +11 -0
  167. package/dist/objects/lighting/index.js +10 -0
  168. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  169. package/dist/objects/lighting/lighting-gui.js +114 -0
  170. package/dist/objects/lighting/occluder.d.ts +34 -0
  171. package/dist/objects/lighting/occluder.js +62 -0
  172. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  173. package/dist/objects/lighting/occlusion-targets.js +276 -0
  174. package/dist/objects/lines/LineInstance.d.ts +21 -0
  175. package/dist/objects/lines/LineInstance.js +73 -0
  176. package/dist/objects/lines/Lines.d.ts +17 -0
  177. package/dist/objects/lines/Lines.js +31 -0
  178. package/dist/objects/lines/index.d.ts +2 -0
  179. package/dist/objects/lines/index.js +1 -0
  180. package/dist/objects/lines/line-container.d.ts +35 -0
  181. package/dist/objects/lines/line-container.js +40 -0
  182. package/dist/objects/materials/ColorMaterial.d.ts +92 -0
  183. package/dist/objects/materials/ColorMaterial.js +124 -0
  184. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  185. package/dist/objects/materials/LineMaterial.js +74 -0
  186. package/dist/objects/materials/ModelMaterial.d.ts +53 -0
  187. package/dist/objects/materials/ModelMaterial.js +111 -0
  188. package/dist/objects/materials/TextureMaterial.d.ts +33 -0
  189. package/dist/objects/materials/TextureMaterial.js +48 -0
  190. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  191. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  192. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  193. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  194. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  195. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  196. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  197. package/dist/objects/materials/effects/ao/index.js +72 -0
  198. package/dist/objects/materials/effects/color-dim.d.ts +115 -0
  199. package/dist/objects/materials/effects/color-dim.js +206 -0
  200. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  201. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  202. package/dist/objects/materials/effects/effects-context.d.ts +53 -0
  203. package/dist/objects/materials/effects/effects-context.js +24 -0
  204. package/dist/objects/materials/shared.d.ts +24 -0
  205. package/dist/objects/materials/shared.js +72 -0
  206. package/dist/objects/models/ModelInstance.d.ts +22 -0
  207. package/dist/objects/models/ModelInstance.js +33 -0
  208. package/dist/objects/models/Models.d.ts +22 -0
  209. package/dist/objects/models/Models.js +186 -0
  210. package/dist/objects/models/gltf-cache.d.ts +17 -0
  211. package/dist/objects/models/gltf-cache.js +50 -0
  212. package/dist/objects/models/index.d.ts +2 -0
  213. package/dist/objects/models/index.js +1 -0
  214. package/dist/objects/models/model-asset.d.ts +48 -0
  215. package/dist/objects/models/model-asset.js +77 -0
  216. package/dist/objects/models/model-container.d.ts +123 -0
  217. package/dist/objects/models/model-container.js +207 -0
  218. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  219. package/dist/objects/shapes/ShapeInstance.js +39 -0
  220. package/dist/objects/shapes/Shapes.d.ts +21 -0
  221. package/dist/objects/shapes/Shapes.js +131 -0
  222. package/dist/objects/shapes/index.d.ts +2 -0
  223. package/dist/objects/shapes/index.js +1 -0
  224. package/dist/objects/shapes/shape-container.d.ts +141 -0
  225. package/dist/objects/shapes/shape-container.js +242 -0
  226. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  227. package/dist/objects/shapes/shape-geometry.js +94 -0
  228. package/dist/objects/text/TextInstance.d.ts +22 -0
  229. package/dist/objects/text/TextInstance.js +95 -0
  230. package/dist/objects/text/Texts.d.ts +30 -0
  231. package/dist/objects/text/Texts.js +43 -0
  232. package/dist/objects/text/index.d.ts +2 -0
  233. package/dist/objects/text/index.js +1 -0
  234. package/dist/objects/text/text-container.d.ts +121 -0
  235. package/dist/objects/text/text-container.js +229 -0
  236. package/dist/objects/text/text-layout.d.ts +92 -0
  237. package/dist/objects/text/text-layout.js +180 -0
  238. package/dist/space/Camera.d.ts +103 -0
  239. package/dist/space/Camera.js +321 -0
  240. package/dist/space/Scene.d.ts +47 -0
  241. package/dist/space/Scene.js +127 -0
  242. package/dist/space/Space.d.ts +325 -0
  243. package/dist/space/Space.js +359 -0
  244. package/dist/space/VisibleRect.d.ts +45 -0
  245. package/dist/space/VisibleRect.js +129 -0
  246. package/dist/space/camera-handle.d.ts +67 -0
  247. package/dist/space/camera-handle.js +63 -0
  248. package/dist/space/camera-kind.d.ts +26 -0
  249. package/dist/space/camera-kind.js +30 -0
  250. package/dist/space/canvas-size.d.ts +24 -0
  251. package/dist/space/canvas-size.js +20 -0
  252. package/dist/space/clipping.d.ts +69 -0
  253. package/dist/space/clipping.js +130 -0
  254. package/dist/space/layout-measure.d.ts +67 -0
  255. package/dist/space/layout-measure.js +175 -0
  256. package/dist/space/viewport.d.ts +112 -0
  257. package/dist/space/viewport.js +67 -0
  258. package/dist/types/defs.d.ts +234 -0
  259. package/dist/types/defs.js +1 -0
  260. package/dist/types/events.d.ts +198 -0
  261. package/dist/types/events.js +1 -0
  262. package/dist/types/guards.d.ts +21 -0
  263. package/dist/types/guards.js +52 -0
  264. package/dist/update/index.d.ts +3 -0
  265. package/dist/update/index.js +2 -0
  266. package/dist/update/slot-writers.d.ts +30 -0
  267. package/dist/update/slot-writers.js +76 -0
  268. package/dist/update/store.d.ts +41 -0
  269. package/dist/update/store.js +104 -0
  270. package/dist/utils/arrays.d.ts +13 -0
  271. package/dist/utils/arrays.js +20 -0
  272. package/dist/utils/colors.d.ts +19 -0
  273. package/dist/utils/colors.js +27 -0
  274. package/dist/utils/hot-state.d.ts +41 -0
  275. package/dist/utils/hot-state.js +37 -0
  276. package/dist/utils/logging.d.ts +29 -0
  277. package/dist/utils/logging.js +55 -0
  278. package/dist/utils/math.d.ts +52 -0
  279. package/dist/utils/math.js +76 -0
  280. package/package.json +79 -0
@@ -0,0 +1,359 @@
1
+ import { Point } from '@expofp/geometry';
2
+ import { createContext, use } from 'react';
3
+ import { Matrix4, Plane, Ray, Raycaster, Vector2, Vector3, Vector4, } from 'three';
4
+ import { createLogger } from '../utils/logging';
5
+ import { axisLength, clamp } from '../utils/math';
6
+ import { projectsFromFrustum } from './camera-kind';
7
+ const logger = createLogger('space');
8
+ /**
9
+ * The scene's space.
10
+ * @returns The space {@link Scene} published, or the identity one when rendered outside a scene.
11
+ */
12
+ export function useSpace() {
13
+ return use(SpaceContext);
14
+ }
15
+ /**
16
+ * The mapping between the plan's own coordinates and the world the camera is placed in, and the
17
+ * conversions that ride on it.
18
+ *
19
+ * The mapping is one matrix. Without a `staticTransform` it is the **viewbox fit**: centre the
20
+ * viewbox on the world origin and scale it uniformly to best fill the drawing buffer. That fit is
21
+ * what makes one world unit one device pixel when the plan exactly fills the canvas, which in turn
22
+ * is what lets the camera's zoom math be written without reference to the scene at all — see
23
+ * {@link Camera}.
24
+ *
25
+ * With a `staticTransform` the matrix is whatever the caller supplied, and the fit is not computed:
26
+ * a host like maplibre places the plan itself, in its own space.
27
+ *
28
+ * Either way the matrix ends with {@link HANDEDNESS_FLIP}, which is what makes the space three's
29
+ * rather than the plan's or a host's — a positive elevation then lands nearer the camera.
30
+ *
31
+ * **Stable for the scene's lifetime.** A resize does not build a new space — {@link Scene} refits
32
+ * this one in place — so every holder reads the current fit through the same object and none of
33
+ * them re-render for it. What makes that safe is a reading discipline: render output must never be
34
+ * derived from these fields, because no render follows a refit. Read them at the moment of use —
35
+ * in event handlers, effects and the frame loop, which always run against the current fit. `Scene`
36
+ * itself is the one exception, and it re-applies the world matrix from the same effect that refits.
37
+ */
38
+ export class Space {
39
+ /** Plan → world. */
40
+ matrix = new Matrix4();
41
+ /** World → plan. */
42
+ matrixInverse = new Matrix4();
43
+ _scale = 1;
44
+ _isExternal = false;
45
+ /** The canvas width in css pixels, which the canvas conversions measure against. */
46
+ width = 0;
47
+ /** The canvas height in css pixels. */
48
+ height = 0;
49
+ /**
50
+ * @param options - How to map the plan onto the canvas. Omit them entirely for the identity
51
+ * space, in which world coordinates already read as plan coordinates; the individual fields are
52
+ * not separately optional, because a fit needs all of them.
53
+ */
54
+ constructor(options) {
55
+ if (options)
56
+ this.refit(options);
57
+ }
58
+ /**
59
+ * World units per plan unit: the geometric mean of the two transformed axes, rather than the
60
+ * length of one of them.
61
+ *
62
+ * Both paths that build a matrix produce a similarity — the fit scales x and y alike, and the
63
+ * maplibre solver forces uniform scale and no shear — so the two axes agree and the mean is
64
+ * simply their common length. It differs only for a transform this renderer does not produce, and
65
+ * there no single number is right anyway: an anisotropic plan covers different amounts of plan per
66
+ * pixel along each axis, and the mean is the one that preserves area.
67
+ */
68
+ get scale() {
69
+ return this._scale;
70
+ }
71
+ /**
72
+ * Whether an external host placed the plan itself, by supplying a valid `staticTransform`,
73
+ * rather than this renderer fitting the viewbox to the canvas — external mode, where the host
74
+ * owns the canvas, the camera and the frames.
75
+ */
76
+ get isExternal() {
77
+ return this._isExternal;
78
+ }
79
+ /**
80
+ * Refits the plan onto the canvas in place, leaving every held reference reading the new fit.
81
+ * @param options - How to map the plan onto the canvas.
82
+ */
83
+ refit(options) {
84
+ this.width = options.width;
85
+ this.height = options.height;
86
+ this._isExternal = this.validateStaticTransform(options.staticTransform);
87
+ if (this._isExternal) {
88
+ this.matrix.fromArray(options.staticTransform);
89
+ }
90
+ else {
91
+ const fit = fitScale(options.viewbox, this.width * options.dpr, this.height * options.dpr);
92
+ const { x: centerX, y: centerY } = options.viewbox.center;
93
+ // three composes a matrix as T·R·S, so the plan-space translation has to be pre-scaled for the
94
+ // product to come out as scale∘translate — the order the imperative renderer built it in.
95
+ this.matrix
96
+ .makeTranslation(-centerX * fit, -centerY * fit, 0)
97
+ .scale(new Vector3(fit, fit, fit));
98
+ }
99
+ // Both paths land in a host's space or ours; this is what makes either one three's.
100
+ this.matrix.premultiply(HANDEDNESS_FLIP);
101
+ this.matrixInverse.copy(this.matrix).invert();
102
+ this._scale = Math.sqrt(axisLength(this.matrix, 0) * axisLength(this.matrix, 1));
103
+ }
104
+ /**
105
+ * Converts a point from plan to world coordinates.
106
+ * @param point - The plan-space point; a missing `z` is read as `0`.
107
+ * @param out - Where to write the result.
108
+ * @returns The world-space point.
109
+ */
110
+ planToWorld(point, out = new Vector3()) {
111
+ return out.set(point.x, point.y, point.z ?? 0).applyMatrix4(this.matrix);
112
+ }
113
+ /**
114
+ * Converts a point from world to plan coordinates.
115
+ * @param point - The world-space point.
116
+ * @param out - Where to write the result.
117
+ * @returns The plan-space point.
118
+ */
119
+ worldToPlan(point, out = new Point()) {
120
+ _world.set(point.x, point.y, point.z).applyMatrix4(this.matrixInverse);
121
+ out.set(_world.x, _world.y, _world.z);
122
+ return out;
123
+ }
124
+ /**
125
+ * Converts a point on the canvas — css pixels from its top-left corner — to normalized device
126
+ * coordinates, clamped to the canvas.
127
+ * @param point - The canvas-space point.
128
+ * @param out - Where to write the result.
129
+ * @returns The point in normalized device coordinates.
130
+ */
131
+ canvasToNdc(point, out = new Vector2()) {
132
+ if (this.width <= 0 || this.height <= 0) {
133
+ logger.warn('canvasToNdc: the canvas has no size');
134
+ return out.set(0, 0);
135
+ }
136
+ const x = (point.x / this.width) * 2 - 1;
137
+ // NDC y grows upward where canvas y grows downward.
138
+ const y = -(point.y / this.height) * 2 + 1;
139
+ return out.set(clamp(x, -1, 1), clamp(y, -1, 1));
140
+ }
141
+ /**
142
+ * Where a point in normalized device coordinates lands on the canvas, in css pixels from its
143
+ * top-left corner.
144
+ *
145
+ * **Unclamped, where {@link canvasToNdc} clamps**, and the asymmetry is the point: clamping on the
146
+ * way in keeps a pointer event on the canvas it was dispatched from, while clamping on the way out
147
+ * would erase the one thing a caller asking about an off-screen point wants to know.
148
+ *
149
+ * No guard on a canvas with no size either, where `canvasToNdc` needs one — this multiplies by the
150
+ * size where that divides by it, so an unsized canvas gives an honest zero rather than a `NaN`.
151
+ * @param ndc - The point in normalized device coordinates.
152
+ * @param out - Where to write the result.
153
+ * @returns The canvas-space point.
154
+ */
155
+ ndcToCanvas(ndc, out = new Point()) {
156
+ // Canvas y grows downward where NDC y grows upward. Zero elevation, always: a point on the
157
+ // canvas has none, and an `out` that carried one from an earlier use must not keep it.
158
+ return out.set(((ndc.x + 1) / 2) * this.width, ((1 - ndc.y) / 2) * this.height, 0);
159
+ }
160
+ /**
161
+ * Where a ray crosses the plan, in plan units.
162
+ *
163
+ * The ray is taken into plan space and crossed with plan `z = 0` there, rather than crossed with
164
+ * the plan's image in world space and converted back. Both agree for the viewbox fit; only this
165
+ * one stays correct for a `staticTransform` that rotates or lifts the plan off world `z = 0`.
166
+ * @param ray - The ray, in world space.
167
+ * @param out - Where to write the result.
168
+ * @returns The crossing, or `undefined` when the ray runs parallel to the plan.
169
+ */
170
+ rayToPlan(ray, out = new Point()) {
171
+ _ray.copy(ray).applyMatrix4(this.matrixInverse);
172
+ if (!_ray.intersectPlane(PLAN_PLANE, _world))
173
+ return undefined;
174
+ return out.set(_world.x, _world.y, 0);
175
+ }
176
+ /**
177
+ * Where a point in normalized device coordinates lands on the plan.
178
+ * @param ndc - The point in normalized device coordinates.
179
+ * @param camera - The camera the canvas is drawn through.
180
+ * @param out - Where to write the result.
181
+ * @returns The plan position, or `undefined` when the camera never meets the plan.
182
+ */
183
+ ndcToPlan(ndc, camera, out) {
184
+ castRay(_raycaster, ndc, camera);
185
+ return this.rayToPlan(_raycaster.ray, out);
186
+ }
187
+ /**
188
+ * Where a point on the plan lands in normalized device coordinates — the inverse of
189
+ * {@link ndcToPlan}, and the projection every question about "where is this on screen" is built
190
+ * from.
191
+ *
192
+ * Carried through **clip space** rather than through three's `Vector3.project`, which divides by
193
+ * `w` without looking at its sign: a point behind the camera comes back mirrored through the
194
+ * origin, and so lands inside the canvas as convincingly as a point in front of it. Reading `w`
195
+ * first is what separates the two.
196
+ *
197
+ * The camera's view offset rides in `projectionMatrix`, so a scene framed inside a `visibleRect`
198
+ * projects through this with no seam — the same way picking and culling follow it.
199
+ * @param point - The plan-space point; a missing `z` is read as `0`.
200
+ * @param camera - The camera the canvas is drawn through.
201
+ * @param out - Where to write the result.
202
+ * @returns The point in normalized device coordinates, unclamped, or `undefined` when it sits
203
+ * behind the camera and has no image on the canvas at all.
204
+ */
205
+ planToNdc(point, camera, out = new Vector2()) {
206
+ this.planToWorld(point, _projected);
207
+ _clip
208
+ .set(_projected.x, _projected.y, _projected.z, 1)
209
+ .applyMatrix4(camera.matrixWorldInverse)
210
+ .applyMatrix4(camera.projectionMatrix);
211
+ if (_clip.w <= 0)
212
+ return undefined;
213
+ return out.set(_clip.x / _clip.w, _clip.y / _clip.w);
214
+ }
215
+ /**
216
+ * Where a point on the canvas — css pixels from its top-left corner — lands on the plan.
217
+ * @param point - The canvas-space point.
218
+ * @param camera - The camera the canvas is drawn through.
219
+ * @param out - Where to write the result.
220
+ * @returns The plan position, or `undefined` when the camera never meets the plan.
221
+ */
222
+ canvasToPlan(point, camera, out) {
223
+ return this.ndcToPlan(this.canvasToNdc(point, _ndc), camera, out);
224
+ }
225
+ /**
226
+ * Where a point on the plan lands on the canvas, in css pixels from its top-left corner — the
227
+ * inverse of {@link canvasToPlan}, composed from the same two halves the other way round.
228
+ * @param point - The plan-space point; a missing `z` is read as `0`.
229
+ * @param camera - The camera the canvas is drawn through.
230
+ * @param out - Where to write the result.
231
+ * @returns The canvas position, unclamped, or `undefined` when the point sits behind the camera.
232
+ */
233
+ planToCanvas(point, camera, out) {
234
+ const ndc = this.planToNdc(point, camera, _screen);
235
+ if (!ndc)
236
+ return undefined;
237
+ return this.ndcToCanvas(ndc, out);
238
+ }
239
+ /**
240
+ * Whether a caller supplied a usable plan → world matrix, warning when they supplied an unusable
241
+ * one rather than silently falling back.
242
+ * @param staticTransform - What the caller supplied, if anything.
243
+ * @returns Whether it should be used as the matrix.
244
+ */
245
+ validateStaticTransform(staticTransform) {
246
+ if (staticTransform === undefined)
247
+ return false;
248
+ if (staticTransform.length !== 16) {
249
+ logger.warn(`staticTransform must be 16 elements long, got ${staticTransform.length}`);
250
+ return false;
251
+ }
252
+ return true;
253
+ }
254
+ }
255
+ /**
256
+ * The scale that fits a viewbox inside a drawing buffer, contain-style: the constraining axis fills
257
+ * and the other one keeps slack.
258
+ *
259
+ * Exported because it is also what tells a caller how large the plan is drawn: `VisibleRect` needs
260
+ * the plan's footprint in device pixels, and deriving it from this rather than from `Space.scale`
261
+ * keeps it free of the commit-ordering hazard the class documents — the fit is a function of the
262
+ * props it already holds.
263
+ * @param viewbox - The region to frame, in plan units.
264
+ * @param bufferWidth - The drawing buffer's width in device pixels.
265
+ * @param bufferHeight - The drawing buffer's height in device pixels.
266
+ * @returns World units per plan unit, or `1` while either is degenerate.
267
+ */
268
+ export function fitScale(viewbox, bufferWidth, bufferHeight) {
269
+ const { width, height } = viewbox.size;
270
+ if (width <= 0 || height <= 0 || bufferWidth <= 0 || bufferHeight <= 0)
271
+ return 1;
272
+ return Math.min(bufferWidth / width, bufferHeight / height);
273
+ }
274
+ /**
275
+ * The scene's space, published by {@link Scene}. The default is the identity space, so a component
276
+ * rendered without one still converts — it just converts nothing.
277
+ */
278
+ export const SpaceContext = createContext(new Space());
279
+ /**
280
+ * Points a raycaster from a camera through a point in normalized device coordinates.
281
+ *
282
+ * The fallback is for a camera whose projection was handed to it rather than derived from a frustum
283
+ * — an external host like maplibre supplies one — which three's own `setFromCamera` refuses. It unprojects the
284
+ * same screen point on the near and far planes; both lie on the true ray whatever the matrix folds,
285
+ * where anchoring the origin at the eye would need a camera transform such a camera does not carry.
286
+ *
287
+ * Exported for the external event system, which must aim R3F's raycaster the same way; the space's own
288
+ * conversions go through the module's private raycaster.
289
+ * @param raycaster - The raycaster to aim.
290
+ * @param ndc - The point in normalized device coordinates.
291
+ * @param camera - The camera to cast from.
292
+ */
293
+ export function castRay(raycaster, ndc, camera) {
294
+ if (projectsFromFrustum(camera)) {
295
+ raycaster.setFromCamera(_cast.set(ndc.x, ndc.y), camera);
296
+ return;
297
+ }
298
+ raycaster.ray.origin.set(ndc.x, ndc.y, -1).unproject(camera);
299
+ raycaster.ray.direction
300
+ .set(ndc.x, ndc.y, 1)
301
+ .unproject(camera)
302
+ .sub(raycaster.ray.origin)
303
+ .normalize();
304
+ }
305
+ /**
306
+ * The last step of both paths into world space: mirror z.
307
+ *
308
+ * Plan space is left-handed — x right, y **down**, z up — where three's world is right-handed, so
309
+ * one axis has to flip and z is it: the plan's up is world `-z`, the side the camera is on.
310
+ *
311
+ * The flip belongs in the plan → world matrix and nowhere else. three reads a face's winding from
312
+ * that matrix's determinant alone (`matrixWorld.determinantAffine() < 0`) and never from the
313
+ * projection, so a reflection anywhere else is invisible to it: every `DoubleSide` lit surface
314
+ * negates its normal and shades from behind.
315
+ *
316
+ * **Assumes a host's world shares the plan's handedness**, which mercator does — x east, y south,
317
+ * z up — resolving the clash inside its own projection instead (`MercatorTransform._calcMatrices`
318
+ * scales by `[1, -1, 1]`). So the same flip serves both paths, and external mode undoes it on the
319
+ * host's own matrix (`setExternalCameraProjection`). A host with a right-handed world would
320
+ * need it skipped on both — a `leftHandedWorld` flag on the external contract is where that choice
321
+ * would go.
322
+ */
323
+ export const HANDEDNESS_FLIP = new Matrix4().makeScale(1, 1, -1);
324
+ /**
325
+ * The plan's y-down convention, mirrored for a consumer that reads y as up.
326
+ *
327
+ * One value, two unrelated roles. In `ImageInstance.tsx` it is the plan-to-view y mirror composed
328
+ * into a screen-space quad's own 2D linear map, so the quad's offset reads correctly once the
329
+ * shader plants it in the view basis. In `MemberTexture.ts` it is the camera's own rest pose
330
+ * relative to the plan. Its job there is to stop that pose being applied twice, once baked into a
331
+ * member's own matrix and once more by the ordinary view transform at draw time. That role does
332
+ * not depend on the sign of a member's own `scale.y`. The requirement is that the billboard matrix
333
+ * equal the flat matrix at the rest pose, and the scale cancels from both sides of that equality.
334
+ *
335
+ * The two roles share a value by coincidence of geometry, not by a shared derivation. Check both
336
+ * call sites before changing this constant for either reason.
337
+ */
338
+ export const Y_DOWN_FLIP = new Matrix4().makeScale(1, -1, 1);
339
+ /** The plan's own plane in plan space, which every pointer position is reported on. */
340
+ const PLAN_PLANE = new Plane(new Vector3(0, 0, 1), 0);
341
+ /**
342
+ * One raycaster for the module, not one per space and deliberately not R3F's.
343
+ *
344
+ * `Space` is React-free, so it cannot reach `state.raycaster`; and it should not, because R3F sets
345
+ * that one up for the pointer event it is dispatching — a coordinate query that overwrote its ray
346
+ * mid-dispatch would corrupt the hit test in flight.
347
+ */
348
+ const _raycaster = new Raycaster();
349
+ const _ray = new Ray();
350
+ const _ndc = new Vector2();
351
+ /** The screen point {@link castRay} hands three's `setFromCamera`. */
352
+ const _cast = new Vector2();
353
+ const _world = new Vector3();
354
+ /** The world point {@link Space.planToNdc} projects, kept apart from `_world` so the two never alias. */
355
+ const _projected = new Vector3();
356
+ /** Where that projection lands before the perspective divide, which is where `w` can still be read. */
357
+ const _clip = new Vector4();
358
+ /** The projected point {@link Space.planToCanvas} hands on to {@link Space.ndcToCanvas}. */
359
+ const _screen = new Vector2();
@@ -0,0 +1,45 @@
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
+ //# 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
+ 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;