@expofp/renderer 0.0.0-experimental.cf063fc

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,28 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Options for the zoom handler, which are also its props. */
3
+ export interface ZoomConfig {
4
+ /**
5
+ * What a double-click or a double-tap multiplies the zoom factor by, zooming toward the point
6
+ * under the pointer and keeping it there. Off when left out.
7
+ */
8
+ dblclick?: number;
9
+ }
10
+ /**
11
+ * Zooming: the wheel, a two-finger pinch, and — when asked for — a double-click or double-tap.
12
+ *
13
+ * Nothing here beyond the actions for the first two — camera-controls recognizes both gestures
14
+ * itself, and `Controls` has already told it to dolly toward the cursor rather than the centre. The
15
+ * two-finger action pans as well as pinches, which is what makes a pinch feel like it holds the plan
16
+ * under the fingers; the twist half of a two-finger gesture is `Roll`'s, and comes off the gesture
17
+ * recognizer rather than from here, so the two never contend for `touches.two`.
18
+ *
19
+ * The double-click is not camera-controls' either, so it comes off the gesture recognizer, which
20
+ * reads a mouse and a finger alike: see {@link DoubleTapZoom}.
21
+ *
22
+ * Mounting is enabling. The actions are set when this mounts and returned to `NONE` when it
23
+ * unmounts, which is the whole of what the imperative renderer's `enable()` / `disable()` did.
24
+ * @param props - Whether a double-click zooms, and by how much.
25
+ * @returns The double-click zoom, when there is one.
26
+ */
27
+ export declare function Zoom(props: ZoomConfig): ReactNode;
28
+ //# sourceMappingURL=Zoom.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { CameraControlsAction } from '../CameraControls';
3
+ import { useControlsEffect } from '../hooks';
4
+ import { DoubleTapZoom } from './DoubleTapZoom';
5
+ /**
6
+ * Zooming: the wheel, a two-finger pinch, and — when asked for — a double-click or double-tap.
7
+ *
8
+ * Nothing here beyond the actions for the first two — camera-controls recognizes both gestures
9
+ * itself, and `Controls` has already told it to dolly toward the cursor rather than the centre. The
10
+ * two-finger action pans as well as pinches, which is what makes a pinch feel like it holds the plan
11
+ * under the fingers; the twist half of a two-finger gesture is `Roll`'s, and comes off the gesture
12
+ * recognizer rather than from here, so the two never contend for `touches.two`.
13
+ *
14
+ * The double-click is not camera-controls' either, so it comes off the gesture recognizer, which
15
+ * reads a mouse and a finger alike: see {@link DoubleTapZoom}.
16
+ *
17
+ * Mounting is enabling. The actions are set when this mounts and returned to `NONE` when it
18
+ * unmounts, which is the whole of what the imperative renderer's `enable()` / `disable()` did.
19
+ * @param props - Whether a double-click zooms, and by how much.
20
+ * @returns The double-click zoom, when there is one.
21
+ */
22
+ export function Zoom(props) {
23
+ useControlsEffect((controls) => {
24
+ controls.mouseButtons.wheel = CameraControlsAction.DOLLY;
25
+ controls.touches.two = CameraControlsAction.TOUCH_DOLLY_SCREEN_PAN;
26
+ return () => {
27
+ controls.mouseButtons.wheel = CameraControlsAction.NONE;
28
+ controls.touches.two = CameraControlsAction.NONE;
29
+ };
30
+ });
31
+ if (props.dblclick === undefined)
32
+ return null;
33
+ return _jsx(DoubleTapZoom, { factor: props.dblclick });
34
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Every hook a component reaches the camera through, and the state they read.
3
+ *
4
+ * Beside `CameraControls.tsx` rather than in it, because Fast Refresh swaps a module in place only
5
+ * when every value it exports is a component. A hook exported next to one sends that module, and
6
+ * everything importing it, through a full page reload instead.
7
+ *
8
+ * The dependency runs one way, from here into `camera-api.ts`. That file holds the commands and the
9
+ * arithmetic behind them and imports nothing back, which is what lets `useCameraAPI` sit here with
10
+ * the rest.
11
+ */
12
+ import { Point } from '@expofp/geometry';
13
+ import { type CameraAPI } from './camera-api';
14
+ import { type CameraControlsImpl } from './CameraControls';
15
+ /**
16
+ * The scene's camera controls, once {@link Controls} has mounted them.
17
+ *
18
+ * For **reading** them, and for handing them to something else that does the writing — a gesture
19
+ * class, a measurement, a helper taking them as an argument. What the React Compiler rejects is
20
+ * assigning to a *field* of a hook's return value, which is a build error rather than a bug; a
21
+ * method call on it, or passing it along, is fine. A component with a write it cannot route through
22
+ * a function uses {@link useControlsEffect}, which hands the controls over as an argument.
23
+ * @returns The controls, or `undefined` until they mount.
24
+ */
25
+ export declare function useCameraControls(): CameraControlsImpl | undefined;
26
+ /**
27
+ * Runs an effect against the scene's camera controls, for as long as the component is mounted.
28
+ *
29
+ * The controls arrive as an argument rather than from a hook, which is what lets the effect write to
30
+ * them; and the "wait for them to mount" subscription lives here rather than in every caller, so a
31
+ * component that claims a mouse action is three lines and says only what it claims. Together with
32
+ * {@link useCameraControls} that is the whole surface — read them, or write to them in an effect.
33
+ *
34
+ * Re-runs when the controls change and whenever `effect` does, which — the React Compiler caching
35
+ * the closure against what it captures — is whenever the component's own inputs move.
36
+ * @param effect - What to do with the controls; may return a cleanup, as any effect may.
37
+ */
38
+ export declare function useControlsEffect(effect: (controls: CameraControlsImpl) => (() => void) | void): void;
39
+ /**
40
+ * Runs a handler once the camera has come to rest after moving — one call per movement rather than
41
+ * one per frame, whether a gesture or a command drove it. The event the imperative renderer
42
+ * published as `navigation:change`, under a name that says what happened.
43
+ *
44
+ * At rest and not at the start, so a caller that reads the camera back reads where the movement
45
+ * actually ended: the framing a kiosk saves, or the rect an overlay measures itself against. Every
46
+ * movement is reported because the one that moved the camera is not always the one that cares — the
47
+ * zoom buttons reach the camera through the store, and the screen recording the framing never sees
48
+ * that command.
49
+ *
50
+ * `sleep` rather than `rest`, which the library recommends for "the camera has stopped": `rest` is
51
+ * dispatched from the damping tail, and a pan here has none to dispatch from ({@link Controls}
52
+ * zeroes the control smoothing for truck and rotate), so a drag can finish without ever resting.
53
+ * `sleep` is dispatched for every movement, a jump included.
54
+ *
55
+ * A camera-level event rather than the controls themselves, so a caller recording where the plan was
56
+ * left never learns which library drives the camera. For as long as the component is mounted; where
57
+ * the camera *is* once it settles is `useCameraState`'s business.
58
+ * @param handler - Called once each movement has come to rest. Re-subscribed when its identity
59
+ * changes, so pass a stable function — under the React Compiler an inline closure over stable
60
+ * values is one.
61
+ */
62
+ export declare function useCameraMoveEnd(handler: () => void): void;
63
+ /**
64
+ * Runs a handler while the user is moving the camera by hand — a drag, a pinch and a wheel tick
65
+ * alike, as each one reaches the camera.
66
+ *
67
+ * The user's own movement and nothing else, which is what an idle timer wants: a kiosk that treated
68
+ * a command as activity would have the idle reset's own camera restore re-arm the timer it had just
69
+ * fired, and would never return home. Deliberately not bracketed by the gesture's start and end —
70
+ * the controls report neither for the wheel, since scrolling arrives in intermittent ticks with no
71
+ * way to tell a last one from a pause.
72
+ * @param handler - Called as each movement of the user's reaches the camera, which may be once per
73
+ * frame for as long as a drag lasts. Re-subscribed when its identity changes, so pass a stable
74
+ * function — under the React Compiler an inline closure over stable values is one.
75
+ */
76
+ export declare function useCameraGesture(handler: () => void): void;
77
+ /** Where the camera is, at the moment it was asked. */
78
+ export interface CameraState {
79
+ /** The point on the plan it is looking at, in plan units. */
80
+ center: Point;
81
+ /** How far in it is zoomed, as a factor: `1` frames the whole viewbox. */
82
+ zoom: number;
83
+ /** Which way the plan is turned, in degrees clockwise from north, in `[0, 360)`. */
84
+ roll: number;
85
+ /** How far the plan is tilted away from face-on, in degrees. */
86
+ pitch: number;
87
+ /**
88
+ * How much of the plan one CSS pixel spans, in plan units — the factor anything sized in
89
+ * pixels is laid out against, and the quantity the imperative renderer published as `ptScale`.
90
+ * The device ratio is not part of it.
91
+ */
92
+ pixelSize: number;
93
+ }
94
+ /**
95
+ * Reads where the camera is.
96
+ *
97
+ * A **getter**, not a value: nothing here belongs in render output — a component that re-rendered
98
+ * whenever the camera moved would re-render on every frame of every pan — and every caller wants it
99
+ * at a moment of its own choosing, in an event handler, an effect, or a snapshot a host asked for.
100
+ * That is the same reading discipline `Space` documents, for the same reason.
101
+ *
102
+ * The five numbers come from four different places, which is why this exists at all rather than
103
+ * being arithmetic a caller could do: the target from the controls and the plan it lands on from the
104
+ * space, the zoom factor from the camera's handle, and the pixel size from the re-layout registry —
105
+ * which is what the plan's own labels are sized against, so a caller measuring against anything else
106
+ * would disagree with what is drawn.
107
+ *
108
+ * Outside a mounted scene, or in the frames before the controls publish themselves, it reports the
109
+ * camera at rest: the middle of the plan, framed whole, facing north.
110
+ *
111
+ * As with {@link useCameraAPI}, the reader is **new on every render** and this module memoizes
112
+ * nothing; a consumer that needs a settled identity holds it in a ref.
113
+ * @returns A function that reads the current state.
114
+ */
115
+ export declare function useCameraState(): () => CameraState;
116
+ /**
117
+ * The scene's camera commands.
118
+ *
119
+ * A hook rather than a ref handed out from above, and that is the point: it only resolves inside a
120
+ * component mounted under `<Renderer>`, and R3F mounts those only once the canvas and its context
121
+ * exist — so "the camera cannot be driven before the scene is ready" is enforced by where the code
122
+ * can live rather than by a null check every caller has to remember. A host that needs to drive the
123
+ * camera from outside React publishes this object from an effect, which is exactly when it is safe.
124
+ *
125
+ * Before the controls mount every command is a no-op that resolves at once, which is the same frame
126
+ * or two the hook's own consumers spend waiting for their effects.
127
+ *
128
+ * **The returned object is new on every render, deliberately.** This module is not memoized by hand
129
+ * — the React Compiler is scoped to `.tsx` under `src/` (see `vitest.config.mts`), and reaching for
130
+ * `useMemo` here would put a cache in the library that every consumer then has to trust. A consumer
131
+ * that cannot re-subscribe cheaply should hold this in a ref rather than in a dependency list; that
132
+ * way the decision, and its cost, sit with the code that knows how often it renders.
133
+ * @returns The commands.
134
+ */
135
+ export declare function useCameraAPI(): CameraAPI;
136
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Every hook a component reaches the camera through, and the state they read.
3
+ *
4
+ * Beside `CameraControls.tsx` rather than in it, because Fast Refresh swaps a module in place only
5
+ * when every value it exports is a component. A hook exported next to one sends that module, and
6
+ * everything importing it, through a full page reload instead.
7
+ *
8
+ * The dependency runs one way, from here into `camera-api.ts`. That file holds the commands and the
9
+ * arithmetic behind them and imports nothing back, which is what lets `useCameraAPI` sit here with
10
+ * the rest.
11
+ */
12
+ import { Point, radToDeg } from '@expofp/geometry';
13
+ import { useThree } from '@react-three/fiber';
14
+ import { use, useEffect } from 'react';
15
+ import { Vector3 } from 'three';
16
+ import { LayoutTargetsContext } from '../events/layout-targets';
17
+ import { useCamera } from '../space/camera-handle';
18
+ import { useCanvasSize } from '../space/canvas-size';
19
+ import { useSpace } from '../space/Space';
20
+ import { bearing, CameraCommands } from './camera-api';
21
+ /**
22
+ * The scene's camera controls, once {@link Controls} has mounted them.
23
+ *
24
+ * For **reading** them, and for handing them to something else that does the writing — a gesture
25
+ * class, a measurement, a helper taking them as an argument. What the React Compiler rejects is
26
+ * assigning to a *field* of a hook's return value, which is a build error rather than a bug; a
27
+ * method call on it, or passing it along, is fine. A component with a write it cannot route through
28
+ * a function uses {@link useControlsEffect}, which hands the controls over as an argument.
29
+ * @returns The controls, or `undefined` until they mount.
30
+ */
31
+ export function useCameraControls() {
32
+ return useThree((state) => state.controls) ?? undefined;
33
+ }
34
+ /**
35
+ * Runs an effect against the scene's camera controls, for as long as the component is mounted.
36
+ *
37
+ * The controls arrive as an argument rather than from a hook, which is what lets the effect write to
38
+ * them; and the "wait for them to mount" subscription lives here rather than in every caller, so a
39
+ * component that claims a mouse action is three lines and says only what it claims. Together with
40
+ * {@link useCameraControls} that is the whole surface — read them, or write to them in an effect.
41
+ *
42
+ * Re-runs when the controls change and whenever `effect` does, which — the React Compiler caching
43
+ * the closure against what it captures — is whenever the component's own inputs move.
44
+ * @param effect - What to do with the controls; may return a cleanup, as any effect may.
45
+ */
46
+ export function useControlsEffect(effect) {
47
+ const get = useThree((state) => state.get);
48
+ // A subscription, so this re-runs once the controls have mounted. Deliberately not the value the
49
+ // effect uses: that is read fresh from the store below.
50
+ const mounted = useThree((state) => state.controls);
51
+ useEffect(() => {
52
+ const controls = get().controls;
53
+ if (!controls)
54
+ return undefined;
55
+ return effect(controls) ?? undefined;
56
+ }, [effect, get, mounted]);
57
+ }
58
+ /**
59
+ * Runs a handler once the camera has come to rest after moving — one call per movement rather than
60
+ * one per frame, whether a gesture or a command drove it. The event the imperative renderer
61
+ * published as `navigation:change`, under a name that says what happened.
62
+ *
63
+ * At rest and not at the start, so a caller that reads the camera back reads where the movement
64
+ * actually ended: the framing a kiosk saves, or the rect an overlay measures itself against. Every
65
+ * movement is reported because the one that moved the camera is not always the one that cares — the
66
+ * zoom buttons reach the camera through the store, and the screen recording the framing never sees
67
+ * that command.
68
+ *
69
+ * `sleep` rather than `rest`, which the library recommends for "the camera has stopped": `rest` is
70
+ * dispatched from the damping tail, and a pan here has none to dispatch from ({@link Controls}
71
+ * zeroes the control smoothing for truck and rotate), so a drag can finish without ever resting.
72
+ * `sleep` is dispatched for every movement, a jump included.
73
+ *
74
+ * A camera-level event rather than the controls themselves, so a caller recording where the plan was
75
+ * left never learns which library drives the camera. For as long as the component is mounted; where
76
+ * the camera *is* once it settles is `useCameraState`'s business.
77
+ * @param handler - Called once each movement has come to rest. Re-subscribed when its identity
78
+ * changes, so pass a stable function — under the React Compiler an inline closure over stable
79
+ * values is one.
80
+ */
81
+ export function useCameraMoveEnd(handler) {
82
+ useControlsEffect((controls) => {
83
+ controls.addEventListener('sleep', handler);
84
+ return () => controls.removeEventListener('sleep', handler);
85
+ });
86
+ }
87
+ /**
88
+ * Runs a handler while the user is moving the camera by hand — a drag, a pinch and a wheel tick
89
+ * alike, as each one reaches the camera.
90
+ *
91
+ * The user's own movement and nothing else, which is what an idle timer wants: a kiosk that treated
92
+ * a command as activity would have the idle reset's own camera restore re-arm the timer it had just
93
+ * fired, and would never return home. Deliberately not bracketed by the gesture's start and end —
94
+ * the controls report neither for the wheel, since scrolling arrives in intermittent ticks with no
95
+ * way to tell a last one from a pause.
96
+ * @param handler - Called as each movement of the user's reaches the camera, which may be once per
97
+ * frame for as long as a drag lasts. Re-subscribed when its identity changes, so pass a stable
98
+ * function — under the React Compiler an inline closure over stable values is one.
99
+ */
100
+ export function useCameraGesture(handler) {
101
+ useControlsEffect((controls) => {
102
+ controls.addEventListener('control', handler);
103
+ return () => controls.removeEventListener('control', handler);
104
+ });
105
+ }
106
+ /**
107
+ * Reads where the camera is.
108
+ *
109
+ * A **getter**, not a value: nothing here belongs in render output — a component that re-rendered
110
+ * whenever the camera moved would re-render on every frame of every pan — and every caller wants it
111
+ * at a moment of its own choosing, in an event handler, an effect, or a snapshot a host asked for.
112
+ * That is the same reading discipline `Space` documents, for the same reason.
113
+ *
114
+ * The five numbers come from four different places, which is why this exists at all rather than
115
+ * being arithmetic a caller could do: the target from the controls and the plan it lands on from the
116
+ * space, the zoom factor from the camera's handle, and the pixel size from the re-layout registry —
117
+ * which is what the plan's own labels are sized against, so a caller measuring against anything else
118
+ * would disagree with what is drawn.
119
+ *
120
+ * Outside a mounted scene, or in the frames before the controls publish themselves, it reports the
121
+ * camera at rest: the middle of the plan, framed whole, facing north.
122
+ *
123
+ * As with {@link useCameraAPI}, the reader is **new on every render** and this module memoizes
124
+ * nothing; a consumer that needs a settled identity holds it in a ref.
125
+ * @returns A function that reads the current state.
126
+ */
127
+ export function useCameraState() {
128
+ const controls = useCameraControls();
129
+ const camera = useCamera();
130
+ const space = useSpace();
131
+ const targets = use(LayoutTargetsContext);
132
+ return () => readCameraState({ controls, camera, space, targets });
133
+ }
134
+ /**
135
+ * Reads the camera, from whatever the scene holds at the moment of asking.
136
+ * @param context - What the scene holds.
137
+ * @returns Where the camera is.
138
+ */
139
+ function readCameraState(context) {
140
+ const { controls, camera, space, targets } = context;
141
+ const pixelSize = targets?.pixelSize ?? 1;
142
+ // A fresh centre every time: a caller that keeps the point it was handed must never find it
143
+ // moved under them.
144
+ if (!controls)
145
+ return { ...atRest(), pixelSize };
146
+ controls.getTarget(_target);
147
+ return {
148
+ center: space.worldToPlan(_target, new Point()),
149
+ zoom: camera?.zoomFactor ?? 1,
150
+ roll: radToDeg(bearing(controls)),
151
+ pitch: radToDeg(controls.polarAngle),
152
+ pixelSize,
153
+ };
154
+ }
155
+ /**
156
+ * The camera as it stands before anything has moved it, which is what a read outside a mounted
157
+ * scene reports: the middle of the plan, framed whole, facing north.
158
+ * @returns The state.
159
+ */
160
+ function atRest() {
161
+ return { center: new Point(), zoom: 1, roll: 0, pitch: 0, pixelSize: 1 };
162
+ }
163
+ const _target = new Vector3();
164
+ /**
165
+ * The scene's camera commands.
166
+ *
167
+ * A hook rather than a ref handed out from above, and that is the point: it only resolves inside a
168
+ * component mounted under `<Renderer>`, and R3F mounts those only once the canvas and its context
169
+ * exist — so "the camera cannot be driven before the scene is ready" is enforced by where the code
170
+ * can live rather than by a null check every caller has to remember. A host that needs to drive the
171
+ * camera from outside React publishes this object from an effect, which is exactly when it is safe.
172
+ *
173
+ * Before the controls mount every command is a no-op that resolves at once, which is the same frame
174
+ * or two the hook's own consumers spend waiting for their effects.
175
+ *
176
+ * **The returned object is new on every render, deliberately.** This module is not memoized by hand
177
+ * — the React Compiler is scoped to `.tsx` under `src/` (see `vitest.config.mts`), and reaching for
178
+ * `useMemo` here would put a cache in the library that every consumer then has to trust. A consumer
179
+ * that cannot re-subscribe cheaply should hold this in a ref rather than in a dependency list; that
180
+ * way the decision, and its cost, sit with the code that knows how often it renders.
181
+ * @returns The commands.
182
+ */
183
+ export function useCameraAPI() {
184
+ const get = useThree((state) => state.get);
185
+ const invalidate = useThree((state) => state.invalidate);
186
+ const controls = useCameraControls();
187
+ const camera = useCamera();
188
+ const space = useSpace();
189
+ const { width, height, dpr } = useCanvasSize();
190
+ return new CameraCommands({
191
+ controls,
192
+ camera,
193
+ space,
194
+ view: () => get().camera.view,
195
+ width,
196
+ height,
197
+ dpr,
198
+ invalidate,
199
+ });
200
+ }
@@ -0,0 +1,9 @@
1
+ export { bearing, type CameraAPI, type CameraAxes, CameraCommands, type TransitionOptions, type ZoomToOptions, } from './camera-api';
2
+ export { CameraControls, CameraControlsAction, type CameraControlsImpl } from './CameraControls';
3
+ export { Controls, type ControlsConfig, type ControlsProps } from './Controls';
4
+ export type { InertiaConfig } from './handlers/Inertia';
5
+ export type { PanConfig } from './handlers/Pan';
6
+ export type { RollConfig } from './handlers/Roll';
7
+ export type { ZoomConfig } from './handlers/Zoom';
8
+ export { type CameraState, useCameraAPI, useCameraControls, useCameraGesture, useCameraMoveEnd, useCameraState, useControlsEffect, } from './hooks';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ // The package's controls surface. A barrel and nothing else: a module that both re-exports and
2
+ // declares a component cannot be a Fast Refresh boundary, and every hook below would send `Controls`
3
+ // through a full reload with it.
4
+ export { bearing, CameraCommands, } from './camera-api';
5
+ export { CameraControls, CameraControlsAction } from './CameraControls';
6
+ export { Controls } from './Controls';
7
+ export { useCameraAPI, useCameraControls, useCameraGesture, useCameraMoveEnd, useCameraState, useControlsEffect, } from './hooks';
@@ -0,0 +1,89 @@
1
+ import { Box, type Point2Like } from '@expofp/geometry';
2
+ import { type PerspectiveCamera } from 'three';
3
+ import { type Space } from '../space/Space';
4
+ /**
5
+ * The part of the canvas a target is framed inside, in **device** pixels, as the camera's own
6
+ * framing measures it.
7
+ *
8
+ * The offset is from the drawing buffer's centre rather than from its corner, because that is the
9
+ * only thing the fit does with it — a region centred on the canvas frames exactly as no region at
10
+ * all, and says so by being all zeroes.
11
+ */
12
+ export interface FramingRegion {
13
+ /** How wide the region is. */
14
+ width: number;
15
+ /** How tall it is. */
16
+ height: number;
17
+ /** How far its centre sits to the right of the buffer's centre; negative for left. */
18
+ offsetX: number;
19
+ /** How far below; negative for above. */
20
+ offsetY: number;
21
+ }
22
+ /**
23
+ * What a camera's projection was offset by — three's own `camera.view`, which `VisibleRect` is what
24
+ * writes. Aliased rather than restated so the two can never drift.
25
+ */
26
+ export type ViewOffset = PerspectiveCamera['view'];
27
+ /** How {@link fitBox} should frame its target. */
28
+ export interface FitOptions {
29
+ /** The target, in plan units. */
30
+ target: Box;
31
+ /** The scene's space, which takes the target into world units. */
32
+ space: Space;
33
+ /** How far the plan is turned, in radians — the camera's azimuth, negated. */
34
+ bearing: number;
35
+ /** The region to frame the target inside. */
36
+ region: FramingRegion;
37
+ /** Fraction of the region left as margin on each side; `0.25` spends half of it. */
38
+ padding?: number;
39
+ /** The closest the fit may bring the plan, as a zoom factor. */
40
+ maxZoom?: number;
41
+ }
42
+ /** Where the camera has to be for a target to fill its region. */
43
+ export interface Fit {
44
+ /** Where the camera should look, in world units. */
45
+ center: Point2Like;
46
+ /** The zoom factor that frames the target. */
47
+ zoom: number;
48
+ }
49
+ /**
50
+ * Where the camera has to be for a box of the plan to fill a region of the canvas.
51
+ *
52
+ * Pure arithmetic, kept out of the hook so the part that can be wrong is the part that can be
53
+ * tested. It rests on the one invariant the viewbox fit exists to establish — at zoom factor `1` one
54
+ * world unit is one device pixel — which is what lets a world size and a pixel size be divided by
55
+ * each other below.
56
+ *
57
+ * The target is measured **in the camera's own frame**: its world box is turned about its centre by
58
+ * the bearing before its bounds are taken, so a plan the user has rolled frames the box they can see
59
+ * rather than its axis-aligned shadow. Turning about the centre is what leaves the centre alone,
60
+ * which is why only the size of that box is read.
61
+ *
62
+ * The pan target then picks up the region's offset from the buffer's centre, which is what lands the
63
+ * box in the middle of *that region* rather than in the middle of the canvas. The offset is divided
64
+ * by the zoom because it arrives as a distance in pixels and has to leave as one in world units, and
65
+ * turned by the bearing for the same reason the box was.
66
+ * @param options - The target, and how to frame it.
67
+ * @returns The framing, or `undefined` for a target with no area, which nothing can be fitted to.
68
+ */
69
+ export declare function fitBox(options: FitOptions): Fit | undefined;
70
+ /**
71
+ * The region a caller named, in the coordinates the fit works in.
72
+ *
73
+ * A `within` rect is given in css pixels of the **canvas**, which is where a host's own panels are.
74
+ * The fit works in the camera's framing instead — the canvas as it would be with no visible rect —
75
+ * so a rect has to come back through whatever `VisibleRect` put on the projection: a uniform scale
76
+ * about the rect's centre, and nothing else. With no view offset that reduces to a change of origin
77
+ * and a multiplication by the device pixel ratio.
78
+ *
79
+ * This is the one place in the renderer where a region of the canvas and the camera's framing meet,
80
+ * and it is the method whose whole job is relating the two. Everything else stays clear of it.
81
+ * @param within - The region, in css pixels from the canvas's top-left corner; omitted means all of it.
82
+ * @param width - The canvas width in css pixels.
83
+ * @param height - The canvas height in css pixels.
84
+ * @param dpr - Device pixels per css pixel.
85
+ * @param view - The camera's view offset, when one is in force.
86
+ * @returns The region in device pixels, offset from the buffer's centre.
87
+ */
88
+ export declare function framingRegion(within: Box | undefined, width: number, height: number, dpr: number, view?: ViewOffset | null): FramingRegion;
89
+ //# sourceMappingURL=zoom-to.d.ts.map
@@ -0,0 +1,91 @@
1
+ import { Box, Point, rectBounds } from '@expofp/geometry';
2
+ /**
3
+ * Where the camera has to be for a box of the plan to fill a region of the canvas.
4
+ *
5
+ * Pure arithmetic, kept out of the hook so the part that can be wrong is the part that can be
6
+ * tested. It rests on the one invariant the viewbox fit exists to establish — at zoom factor `1` one
7
+ * world unit is one device pixel — which is what lets a world size and a pixel size be divided by
8
+ * each other below.
9
+ *
10
+ * The target is measured **in the camera's own frame**: its world box is turned about its centre by
11
+ * the bearing before its bounds are taken, so a plan the user has rolled frames the box they can see
12
+ * rather than its axis-aligned shadow. Turning about the centre is what leaves the centre alone,
13
+ * which is why only the size of that box is read.
14
+ *
15
+ * The pan target then picks up the region's offset from the buffer's centre, which is what lands the
16
+ * box in the middle of *that region* rather than in the middle of the canvas. The offset is divided
17
+ * by the zoom because it arrives as a distance in pixels and has to leave as one in world units, and
18
+ * turned by the bearing for the same reason the box was.
19
+ * @param options - The target, and how to frame it.
20
+ * @returns The framing, or `undefined` for a target with no area, which nothing can be fitted to.
21
+ */
22
+ export function fitBox(options) {
23
+ const { target, space, bearing, region, padding = 0, maxZoom } = options;
24
+ // The bounds of the target once the plan is turned — geometry's own rect maths, given the world
25
+ // box as a rect with a rotation. Turning is about the centre, so the centre below is still the
26
+ // target's and only the extent has changed.
27
+ const world = worldBounds(target, space);
28
+ const source = rectBounds({ center: world.center, size: world.size, rotation: bearing });
29
+ const { width: sourceWidth, height: sourceHeight } = source.size;
30
+ if (sourceWidth <= 0 || sourceHeight <= 0)
31
+ return undefined;
32
+ if (region.width <= 0 || region.height <= 0)
33
+ return undefined;
34
+ const margin = 1 - 2 * padding;
35
+ const fitted = Math.min((region.width * margin) / sourceWidth, (region.height * margin) / sourceHeight);
36
+ const zoom = maxZoom === undefined ? fitted : Math.min(fitted, maxZoom);
37
+ if (!(zoom > 0))
38
+ return undefined;
39
+ const pan = new Point(-region.offsetX / zoom, -region.offsetY / zoom).rotate(bearing, ORIGIN);
40
+ return { center: { x: source.center.x + pan.x, y: source.center.y + pan.y }, zoom };
41
+ }
42
+ /**
43
+ * The region a caller named, in the coordinates the fit works in.
44
+ *
45
+ * A `within` rect is given in css pixels of the **canvas**, which is where a host's own panels are.
46
+ * The fit works in the camera's framing instead — the canvas as it would be with no visible rect —
47
+ * so a rect has to come back through whatever `VisibleRect` put on the projection: a uniform scale
48
+ * about the rect's centre, and nothing else. With no view offset that reduces to a change of origin
49
+ * and a multiplication by the device pixel ratio.
50
+ *
51
+ * This is the one place in the renderer where a region of the canvas and the camera's framing meet,
52
+ * and it is the method whose whole job is relating the two. Everything else stays clear of it.
53
+ * @param within - The region, in css pixels from the canvas's top-left corner; omitted means all of it.
54
+ * @param width - The canvas width in css pixels.
55
+ * @param height - The canvas height in css pixels.
56
+ * @param dpr - Device pixels per css pixel.
57
+ * @param view - The camera's view offset, when one is in force.
58
+ * @returns The region in device pixels, offset from the buffer's centre.
59
+ */
60
+ export function framingRegion(within, width, height, dpr, view) {
61
+ if (!within) {
62
+ return { width: width * dpr, height: height * dpr, offsetX: 0, offsetY: 0 };
63
+ }
64
+ // What the framing was scaled by, and where its centre landed — read straight back off the
65
+ // parameters `VisibleRect` handed `setViewOffset`.
66
+ const scale = view?.enabled ? view.fullWidth / view.width : 1;
67
+ const landingX = view?.enabled ? view.fullWidth / 2 - view.offsetX : width / 2;
68
+ const landingY = view?.enabled ? view.fullHeight / 2 - view.offsetY : height / 2;
69
+ return {
70
+ width: (within.size.width / scale) * dpr,
71
+ height: (within.size.height / scale) * dpr,
72
+ offsetX: ((within.center.x - landingX) / scale) * dpr,
73
+ offsetY: ((within.center.y - landingY) / scale) * dpr,
74
+ };
75
+ }
76
+ /**
77
+ * The target's bounds in world space.
78
+ *
79
+ * Two corners are the whole box: the plan-to-world mapping scales and translates, so opposite
80
+ * corners stay opposite. `fromMinMax` normalizes them, so a transform that mirrors an axis and
81
+ * hands them over the other way round still comes out right.
82
+ * @param target - The target, in plan units.
83
+ * @param space - The scene's space.
84
+ * @returns The bounds, in world units.
85
+ */
86
+ function worldBounds(target, space) {
87
+ const min = space.planToWorld({ ...target.min, z: 0 });
88
+ const max = space.planToWorld({ ...target.max, z: 0 });
89
+ return Box.fromMinMax(min, max);
90
+ }
91
+ const ORIGIN = { x: 0, y: 0 };