@expofp/renderer 3.40.2 → 3.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type PanConfig } from './handlers/Pan';
3
3
  import { type RollConfig } from './handlers/Roll';
4
+ import { type ZoomConfig } from './handlers/Zoom';
4
5
  /**
5
6
  * What the user may do with the plan.
6
7
  *
@@ -18,8 +19,11 @@ import { type RollConfig } from './handlers/Roll';
18
19
  export interface ControlsConfig {
19
20
  /** Left-drag and one-finger drag to pan the plan. Defaults on. */
20
21
  pan?: boolean | PanConfig;
21
- /** Wheel and two-finger pinch to zoom, toward the cursor. Defaults on. */
22
- zoom?: boolean;
22
+ /**
23
+ * Wheel and two-finger pinch to zoom, toward the cursor. Defaults on; a double-click or
24
+ * double-tap zooms too only when its options ask for it.
25
+ */
26
+ zoom?: boolean | ZoomConfig;
23
27
  /** Right-drag and two-finger twist to turn the plan. Defaults to a development build. */
24
28
  roll?: boolean | RollConfig;
25
29
  /** Right-drag and two-finger vertical drag to tilt the plan. Defaults to a development build. */
@@ -34,7 +34,7 @@ export function Controls(props) {
34
34
  // which is where the controls already orbit.
35
35
  void controlsRef.current.rotateTo(0, 0);
36
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, {})] }));
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, { ...optionsOf(zoom) }), roll !== false && _jsx(Roll, { ...optionsOf(roll) }), pitch !== false && _jsx(Pitch, {})] }));
38
38
  }
39
39
  /**
40
40
  * The options a handler was configured with, if it was given any rather than a bare flag.
@@ -30,9 +30,10 @@ export interface GestureContext {
30
30
  * **Two fingers are why any of this exists.** camera-controls recognizes a pinch and an orbit, but
31
31
  * neither of the two-finger gestures the plan needs: a twist that turns the plan about the point
32
32
  * between the fingers, and a vertical drag that tilts it. Both come off `mjolnir.js`, whose
33
- * recognizer configuration is the imperative renderer's verbatim.
33
+ * recognizer configuration is the imperative renderer's verbatim — as does the double tap that
34
+ * `Zoom` zooms on, which camera-controls does not recognize either.
34
35
  *
35
- * The recognizer itself is shared: `Roll` and `Pitch` want one on the same canvas, and two would
36
+ * The recognizer itself is shared: `Roll`, `Pitch` and `Zoom` want one on the same canvas, and two would
36
37
  * mean two sets of pointer listeners racing over the same fingers. It is built on the first
37
38
  * subscription and torn down with the last, since it attaches its listeners in its constructor and
38
39
  * offers no disconnect. Every recognizer stays dormant until something subscribes to its event, so
@@ -1,5 +1,5 @@
1
1
  import { useThree } from '@react-three/fiber';
2
- import { EventManager, Pan, Rotate } from 'mjolnir.js';
2
+ import { EventManager, Pan, Rotate, Tap } from 'mjolnir.js';
3
3
  import { useEffect } from 'react';
4
4
  /**
5
5
  * Subscribes to one of the canvas's gestures for as long as the component is mounted.
@@ -7,9 +7,10 @@ import { useEffect } from 'react';
7
7
  * **Two fingers are why any of this exists.** camera-controls recognizes a pinch and an orbit, but
8
8
  * neither of the two-finger gestures the plan needs: a twist that turns the plan about the point
9
9
  * between the fingers, and a vertical drag that tilts it. Both come off `mjolnir.js`, whose
10
- * recognizer configuration is the imperative renderer's verbatim.
10
+ * recognizer configuration is the imperative renderer's verbatim — as does the double tap that
11
+ * `Zoom` zooms on, which camera-controls does not recognize either.
11
12
  *
12
- * The recognizer itself is shared: `Roll` and `Pitch` want one on the same canvas, and two would
13
+ * The recognizer itself is shared: `Roll`, `Pitch` and `Zoom` want one on the same canvas, and two would
13
14
  * mean two sets of pointer listeners racing over the same fingers. It is built on the first
14
15
  * subscription and torn down with the last, since it attaches its listeners in its constructor and
15
16
  * offers no disconnect. Every recognizer stays dormant until something subscribes to its event, so
@@ -46,7 +47,15 @@ export function acquireGestureManager(element) {
46
47
  return held.manager;
47
48
  }
48
49
  const manager = new EventManager(element, {
49
- recognizers: [Rotate, [Pan, { event: 'pitch', pointers: 2 }, 'rotate']],
50
+ recognizers: [
51
+ Rotate,
52
+ [Pan, { event: 'pitch', pointers: 2 }, 'rotate'],
53
+ // A mouse's pointer events feed the recognizer as a finger's do, so one double tap is both
54
+ // the double-click and the double-tap. MapLibre's tap tolerances rather than the
55
+ // recognizer's own: a finger's second tap rarely lands within 10px of its first, and with
56
+ // the default `posThreshold` a real double tap on a phone was never recognized.
57
+ [Tap, { event: 'doubletap', taps: 2, interval: 500, time: 500, posThreshold: 30 }],
58
+ ],
50
59
  });
51
60
  managers.set(element, { manager, refs: 1 });
52
61
  return manager;
@@ -0,0 +1,23 @@
1
+ /** Props for {@link DoubleTapZoom}. */
2
+ export interface DoubleTapZoomProps {
3
+ /** What a double tap multiplies the zoom factor by. */
4
+ factor: number;
5
+ }
6
+ /**
7
+ * A double-click or double-tap: zooms by `factor` about the point under the pointer, which keeps
8
+ * its place on the canvas throughout.
9
+ *
10
+ * Mounted by `Zoom`, and only when a double-click is meant to zoom. One double tap is both the
11
+ * double-click and the double-tap: the gesture recognizer reads a mouse's pointer events as it
12
+ * reads a finger's.
13
+ *
14
+ * Run as a user transition, so it eases the way a wheel tick does — by `controlSmoothTime`, with the
15
+ * target on the dolly's clock — and reports itself as the user's hand.
16
+ *
17
+ * A component of its own rather than a branch inside `Zoom`, because subscribing is a hook and a
18
+ * hook cannot be switched off conditionally — mounting this is what switches it on.
19
+ * @param props - The zoom factor to multiply by.
20
+ * @returns Nothing; this renders no scene node.
21
+ */
22
+ export declare function DoubleTapZoom(props: DoubleTapZoomProps): null;
23
+ //# sourceMappingURL=DoubleTapZoom.d.ts.map
@@ -0,0 +1,82 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ import { Spherical, Vector3 } from 'three';
3
+ import { useSpace, } from '../../space/Space';
4
+ import { clamp } from '../../utils/math';
5
+ import { useGesture } from '../gestures';
6
+ import { useCameraControls } from '../hooks';
7
+ /**
8
+ * A double-click or double-tap: zooms by `factor` about the point under the pointer, which keeps
9
+ * its place on the canvas throughout.
10
+ *
11
+ * Mounted by `Zoom`, and only when a double-click is meant to zoom. One double tap is both the
12
+ * double-click and the double-tap: the gesture recognizer reads a mouse's pointer events as it
13
+ * reads a finger's.
14
+ *
15
+ * Run as a user transition, so it eases the way a wheel tick does — by `controlSmoothTime`, with the
16
+ * target on the dolly's clock — and reports itself as the user's hand.
17
+ *
18
+ * A component of its own rather than a branch inside `Zoom`, because subscribing is a hook and a
19
+ * hook cannot be switched off conditionally — mounting this is what switches it on.
20
+ * @param props - The zoom factor to multiply by.
21
+ * @returns Nothing; this renders no scene node.
22
+ */
23
+ export function DoubleTapZoom(props) {
24
+ const { factor } = props;
25
+ const get = useThree((state) => state.get);
26
+ const invalidate = useThree((state) => state.invalidate);
27
+ const space = useSpace();
28
+ // Read, not written: `userTransition` takes the move as a callback, which does the writing.
29
+ const controls = useCameraControls();
30
+ const onDoubleTap = (event) => {
31
+ if (!controls)
32
+ return;
33
+ // mjolnir measures the tap from the target's own padding edge, as `canvasPoint` does.
34
+ const around = space.canvasToPlan(event.offsetCenter, get().camera);
35
+ if (!around)
36
+ return;
37
+ void controls.userTransition(() => zoomAbout(controls, space, factor, around));
38
+ // The move happens outside a React commit, so a demand frame loop has to be asked for a frame.
39
+ invalidate();
40
+ };
41
+ useGesture('doubletap', onDoubleTap);
42
+ return null;
43
+ }
44
+ /**
45
+ * Zooms by a factor about a point on the plan, which keeps its place on the canvas.
46
+ *
47
+ * Scaling the whole camera about the point, orientation untouched, is what leaves the point where
48
+ * it was: the target slides toward it in the proportion the distance shrinks. Exact under a visible
49
+ * rect too, whose offset rides in the projection this leaves alone. It holds **during** the ease
50
+ * only inside a user transition, which puts the target on the dolly's clock.
51
+ *
52
+ * **Scaled from where the camera is now, sized from where it is heading.** A double tap often lands
53
+ * while the last one is still easing. The point under the finger is found through the camera as it
54
+ * is drawn, so it is the camera's current stance that has to be scaled about it — that keeps the
55
+ * point under the finger from here on. The zoom is sized from the distance the camera is heading
56
+ * to, so a second double tap zooms a full step past the first rather than a step past wherever the
57
+ * first had got to, as successive wheel notches do. The clamped distance sets the proportion, so a
58
+ * zoom stopped short by the camera's limits does not slide the target as far as the one asked for.
59
+ * @param controls - The controls to move.
60
+ * @param space - The scene's space.
61
+ * @param factor - What to multiply the zoom factor by.
62
+ * @param around - The point to zoom about, in plan units.
63
+ * @returns When the move has settled.
64
+ */
65
+ function zoomAbout(controls, space, factor, around) {
66
+ const from = controls.distance;
67
+ // The zoom factor is inversely proportional to the distance.
68
+ const to = clamp(controls.getSpherical(_spherical, true).radius / factor, controls.minDistance, controls.maxDistance);
69
+ const pivot = space.planToWorld(around, _pivot);
70
+ const target = controls
71
+ .getTarget(_target, false)
72
+ .sub(pivot)
73
+ .multiplyScalar(to / from)
74
+ .add(pivot);
75
+ return Promise.all([
76
+ controls.moveTo(target.x, target.y, target.z, true),
77
+ controls.dollyTo(to, true),
78
+ ]);
79
+ }
80
+ const _pivot = new Vector3();
81
+ const _target = new Vector3();
82
+ const _spherical = new Spherical();
@@ -1,21 +1,21 @@
1
1
  /**
2
- * How the glide a pan leaves behind is measured, and how it slows.
2
+ * How the glide a motion leaves behind is measured, and how it slows.
3
3
  *
4
- * The speed is taken from where the camera's target actually traveled over the last stretch of the
5
- * drag, so these are all expressed against that: world units and milliseconds.
4
+ * The speed is taken from the motion's own samples over the last stretch of the interaction, so
5
+ * the threshold is in the motion's units per millisecond: css pixels for a pan.
6
6
  */
7
7
  export interface InertiaConfig {
8
8
  /**
9
- * How far back the drag is sampled to measure its speed, in milliseconds; defaults to `50`. The
10
- * oldest sample at least this old is the one differenced against, so a flick that ended in a
11
- * stall glides at the stall's speed rather than the flick's.
9
+ * How far back the interaction is sampled to measure its speed, in milliseconds; defaults to
10
+ * `50`. The oldest sample at least this old is the one differenced against, so a flick that ended
11
+ * in a stall glides at the stall's speed rather than the flick's.
12
12
  */
13
13
  minSampleMs?: number;
14
14
  /** How long the glide lasts, in milliseconds; defaults to `1000`. */
15
15
  durationMs?: number;
16
16
  /**
17
- * The speed below which a lifted pointer leaves no glide at all, in world units per millisecond;
18
- * defaults to `0.08`.
17
+ * The speed below which a lifted pointer leaves no glide at all, in the motion's units per
18
+ * millisecond; defaults to `0.08`, sized for a pan in css pixels.
19
19
  */
20
20
  speedThreshold?: number;
21
21
  /**
@@ -27,19 +27,34 @@ export interface InertiaConfig {
27
27
  */
28
28
  ease?: (t: number) => number;
29
29
  }
30
+ /** Props for {@link Inertia}: the motion it carries on, and how. */
31
+ export interface InertiaProps extends InertiaConfig {
32
+ /**
33
+ * How far the motion moved since the last call, or `undefined` when this step of the interaction
34
+ * was not one of its own. Called on every `control` event the controls report, so it measures the
35
+ * motion directly rather than inferring it from where the camera went.
36
+ */
37
+ sample: () => readonly number[] | undefined;
38
+ /**
39
+ * Moves the camera by one step of the glide, in the same units as the samples.
40
+ * @returns Whether it moved; `false` ends the glide.
41
+ */
42
+ apply: (step: readonly number[]) => boolean;
43
+ }
30
44
  /**
31
- * The glide a lifted pointer leaves behind.
45
+ * The glide a lifted pointer leaves behind, for one kind of motion.
32
46
  *
33
- * Mounted by {@link Pan}, and only when a pan is meant to keep moving. It watches the controls'
34
- * own interaction events to learn how fast the plan was traveling when the pointer left, then eases
35
- * that speed away over the following second.
47
+ * MapLibre's shape: each kind of motion measures itself while the user drives it, and when the
48
+ * interaction ends it glides on at the speed it ended with — rather than one glide reading the
49
+ * camera after the fact, where a pan, a pinch's slide toward the fingers and a twist's turn about
50
+ * them all arrive mixed into the same target. Generic over the motion: the caller says how to
51
+ * sample it and how to apply a step of it, and this owns the timing.
36
52
  *
37
53
  * Runs its frame callback **ahead of the controls' own** (see {@link INERTIA_PRIORITY}), so the
38
- * target it moves is the one that frame draws rather than the next — a frame tighter than the
39
- * imperative renderer, which updated the controller before its handlers. It asks for the next frame
54
+ * camera it moves is the one that frame draws rather than the next. It asks for the next frame
40
55
  * itself, which is what keeps a glide alive under the on-demand loop.
41
- * @param props - How the glide is measured and how it slows.
56
+ * @param props - What the glide carries on, and how it is measured and slows.
42
57
  * @returns Nothing; this renders no scene node.
43
58
  */
44
- export declare function Inertia(props: InertiaConfig): null;
59
+ export declare function Inertia(props: InertiaProps): null;
45
60
  //# sourceMappingURL=Inertia.d.ts.map
@@ -1,19 +1,19 @@
1
1
  import { useFrame, useThree } from '@react-three/fiber';
2
2
  import { useEffect, useState } from 'react';
3
- import { Vector3 } from 'three';
4
3
  import { useControlsEffect } from '../hooks';
5
4
  /**
6
- * The glide a lifted pointer leaves behind.
5
+ * The glide a lifted pointer leaves behind, for one kind of motion.
7
6
  *
8
- * Mounted by {@link Pan}, and only when a pan is meant to keep moving. It watches the controls'
9
- * own interaction events to learn how fast the plan was traveling when the pointer left, then eases
10
- * that speed away over the following second.
7
+ * MapLibre's shape: each kind of motion measures itself while the user drives it, and when the
8
+ * interaction ends it glides on at the speed it ended with — rather than one glide reading the
9
+ * camera after the fact, where a pan, a pinch's slide toward the fingers and a twist's turn about
10
+ * them all arrive mixed into the same target. Generic over the motion: the caller says how to
11
+ * sample it and how to apply a step of it, and this owns the timing.
11
12
  *
12
13
  * Runs its frame callback **ahead of the controls' own** (see {@link INERTIA_PRIORITY}), so the
13
- * target it moves is the one that frame draws rather than the next — a frame tighter than the
14
- * imperative renderer, which updated the controller before its handlers. It asks for the next frame
14
+ * camera it moves is the one that frame draws rather than the next. It asks for the next frame
15
15
  * itself, which is what keeps a glide alive under the on-demand loop.
16
- * @param props - How the glide is measured and how it slows.
16
+ * @param props - What the glide carries on, and how it is measured and slows.
17
17
  * @returns Nothing; this renders no scene node.
18
18
  */
19
19
  export function Inertia(props) {
@@ -21,13 +21,15 @@ export function Inertia(props) {
21
21
  const durationMs = props.durationMs ?? DEFAULT_DURATION_MS;
22
22
  const speedThreshold = props.speedThreshold ?? DEFAULT_SPEED_THRESHOLD;
23
23
  const ease = props.ease ?? easeInertia;
24
+ const { sample, apply } = props;
24
25
  const invalidate = useThree((state) => state.invalidate);
25
- const [inertia] = useState(() => new InertiaController({ minSampleMs, durationMs, speedThreshold, ease }));
26
- // Assignments only, so a caller passing an inline `ease` re-syncs four fields and disturbs
27
- // nothing else — in particular it never re-subscribes, which is why this is its own effect.
26
+ const [inertia] = useState(() => new InertiaController({ minSampleMs, durationMs, speedThreshold, ease }, sample, apply));
27
+ // Assignments only, so a caller passing an inline `ease` or a fresh `sample` re-syncs fields and
28
+ // disturbs nothing else — in particular it never re-subscribes, which is why this is its own
29
+ // effect.
28
30
  useEffect(() => {
29
- inertia.configure({ minSampleMs, durationMs, speedThreshold, ease });
30
- }, [inertia, minSampleMs, durationMs, speedThreshold, ease]);
31
+ inertia.configure({ minSampleMs, durationMs, speedThreshold, ease }, sample, apply);
32
+ }, [inertia, minSampleMs, durationMs, speedThreshold, ease, sample, apply]);
31
33
  useControlsEffect((controls) => {
32
34
  inertia.connect(controls);
33
35
  return () => inertia.disconnect();
@@ -48,59 +50,65 @@ function easeInertia(t) {
48
50
  return Math.pow(2, 10 * (t - 1));
49
51
  }
50
52
  /**
51
- * Carries a pan on after the pointer lifts.
53
+ * Carries a motion on after the pointer lifts.
52
54
  *
53
- * Framework-free on purpose: everything here is arithmetic over the camera's target and a list of
55
+ * Framework-free on purpose: everything here is arithmetic over a running total and a list of
54
56
  * timestamps, which is the half worth testing without a canvas around it. {@link Inertia} is the
55
57
  * React half — when to connect, when to configure, when to step.
56
58
  *
57
- * **The speed comes from where the target actually went**, not from the pointer. Every interaction
58
- * frame records the target; when the pointer lifts, the oldest sample at least `minSampleMs` old is
59
- * diffed against the current target. A flick that ended in a stall therefore glides at the
60
- * stall's speed, because the samples covering the stall are the recent ones.
59
+ * **The speed comes from the motion's own samples.** Each `control` event adds the motion's delta
60
+ * to a running total and records it; when the pointer lifts, the oldest record at least
61
+ * `minSampleMs` old is diffed against the total. A flick that ended in a stall therefore glides at
62
+ * the stall's speed, because the records covering the stall are the recent ones. A step the motion
63
+ * sat out adds nothing, so an interaction it never took part in glides nowhere.
61
64
  */
62
65
  class InertiaController {
63
66
  minSampleMs;
64
67
  durationMs;
65
68
  speedThreshold;
66
69
  ease;
70
+ sample;
71
+ apply;
67
72
  controls;
68
- samples = [];
69
- target = new Vector3();
70
- delta = new Vector3();
71
- velocity = new Vector3();
73
+ total = [];
74
+ records = [];
75
+ velocity = [];
76
+ step = [];
72
77
  active = false;
73
78
  elapsedMs = 0;
74
79
  prevFactor = 1;
75
80
  /**
76
- * Takes every value rather than defaulting any: what an omitted option means is `resolveControls`'s
81
+ * Takes every value rather than defaulting any: what an omitted option means is {@link Inertia}'s
77
82
  * to answer, and answering it twice is how the two drift apart.
78
83
  * @param options - How the glide is measured and how it slows.
84
+ * @param sample - How far the motion moved since the last call.
85
+ * @param apply - Moves the camera by one step of the glide.
79
86
  */
80
- constructor(options) {
87
+ constructor(options, sample, apply) {
81
88
  this.minSampleMs = options.minSampleMs;
82
89
  this.durationMs = options.durationMs;
83
90
  this.speedThreshold = options.speedThreshold;
84
91
  this.ease = options.ease;
92
+ this.sample = sample;
93
+ this.apply = apply;
85
94
  }
86
95
  /**
87
- * Sets how the glide is measured and how it slows. Every field is optional, so a partial update
88
- * leaves the rest as it was.
89
- * @param options - The values to change.
96
+ * Sets the motion, and how the glide is measured and slows.
97
+ * @param options - How the glide is measured and how it slows.
98
+ * @param sample - How far the motion moved since the last call.
99
+ * @param apply - Moves the camera by one step of the glide.
90
100
  */
91
- configure(options) {
92
- if (options.minSampleMs !== undefined)
93
- this.minSampleMs = options.minSampleMs;
94
- if (options.durationMs !== undefined)
95
- this.durationMs = options.durationMs;
96
- if (options.speedThreshold !== undefined)
97
- this.speedThreshold = options.speedThreshold;
98
- if (options.ease !== undefined)
99
- this.ease = options.ease;
101
+ configure(options, sample, apply) {
102
+ this.minSampleMs = options.minSampleMs;
103
+ this.durationMs = options.durationMs;
104
+ this.speedThreshold = options.speedThreshold;
105
+ this.ease = options.ease;
106
+ this.sample = sample;
107
+ this.apply = apply;
100
108
  }
101
109
  /**
102
110
  * Starts watching a controls instance for the interactions a glide follows.
103
- * @param controls - The controls to watch and to move.
111
+ * @param controls - The controls to watch.
104
112
  */
105
113
  connect(controls) {
106
114
  if (this.controls)
@@ -116,7 +124,7 @@ class InertiaController {
116
124
  if (!controls)
117
125
  return;
118
126
  this.active = false;
119
- this.samples.length = 0;
127
+ this.records.length = 0;
120
128
  controls.removeEventListener('controlstart', this.onControlStart);
121
129
  controls.removeEventListener('control', this.onControl);
122
130
  controls.removeEventListener('controlend', this.onControlEnd);
@@ -132,8 +140,7 @@ class InertiaController {
132
140
  * @returns Whether the camera moved, and so whether another frame is wanted.
133
141
  */
134
142
  update(delta) {
135
- const controls = this.controls;
136
- if (!this.active || !controls)
143
+ if (!this.active)
137
144
  return false;
138
145
  const deltaMs = delta * 1000;
139
146
  this.elapsedMs += deltaMs;
@@ -141,54 +148,54 @@ class InertiaController {
141
148
  const factor = this.ease(1 - t); // 1 -> 0
142
149
  const avgFactor = (factor + this.prevFactor) * 0.5;
143
150
  this.prevFactor = factor;
144
- // The current target rather than the one being eased toward: a glide is a chain of immediate
145
- // moves, so the two agree — asking for the end value would compound with any transition the
146
- // controls happen to be running.
147
- controls.getTarget(this.target, false);
148
- this.delta.copy(this.velocity).multiplyScalar(avgFactor * deltaMs);
149
- this.target.add(this.delta);
150
- // Only the target is moved: camera-controls carries the camera along with it, which is why the
151
- // imperative renderer's parallel arithmetic on the camera position was never read back.
152
- void controls.moveTo(this.target.x, this.target.y, this.target.z, false);
151
+ for (let i = 0; i < this.velocity.length; i++) {
152
+ this.step[i] = this.velocity[i] * avgFactor * deltaMs;
153
+ }
154
+ if (!this.apply(this.step)) {
155
+ this.active = false;
156
+ return false;
157
+ }
153
158
  if (factor <= REST_FACTOR || t >= 1)
154
159
  this.active = false;
155
160
  return true;
156
161
  }
157
162
  onControlStart = () => {
158
163
  this.active = false;
159
- this.samples.length = 0;
164
+ this.records.length = 0;
165
+ this.total.fill(0);
160
166
  };
161
167
  onControl = () => {
162
- const controls = this.controls;
163
- if (!controls)
164
- return;
168
+ const delta = this.sample();
165
169
  const now = performance.now();
166
- controls.getTarget(this.target, false);
167
- this.samples.push({ t: now, target: this.target.clone() });
170
+ if (delta) {
171
+ for (let i = 0; i < delta.length; i++)
172
+ this.total[i] = (this.total[i] ?? 0) + delta[i];
173
+ }
174
+ this.records.push({ t: now, total: this.total.slice() });
168
175
  const cutoff = now - SAMPLE_WINDOW_MS;
169
- while (this.samples.length && this.samples[0].t < cutoff)
170
- this.samples.shift();
176
+ while (this.records.length && this.records[0].t < cutoff)
177
+ this.records.shift();
171
178
  };
172
179
  onControlEnd = () => {
173
- const controls = this.controls;
174
- if (!controls)
175
- return;
176
180
  const now = performance.now();
177
181
  const oldest = now - this.minSampleMs;
178
- for (let i = this.samples.length - 1; i >= 0; i--) {
179
- const sample = this.samples[i];
180
- // The first sample old enough to measure against — or the oldest there is, when the whole
181
- // drag was shorter than the sampling window.
182
- if (sample.t >= oldest && i !== 0)
182
+ for (let i = this.records.length - 1; i >= 0; i--) {
183
+ const record = this.records[i];
184
+ // The first record old enough to measure against — or the oldest there is, when the whole
185
+ // interaction was shorter than the sampling window.
186
+ if (record.t >= oldest && i !== 0)
183
187
  continue;
184
- const dt = now - sample.t;
188
+ const dt = now - record.t;
185
189
  if (dt <= 0)
186
190
  return;
187
- controls.getTarget(this.target, false);
188
- this.velocity.subVectors(this.target, sample.target).divideScalar(dt);
189
- // A pan never changes elevation, and a glide must not introduce one.
190
- this.velocity.z = 0;
191
- if (this.velocity.length() <= this.speedThreshold)
191
+ let speedSquared = 0;
192
+ this.velocity.length = this.total.length;
193
+ this.step.length = this.total.length;
194
+ for (let axis = 0; axis < this.total.length; axis++) {
195
+ this.velocity[axis] = (this.total[axis] - (record.total[axis] ?? 0)) / dt;
196
+ speedSquared += this.velocity[axis] * this.velocity[axis];
197
+ }
198
+ if (Math.sqrt(speedSquared) <= this.speedThreshold)
192
199
  return;
193
200
  this.elapsedMs = 0;
194
201
  this.prevFactor = 1;
@@ -203,13 +210,13 @@ class InertiaController {
203
210
  * Negative on both counts, so neither takes over rendering from R3F — a positive priority would.
204
211
  */
205
212
  const INERTIA_PRIORITY = -2;
206
- /** How much of a drag is kept to measure its speed from, in milliseconds. */
213
+ /** How much of an interaction is kept to measure its speed from, in milliseconds. */
207
214
  const SAMPLE_WINDOW_MS = 300;
208
215
  /** The easing factor below which what is left of a glide is not worth another frame. */
209
216
  const REST_FACTOR = 0.02;
210
- /** How far back a drag is sampled to measure the speed it should glide at, in milliseconds. */
217
+ /** How far back an interaction is sampled to measure the speed it should glide at, in milliseconds. */
211
218
  const DEFAULT_SAMPLE_MS = 50;
212
219
  /** How long a glide lasts, in milliseconds. */
213
220
  const DEFAULT_DURATION_MS = 1000;
214
- /** The speed below which a lifted pointer leaves no glide, in world units per millisecond. */
221
+ /** The speed below which a lifted pointer leaves no glide, in the motion's units per millisecond. */
215
222
  const DEFAULT_SPEED_THRESHOLD = 0.08;
@@ -12,7 +12,7 @@ export interface PanConfig {
12
12
  * along the camera's own axes — the imperative renderer's choice, and what makes a tilted plan drag
13
13
  * the way a map does.
14
14
  *
15
- * Owns the glide a lifted pointer leaves by rendering {@link Inertia}, which is the one piece of
15
+ * Owns the glide a lifted pointer leaves by rendering {@link PanInertia}, which is the one piece of
16
16
  * panning camera-controls does not do itself.
17
17
  *
18
18
  * Mounting is enabling: the actions are set on mount and returned to `NONE` on unmount.
@@ -1,6 +1,12 @@
1
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';
2
7
  import { CameraControlsAction } from '../CameraControls';
3
- import { useControlsEffect } from '../hooks';
8
+ import { canvasPoint } from '../gestures';
9
+ import { useCameraControls, useControlsEffect } from '../hooks';
4
10
  import { Inertia } from './Inertia';
5
11
  /**
6
12
  * Panning: a left-drag, and a one-finger drag.
@@ -9,7 +15,7 @@ import { Inertia } from './Inertia';
9
15
  * along the camera's own axes — the imperative renderer's choice, and what makes a tilted plan drag
10
16
  * the way a map does.
11
17
  *
12
- * Owns the glide a lifted pointer leaves by rendering {@link Inertia}, which is the one piece of
18
+ * Owns the glide a lifted pointer leaves by rendering {@link PanInertia}, which is the one piece of
13
19
  * panning camera-controls does not do itself.
14
20
  *
15
21
  * Mounting is enabling: the actions are set on mount and returned to `NONE` on unmount.
@@ -28,5 +34,179 @@ export function Pan(props) {
28
34
  });
29
35
  if (inertia === false)
30
36
  return null;
31
- return _jsx(Inertia, { ...(inertia === true ? {} : inertia) });
37
+ return _jsx(PanInertia, { ...(inertia === true ? {} : inertia) });
32
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();
@@ -98,21 +98,30 @@ class PitchGesture {
98
98
  if (this.prevTwoFingerAction === undefined) {
99
99
  this.prevTwoFingerAction = controls.touches.two;
100
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' });
101
104
  }
102
105
  lastPoints[0].add(this.p0);
103
106
  lastPoints[1].add(this.p1);
104
107
  const yDeltaAverage = (this.p0.y + this.p1.y) / 2;
105
108
  const deltaAngle = degToRad(yDeltaAverage * DEGREES_PER_PIXEL);
106
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' });
107
113
  }
108
114
  /**
109
- * Puts back the two-finger action this suppressed, if it suppressed one, and forgets the gesture.
115
+ * Puts back the two-finger action this suppressed, if it suppressed one, closes the
116
+ * `controlstart` the tilt opened, and forgets the gesture.
110
117
  *
111
118
  * Nothing is restored when the captured action was already `NONE`: there was nothing to suppress,
112
119
  * and writing it back would claim a field this handler does not own.
113
- * @param controls - The controls to restore, if they are still there.
120
+ * @param controls - The controls to restore and report to, if they are still there.
114
121
  */
115
122
  end(controls) {
123
+ if (this.prevTwoFingerAction !== undefined)
124
+ controls?.dispatchEvent({ type: 'controlend' });
116
125
  if (controls && this.prevTwoFingerAction)
117
126
  controls.touches.two = this.prevTwoFingerAction;
118
127
  this.prevTwoFingerAction = undefined;
@@ -34,6 +34,8 @@ export function Roll(props) {
34
34
  controls.mouseButtons.right |= CameraControlsAction.ROTATE_AZIMUTH;
35
35
  return () => {
36
36
  controls.mouseButtons.right &= ~CameraControlsAction.ROTATE_AZIMUTH;
37
+ // Unmounting mid-twist would otherwise leave a `controlstart` without its `controlend`.
38
+ gesture.end(controls);
37
39
  };
38
40
  });
39
41
  // Each of these is cached by the React Compiler against what it captures, so `useGesture`
@@ -43,7 +45,7 @@ export function Roll(props) {
43
45
  if (controls)
44
46
  gesture.move(event, threshold, { controls, camera: get().camera, space });
45
47
  };
46
- const onRotateEnd = () => gesture.end();
48
+ const onRotateEnd = () => gesture.end(controls);
47
49
  useGesture('rotatestart', onRotateStart);
48
50
  useGesture('rotate', onRotate);
49
51
  useGesture('rotateend', onRotateEnd);
@@ -102,6 +104,9 @@ class RollGesture {
102
104
  return;
103
105
  this.isRolling = true;
104
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' });
105
110
  }
106
111
  // Negated: the recognizer measures the twist clockwise on screen, and the azimuth it drives
107
112
  // grows the other way.
@@ -118,9 +123,18 @@ class RollGesture {
118
123
  this.cameraForward.applyMatrix4(this.rotation);
119
124
  this.targetWorld.copy(this.cameraPosition).add(this.cameraForward);
120
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' });
121
129
  }
122
- /** Forgets the gesture, so the next twist measures its threshold afresh. */
123
- end() {
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' });
124
138
  this.isRolling = false;
125
139
  this.startVector = undefined;
126
140
  this.minDiameter = 0;
@@ -1,15 +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
+ }
1
10
  /**
2
- * Zooming: the wheel, and a two-finger pinch.
11
+ * Zooming: the wheel, a two-finger pinch, and — when asked for — a double-click or double-tap.
3
12
  *
4
- * Nothing here beyond the actions — camera-controls recognizes both gestures itself, and `Controls`
5
- * has already told it to dolly toward the cursor rather than the centre. The two-finger action pans
6
- * as well as pinches, which is what makes a pinch feel like it holds the plan under the fingers;
7
- * the twist half of a two-finger gesture is `Roll`'s, and comes off the gesture recognizer rather
8
- * than from here, so the two never contend for `touches.two`.
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}.
9
21
  *
10
22
  * Mounting is enabling. The actions are set when this mounts and returned to `NONE` when it
11
23
  * unmounts, which is the whole of what the imperative renderer's `enable()` / `disable()` did.
12
- * @returns Nothing; this renders no scene node.
24
+ * @param props - Whether a double-click zooms, and by how much.
25
+ * @returns The double-click zoom, when there is one.
13
26
  */
14
- export declare function Zoom(): null;
27
+ export declare function Zoom(props: ZoomConfig): ReactNode;
15
28
  //# sourceMappingURL=Zoom.d.ts.map
@@ -1,19 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { CameraControlsAction } from '../CameraControls';
2
3
  import { useControlsEffect } from '../hooks';
4
+ import { DoubleTapZoom } from './DoubleTapZoom';
3
5
  /**
4
- * Zooming: the wheel, and a two-finger pinch.
6
+ * Zooming: the wheel, a two-finger pinch, and — when asked for — a double-click or double-tap.
5
7
  *
6
- * Nothing here beyond the actions — camera-controls recognizes both gestures itself, and `Controls`
7
- * has already told it to dolly toward the cursor rather than the centre. The two-finger action pans
8
- * as well as pinches, which is what makes a pinch feel like it holds the plan under the fingers;
9
- * the twist half of a two-finger gesture is `Roll`'s, and comes off the gesture recognizer rather
10
- * than from here, so the two never contend for `touches.two`.
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}.
11
16
  *
12
17
  * Mounting is enabling. The actions are set when this mounts and returned to `NONE` when it
13
18
  * unmounts, which is the whole of what the imperative renderer's `enable()` / `disable()` did.
14
- * @returns Nothing; this renders no scene node.
19
+ * @param props - Whether a double-click zooms, and by how much.
20
+ * @returns The double-click zoom, when there is one.
15
21
  */
16
- export function Zoom() {
22
+ export function Zoom(props) {
17
23
  useControlsEffect((controls) => {
18
24
  controls.mouseButtons.wheel = CameraControlsAction.DOLLY;
19
25
  controls.touches.two = CameraControlsAction.TOUCH_DOLLY_SCREEN_PAN;
@@ -22,5 +28,7 @@ export function Zoom() {
22
28
  controls.touches.two = CameraControlsAction.NONE;
23
29
  };
24
30
  });
25
- return null;
31
+ if (props.dblclick === undefined)
32
+ return null;
33
+ return _jsx(DoubleTapZoom, { factor: props.dblclick });
26
34
  }
@@ -4,5 +4,6 @@ export { Controls, type ControlsConfig, type ControlsProps } from './Controls';
4
4
  export type { InertiaConfig } from './handlers/Inertia';
5
5
  export type { PanConfig } from './handlers/Pan';
6
6
  export type { RollConfig } from './handlers/Roll';
7
+ export type { ZoomConfig } from './handlers/Zoom';
7
8
  export { type CameraState, useCameraAPI, useCameraControls, useCameraGesture, useCameraMoveEnd, useCameraState, useControlsEffect, } from './hooks';
8
9
  //# sourceMappingURL=index.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type CameraAPI, type CameraAxes, type CameraState, type ControlsConfig, type InertiaConfig, type PanConfig, type RollConfig, type TransitionOptions, useCameraAPI, useCameraGesture, useCameraMoveEnd, useCameraState, type ZoomToOptions, } from './controls';
1
+ export { type CameraAPI, type CameraAxes, type CameraState, type ControlsConfig, type InertiaConfig, type PanConfig, type RollConfig, type TransitionOptions, useCameraAPI, useCameraGesture, useCameraMoveEnd, useCameraState, type ZoomConfig, type ZoomToOptions, } from './controls';
2
2
  export { prepareDebug } from './debug/debug-chunk';
3
3
  export { useDebugPanel } from './debug/debug-context';
4
4
  export { type LightingProps, type ShadowProps } from './objects/lighting';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "3.40.2",
3
+ "version": "3.41.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/expofp/efp-app.git",
@@ -32,7 +32,7 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "@react-three/fiber": "^9.6.1",
35
- "camera-controls": "github:expofp/camera-controls#v3.1.3-expofp.5",
35
+ "camera-controls": "github:expofp/camera-controls#v3.1.3-expofp.7",
36
36
  "colord": "^2.9.3",
37
37
  "lil-gui": "^0.21.0",
38
38
  "maxrects-packer": "^2.7.3",
@@ -42,7 +42,7 @@
42
42
  "troika-three-text": "^0.52.5",
43
43
  "tslib": "^2.3.0",
44
44
  "webgl-memory": "^1.1.3",
45
- "@expofp/geometry": "3.40.2"
45
+ "@expofp/geometry": "3.41.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@react-three/test-renderer": "^9.1.0",