@expofp/renderer 3.31.3 → 3.31.5

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.
@@ -2,41 +2,6 @@ import { type NonFunctionKeys } from '@react-three/fiber/dist/declarations/src/c
2
2
  import { MapControls as CameraControlsImpl, type SmoothTimeOption } from 'camera-controls';
3
3
  import { type Ref } from 'react';
4
4
  import { type OrthographicCamera, type PerspectiveCamera } from 'three';
5
- /**
6
- * The scene's camera controls, once {@link Controls} has mounted them.
7
- *
8
- * For **reading** them, and for handing them to something else that does the writing — a gesture
9
- * class, a measurement, a helper taking them as an argument. What the React Compiler rejects is
10
- * assigning to a *field* of a hook's return value, which is a build error rather than a bug; a
11
- * method call on it, or passing it along, is fine. A component with a write it cannot route through
12
- * a function uses {@link useControlsEffect}, which hands the controls over as an argument.
13
- * @returns The controls, or `undefined` until they mount.
14
- */
15
- export declare function useCameraControls(): CameraControlsImpl | undefined;
16
- /**
17
- * How far the plan is turned from north: clockwise, in radians, in `[0, 2π)`.
18
- *
19
- * The camera holds an azimuth, which runs the other way and over any range the user has spun it
20
- * through. Every reader of "which way is the plan facing" wants this instead, so the negation and
21
- * the wrap live here once rather than at each of them — the same getter the imperative renderer put
22
- * on its controller.
23
- * @param controls - The scene's controls.
24
- * @returns The bearing in radians.
25
- */
26
- export declare function bearing(controls: CameraControlsImpl): number;
27
- /**
28
- * Runs an effect against the scene's camera controls, for as long as the component is mounted.
29
- *
30
- * The controls arrive as an argument rather than from a hook, which is what lets the effect write to
31
- * them; and the "wait for them to mount" subscription lives here rather than in every caller, so a
32
- * component that claims a mouse action is three lines and says only what it claims. Together with
33
- * {@link useCameraControls} that is the whole surface — read them, or write to them in an effect.
34
- *
35
- * Re-runs when the controls change and whenever `effect` does, which — the React Compiler caching
36
- * the closure against what it captures — is whenever the component's own inputs move.
37
- * @param effect - What to do with the controls; may return a cleanup, as any effect may.
38
- */
39
- export declare function useControlsEffect(effect: (controls: CameraControlsImpl) => (() => void) | void): void;
40
5
  /** Props for {@link CameraControls}: camera-controls element props plus lifecycle callbacks. */
41
6
  export type CameraControlsProps = Partial<Omit<Pick<CameraControlsImpl, NonFunctionKeys<CameraControlsImpl>>, 'smoothTime' | 'controlSmoothTime'>> & {
42
7
  /** Ref to hold the camera-controls instance */
@@ -98,5 +63,46 @@ export type CameraControlsProps = Partial<Omit<Pick<CameraControlsImpl, NonFunct
98
63
  * @returns The controls instance as an R3F `<primitive>`.
99
64
  */
100
65
  export declare function CameraControls(props: CameraControlsProps): import("react").JSX.Element;
101
- export { CameraControlsImpl };
66
+ /**
67
+ * The controls class, under the name the rest of this package calls it by.
68
+ *
69
+ * A **type** and not a value: the only code that needs the class itself is right here, and a type
70
+ * survives an import cycle where a class would not — so a handler this module's own parent loads
71
+ * can name it freely. Everything else the library offers arrives through
72
+ * {@link CameraControlsAction}.
73
+ */
74
+ export { type CameraControlsImpl };
75
+ /**
76
+ * The mouse and touch actions a handler claims, from the library's own enum.
77
+ *
78
+ * Beside the component rather than in a module of its own: a constant is the one thing Fast Refresh
79
+ * tolerates next to a component, and this one never changes.
80
+ */
81
+ export declare const CameraControlsAction: Readonly<{
82
+ readonly NONE: 0;
83
+ readonly ROTATE_AZIMUTH: 1;
84
+ readonly ROTATE_POLAR: 2;
85
+ readonly ROTATE: number;
86
+ readonly TRUCK: 4;
87
+ readonly SCREEN_PAN: 8;
88
+ readonly OFFSET: 16;
89
+ readonly DOLLY: 32;
90
+ readonly ZOOM: 64;
91
+ readonly TOUCH_ROTATE_AZIMUTH: 128;
92
+ readonly TOUCH_ROTATE_POLAR: 256;
93
+ readonly TOUCH_ROTATE: number;
94
+ readonly TOUCH_TRUCK: 512;
95
+ readonly TOUCH_SCREEN_PAN: 1024;
96
+ readonly TOUCH_OFFSET: 2048;
97
+ readonly TOUCH_DOLLY: 4096;
98
+ readonly TOUCH_ZOOM: 8192;
99
+ readonly TOUCH_DOLLY_TRUCK: number;
100
+ readonly TOUCH_DOLLY_SCREEN_PAN: number;
101
+ readonly TOUCH_DOLLY_OFFSET: number;
102
+ readonly TOUCH_DOLLY_ROTATE: number;
103
+ readonly TOUCH_ZOOM_TRUCK: number;
104
+ readonly TOUCH_ZOOM_OFFSET: number;
105
+ readonly TOUCH_ZOOM_SCREEN_PAN: number;
106
+ readonly TOUCH_ZOOM_ROTATE: number;
107
+ }>;
102
108
  //# sourceMappingURL=CameraControls.d.ts.map
@@ -5,57 +5,6 @@ import { MapControls as CameraControlsImpl } from 'camera-controls';
5
5
  import { useEffect, useState } from 'react';
6
6
  import { Box3, MathUtils, Matrix4, Quaternion, Raycaster, Sphere, Spherical, Vector2, Vector3, Vector4, } from 'three';
7
7
  import { useDebugPanel } from '../debug/debug-context';
8
- import { euclideanModulo } from '../utils/math';
9
- /**
10
- * The scene's camera controls, once {@link Controls} has mounted them.
11
- *
12
- * For **reading** them, and for handing them to something else that does the writing — a gesture
13
- * class, a measurement, a helper taking them as an argument. What the React Compiler rejects is
14
- * assigning to a *field* of a hook's return value, which is a build error rather than a bug; a
15
- * method call on it, or passing it along, is fine. A component with a write it cannot route through
16
- * a function uses {@link useControlsEffect}, which hands the controls over as an argument.
17
- * @returns The controls, or `undefined` until they mount.
18
- */
19
- export function useCameraControls() {
20
- return useThree((state) => state.controls) ?? undefined;
21
- }
22
- /**
23
- * How far the plan is turned from north: clockwise, in radians, in `[0, 2π)`.
24
- *
25
- * The camera holds an azimuth, which runs the other way and over any range the user has spun it
26
- * through. Every reader of "which way is the plan facing" wants this instead, so the negation and
27
- * the wrap live here once rather than at each of them — the same getter the imperative renderer put
28
- * on its controller.
29
- * @param controls - The scene's controls.
30
- * @returns The bearing in radians.
31
- */
32
- export function bearing(controls) {
33
- return euclideanModulo(-controls.azimuthAngle, 2 * Math.PI);
34
- }
35
- /**
36
- * Runs an effect against the scene's camera controls, for as long as the component is mounted.
37
- *
38
- * The controls arrive as an argument rather than from a hook, which is what lets the effect write to
39
- * them; and the "wait for them to mount" subscription lives here rather than in every caller, so a
40
- * component that claims a mouse action is three lines and says only what it claims. Together with
41
- * {@link useCameraControls} that is the whole surface — read them, or write to them in an effect.
42
- *
43
- * Re-runs when the controls change and whenever `effect` does, which — the React Compiler caching
44
- * the closure against what it captures — is whenever the component's own inputs move.
45
- * @param effect - What to do with the controls; may return a cleanup, as any effect may.
46
- */
47
- export function useControlsEffect(effect) {
48
- const get = useThree((state) => state.get);
49
- // A subscription, so this re-runs once the controls have mounted. Deliberately not the value the
50
- // effect uses: that is read fresh from the store below.
51
- const mounted = useThree((state) => state.controls);
52
- useEffect(() => {
53
- const controls = get().controls;
54
- if (!controls)
55
- return undefined;
56
- return effect(controls) ?? undefined;
57
- }, [effect, get, mounted]);
58
- }
59
8
  // Install the subset of THREE that camera-controls uses and register it with
60
9
  // R3F, once at module load — before any controls are constructed during render,
61
10
  // so the `new CameraControlsImpl(...)` below never runs uninstalled. Importing
@@ -229,11 +178,17 @@ export function CameraControls(props) {
229
178
  */
230
179
  function createControls(camera) {
231
180
  const controls = new CameraControlsImpl(camera);
232
- const { NONE } = CameraControlsImpl.ACTION;
181
+ const { NONE } = CameraControlsAction;
233
182
  controls.mouseButtons = { left: NONE, middle: NONE, right: NONE, wheel: NONE };
234
183
  controls.touches = { one: NONE, two: NONE, three: NONE };
235
184
  return controls;
236
185
  }
237
- export { CameraControlsImpl };
186
+ /**
187
+ * The mouse and touch actions a handler claims, from the library's own enum.
188
+ *
189
+ * Beside the component rather than in a module of its own: a constant is the one thing Fast Refresh
190
+ * tolerates next to a component, and this one never changes.
191
+ */
192
+ export const CameraControlsAction = CameraControlsImpl.ACTION;
238
193
  const _target = new Vector3();
239
194
  const _spherical = new Spherical();
@@ -0,0 +1,49 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PanConfig } from './handlers/Pan';
3
+ import { type RollConfig } from './handlers/Roll';
4
+ /**
5
+ * What the user may do with the plan.
6
+ *
7
+ * Each handler takes `false` to switch it off, `true` or nothing for its defaults, and — where it
8
+ * has any — an options object, whose own defaults the handler applies.
9
+ *
10
+ * **Pan and zoom default on; roll and pitch default to whether this is a development build**, so
11
+ * the renderer's own Storybook and dev server carry them and a production plan does not until a
12
+ * caller asks. An explicit value always wins, so a host driving roll from a user-facing setting
13
+ * never has to think about the dev default.
14
+ *
15
+ * The pitch and zoom **limits** are not here: they hold whether or not the matching gesture does,
16
+ * which makes them the camera's — see `CameraProps`.
17
+ */
18
+ export interface ControlsConfig {
19
+ /** Left-drag and one-finger drag to pan the plan. Defaults on. */
20
+ pan?: boolean | PanConfig;
21
+ /** Wheel and two-finger pinch to zoom, toward the cursor. Defaults on. */
22
+ zoom?: boolean;
23
+ /** Right-drag and two-finger twist to turn the plan. Defaults to a development build. */
24
+ roll?: boolean | RollConfig;
25
+ /** Right-drag and two-finger vertical drag to tilt the plan. Defaults to a development build. */
26
+ pitch?: boolean;
27
+ }
28
+ /** Props for {@link Controls}. */
29
+ export interface ControlsProps {
30
+ /** What the user may do with the plan; see {@link ControlsConfig}. */
31
+ config?: ControlsConfig;
32
+ }
33
+ /**
34
+ * The scene's camera controls, and the gesture handlers that drive them.
35
+ *
36
+ * Registered as R3F's default controls (`makeDefault`) so everything else reaches them through
37
+ * `state.controls`. Every mouse and touch action starts switched off, and each handler mounted
38
+ * below claims the ones it needs — so what a user can do is exactly the set of handlers standing
39
+ * here, and turning one off is unmounting it.
40
+ *
41
+ * **How far the camera may go is not decided here.** The zoom and pitch bounds constrain a
42
+ * programmatic move as much as a dragged one, so they belong to the camera; `Camera` writes them,
43
+ * beside the clipping planes derived from the same numbers. This component is only about what the
44
+ * user may do.
45
+ * @param props - What the user may do with the plan.
46
+ * @returns The controls and their handlers.
47
+ */
48
+ export declare function Controls(props: ControlsProps): ReactNode;
49
+ //# sourceMappingURL=Controls.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { CameraControls } from './CameraControls';
4
+ import { Pan } from './handlers/Pan';
5
+ import { Pitch } from './handlers/Pitch';
6
+ import { Roll } from './handlers/Roll';
7
+ import { Zoom } from './handlers/Zoom';
8
+ /**
9
+ * The scene's camera controls, and the gesture handlers that drive them.
10
+ *
11
+ * Registered as R3F's default controls (`makeDefault`) so everything else reaches them through
12
+ * `state.controls`. Every mouse and touch action starts switched off, and each handler mounted
13
+ * below claims the ones it needs — so what a user can do is exactly the set of handlers standing
14
+ * here, and turning one off is unmounting it.
15
+ *
16
+ * **How far the camera may go is not decided here.** The zoom and pitch bounds constrain a
17
+ * programmatic move as much as a dragged one, so they belong to the camera; `Camera` writes them,
18
+ * beside the clipping planes derived from the same numbers. This component is only about what the
19
+ * user may do.
20
+ * @param props - What the user may do with the plan.
21
+ * @returns The controls and their handlers.
22
+ */
23
+ export function Controls(props) {
24
+ const controlsRef = useRef(null);
25
+ const config = props.config ?? {};
26
+ const pan = config.pan ?? true;
27
+ const zoom = config.zoom ?? true;
28
+ const roll = config.roll ?? DEV;
29
+ const pitch = config.pitch ?? DEV;
30
+ useEffect(() => {
31
+ // Straight down by default. Set through `rotateTo` rather than by assigning `polarAngle`, which
32
+ // skips `Spherical.makeSafe` — and a polar angle of exactly zero causes trouble with roll on
33
+ // mobile. Nothing sets a target: the scene's fit puts the plan's centre on the world origin,
34
+ // which is where the controls already orbit.
35
+ void controlsRef.current.rotateTo(0, 0);
36
+ }, []);
37
+ return (_jsxs(_Fragment, { children: [_jsx(CameraControls, { ref: controlsRef, makeDefault: true, events: true, dollyToCursor: true, controlSmoothTime: { rotate: 0, truck: 0 } }), pan !== false && _jsx(Pan, { ...optionsOf(pan) }), zoom !== false && _jsx(Zoom, {}), roll !== false && _jsx(Roll, { ...optionsOf(roll) }), pitch !== false && _jsx(Pitch, {})] }));
38
+ }
39
+ /**
40
+ * The options a handler was configured with, if it was given any rather than a bare flag.
41
+ * @param config - What the caller wrote for it.
42
+ * @returns The options, or an empty set for a handler switched on without any.
43
+ */
44
+ function optionsOf(config) {
45
+ return typeof config === 'object' ? config : {};
46
+ }
47
+ /**
48
+ * Whether this is a development build, which is what roll and pitch default to.
49
+ *
50
+ * Typed inline rather than through `vite/client`, matching `stories/support/screenshot.ts`: the
51
+ * library build compiles without those types, and the optional chain keeps a consumer whose bundler
52
+ * defines no `import.meta.env` from throwing on it.
53
+ */
54
+ const DEV = import.meta.env?.DEV === true;
@@ -148,26 +148,6 @@ interface CameraContext {
148
148
  /** Device pixels per css pixel. */
149
149
  dpr: number;
150
150
  }
151
- /**
152
- * The scene's camera commands.
153
- *
154
- * A hook rather than a ref handed out from above, and that is the point: it only resolves inside a
155
- * component mounted under `<Renderer>`, and R3F mounts those only once the canvas and its context
156
- * exist — so "the camera cannot be driven before the scene is ready" is enforced by where the code
157
- * can live rather than by a null check every caller has to remember. A host that needs to drive the
158
- * camera from outside React publishes this object from an effect, which is exactly when it is safe.
159
- *
160
- * Before the controls mount every command is a no-op that resolves at once, which is the same frame
161
- * or two the hook's own consumers spend waiting for their effects.
162
- *
163
- * **The returned object is new on every render, deliberately.** This module is not memoized by hand
164
- * — the React Compiler is scoped to `.tsx` under `src/` (see `vitest.config.mts`), and reaching for
165
- * `useMemo` here would put a cache in the library that every consumer then has to trust. A consumer
166
- * that cannot re-subscribe cheaply should hold this in a ref rather than in a dependency list; that
167
- * way the decision, and its cost, sit with the code that knows how often it renders.
168
- * @returns The commands.
169
- */
170
- export declare function useCameraAPI(): CameraAPI;
171
151
  /**
172
152
  * The camera commands, over whatever the scene currently holds.
173
153
  *
@@ -202,5 +182,17 @@ export declare class CameraCommands implements CameraAPI {
202
182
  /** @inheritdoc */
203
183
  reset(axes?: CameraAxes, options?: TransitionOptions): Promise<void>;
204
184
  }
185
+ /**
186
+ * How far the plan is turned from north: clockwise, in radians, in `[0, 2π)`.
187
+ *
188
+ * The camera holds an azimuth, which runs the other way and over any range the user has spun it
189
+ * through. Every reader of "which way is the plan facing" wants this instead, so the negation and
190
+ * the wrap live here once rather than at each of them — the same getter the imperative renderer put
191
+ * on its controller. Beside the rest of "where the camera is", which is the only question it
192
+ * answers.
193
+ * @param controls - The scene's controls.
194
+ * @returns The bearing in radians.
195
+ */
196
+ export declare function bearing(controls: CameraControlsImpl): number;
205
197
  export {};
206
198
  //# sourceMappingURL=camera-api.d.ts.map
@@ -1,46 +1,7 @@
1
1
  import { angleDiff, degToRad } from '@expofp/geometry';
2
- import { useThree } from '@react-three/fiber';
3
2
  import { Vector3 } from 'three';
4
- import { useCamera } from '../space/camera-handle';
5
- import { useCanvasSize } from '../space/canvas-size';
6
- import { useSpace } from '../space/Space';
7
- import { bearing, useCameraControls } from './CameraControls';
3
+ import { euclideanModulo } from '../utils/math';
8
4
  import { fitBox, framingRegion } from './zoom-to';
9
- /**
10
- * The scene's camera commands.
11
- *
12
- * A hook rather than a ref handed out from above, and that is the point: it only resolves inside a
13
- * component mounted under `<Renderer>`, and R3F mounts those only once the canvas and its context
14
- * exist — so "the camera cannot be driven before the scene is ready" is enforced by where the code
15
- * can live rather than by a null check every caller has to remember. A host that needs to drive the
16
- * camera from outside React publishes this object from an effect, which is exactly when it is safe.
17
- *
18
- * Before the controls mount every command is a no-op that resolves at once, which is the same frame
19
- * or two the hook's own consumers spend waiting for their effects.
20
- *
21
- * **The returned object is new on every render, deliberately.** This module is not memoized by hand
22
- * — the React Compiler is scoped to `.tsx` under `src/` (see `vitest.config.mts`), and reaching for
23
- * `useMemo` here would put a cache in the library that every consumer then has to trust. A consumer
24
- * that cannot re-subscribe cheaply should hold this in a ref rather than in a dependency list; that
25
- * way the decision, and its cost, sit with the code that knows how often it renders.
26
- * @returns The commands.
27
- */
28
- export function useCameraAPI() {
29
- const get = useThree((state) => state.get);
30
- const controls = useCameraControls();
31
- const camera = useCamera();
32
- const space = useSpace();
33
- const { width, height, dpr } = useCanvasSize();
34
- return new CameraCommands({
35
- controls,
36
- camera,
37
- space,
38
- view: () => get().camera.view,
39
- width,
40
- height,
41
- dpr,
42
- });
43
- }
44
5
  /**
45
6
  * The camera commands, over whatever the scene currently holds.
46
7
  *
@@ -161,6 +122,20 @@ export class CameraCommands {
161
122
  await Promise.all(moves);
162
123
  }
163
124
  }
125
+ /**
126
+ * How far the plan is turned from north: clockwise, in radians, in `[0, 2π)`.
127
+ *
128
+ * The camera holds an azimuth, which runs the other way and over any range the user has spun it
129
+ * through. Every reader of "which way is the plan facing" wants this instead, so the negation and
130
+ * the wrap live here once rather than at each of them — the same getter the imperative renderer put
131
+ * on its controller. Beside the rest of "where the camera is", which is the only question it
132
+ * answers.
133
+ * @param controls - The scene's controls.
134
+ * @returns The bearing in radians.
135
+ */
136
+ export function bearing(controls) {
137
+ return euclideanModulo(-controls.azimuthAngle, 2 * Math.PI);
138
+ }
164
139
  /**
165
140
  * Takes a point on the plan into world coordinates.
166
141
  * @param space - The scene's space.
@@ -1,4 +1,3 @@
1
- import { type CameraControlsImpl } from '../CameraControls';
2
1
  /**
3
2
  * How the glide a pan leaves behind is measured, and how it slows.
4
3
  *
@@ -43,69 +42,4 @@ export interface InertiaConfig {
43
42
  * @returns Nothing; this renders no scene node.
44
43
  */
45
44
  export declare function Inertia(props: InertiaConfig): null;
46
- /**
47
- * The default glide easing: an exponential ease-in, so most of the speed is shed early and the last
48
- * of it trails off.
49
- * @param t - How much of the glide's time is left, from `1` to `0`.
50
- * @returns How much of the original speed to carry, from `1` to `0`.
51
- */
52
- export declare function easeInertia(t: number): number;
53
- /**
54
- * Carries a pan on after the pointer lifts.
55
- *
56
- * Framework-free on purpose: everything here is arithmetic over the camera's target and a list of
57
- * timestamps, which is the half worth testing without a canvas around it. {@link Inertia} is the
58
- * React half — when to connect, when to configure, when to step.
59
- *
60
- * **The speed comes from where the target actually went**, not from the pointer. Every interaction
61
- * frame records the target; when the pointer lifts, the oldest sample at least `minSampleMs` old is
62
- * diffed against the current target. A flick that ended in a stall therefore glides at the
63
- * stall's speed, because the samples covering the stall are the recent ones.
64
- */
65
- export declare class InertiaController {
66
- private minSampleMs;
67
- private durationMs;
68
- private speedThreshold;
69
- private ease;
70
- private controls?;
71
- private samples;
72
- private target;
73
- private delta;
74
- private velocity;
75
- private active;
76
- private elapsedMs;
77
- private prevFactor;
78
- /**
79
- * Takes every value rather than defaulting any: what an omitted option means is `resolveControls`'s
80
- * to answer, and answering it twice is how the two drift apart.
81
- * @param options - How the glide is measured and how it slows.
82
- */
83
- constructor(options: Required<InertiaConfig>);
84
- /**
85
- * Sets how the glide is measured and how it slows. Every field is optional, so a partial update
86
- * leaves the rest as it was.
87
- * @param options - The values to change.
88
- */
89
- configure(options: InertiaConfig): void;
90
- /**
91
- * Starts watching a controls instance for the interactions a glide follows.
92
- * @param controls - The controls to watch and to move.
93
- */
94
- connect(controls: CameraControlsImpl): void;
95
- /** Stops watching, and abandons any glide in flight. */
96
- disconnect(): void;
97
- /**
98
- * Advances a glide in flight by one frame.
99
- *
100
- * The speed applied is the average of this frame's easing factor and the last one's, which
101
- * integrates the curve over the frame rather than sampling it at one end — so the distance
102
- * covered does not depend on how the frames happened to fall.
103
- * @param delta - Time since the last frame, in seconds.
104
- * @returns Whether the camera moved, and so whether another frame is wanted.
105
- */
106
- update(delta: number): boolean;
107
- private onControlStart;
108
- private onControl;
109
- private onControlEnd;
110
- }
111
45
  //# sourceMappingURL=Inertia.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { useFrame, useThree } from '@react-three/fiber';
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Vector3 } from 'three';
4
- import { useControlsEffect } from '../CameraControls';
4
+ import { useControlsEffect } from '../hooks';
5
5
  /**
6
6
  * The glide a lifted pointer leaves behind.
7
7
  *
@@ -44,7 +44,7 @@ export function Inertia(props) {
44
44
  * @param t - How much of the glide's time is left, from `1` to `0`.
45
45
  * @returns How much of the original speed to carry, from `1` to `0`.
46
46
  */
47
- export function easeInertia(t) {
47
+ function easeInertia(t) {
48
48
  return Math.pow(2, 10 * (t - 1));
49
49
  }
50
50
  /**
@@ -59,7 +59,7 @@ export function easeInertia(t) {
59
59
  * diffed against the current target. A flick that ended in a stall therefore glides at the
60
60
  * stall's speed, because the samples covering the stall are the recent ones.
61
61
  */
62
- export class InertiaController {
62
+ class InertiaController {
63
63
  minSampleMs;
64
64
  durationMs;
65
65
  speedThreshold;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { CameraControlsImpl, useControlsEffect } from '../CameraControls';
2
+ import { CameraControlsAction } from '../CameraControls';
3
+ import { useControlsEffect } from '../hooks';
3
4
  import { Inertia } from './Inertia';
4
5
  /**
5
6
  * Panning: a left-drag, and a one-finger drag.
@@ -18,15 +19,14 @@ import { Inertia } from './Inertia';
18
19
  export function Pan(props) {
19
20
  const inertia = props.inertia ?? true;
20
21
  useControlsEffect((controls) => {
21
- controls.mouseButtons.left = ACTION.SCREEN_PAN;
22
- controls.touches.one = ACTION.TOUCH_SCREEN_PAN;
22
+ controls.mouseButtons.left = CameraControlsAction.SCREEN_PAN;
23
+ controls.touches.one = CameraControlsAction.TOUCH_SCREEN_PAN;
23
24
  return () => {
24
- controls.mouseButtons.left = ACTION.NONE;
25
- controls.touches.one = ACTION.NONE;
25
+ controls.mouseButtons.left = CameraControlsAction.NONE;
26
+ controls.touches.one = CameraControlsAction.NONE;
26
27
  };
27
28
  });
28
29
  if (inertia === false)
29
30
  return null;
30
31
  return _jsx(Inertia, { ...(inertia === true ? {} : inertia) });
31
32
  }
32
- const ACTION = CameraControlsImpl.ACTION;
@@ -1,19 +1,3 @@
1
- import { type Point2Like } from '@expofp/geometry';
2
- import { type MjolnirGestureEvent } from 'mjolnir.js';
3
- import { Vector2 } from 'three';
4
- import { CameraControlsImpl } from '../CameraControls';
5
- /**
6
- * How far a two-finger gesture has got toward being judged a pitch.
7
- *
8
- * Split out from the gesture that holds it so {@link beginsVertically} can be exercised on its own:
9
- * the arbitration is the subtle part, and it is pure arithmetic over two movements and a clock.
10
- */
11
- export interface PitchVerdict {
12
- /** Whether this gesture is a pitch — `undefined` while the fingers have not said. */
13
- isValid?: boolean;
14
- /** When the first of the two fingers moved, so the second can be given a moment to follow. */
15
- firstMove?: number;
16
- }
17
1
  /**
18
2
  * Pitching: a right-drag, and two fingers dragged vertically together.
19
3
  *
@@ -32,63 +16,4 @@ export interface PitchVerdict {
32
16
  * @returns Nothing; this renders no scene node.
33
17
  */
34
18
  export declare function Pitch(): null;
35
- /**
36
- * Two fingers dragged vertically together, tilting the plan.
37
- *
38
- * A class rather than fields on the component, for two reasons that agree: the React Compiler
39
- * refuses a direct write to anything `useState` returned, and the gesture's arbitration is worth
40
- * testing without a canvas around it. {@link Pitch} is the React half.
41
- */
42
- export declare class PitchGesture {
43
- private verdict;
44
- private lastPoints?;
45
- private prevTwoFingerAction?;
46
- private p0;
47
- private p1;
48
- /**
49
- * Remembers where the fingers started, and rules the gesture out when they are stacked one above
50
- * the other — from there a vertical drag is indistinguishable from a pinch.
51
- * @param event - The gesture event that began the drag.
52
- */
53
- start(event: MjolnirGestureEvent): void;
54
- /**
55
- * Tilts the plan by how far the fingers moved down the screen, once the gesture has been judged
56
- * a pitch.
57
- * @param event - The gesture event carrying the drag so far.
58
- * @param controls - The controls to tilt.
59
- */
60
- move(event: MjolnirGestureEvent, controls: CameraControlsImpl): void;
61
- /**
62
- * Puts back the two-finger action this suppressed, if it suppressed one, and forgets the gesture.
63
- *
64
- * Nothing is restored when the captured action was already `NONE`: there was nothing to suppress,
65
- * and writing it back would claim a field this handler does not own.
66
- * @param controls - The controls to restore, if they are still there.
67
- */
68
- end(controls: CameraControlsImpl | undefined): void;
69
- }
70
- /**
71
- * Whether a two-finger gesture is a pitch, judged from how the fingers first moved.
72
- *
73
- * Judged once and then held: the verdict is returned unchanged as soon as it has one, so a drag
74
- * that started as a pitch stays one even if the fingers wander, and one that did not can never
75
- * become one. A `false` verdict is what makes a pinch or a twist stay a pinch or a twist.
76
- *
77
- * The two fingers rarely start moving on the same frame, so one moving alone is not by itself a
78
- * verdict — the other is given {@link ALLOWED_SINGLE_TOUCH_TIME} to follow, and only when it fails
79
- * to is the gesture ruled out.
80
- * @param a - How far the first finger moved since the last event.
81
- * @param b - How far the second finger moved since the last event.
82
- * @param timeStamp - When this event happened, in milliseconds.
83
- * @param verdict - How far the judgement has got; updated in place.
84
- * @returns Whether this is a pitch — `false` both for "not a pitch" and for "not yet decided", the
85
- * two cases a caller treats alike by doing nothing.
86
- */
87
- export declare function beginsVertically(a: Vector2, b: Vector2, timeStamp: number, verdict: PitchVerdict): boolean;
88
- /**
89
- * Whether a movement is more down-the-screen than across it.
90
- * @param vector - The movement to judge.
91
- * @returns Whether it is vertical.
92
- */
93
- export declare function isVertical(vector: Point2Like): boolean;
94
19
  //# sourceMappingURL=Pitch.d.ts.map
@@ -1,8 +1,9 @@
1
1
  import { degToRad } from '@expofp/geometry';
2
2
  import { useState } from 'react';
3
3
  import { Vector2 } from 'three';
4
- import { CameraControlsImpl, useCameraControls, useControlsEffect } from '../CameraControls';
4
+ import { CameraControlsAction } from '../CameraControls';
5
5
  import { canvasPoint, useGesture } from '../gestures';
6
+ import { useCameraControls, useControlsEffect } from '../hooks';
6
7
  /**
7
8
  * Pitching: a right-drag, and two fingers dragged vertically together.
8
9
  *
@@ -25,9 +26,9 @@ export function Pitch() {
25
26
  const controls = useCameraControls();
26
27
  const [gesture] = useState(() => new PitchGesture());
27
28
  useControlsEffect((controls) => {
28
- controls.mouseButtons.right |= ACTION.ROTATE_POLAR;
29
+ controls.mouseButtons.right |= CameraControlsAction.ROTATE_POLAR;
29
30
  return () => {
30
- controls.mouseButtons.right &= ~ACTION.ROTATE_POLAR;
31
+ controls.mouseButtons.right &= ~CameraControlsAction.ROTATE_POLAR;
31
32
  // Unmounting mid-gesture would otherwise leave the two-finger action switched off for
32
33
  // whichever handler owns it.
33
34
  gesture.end(controls);
@@ -53,7 +54,7 @@ export function Pitch() {
53
54
  * refuses a direct write to anything `useState` returned, and the gesture's arbitration is worth
54
55
  * testing without a canvas around it. {@link Pitch} is the React half.
55
56
  */
56
- export class PitchGesture {
57
+ class PitchGesture {
57
58
  verdict = {};
58
59
  lastPoints;
59
60
  prevTwoFingerAction;
@@ -96,7 +97,7 @@ export class PitchGesture {
96
97
  // assumed, so whatever the zoom handler put there is what comes back at the end of the gesture.
97
98
  if (this.prevTwoFingerAction === undefined) {
98
99
  this.prevTwoFingerAction = controls.touches.two;
99
- controls.touches.two = ACTION.NONE;
100
+ controls.touches.two = CameraControlsAction.NONE;
100
101
  }
101
102
  lastPoints[0].add(this.p0);
102
103
  lastPoints[1].add(this.p1);
@@ -136,7 +137,7 @@ export class PitchGesture {
136
137
  * @returns Whether this is a pitch — `false` both for "not a pitch" and for "not yet decided", the
137
138
  * two cases a caller treats alike by doing nothing.
138
139
  */
139
- export function beginsVertically(a, b, timeStamp, verdict) {
140
+ function beginsVertically(a, b, timeStamp, verdict) {
140
141
  verdict.isValid = judge(a, b, timeStamp, verdict);
141
142
  return verdict.isValid === true;
142
143
  }
@@ -145,7 +146,7 @@ export function beginsVertically(a, b, timeStamp, verdict) {
145
146
  * @param vector - The movement to judge.
146
147
  * @returns Whether it is vertical.
147
148
  */
148
- export function isVertical(vector) {
149
+ function isVertical(vector) {
149
150
  return Math.abs(vector.y) > Math.abs(vector.x);
150
151
  }
151
152
  /**
@@ -182,7 +183,6 @@ function judge(a, b, timeStamp, verdict) {
182
183
  function byPointerId(event) {
183
184
  return event.pointers.slice().sort((a, b) => a.pointerId - b.pointerId);
184
185
  }
185
- const ACTION = CameraControlsImpl.ACTION;
186
186
  /** How far the plan tilts per pixel the fingers travel down the screen, in degrees. */
187
187
  const DEGREES_PER_PIXEL = -0.5;
188
188
  /** How far a finger must travel before it counts as having moved, in pixels. */