@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,212 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Point } from '@expofp/geometry';
3
+ import { useThree } from '@react-three/fiber';
4
+ import { useEffect, useState } from 'react';
5
+ import { Vector3 } from 'three';
6
+ import { useSpace, } from '../../space/Space';
7
+ import { CameraControlsAction } from '../CameraControls';
8
+ import { canvasPoint } from '../gestures';
9
+ import { useCameraControls, useControlsEffect } from '../hooks';
10
+ import { Inertia } from './Inertia';
11
+ /**
12
+ * Panning: a left-drag, and a one-finger drag.
13
+ *
14
+ * Screen-pan rather than truck, so the plan tracks the pointer at any pitch instead of sliding
15
+ * along the camera's own axes — the imperative renderer's choice, and what makes a tilted plan drag
16
+ * the way a map does.
17
+ *
18
+ * Owns the glide a lifted pointer leaves by rendering {@link PanInertia}, which is the one piece of
19
+ * panning camera-controls does not do itself.
20
+ *
21
+ * Mounting is enabling: the actions are set on mount and returned to `NONE` on unmount.
22
+ * @param props - Whether a pan glides, and how.
23
+ * @returns The glide, when there is one.
24
+ */
25
+ export function Pan(props) {
26
+ const inertia = props.inertia ?? true;
27
+ useControlsEffect((controls) => {
28
+ controls.mouseButtons.left = CameraControlsAction.SCREEN_PAN;
29
+ controls.touches.one = CameraControlsAction.TOUCH_SCREEN_PAN;
30
+ return () => {
31
+ controls.mouseButtons.left = CameraControlsAction.NONE;
32
+ controls.touches.one = CameraControlsAction.NONE;
33
+ };
34
+ });
35
+ if (inertia === false)
36
+ return null;
37
+ return _jsx(PanInertia, { ...(inertia === true ? {} : inertia) });
38
+ }
39
+ /**
40
+ * The glide a pan leaves behind: {@link Inertia}, fed the pan itself.
41
+ *
42
+ * **Measured from the pointers, not from the camera.** Every step of a drag that pans — by mouse,
43
+ * by one finger, or by two fingers pinching and panning at once — samples how far the pointers'
44
+ * centroid moved, in css pixels. That is the pan camera-controls applies on the same step, and
45
+ * nothing else: a pinch spreads the fingers about their centroid, and a twist turns them about it,
46
+ * so neither moves it, and neither leaks into the glide. A step whose action has no pan in it — a
47
+ * right-drag turn, or a tilt, which switches the two-finger action off — samples nothing.
48
+ *
49
+ * Applied the same way round: each step of the glide moves the plan so that the point under the
50
+ * last centroid travels the step's pixels, which glides a tilted or turned plan the way the drag
51
+ * moved it.
52
+ *
53
+ * A component of its own rather than a branch inside {@link Pan}, because its hooks cannot be
54
+ * switched off conditionally — mounting this is what switches the glide on.
55
+ * @param props - How the glide is measured and how it slows.
56
+ * @returns The glide.
57
+ */
58
+ function PanInertia(props) {
59
+ const canvas = useThree((state) => state.gl.domElement);
60
+ const get = useThree((state) => state.get);
61
+ const space = useSpace();
62
+ // Read, not written: `panBy` takes the controls as an argument and does the writing.
63
+ const controls = useCameraControls();
64
+ const [pointers] = useState(() => new PointerCentroid());
65
+ useEffect(() => {
66
+ pointers.connect(canvas);
67
+ return () => pointers.disconnect();
68
+ }, [pointers, canvas]);
69
+ const sample = () => {
70
+ // Always taken, so a step that sat the pan out does not leave its movement for the next one.
71
+ const delta = pointers.sample();
72
+ return controls && (controls.currentAction & PAN) !== 0 ? delta : undefined;
73
+ };
74
+ const apply = (step) => controls !== undefined && panBy(controls, space, get().camera, pointers.anchor, step);
75
+ return _jsx(Inertia, { ...props, sample: sample, apply: apply });
76
+ }
77
+ /**
78
+ * Where the pointers are, and how far their centroid has moved since the last sample.
79
+ *
80
+ * Measured the way MapLibre's touch pan measures it: as the average movement of the pointers that
81
+ * were down at both samples. A finger that lands or lifts between two samples therefore adds no
82
+ * jump of its own, where the raw centroid would leap halfway to it.
83
+ */
84
+ class PointerCentroid {
85
+ /** Where the pointers' centroid was at the last sample, in css pixels from the canvas's corner. */
86
+ anchor = new Point();
87
+ element;
88
+ positions = new Map();
89
+ last = new Map();
90
+ delta = [0, 0];
91
+ /**
92
+ * Starts tracking the pointers that land on an element. Moves and lifts are heard on its
93
+ * document, as camera-controls hears them, so a mouse dragged off the canvas is still followed.
94
+ * @param element - The element pointers land on.
95
+ */
96
+ connect(element) {
97
+ if (this.element)
98
+ this.disconnect();
99
+ this.element = element;
100
+ const document = element.ownerDocument;
101
+ element.addEventListener('pointerdown', this.onDown);
102
+ document.addEventListener('pointermove', this.onMove);
103
+ document.addEventListener('pointerup', this.onUp);
104
+ document.addEventListener('pointercancel', this.onUp);
105
+ }
106
+ /** Stops tracking, and forgets every pointer. */
107
+ disconnect() {
108
+ const element = this.element;
109
+ if (!element)
110
+ return;
111
+ const document = element.ownerDocument;
112
+ element.removeEventListener('pointerdown', this.onDown);
113
+ document.removeEventListener('pointermove', this.onMove);
114
+ document.removeEventListener('pointerup', this.onUp);
115
+ document.removeEventListener('pointercancel', this.onUp);
116
+ this.positions.clear();
117
+ this.last.clear();
118
+ this.element = undefined;
119
+ }
120
+ /**
121
+ * How far the centroid moved since the last call, and where it is now.
122
+ * @returns The movement in css pixels, as `[x, y]`; reused between calls.
123
+ */
124
+ sample() {
125
+ let dx = 0;
126
+ let dy = 0;
127
+ let moved = 0;
128
+ let cx = 0;
129
+ let cy = 0;
130
+ for (const [id, position] of this.positions) {
131
+ cx += position.x;
132
+ cy += position.y;
133
+ const last = this.last.get(id);
134
+ if (!last)
135
+ continue;
136
+ dx += position.x - last.x;
137
+ dy += position.y - last.y;
138
+ last.x = position.x;
139
+ last.y = position.y;
140
+ moved++;
141
+ }
142
+ if (this.positions.size)
143
+ this.anchor.set(cx / this.positions.size, cy / this.positions.size, 0);
144
+ this.delta[0] = moved ? dx / moved : 0;
145
+ this.delta[1] = moved ? dy / moved : 0;
146
+ return this.delta;
147
+ }
148
+ /**
149
+ * A pointer landing on the canvas, whose `offsetX` is measured from the canvas itself. Its later
150
+ * moves are heard on the document and may target any element, so their own `offsetX` is not the
151
+ * canvas's — they are placed by how far they moved in client pixels from where this one landed.
152
+ * @param event - The pointer landing.
153
+ */
154
+ onDown = (event) => {
155
+ const { x, y } = canvasPoint(event);
156
+ this.positions.set(event.pointerId, {
157
+ x,
158
+ y,
159
+ toCanvasX: x - event.clientX,
160
+ toCanvasY: y - event.clientY,
161
+ });
162
+ this.last.set(event.pointerId, { x, y });
163
+ };
164
+ onMove = (event) => {
165
+ const position = this.positions.get(event.pointerId);
166
+ if (!position)
167
+ return;
168
+ position.x = event.clientX + position.toCanvasX;
169
+ position.y = event.clientY + position.toCanvasY;
170
+ };
171
+ onUp = (event) => {
172
+ this.positions.delete(event.pointerId);
173
+ this.last.delete(event.pointerId);
174
+ };
175
+ }
176
+ /**
177
+ * Pans the plan by a step in css pixels: the point under `anchor` moves to `anchor + step`.
178
+ *
179
+ * Both points are found on the plan through the camera and taken out to the world, and the target
180
+ * moves by their difference — so a tilted plan, whose pixels cover more ground toward the horizon,
181
+ * glides as far as the drag would have carried it there.
182
+ * @param controls - The controls to move.
183
+ * @param space - The scene's space.
184
+ * @param camera - The camera the canvas is drawn through.
185
+ * @param anchor - Where the step starts, in css pixels from the canvas's top-left corner.
186
+ * @param step - How far to move, in css pixels, as `[x, y]`.
187
+ * @returns Whether it moved; a step off the plan, toward the horizon, does not.
188
+ */
189
+ function panBy(controls, space, camera, anchor, step) {
190
+ const from = space.canvasToPlan(anchor, camera, _fromPlan);
191
+ const to = space.canvasToPlan({ x: anchor.x + step[0], y: anchor.y + step[1] }, camera, _toPlan);
192
+ if (!from || !to)
193
+ return false;
194
+ space.planToWorld(from, _fromWorld);
195
+ space.planToWorld(to, _toWorld);
196
+ const target = controls.getTarget(_target, false).add(_fromWorld).sub(_toWorld);
197
+ void controls.moveTo(target.x, target.y, target.z, false);
198
+ return true;
199
+ }
200
+ /**
201
+ * The controls' pan bits. A two-finger pinch that pans as it dollies carries one too, and its pan
202
+ * is sampled like any other: the centroid measures the pan alone.
203
+ */
204
+ const PAN = CameraControlsAction.TRUCK |
205
+ CameraControlsAction.SCREEN_PAN |
206
+ CameraControlsAction.TOUCH_TRUCK |
207
+ CameraControlsAction.TOUCH_SCREEN_PAN;
208
+ const _fromPlan = new Point();
209
+ const _toPlan = new Point();
210
+ const _fromWorld = new Vector3();
211
+ const _toWorld = new Vector3();
212
+ const _target = new Vector3();
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pitching: a right-drag, and two fingers dragged vertically together.
3
+ *
4
+ * The right-drag is camera-controls' own polar rotation. The vertical drag is not, and the hard
5
+ * part is not applying it but telling it apart from the pinch and the twist that share two fingers.
6
+ * That judgement lives in {@link beginsVertically}, and until it is settled the gesture does
7
+ * nothing.
8
+ *
9
+ * The pitch **limits** are not here. They constrain the camera whether or not this handler is
10
+ * mounted — the imperative renderer applied them from its pitch handler's constructor for exactly
11
+ * that reason — so they are `Camera`'s, alongside the zoom bounds and the clipping planes derived
12
+ * from both.
13
+ *
14
+ * Mounting is enabling. The mouse action shares a field with `Roll`, so it is set and cleared with
15
+ * `|=` and `&= ~`, which are idempotent where an exclusive-or would not be.
16
+ * @returns Nothing; this renders no scene node.
17
+ */
18
+ export declare function Pitch(): null;
19
+ //# sourceMappingURL=Pitch.d.ts.map
@@ -0,0 +1,200 @@
1
+ import { degToRad } from '@expofp/geometry';
2
+ import { useState } from 'react';
3
+ import { Vector2 } from 'three';
4
+ import { CameraControlsAction } from '../CameraControls';
5
+ import { canvasPoint, useGesture } from '../gestures';
6
+ import { useCameraControls, useControlsEffect } from '../hooks';
7
+ /**
8
+ * Pitching: a right-drag, and two fingers dragged vertically together.
9
+ *
10
+ * The right-drag is camera-controls' own polar rotation. The vertical drag is not, and the hard
11
+ * part is not applying it but telling it apart from the pinch and the twist that share two fingers.
12
+ * That judgement lives in {@link beginsVertically}, and until it is settled the gesture does
13
+ * nothing.
14
+ *
15
+ * The pitch **limits** are not here. They constrain the camera whether or not this handler is
16
+ * mounted — the imperative renderer applied them from its pitch handler's constructor for exactly
17
+ * that reason — so they are `Camera`'s, alongside the zoom bounds and the clipping planes derived
18
+ * from both.
19
+ *
20
+ * Mounting is enabling. The mouse action shares a field with `Roll`, so it is set and cleared with
21
+ * `|=` and `&= ~`, which are idempotent where an exclusive-or would not be.
22
+ * @returns Nothing; this renders no scene node.
23
+ */
24
+ export function Pitch() {
25
+ // Read, not written: the gesture below takes the controls as an argument and does the writing.
26
+ const controls = useCameraControls();
27
+ const [gesture] = useState(() => new PitchGesture());
28
+ useControlsEffect((controls) => {
29
+ controls.mouseButtons.right |= CameraControlsAction.ROTATE_POLAR;
30
+ return () => {
31
+ controls.mouseButtons.right &= ~CameraControlsAction.ROTATE_POLAR;
32
+ // Unmounting mid-gesture would otherwise leave the two-finger action switched off for
33
+ // whichever handler owns it.
34
+ gesture.end(controls);
35
+ };
36
+ });
37
+ // Each of these is cached by the React Compiler against what it captures, so `useGesture`
38
+ // re-subscribes only when one of those actually moves — in practice, never.
39
+ const onPitchStart = (event) => gesture.start(event);
40
+ const onPitch = (event) => {
41
+ if (controls)
42
+ gesture.move(event, controls);
43
+ };
44
+ const onPitchEnd = () => gesture.end(controls);
45
+ useGesture('pitchstart', onPitchStart);
46
+ useGesture('pitch', onPitch);
47
+ useGesture('pitchend', onPitchEnd);
48
+ return null;
49
+ }
50
+ /**
51
+ * Two fingers dragged vertically together, tilting the plan.
52
+ *
53
+ * A class rather than fields on the component, for two reasons that agree: the React Compiler
54
+ * refuses a direct write to anything `useState` returned, and the gesture's arbitration is worth
55
+ * testing without a canvas around it. {@link Pitch} is the React half.
56
+ */
57
+ class PitchGesture {
58
+ verdict = {};
59
+ lastPoints;
60
+ prevTwoFingerAction;
61
+ p0 = new Vector2();
62
+ p1 = new Vector2();
63
+ /**
64
+ * Remembers where the fingers started, and rules the gesture out when they are stacked one above
65
+ * the other — from there a vertical drag is indistinguishable from a pinch.
66
+ * @param event - The gesture event that began the drag.
67
+ */
68
+ start(event) {
69
+ const pointers = byPointerId(event);
70
+ if (pointers.length !== 2)
71
+ return;
72
+ this.p0.copy(canvasPoint(pointers[0]));
73
+ this.p1.copy(canvasPoint(pointers[1]));
74
+ this.lastPoints = [new Vector2().copy(this.p0), new Vector2().copy(this.p1)];
75
+ if (isVertical(this.p0.sub(this.p1)))
76
+ this.verdict.isValid = false;
77
+ }
78
+ /**
79
+ * Tilts the plan by how far the fingers moved down the screen, once the gesture has been judged
80
+ * a pitch.
81
+ * @param event - The gesture event carrying the drag so far.
82
+ * @param controls - The controls to tilt.
83
+ */
84
+ move(event, controls) {
85
+ const lastPoints = this.lastPoints;
86
+ if (!lastPoints)
87
+ return;
88
+ const pointers = byPointerId(event);
89
+ if (pointers.length !== 2)
90
+ return;
91
+ // From here on `p0` and `p1` are movements rather than positions.
92
+ this.p0.copy(canvasPoint(pointers[0])).sub(lastPoints[0]);
93
+ this.p1.copy(canvasPoint(pointers[1])).sub(lastPoints[1]);
94
+ if (!beginsVertically(this.p0, this.p1, event.timeStamp, this.verdict))
95
+ return;
96
+ // The pinch would otherwise dolly the plan while it is being tilted. Captured rather than
97
+ // assumed, so whatever the zoom handler put there is what comes back at the end of the gesture.
98
+ if (this.prevTwoFingerAction === undefined) {
99
+ this.prevTwoFingerAction = controls.touches.two;
100
+ controls.touches.two = CameraControlsAction.NONE;
101
+ // Bracketed from the moment the drag is judged a tilt, which is also the moment it first
102
+ // moves the camera.
103
+ controls.dispatchEvent({ type: 'controlstart' });
104
+ }
105
+ lastPoints[0].add(this.p0);
106
+ lastPoints[1].add(this.p1);
107
+ const yDeltaAverage = (this.p0.y + this.p1.y) / 2;
108
+ const deltaAngle = degToRad(yDeltaAverage * DEGREES_PER_PIXEL);
109
+ void controls.rotatePolarTo(controls.polarAngle + deltaAngle, false);
110
+ // The tilt moves the camera through a public setter, which the controls count as a command
111
+ // rather than the user's hand; reporting it is what tells an idle timer the user is here.
112
+ controls.dispatchEvent({ type: 'control' });
113
+ }
114
+ /**
115
+ * Puts back the two-finger action this suppressed, if it suppressed one, closes the
116
+ * `controlstart` the tilt opened, and forgets the gesture.
117
+ *
118
+ * Nothing is restored when the captured action was already `NONE`: there was nothing to suppress,
119
+ * and writing it back would claim a field this handler does not own.
120
+ * @param controls - The controls to restore and report to, if they are still there.
121
+ */
122
+ end(controls) {
123
+ if (this.prevTwoFingerAction !== undefined)
124
+ controls?.dispatchEvent({ type: 'controlend' });
125
+ if (controls && this.prevTwoFingerAction)
126
+ controls.touches.two = this.prevTwoFingerAction;
127
+ this.prevTwoFingerAction = undefined;
128
+ this.verdict = {};
129
+ this.lastPoints = undefined;
130
+ }
131
+ }
132
+ /**
133
+ * Whether a two-finger gesture is a pitch, judged from how the fingers first moved.
134
+ *
135
+ * Judged once and then held: the verdict is returned unchanged as soon as it has one, so a drag
136
+ * that started as a pitch stays one even if the fingers wander, and one that did not can never
137
+ * become one. A `false` verdict is what makes a pinch or a twist stay a pinch or a twist.
138
+ *
139
+ * The two fingers rarely start moving on the same frame, so one moving alone is not by itself a
140
+ * verdict — the other is given {@link ALLOWED_SINGLE_TOUCH_TIME} to follow, and only when it fails
141
+ * to is the gesture ruled out.
142
+ * @param a - How far the first finger moved since the last event.
143
+ * @param b - How far the second finger moved since the last event.
144
+ * @param timeStamp - When this event happened, in milliseconds.
145
+ * @param verdict - How far the judgement has got; updated in place.
146
+ * @returns Whether this is a pitch — `false` both for "not a pitch" and for "not yet decided", the
147
+ * two cases a caller treats alike by doing nothing.
148
+ */
149
+ function beginsVertically(a, b, timeStamp, verdict) {
150
+ verdict.isValid = judge(a, b, timeStamp, verdict);
151
+ return verdict.isValid === true;
152
+ }
153
+ /**
154
+ * Whether a movement is more down-the-screen than across it.
155
+ * @param vector - The movement to judge.
156
+ * @returns Whether it is vertical.
157
+ */
158
+ function isVertical(vector) {
159
+ return Math.abs(vector.y) > Math.abs(vector.x);
160
+ }
161
+ /**
162
+ * The judgement itself, which unlike {@link beginsVertically} distinguishes "not a pitch" from
163
+ * "the fingers have not said yet".
164
+ * @param a - How far the first finger moved since the last event.
165
+ * @param b - How far the second finger moved since the last event.
166
+ * @param timeStamp - When this event happened, in milliseconds.
167
+ * @param verdict - How far the judgement has got; its `firstMove` is set when one finger moves alone.
168
+ * @returns Whether this is a pitch, or `undefined` while the fingers have not said.
169
+ */
170
+ function judge(a, b, timeStamp, verdict) {
171
+ if (verdict.isValid !== undefined)
172
+ return verdict.isValid;
173
+ const movedA = a.length() >= MOVE_THRESHOLD_PX;
174
+ const movedB = b.length() >= MOVE_THRESHOLD_PX;
175
+ // Neither finger has moved a meaningful amount; wait.
176
+ if (!movedA && !movedB)
177
+ return undefined;
178
+ // One finger has moved and the other has not. Wait a moment for the second, then give up.
179
+ if (!movedA || !movedB) {
180
+ verdict.firstMove ??= timeStamp;
181
+ return timeStamp - verdict.firstMove < ALLOWED_SINGLE_TOUCH_TIME ? undefined : false;
182
+ }
183
+ const sameDirection = a.y > 0 === b.y > 0;
184
+ return isVertical(a) && isVertical(b) && sameDirection;
185
+ }
186
+ /**
187
+ * The gesture's pointers in a stable order, so "the first finger" means the same finger from one
188
+ * event to the next and its movement can be diffed.
189
+ * @param event - The gesture event.
190
+ * @returns The pointers ordered by id — a copy, so the recognizer's own array is left alone.
191
+ */
192
+ function byPointerId(event) {
193
+ return event.pointers.slice().sort((a, b) => a.pointerId - b.pointerId);
194
+ }
195
+ /** How far the plan tilts per pixel the fingers travel down the screen, in degrees. */
196
+ const DEGREES_PER_PIXEL = -0.5;
197
+ /** How far a finger must travel before it counts as having moved, in pixels. */
198
+ const MOVE_THRESHOLD_PX = 2;
199
+ /** How long the second finger has to join the first before the gesture is ruled out, in milliseconds. */
200
+ const ALLOWED_SINGLE_TOUCH_TIME = 100;
@@ -0,0 +1,30 @@
1
+ /** Options for the roll handler, which are also its props. */
2
+ export interface RollConfig {
3
+ /**
4
+ * How far two fingers must twist before the plan starts turning, in pixels along the
5
+ * circumference of the circle they describe; defaults to `25`.
6
+ *
7
+ * Measured along the circumference rather than in degrees — the Mapbox rule — so the same
8
+ * threshold asks for a wide turn when the fingers are close together and a slight one when they
9
+ * are far apart, which is what stops a pinch from turning the plan by accident.
10
+ */
11
+ threshold?: number;
12
+ }
13
+ /**
14
+ * Rolling: a right-drag, and a two-finger twist.
15
+ *
16
+ * The right-drag is camera-controls' own azimuth rotation. The twist is not — it turns the plan
17
+ * about **the point between the fingers** rather than about whatever the camera is looking at,
18
+ * which is what makes two fingers feel like they are holding the plan itself. camera-controls can
19
+ * only orbit its target, so the twist is recognized off the canvas's gesture manager and applied
20
+ * by {@link RollGesture}.
21
+ *
22
+ * Mounting is enabling. The mouse action is a bit of a shared field — `Pitch` owns the other one —
23
+ * so it is set and cleared with `|=` and `&= ~` rather than assigned. Both are idempotent, which an
24
+ * exclusive-or (the imperative renderer's) is not: `StrictMode` re-running an effect would flip a
25
+ * bit back on.
26
+ * @param props - How far a twist must travel before it turns the plan.
27
+ * @returns Nothing; this renders no scene node.
28
+ */
29
+ export declare function Roll(props: RollConfig): null;
30
+ //# sourceMappingURL=Roll.d.ts.map
@@ -0,0 +1,196 @@
1
+ import { degToRad, Point, radToDeg } from '@expofp/geometry';
2
+ import { useThree } from '@react-three/fiber';
3
+ import { useState } from 'react';
4
+ import { Matrix4, Vector2, Vector3 } from 'three';
5
+ import { useSpace } from '../../space/Space';
6
+ import { CameraControlsAction } from '../CameraControls';
7
+ import { canvasPoint, useGesture } from '../gestures';
8
+ import { useCameraControls, useControlsEffect } from '../hooks';
9
+ /**
10
+ * Rolling: a right-drag, and a two-finger twist.
11
+ *
12
+ * The right-drag is camera-controls' own azimuth rotation. The twist is not — it turns the plan
13
+ * about **the point between the fingers** rather than about whatever the camera is looking at,
14
+ * which is what makes two fingers feel like they are holding the plan itself. camera-controls can
15
+ * only orbit its target, so the twist is recognized off the canvas's gesture manager and applied
16
+ * by {@link RollGesture}.
17
+ *
18
+ * Mounting is enabling. The mouse action is a bit of a shared field — `Pitch` owns the other one —
19
+ * so it is set and cleared with `|=` and `&= ~` rather than assigned. Both are idempotent, which an
20
+ * exclusive-or (the imperative renderer's) is not: `StrictMode` re-running an effect would flip a
21
+ * bit back on.
22
+ * @param props - How far a twist must travel before it turns the plan.
23
+ * @returns Nothing; this renders no scene node.
24
+ */
25
+ export function Roll(props) {
26
+ const threshold = props.threshold ?? DEFAULT_THRESHOLD;
27
+ const get = useThree((state) => state.get);
28
+ // Read, not written: the gesture below takes the controls as an argument and does the writing, so
29
+ // nothing here ever assigns to them — which is the line the React Compiler draws.
30
+ const controls = useCameraControls();
31
+ const space = useSpace();
32
+ const [gesture] = useState(() => new RollGesture());
33
+ useControlsEffect((controls) => {
34
+ controls.mouseButtons.right |= CameraControlsAction.ROTATE_AZIMUTH;
35
+ return () => {
36
+ controls.mouseButtons.right &= ~CameraControlsAction.ROTATE_AZIMUTH;
37
+ // Unmounting mid-twist would otherwise leave a `controlstart` without its `controlend`.
38
+ gesture.end(controls);
39
+ };
40
+ });
41
+ // Each of these is cached by the React Compiler against what it captures, so `useGesture`
42
+ // re-subscribes only when one of those actually moves — in practice, never.
43
+ const onRotateStart = (event) => gesture.start(event);
44
+ const onRotate = (event) => {
45
+ if (controls)
46
+ gesture.move(event, threshold, { controls, camera: get().camera, space });
47
+ };
48
+ const onRotateEnd = () => gesture.end(controls);
49
+ useGesture('rotatestart', onRotateStart);
50
+ useGesture('rotate', onRotate);
51
+ useGesture('rotateend', onRotateEnd);
52
+ return null;
53
+ }
54
+ /**
55
+ * A two-finger twist, turning the plan about the point between the fingers.
56
+ *
57
+ * A class rather than fields on the component, for two reasons that agree: the React Compiler
58
+ * refuses a direct write to anything `useState` returned, and this half — where the fingers are,
59
+ * how far they have turned, where the pivot lands — is worth testing without a canvas around it.
60
+ * {@link Roll} is the React half: when to subscribe, and what the scene currently holds.
61
+ */
62
+ class RollGesture {
63
+ isRolling = false;
64
+ startVector;
65
+ minDiameter = 0;
66
+ prevAngle = 0;
67
+ vector = new Vector2();
68
+ p0 = new Vector2();
69
+ p1 = new Vector2();
70
+ pivotPlan = new Point();
71
+ pivotWorld = new Vector3();
72
+ targetWorld = new Vector3();
73
+ cameraPosition = new Vector3();
74
+ cameraForward = new Vector3();
75
+ rotation = new Matrix4();
76
+ /**
77
+ * Remembers where the fingers started, which the twist is measured against.
78
+ * @param event - The gesture event that began the twist.
79
+ */
80
+ start(event) {
81
+ const { pointers } = event;
82
+ this.p0.copy(canvasPoint(pointers[0]));
83
+ this.p1.copy(canvasPoint(pointers[1]));
84
+ this.startVector = new Vector2().copy(this.p0).sub(this.p1);
85
+ this.minDiameter = this.startVector.length();
86
+ }
87
+ /**
88
+ * Turns the plan by however far the fingers have twisted since the last event, once the gesture
89
+ * has passed its threshold.
90
+ * @param event - The gesture event carrying the twist so far.
91
+ * @param threshold - How far a twist must travel before it takes, in pixels of circumference.
92
+ * @param context - What the scene currently holds.
93
+ */
94
+ move(event, threshold, context) {
95
+ const { controls } = context;
96
+ const { pointers } = event;
97
+ if (!this.isRolling) {
98
+ if (pointers.length !== 2)
99
+ return;
100
+ this.p0.copy(canvasPoint(pointers[0]));
101
+ this.p1.copy(canvasPoint(pointers[1]));
102
+ this.vector.copy(this.p0).sub(this.p1);
103
+ if (this.belowThreshold(threshold))
104
+ return;
105
+ this.isRolling = true;
106
+ this.prevAngle = event.rotation;
107
+ // Bracketed from the moment the plan starts turning, not from the touch: a twist still inside
108
+ // its threshold has not moved the camera, and may yet turn out to be a pinch.
109
+ controls.dispatchEvent({ type: 'controlstart' });
110
+ }
111
+ // Negated: the recognizer measures the twist clockwise on screen, and the azimuth it drives
112
+ // grows the other way.
113
+ const deltaAngle = -degToRad(event.rotation - this.prevAngle);
114
+ this.prevAngle = event.rotation;
115
+ if (Math.abs(deltaAngle) < MIN_ROLL_STEP)
116
+ return;
117
+ // mjolnir measures the midpoint from the target's own padding edge, as `canvasPoint` does.
118
+ if (!this.setPivot(event.offsetCenter, context))
119
+ return;
120
+ // The plan's own up axis is world z, so turning the plan is a rotation about it.
121
+ this.rotation.makeRotationZ(deltaAngle);
122
+ this.cameraPosition.sub(this.pivotWorld).applyMatrix4(this.rotation).add(this.pivotWorld);
123
+ this.cameraForward.applyMatrix4(this.rotation);
124
+ this.targetWorld.copy(this.cameraPosition).add(this.cameraForward);
125
+ void controls.setLookAt(this.cameraPosition.x, this.cameraPosition.y, this.cameraPosition.z, this.targetWorld.x, this.targetWorld.y, this.targetWorld.z, false);
126
+ // The twist moves the camera through the public setters, which the controls count as a command
127
+ // rather than the user's hand; reporting it is what tells an idle timer the user is here.
128
+ controls.dispatchEvent({ type: 'control' });
129
+ }
130
+ /**
131
+ * Forgets the gesture, so the next twist measures its threshold afresh, and closes the
132
+ * `controlstart` the twist opened, if it got far enough to open one.
133
+ * @param controls - The controls to report to, if they are still there.
134
+ */
135
+ end(controls) {
136
+ if (this.isRolling)
137
+ controls?.dispatchEvent({ type: 'controlend' });
138
+ this.isRolling = false;
139
+ this.startVector = undefined;
140
+ this.minDiameter = 0;
141
+ }
142
+ /**
143
+ * Whether the twist so far is still inside its threshold, narrowing the remembered diameter as it
144
+ * goes.
145
+ *
146
+ * The narrowest the fingers have been is what the threshold is sized against, rather than where
147
+ * they are now, so pinching in and out during a twist does not make it progressively touchier.
148
+ * @param threshold - The distance along the circumference, in pixels.
149
+ * @returns Whether the plan should stay put.
150
+ */
151
+ belowThreshold(threshold) {
152
+ this.minDiameter = Math.min(this.minDiameter, this.vector.length());
153
+ const startVector = this.startVector;
154
+ if (!startVector)
155
+ return false;
156
+ return (radToDeg(this.vector.angleTo(startVector)) < rollThresholdDegrees(this.minDiameter, threshold));
157
+ }
158
+ /**
159
+ * Puts the pivot under the point between the fingers, and reads the camera's stance around it.
160
+ *
161
+ * The pivot is found on the plan and converted back out to the world, rather than crossed with
162
+ * the world's own `z = 0` plane. The two agree under the viewbox fit; only this one stays right
163
+ * for a host that placed the plan itself, at a tilt or off the world origin.
164
+ * @param center - The point between the fingers, in css pixels from the canvas's top-left corner.
165
+ * @param context - What the scene currently holds.
166
+ * @returns Whether a pivot was found — a camera looking along the plan never meets it.
167
+ */
168
+ setPivot(center, context) {
169
+ const { controls, camera, space } = context;
170
+ if (!space.canvasToPlan(center, camera, this.pivotPlan))
171
+ return false;
172
+ space.planToWorld(this.pivotPlan, this.pivotWorld);
173
+ controls.getPosition(this.cameraPosition);
174
+ controls.getTarget(this.targetWorld);
175
+ this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
176
+ return true;
177
+ }
178
+ }
179
+ /**
180
+ * How far a twist must turn before it takes, in degrees, for fingers this far apart.
181
+ *
182
+ * The threshold is a distance along the circumference of the circle the fingers describe — the
183
+ * Mapbox rule — so the angle it works out to is wide when the fingers are close together and narrow
184
+ * when they are far apart. That is what keeps a pinch from turning the plan by accident while
185
+ * leaving a deliberate two-handed twist responsive.
186
+ * @param minDiameter - The narrowest the fingers have been during the gesture, in pixels.
187
+ * @param threshold - The distance along the circumference, in pixels.
188
+ * @returns The angle in degrees; infinite while the fingers are together, so nothing turns.
189
+ */
190
+ function rollThresholdDegrees(minDiameter, threshold) {
191
+ return (threshold / (Math.PI * minDiameter)) * 360;
192
+ }
193
+ /** The smallest turn worth applying, in radians — below this the twist is recognizer noise. */
194
+ const MIN_ROLL_STEP = 0.001;
195
+ /** How far a two-finger twist must travel before it turns the plan, in pixels of circumference. */
196
+ const DEFAULT_THRESHOLD = 25;