@expofp/renderer 3.31.3 → 3.31.4
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.
- package/dist/controls/CameraControls.d.ts +42 -36
- package/dist/controls/CameraControls.js +8 -53
- package/dist/controls/Controls.d.ts +49 -0
- package/dist/controls/Controls.js +54 -0
- package/dist/controls/camera-api.d.ts +12 -20
- package/dist/controls/camera-api.js +15 -40
- package/dist/controls/handlers/Inertia.d.ts +0 -66
- package/dist/controls/handlers/Inertia.js +3 -3
- package/dist/controls/handlers/Pan.js +6 -6
- package/dist/controls/handlers/Pitch.d.ts +0 -75
- package/dist/controls/handlers/Pitch.js +8 -8
- package/dist/controls/handlers/Roll.d.ts +0 -73
- package/dist/controls/handlers/Roll.js +6 -6
- package/dist/controls/handlers/Zoom.js +6 -6
- package/dist/controls/hooks.d.ts +109 -0
- package/dist/controls/hooks.js +167 -0
- package/dist/controls/index.d.ts +4 -52
- package/dist/controls/index.js +7 -58
- package/dist/debug/debug-chunk.d.ts +26 -0
- package/dist/debug/debug-chunk.js +32 -0
- package/dist/debug/index.d.ts +0 -10
- package/dist/debug/index.js +1 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/space/VisibleRect.d.ts +0 -27
- package/dist/space/VisibleRect.js +1 -1
- package/dist/space/layout-measure.d.ts +1 -1
- package/dist/space/layout-measure.js +1 -1
- package/package.json +2 -2
- package/dist/controls/camera-move.d.ts +0 -13
- package/dist/controls/camera-move.js +0 -18
- package/dist/controls/camera-state.d.ts +0 -40
- package/dist/controls/camera-state.js +0 -65
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { type MjolnirGestureEvent } from 'mjolnir.js';
|
|
2
|
-
import { type GestureContext } from '../gestures';
|
|
3
1
|
/** Options for the roll handler, which are also its props. */
|
|
4
2
|
export interface RollConfig {
|
|
5
3
|
/**
|
|
@@ -29,75 +27,4 @@ export interface RollConfig {
|
|
|
29
27
|
* @returns Nothing; this renders no scene node.
|
|
30
28
|
*/
|
|
31
29
|
export declare function Roll(props: RollConfig): null;
|
|
32
|
-
/**
|
|
33
|
-
* A two-finger twist, turning the plan about the point between the fingers.
|
|
34
|
-
*
|
|
35
|
-
* A class rather than fields on the component, for two reasons that agree: the React Compiler
|
|
36
|
-
* refuses a direct write to anything `useState` returned, and this half — where the fingers are,
|
|
37
|
-
* how far they have turned, where the pivot lands — is worth testing without a canvas around it.
|
|
38
|
-
* {@link Roll} is the React half: when to subscribe, and what the scene currently holds.
|
|
39
|
-
*/
|
|
40
|
-
export declare class RollGesture {
|
|
41
|
-
private isRolling;
|
|
42
|
-
private startVector?;
|
|
43
|
-
private minDiameter;
|
|
44
|
-
private prevAngle;
|
|
45
|
-
private vector;
|
|
46
|
-
private p0;
|
|
47
|
-
private p1;
|
|
48
|
-
private pivotPlan;
|
|
49
|
-
private pivotWorld;
|
|
50
|
-
private targetWorld;
|
|
51
|
-
private cameraPosition;
|
|
52
|
-
private cameraForward;
|
|
53
|
-
private rotation;
|
|
54
|
-
/**
|
|
55
|
-
* Remembers where the fingers started, which the twist is measured against.
|
|
56
|
-
* @param event - The gesture event that began the twist.
|
|
57
|
-
*/
|
|
58
|
-
start(event: MjolnirGestureEvent): void;
|
|
59
|
-
/**
|
|
60
|
-
* Turns the plan by however far the fingers have twisted since the last event, once the gesture
|
|
61
|
-
* has passed its threshold.
|
|
62
|
-
* @param event - The gesture event carrying the twist so far.
|
|
63
|
-
* @param threshold - How far a twist must travel before it takes, in pixels of circumference.
|
|
64
|
-
* @param context - What the scene currently holds.
|
|
65
|
-
*/
|
|
66
|
-
move(event: MjolnirGestureEvent, threshold: number, context: GestureContext): void;
|
|
67
|
-
/** Forgets the gesture, so the next twist measures its threshold afresh. */
|
|
68
|
-
end(): void;
|
|
69
|
-
/**
|
|
70
|
-
* Whether the twist so far is still inside its threshold, narrowing the remembered diameter as it
|
|
71
|
-
* goes.
|
|
72
|
-
*
|
|
73
|
-
* The narrowest the fingers have been is what the threshold is sized against, rather than where
|
|
74
|
-
* they are now, so pinching in and out during a twist does not make it progressively touchier.
|
|
75
|
-
* @param threshold - The distance along the circumference, in pixels.
|
|
76
|
-
* @returns Whether the plan should stay put.
|
|
77
|
-
*/
|
|
78
|
-
private belowThreshold;
|
|
79
|
-
/**
|
|
80
|
-
* Puts the pivot under the point between the fingers, and reads the camera's stance around it.
|
|
81
|
-
*
|
|
82
|
-
* The pivot is found on the plan and converted back out to the world, rather than crossed with
|
|
83
|
-
* the world's own `z = 0` plane. The two agree under the viewbox fit; only this one stays right
|
|
84
|
-
* for a host that placed the plan itself, at a tilt or off the world origin.
|
|
85
|
-
* @param center - The point between the fingers, in css pixels from the canvas's top-left corner.
|
|
86
|
-
* @param context - What the scene currently holds.
|
|
87
|
-
* @returns Whether a pivot was found — a camera looking along the plan never meets it.
|
|
88
|
-
*/
|
|
89
|
-
private setPivot;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* How far a twist must turn before it takes, in degrees, for fingers this far apart.
|
|
93
|
-
*
|
|
94
|
-
* The threshold is a distance along the circumference of the circle the fingers describe — the
|
|
95
|
-
* Mapbox rule — so the angle it works out to is wide when the fingers are close together and narrow
|
|
96
|
-
* when they are far apart. That is what keeps a pinch from turning the plan by accident while
|
|
97
|
-
* leaving a deliberate two-handed twist responsive.
|
|
98
|
-
* @param minDiameter - The narrowest the fingers have been during the gesture, in pixels.
|
|
99
|
-
* @param threshold - The distance along the circumference, in pixels.
|
|
100
|
-
* @returns The angle in degrees; infinite while the fingers are together, so nothing turns.
|
|
101
|
-
*/
|
|
102
|
-
export declare function rollThresholdDegrees(minDiameter: number, threshold: number): number;
|
|
103
30
|
//# sourceMappingURL=Roll.d.ts.map
|
|
@@ -3,8 +3,9 @@ import { useThree } from '@react-three/fiber';
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { Matrix4, Vector2, Vector3 } from 'three';
|
|
5
5
|
import { useSpace } from '../../space/Space';
|
|
6
|
-
import {
|
|
6
|
+
import { CameraControlsAction } from '../CameraControls';
|
|
7
7
|
import { canvasPoint, useGesture } from '../gestures';
|
|
8
|
+
import { useCameraControls, useControlsEffect } from '../hooks';
|
|
8
9
|
/**
|
|
9
10
|
* Rolling: a right-drag, and a two-finger twist.
|
|
10
11
|
*
|
|
@@ -30,9 +31,9 @@ export function Roll(props) {
|
|
|
30
31
|
const space = useSpace();
|
|
31
32
|
const [gesture] = useState(() => new RollGesture());
|
|
32
33
|
useControlsEffect((controls) => {
|
|
33
|
-
controls.mouseButtons.right |=
|
|
34
|
+
controls.mouseButtons.right |= CameraControlsAction.ROTATE_AZIMUTH;
|
|
34
35
|
return () => {
|
|
35
|
-
controls.mouseButtons.right &= ~
|
|
36
|
+
controls.mouseButtons.right &= ~CameraControlsAction.ROTATE_AZIMUTH;
|
|
36
37
|
};
|
|
37
38
|
});
|
|
38
39
|
// Each of these is cached by the React Compiler against what it captures, so `useGesture`
|
|
@@ -56,7 +57,7 @@ export function Roll(props) {
|
|
|
56
57
|
* how far they have turned, where the pivot lands — is worth testing without a canvas around it.
|
|
57
58
|
* {@link Roll} is the React half: when to subscribe, and what the scene currently holds.
|
|
58
59
|
*/
|
|
59
|
-
|
|
60
|
+
class RollGesture {
|
|
60
61
|
isRolling = false;
|
|
61
62
|
startVector;
|
|
62
63
|
minDiameter = 0;
|
|
@@ -172,10 +173,9 @@ export class RollGesture {
|
|
|
172
173
|
* @param threshold - The distance along the circumference, in pixels.
|
|
173
174
|
* @returns The angle in degrees; infinite while the fingers are together, so nothing turns.
|
|
174
175
|
*/
|
|
175
|
-
|
|
176
|
+
function rollThresholdDegrees(minDiameter, threshold) {
|
|
176
177
|
return (threshold / (Math.PI * minDiameter)) * 360;
|
|
177
178
|
}
|
|
178
|
-
const ACTION = CameraControlsImpl.ACTION;
|
|
179
179
|
/** The smallest turn worth applying, in radians — below this the twist is recognizer noise. */
|
|
180
180
|
const MIN_ROLL_STEP = 0.001;
|
|
181
181
|
/** How far a two-finger twist must travel before it turns the plan, in pixels of circumference. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CameraControlsAction } from '../CameraControls';
|
|
2
|
+
import { useControlsEffect } from '../hooks';
|
|
2
3
|
/**
|
|
3
4
|
* Zooming: the wheel, and a two-finger pinch.
|
|
4
5
|
*
|
|
@@ -14,13 +15,12 @@ import { CameraControlsImpl, useControlsEffect } from '../CameraControls';
|
|
|
14
15
|
*/
|
|
15
16
|
export function Zoom() {
|
|
16
17
|
useControlsEffect((controls) => {
|
|
17
|
-
controls.mouseButtons.wheel =
|
|
18
|
-
controls.touches.two =
|
|
18
|
+
controls.mouseButtons.wheel = CameraControlsAction.DOLLY;
|
|
19
|
+
controls.touches.two = CameraControlsAction.TOUCH_DOLLY_SCREEN_PAN;
|
|
19
20
|
return () => {
|
|
20
|
-
controls.mouseButtons.wheel =
|
|
21
|
-
controls.touches.two =
|
|
21
|
+
controls.mouseButtons.wheel = CameraControlsAction.NONE;
|
|
22
|
+
controls.touches.two = CameraControlsAction.NONE;
|
|
22
23
|
};
|
|
23
24
|
});
|
|
24
25
|
return null;
|
|
25
26
|
}
|
|
26
|
-
const ACTION = CameraControlsImpl.ACTION;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every hook a component reaches the camera through, and the state they read.
|
|
3
|
+
*
|
|
4
|
+
* Beside `CameraControls.tsx` rather than in it, because Fast Refresh swaps a module in place only
|
|
5
|
+
* when every value it exports is a component. A hook exported next to one sends that module, and
|
|
6
|
+
* everything importing it, through a full page reload instead.
|
|
7
|
+
*
|
|
8
|
+
* The dependency runs one way, from here into `camera-api.ts`. That file holds the commands and the
|
|
9
|
+
* arithmetic behind them and imports nothing back, which is what lets `useCameraAPI` sit here with
|
|
10
|
+
* the rest.
|
|
11
|
+
*/
|
|
12
|
+
import { Point } from '@expofp/geometry';
|
|
13
|
+
import { type CameraAPI } from './camera-api';
|
|
14
|
+
import { type CameraControlsImpl } from './CameraControls';
|
|
15
|
+
/**
|
|
16
|
+
* The scene's camera controls, once {@link Controls} has mounted them.
|
|
17
|
+
*
|
|
18
|
+
* For **reading** them, and for handing them to something else that does the writing — a gesture
|
|
19
|
+
* class, a measurement, a helper taking them as an argument. What the React Compiler rejects is
|
|
20
|
+
* assigning to a *field* of a hook's return value, which is a build error rather than a bug; a
|
|
21
|
+
* method call on it, or passing it along, is fine. A component with a write it cannot route through
|
|
22
|
+
* a function uses {@link useControlsEffect}, which hands the controls over as an argument.
|
|
23
|
+
* @returns The controls, or `undefined` until they mount.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useCameraControls(): CameraControlsImpl | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Runs an effect against the scene's camera controls, for as long as the component is mounted.
|
|
28
|
+
*
|
|
29
|
+
* The controls arrive as an argument rather than from a hook, which is what lets the effect write to
|
|
30
|
+
* them; and the "wait for them to mount" subscription lives here rather than in every caller, so a
|
|
31
|
+
* component that claims a mouse action is three lines and says only what it claims. Together with
|
|
32
|
+
* {@link useCameraControls} that is the whole surface — read them, or write to them in an effect.
|
|
33
|
+
*
|
|
34
|
+
* Re-runs when the controls change and whenever `effect` does, which — the React Compiler caching
|
|
35
|
+
* the closure against what it captures — is whenever the component's own inputs move.
|
|
36
|
+
* @param effect - What to do with the controls; may return a cleanup, as any effect may.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useControlsEffect(effect: (controls: CameraControlsImpl) => (() => void) | void): void;
|
|
39
|
+
/**
|
|
40
|
+
* Runs a handler when a camera movement begins — a gesture and a command alike, one call per
|
|
41
|
+
* movement rather than one per frame. The event the imperative renderer published as
|
|
42
|
+
* `navigation:change`, under a name that says what happened.
|
|
43
|
+
*
|
|
44
|
+
* A camera-level event rather than the controls themselves, so a caller resetting an idle timer or
|
|
45
|
+
* dismissing an overlay never learns which library drives the camera. For as long as the component
|
|
46
|
+
* is mounted; where the camera *is* once it settles is `useCameraState`'s business.
|
|
47
|
+
* @param handler - Called as each movement starts. Re-subscribed when its identity changes, so pass
|
|
48
|
+
* a stable function — under the React Compiler an inline closure over stable values is one.
|
|
49
|
+
*/
|
|
50
|
+
export declare function useCameraMoveStart(handler: () => void): void;
|
|
51
|
+
/** Where the camera is, at the moment it was asked. */
|
|
52
|
+
export interface CameraState {
|
|
53
|
+
/** The point on the plan it is looking at, in plan units. */
|
|
54
|
+
center: Point;
|
|
55
|
+
/** How far in it is zoomed, as a factor: `1` frames the whole viewbox. */
|
|
56
|
+
zoom: number;
|
|
57
|
+
/** Which way the plan is turned, in degrees clockwise from north, in `[0, 360)`. */
|
|
58
|
+
roll: number;
|
|
59
|
+
/** How far the plan is tilted away from face-on, in degrees. */
|
|
60
|
+
pitch: number;
|
|
61
|
+
/**
|
|
62
|
+
* How much of the plan one device pixel spans, in plan units — the factor anything sized in
|
|
63
|
+
* pixels is laid out against, and the quantity the imperative renderer published as `ptScale`.
|
|
64
|
+
*/
|
|
65
|
+
pixelSize: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reads where the camera is.
|
|
69
|
+
*
|
|
70
|
+
* A **getter**, not a value: nothing here belongs in render output — a component that re-rendered
|
|
71
|
+
* whenever the camera moved would re-render on every frame of every pan — and every caller wants it
|
|
72
|
+
* at a moment of its own choosing, in an event handler, an effect, or a snapshot a host asked for.
|
|
73
|
+
* That is the same reading discipline `Space` documents, for the same reason.
|
|
74
|
+
*
|
|
75
|
+
* The five numbers come from four different places, which is why this exists at all rather than
|
|
76
|
+
* being arithmetic a caller could do: the target from the controls and the plan it lands on from the
|
|
77
|
+
* space, the zoom factor from the camera's handle, and the pixel size from the re-layout registry —
|
|
78
|
+
* which is what the plan's own labels are sized against, so a caller measuring against anything else
|
|
79
|
+
* would disagree with what is drawn.
|
|
80
|
+
*
|
|
81
|
+
* Outside a mounted scene, or in the frames before the controls publish themselves, it reports the
|
|
82
|
+
* camera at rest: the middle of the plan, framed whole, facing north.
|
|
83
|
+
*
|
|
84
|
+
* As with {@link useCameraAPI}, the reader is **new on every render** and this module memoizes
|
|
85
|
+
* nothing; a consumer that needs a settled identity holds it in a ref.
|
|
86
|
+
* @returns A function that reads the current state.
|
|
87
|
+
*/
|
|
88
|
+
export declare function useCameraState(): () => CameraState;
|
|
89
|
+
/**
|
|
90
|
+
* The scene's camera commands.
|
|
91
|
+
*
|
|
92
|
+
* A hook rather than a ref handed out from above, and that is the point: it only resolves inside a
|
|
93
|
+
* component mounted under `<Renderer>`, and R3F mounts those only once the canvas and its context
|
|
94
|
+
* exist — so "the camera cannot be driven before the scene is ready" is enforced by where the code
|
|
95
|
+
* can live rather than by a null check every caller has to remember. A host that needs to drive the
|
|
96
|
+
* camera from outside React publishes this object from an effect, which is exactly when it is safe.
|
|
97
|
+
*
|
|
98
|
+
* Before the controls mount every command is a no-op that resolves at once, which is the same frame
|
|
99
|
+
* or two the hook's own consumers spend waiting for their effects.
|
|
100
|
+
*
|
|
101
|
+
* **The returned object is new on every render, deliberately.** This module is not memoized by hand
|
|
102
|
+
* — the React Compiler is scoped to `.tsx` under `src/` (see `vitest.config.mts`), and reaching for
|
|
103
|
+
* `useMemo` here would put a cache in the library that every consumer then has to trust. A consumer
|
|
104
|
+
* that cannot re-subscribe cheaply should hold this in a ref rather than in a dependency list; that
|
|
105
|
+
* way the decision, and its cost, sit with the code that knows how often it renders.
|
|
106
|
+
* @returns The commands.
|
|
107
|
+
*/
|
|
108
|
+
export declare function useCameraAPI(): CameraAPI;
|
|
109
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every hook a component reaches the camera through, and the state they read.
|
|
3
|
+
*
|
|
4
|
+
* Beside `CameraControls.tsx` rather than in it, because Fast Refresh swaps a module in place only
|
|
5
|
+
* when every value it exports is a component. A hook exported next to one sends that module, and
|
|
6
|
+
* everything importing it, through a full page reload instead.
|
|
7
|
+
*
|
|
8
|
+
* The dependency runs one way, from here into `camera-api.ts`. That file holds the commands and the
|
|
9
|
+
* arithmetic behind them and imports nothing back, which is what lets `useCameraAPI` sit here with
|
|
10
|
+
* the rest.
|
|
11
|
+
*/
|
|
12
|
+
import { Point, radToDeg } from '@expofp/geometry';
|
|
13
|
+
import { useThree } from '@react-three/fiber';
|
|
14
|
+
import { use, useEffect } from 'react';
|
|
15
|
+
import { Vector3 } from 'three';
|
|
16
|
+
import { LayoutTargetsContext } from '../events/layout-targets';
|
|
17
|
+
import { useCamera } from '../space/camera-handle';
|
|
18
|
+
import { useCanvasSize } from '../space/canvas-size';
|
|
19
|
+
import { useSpace } from '../space/Space';
|
|
20
|
+
import { bearing, CameraCommands } from './camera-api';
|
|
21
|
+
/**
|
|
22
|
+
* The scene's camera controls, once {@link Controls} has mounted them.
|
|
23
|
+
*
|
|
24
|
+
* For **reading** them, and for handing them to something else that does the writing — a gesture
|
|
25
|
+
* class, a measurement, a helper taking them as an argument. What the React Compiler rejects is
|
|
26
|
+
* assigning to a *field* of a hook's return value, which is a build error rather than a bug; a
|
|
27
|
+
* method call on it, or passing it along, is fine. A component with a write it cannot route through
|
|
28
|
+
* a function uses {@link useControlsEffect}, which hands the controls over as an argument.
|
|
29
|
+
* @returns The controls, or `undefined` until they mount.
|
|
30
|
+
*/
|
|
31
|
+
export function useCameraControls() {
|
|
32
|
+
return useThree((state) => state.controls) ?? undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runs an effect against the scene's camera controls, for as long as the component is mounted.
|
|
36
|
+
*
|
|
37
|
+
* The controls arrive as an argument rather than from a hook, which is what lets the effect write to
|
|
38
|
+
* them; and the "wait for them to mount" subscription lives here rather than in every caller, so a
|
|
39
|
+
* component that claims a mouse action is three lines and says only what it claims. Together with
|
|
40
|
+
* {@link useCameraControls} that is the whole surface — read them, or write to them in an effect.
|
|
41
|
+
*
|
|
42
|
+
* Re-runs when the controls change and whenever `effect` does, which — the React Compiler caching
|
|
43
|
+
* the closure against what it captures — is whenever the component's own inputs move.
|
|
44
|
+
* @param effect - What to do with the controls; may return a cleanup, as any effect may.
|
|
45
|
+
*/
|
|
46
|
+
export function useControlsEffect(effect) {
|
|
47
|
+
const get = useThree((state) => state.get);
|
|
48
|
+
// A subscription, so this re-runs once the controls have mounted. Deliberately not the value the
|
|
49
|
+
// effect uses: that is read fresh from the store below.
|
|
50
|
+
const mounted = useThree((state) => state.controls);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const controls = get().controls;
|
|
53
|
+
if (!controls)
|
|
54
|
+
return undefined;
|
|
55
|
+
return effect(controls) ?? undefined;
|
|
56
|
+
}, [effect, get, mounted]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Runs a handler when a camera movement begins — a gesture and a command alike, one call per
|
|
60
|
+
* movement rather than one per frame. The event the imperative renderer published as
|
|
61
|
+
* `navigation:change`, under a name that says what happened.
|
|
62
|
+
*
|
|
63
|
+
* A camera-level event rather than the controls themselves, so a caller resetting an idle timer or
|
|
64
|
+
* dismissing an overlay never learns which library drives the camera. For as long as the component
|
|
65
|
+
* is mounted; where the camera *is* once it settles is `useCameraState`'s business.
|
|
66
|
+
* @param handler - Called as each movement starts. Re-subscribed when its identity changes, so pass
|
|
67
|
+
* a stable function — under the React Compiler an inline closure over stable values is one.
|
|
68
|
+
*/
|
|
69
|
+
export function useCameraMoveStart(handler) {
|
|
70
|
+
useControlsEffect((controls) => {
|
|
71
|
+
controls.addEventListener('transitionstart', handler);
|
|
72
|
+
return () => controls.removeEventListener('transitionstart', handler);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reads where the camera is.
|
|
77
|
+
*
|
|
78
|
+
* A **getter**, not a value: nothing here belongs in render output — a component that re-rendered
|
|
79
|
+
* whenever the camera moved would re-render on every frame of every pan — and every caller wants it
|
|
80
|
+
* at a moment of its own choosing, in an event handler, an effect, or a snapshot a host asked for.
|
|
81
|
+
* That is the same reading discipline `Space` documents, for the same reason.
|
|
82
|
+
*
|
|
83
|
+
* The five numbers come from four different places, which is why this exists at all rather than
|
|
84
|
+
* being arithmetic a caller could do: the target from the controls and the plan it lands on from the
|
|
85
|
+
* space, the zoom factor from the camera's handle, and the pixel size from the re-layout registry —
|
|
86
|
+
* which is what the plan's own labels are sized against, so a caller measuring against anything else
|
|
87
|
+
* would disagree with what is drawn.
|
|
88
|
+
*
|
|
89
|
+
* Outside a mounted scene, or in the frames before the controls publish themselves, it reports the
|
|
90
|
+
* camera at rest: the middle of the plan, framed whole, facing north.
|
|
91
|
+
*
|
|
92
|
+
* As with {@link useCameraAPI}, the reader is **new on every render** and this module memoizes
|
|
93
|
+
* nothing; a consumer that needs a settled identity holds it in a ref.
|
|
94
|
+
* @returns A function that reads the current state.
|
|
95
|
+
*/
|
|
96
|
+
export function useCameraState() {
|
|
97
|
+
const controls = useCameraControls();
|
|
98
|
+
const camera = useCamera();
|
|
99
|
+
const space = useSpace();
|
|
100
|
+
const targets = use(LayoutTargetsContext);
|
|
101
|
+
return () => readCameraState({ controls, camera, space, targets });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Reads the camera, from whatever the scene holds at the moment of asking.
|
|
105
|
+
* @param context - What the scene holds.
|
|
106
|
+
* @returns Where the camera is.
|
|
107
|
+
*/
|
|
108
|
+
function readCameraState(context) {
|
|
109
|
+
const { controls, camera, space, targets } = context;
|
|
110
|
+
const pixelSize = targets?.pixelSize ?? 1;
|
|
111
|
+
// A fresh centre every time: a caller that keeps the point it was handed must never find it
|
|
112
|
+
// moved under them.
|
|
113
|
+
if (!controls)
|
|
114
|
+
return { ...atRest(), pixelSize };
|
|
115
|
+
controls.getTarget(_target);
|
|
116
|
+
return {
|
|
117
|
+
center: space.worldToPlan(_target, new Point()),
|
|
118
|
+
zoom: camera?.zoomFactor ?? 1,
|
|
119
|
+
roll: radToDeg(bearing(controls)),
|
|
120
|
+
pitch: radToDeg(controls.polarAngle),
|
|
121
|
+
pixelSize,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The camera as it stands before anything has moved it, which is what a read outside a mounted
|
|
126
|
+
* scene reports: the middle of the plan, framed whole, facing north.
|
|
127
|
+
* @returns The state.
|
|
128
|
+
*/
|
|
129
|
+
function atRest() {
|
|
130
|
+
return { center: new Point(), zoom: 1, roll: 0, pitch: 0, pixelSize: 1 };
|
|
131
|
+
}
|
|
132
|
+
const _target = new Vector3();
|
|
133
|
+
/**
|
|
134
|
+
* The scene's camera commands.
|
|
135
|
+
*
|
|
136
|
+
* A hook rather than a ref handed out from above, and that is the point: it only resolves inside a
|
|
137
|
+
* component mounted under `<Renderer>`, and R3F mounts those only once the canvas and its context
|
|
138
|
+
* exist — so "the camera cannot be driven before the scene is ready" is enforced by where the code
|
|
139
|
+
* can live rather than by a null check every caller has to remember. A host that needs to drive the
|
|
140
|
+
* camera from outside React publishes this object from an effect, which is exactly when it is safe.
|
|
141
|
+
*
|
|
142
|
+
* Before the controls mount every command is a no-op that resolves at once, which is the same frame
|
|
143
|
+
* or two the hook's own consumers spend waiting for their effects.
|
|
144
|
+
*
|
|
145
|
+
* **The returned object is new on every render, deliberately.** This module is not memoized by hand
|
|
146
|
+
* — the React Compiler is scoped to `.tsx` under `src/` (see `vitest.config.mts`), and reaching for
|
|
147
|
+
* `useMemo` here would put a cache in the library that every consumer then has to trust. A consumer
|
|
148
|
+
* that cannot re-subscribe cheaply should hold this in a ref rather than in a dependency list; that
|
|
149
|
+
* way the decision, and its cost, sit with the code that knows how often it renders.
|
|
150
|
+
* @returns The commands.
|
|
151
|
+
*/
|
|
152
|
+
export function useCameraAPI() {
|
|
153
|
+
const get = useThree((state) => state.get);
|
|
154
|
+
const controls = useCameraControls();
|
|
155
|
+
const camera = useCamera();
|
|
156
|
+
const space = useSpace();
|
|
157
|
+
const { width, height, dpr } = useCanvasSize();
|
|
158
|
+
return new CameraCommands({
|
|
159
|
+
controls,
|
|
160
|
+
camera,
|
|
161
|
+
space,
|
|
162
|
+
view: () => get().camera.view,
|
|
163
|
+
width,
|
|
164
|
+
height,
|
|
165
|
+
dpr,
|
|
166
|
+
});
|
|
167
|
+
}
|
package/dist/controls/index.d.ts
CHANGED
|
@@ -1,56 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export { type CameraAPI, type CameraAxes, CameraCommands, type TransitionOptions, useCameraAPI, type ZoomToOptions, } from './camera-api';
|
|
5
|
-
export { useCameraMoveStart } from './camera-move';
|
|
6
|
-
export { type CameraState, useCameraState } from './camera-state';
|
|
7
|
-
export { bearing, CameraControls, CameraControlsImpl, useCameraControls, useControlsEffect, } from './CameraControls';
|
|
1
|
+
export { bearing, type CameraAPI, type CameraAxes, CameraCommands, type TransitionOptions, type ZoomToOptions, } from './camera-api';
|
|
2
|
+
export { CameraControls, CameraControlsAction, type CameraControlsImpl } from './CameraControls';
|
|
3
|
+
export { Controls, type ControlsConfig, type ControlsProps } from './Controls';
|
|
8
4
|
export type { InertiaConfig } from './handlers/Inertia';
|
|
9
5
|
export type { PanConfig } from './handlers/Pan';
|
|
10
6
|
export type { RollConfig } from './handlers/Roll';
|
|
11
|
-
|
|
12
|
-
* What the user may do with the plan.
|
|
13
|
-
*
|
|
14
|
-
* Each handler takes `false` to switch it off, `true` or nothing for its defaults, and — where it
|
|
15
|
-
* has any — an options object, whose own defaults the handler applies.
|
|
16
|
-
*
|
|
17
|
-
* **Pan and zoom default on; roll and pitch default to whether this is a development build**, so
|
|
18
|
-
* the renderer's own Storybook and dev server carry them and a production plan does not until a
|
|
19
|
-
* caller asks. An explicit value always wins, so a host driving roll from a user-facing setting
|
|
20
|
-
* never has to think about the dev default.
|
|
21
|
-
*
|
|
22
|
-
* The pitch and zoom **limits** are not here: they hold whether or not the matching gesture does,
|
|
23
|
-
* which makes them the camera's — see `CameraProps`.
|
|
24
|
-
*/
|
|
25
|
-
export interface ControlsConfig {
|
|
26
|
-
/** Left-drag and one-finger drag to pan the plan. Defaults on. */
|
|
27
|
-
pan?: boolean | PanConfig;
|
|
28
|
-
/** Wheel and two-finger pinch to zoom, toward the cursor. Defaults on. */
|
|
29
|
-
zoom?: boolean;
|
|
30
|
-
/** Right-drag and two-finger twist to turn the plan. Defaults to a development build. */
|
|
31
|
-
roll?: boolean | RollConfig;
|
|
32
|
-
/** Right-drag and two-finger vertical drag to tilt the plan. Defaults to a development build. */
|
|
33
|
-
pitch?: boolean;
|
|
34
|
-
}
|
|
35
|
-
/** Props for {@link Controls}. */
|
|
36
|
-
export interface ControlsProps {
|
|
37
|
-
/** What the user may do with the plan; see {@link ControlsConfig}. */
|
|
38
|
-
config?: ControlsConfig;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* The scene's camera controls, and the gesture handlers that drive them.
|
|
42
|
-
*
|
|
43
|
-
* Registered as R3F's default controls (`makeDefault`) so everything else reaches them through
|
|
44
|
-
* `state.controls`. Every mouse and touch action starts switched off, and each handler mounted
|
|
45
|
-
* below claims the ones it needs — so what a user can do is exactly the set of handlers standing
|
|
46
|
-
* here, and turning one off is unmounting it.
|
|
47
|
-
*
|
|
48
|
-
* **How far the camera may go is not decided here.** The zoom and pitch bounds constrain a
|
|
49
|
-
* programmatic move as much as a dragged one, so they belong to the camera; `Camera` writes them,
|
|
50
|
-
* beside the clipping planes derived from the same numbers. This component is only about what the
|
|
51
|
-
* user may do.
|
|
52
|
-
* @param props - What the user may do with the plan.
|
|
53
|
-
* @returns The controls and their handlers.
|
|
54
|
-
*/
|
|
55
|
-
export declare function Controls(props: ControlsProps): ReactNode;
|
|
7
|
+
export { type CameraState, useCameraAPI, useCameraControls, useCameraMoveStart, useCameraState, useControlsEffect, } from './hooks';
|
|
56
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/controls/index.js
CHANGED
|
@@ -1,58 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export { CameraCommands, useCameraAPI, } from './camera-api';
|
|
9
|
-
export { useCameraMoveStart } from './camera-move';
|
|
10
|
-
export { useCameraState } from './camera-state';
|
|
11
|
-
export { bearing, CameraControls, CameraControlsImpl, useCameraControls, useControlsEffect, } from './CameraControls';
|
|
12
|
-
/**
|
|
13
|
-
* The scene's camera controls, and the gesture handlers that drive them.
|
|
14
|
-
*
|
|
15
|
-
* Registered as R3F's default controls (`makeDefault`) so everything else reaches them through
|
|
16
|
-
* `state.controls`. Every mouse and touch action starts switched off, and each handler mounted
|
|
17
|
-
* below claims the ones it needs — so what a user can do is exactly the set of handlers standing
|
|
18
|
-
* here, and turning one off is unmounting it.
|
|
19
|
-
*
|
|
20
|
-
* **How far the camera may go is not decided here.** The zoom and pitch bounds constrain a
|
|
21
|
-
* programmatic move as much as a dragged one, so they belong to the camera; `Camera` writes them,
|
|
22
|
-
* beside the clipping planes derived from the same numbers. This component is only about what the
|
|
23
|
-
* user may do.
|
|
24
|
-
* @param props - What the user may do with the plan.
|
|
25
|
-
* @returns The controls and their handlers.
|
|
26
|
-
*/
|
|
27
|
-
export function Controls(props) {
|
|
28
|
-
const controlsRef = useRef(null);
|
|
29
|
-
const config = props.config ?? {};
|
|
30
|
-
const pan = config.pan ?? true;
|
|
31
|
-
const zoom = config.zoom ?? true;
|
|
32
|
-
const roll = config.roll ?? DEV;
|
|
33
|
-
const pitch = config.pitch ?? DEV;
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
// Straight down by default. Set through `rotateTo` rather than by assigning `polarAngle`, which
|
|
36
|
-
// skips `Spherical.makeSafe` — and a polar angle of exactly zero causes trouble with roll on
|
|
37
|
-
// mobile. Nothing sets a target: the scene's fit puts the plan's centre on the world origin,
|
|
38
|
-
// which is where the controls already orbit.
|
|
39
|
-
void controlsRef.current.rotateTo(0, 0);
|
|
40
|
-
}, []);
|
|
41
|
-
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, {})] }));
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The options a handler was configured with, if it was given any rather than a bare flag.
|
|
45
|
-
* @param config - What the caller wrote for it.
|
|
46
|
-
* @returns The options, or an empty set for a handler switched on without any.
|
|
47
|
-
*/
|
|
48
|
-
function optionsOf(config) {
|
|
49
|
-
return typeof config === 'object' ? config : {};
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Whether this is a development build, which is what roll and pitch default to.
|
|
53
|
-
*
|
|
54
|
-
* Typed inline rather than through `vite/client`, matching `stories/support/screenshot.ts`: the
|
|
55
|
-
* library build compiles without those types, and the optional chain keeps a consumer whose bundler
|
|
56
|
-
* defines no `import.meta.env` from throwing on it.
|
|
57
|
-
*/
|
|
58
|
-
const DEV = import.meta.env?.DEV === true;
|
|
1
|
+
// The package's controls surface. A barrel and nothing else: a module that both re-exports and
|
|
2
|
+
// declares a component cannot be a Fast Refresh boundary, and every hook below would send `Controls`
|
|
3
|
+
// through a full reload with it.
|
|
4
|
+
export { bearing, CameraCommands, } from './camera-api';
|
|
5
|
+
export { CameraControls, CameraControlsAction } from './CameraControls';
|
|
6
|
+
export { Controls } from './Controls';
|
|
7
|
+
export { useCameraAPI, useCameraControls, useCameraMoveStart, useCameraState, useControlsEffect, } from './hooks';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one debug chunk, and the two ways into it.
|
|
3
|
+
*
|
|
4
|
+
* Beside `index.tsx` rather than in it, because Fast Refresh swaps a module in place only when every
|
|
5
|
+
* value it exports is a component. `prepareDebug` is this package's public API and cannot stop being
|
|
6
|
+
* exported, so it moves instead.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Imports the debug chunk, memoized so `use()` sees a stable promise and it is fetched once.
|
|
10
|
+
*
|
|
11
|
+
* Shared by every consumer: the internal mount's context-patch gate, the overlay in both mounts'
|
|
12
|
+
* trees, and {@link prepareDebug}.
|
|
13
|
+
* @returns The import promise, the same one each call.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadDebugChunk(): Promise<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Loads the debug chunk ahead of time — for a host that creates its own WebGL context.
|
|
18
|
+
*
|
|
19
|
+
* The chunk's evaluation is what instruments context creation for the GPU-memory panel
|
|
20
|
+
* (webgl-memory patches `getContext` as it loads), so a context created before it exists can never
|
|
21
|
+
* report memory figures. The renderer's own mount orders this itself; a MapLibre host does it by
|
|
22
|
+
* awaiting this before constructing the map, when debug is on. Idempotent, and the only eager cost
|
|
23
|
+
* is the call: the debug code still lives in its own lazy chunk.
|
|
24
|
+
*/
|
|
25
|
+
export declare function prepareDebug(): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=debug-chunk.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one debug chunk, and the two ways into it.
|
|
3
|
+
*
|
|
4
|
+
* Beside `index.tsx` rather than in it, because Fast Refresh swaps a module in place only when every
|
|
5
|
+
* value it exports is a component. `prepareDebug` is this package's public API and cannot stop being
|
|
6
|
+
* exported, so it moves instead.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Imports the debug chunk, memoized so `use()` sees a stable promise and it is fetched once.
|
|
10
|
+
*
|
|
11
|
+
* Shared by every consumer: the internal mount's context-patch gate, the overlay in both mounts'
|
|
12
|
+
* trees, and {@link prepareDebug}.
|
|
13
|
+
* @returns The import promise, the same one each call.
|
|
14
|
+
*/
|
|
15
|
+
export function loadDebugChunk() {
|
|
16
|
+
debugChunk ??= import('./components/DebugTools');
|
|
17
|
+
return debugChunk;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Loads the debug chunk ahead of time — for a host that creates its own WebGL context.
|
|
21
|
+
*
|
|
22
|
+
* The chunk's evaluation is what instruments context creation for the GPU-memory panel
|
|
23
|
+
* (webgl-memory patches `getContext` as it loads), so a context created before it exists can never
|
|
24
|
+
* report memory figures. The renderer's own mount orders this itself; a MapLibre host does it by
|
|
25
|
+
* awaiting this before constructing the map, when debug is on. Idempotent, and the only eager cost
|
|
26
|
+
* is the call: the debug code still lives in its own lazy chunk.
|
|
27
|
+
*/
|
|
28
|
+
export async function prepareDebug() {
|
|
29
|
+
await loadDebugChunk();
|
|
30
|
+
}
|
|
31
|
+
/** The memoized import, set by the first {@link loadDebugChunk} call. */
|
|
32
|
+
let debugChunk;
|
package/dist/debug/index.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { type CanvasProps } from '@react-three/fiber';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
|
-
/**
|
|
4
|
-
* Loads the debug chunk ahead of time — for a host that creates its own WebGL context.
|
|
5
|
-
*
|
|
6
|
-
* The chunk's evaluation is what instruments context creation for the GPU-memory panel
|
|
7
|
-
* (webgl-memory patches `getContext` as it loads), so a context created before it exists can never
|
|
8
|
-
* report memory figures. The renderer's own mount orders this itself; a MapLibre host does it by
|
|
9
|
-
* awaiting this before constructing the map, when debug is on. Idempotent, and the only eager cost
|
|
10
|
-
* is the call: the debug code still lives in its own lazy chunk.
|
|
11
|
-
*/
|
|
12
|
-
export declare function prepareDebug(): Promise<void>;
|
|
13
3
|
/** Props for {@link CanvasWithDebug}: the Canvas props plus the debug toggle. */
|
|
14
4
|
export interface CanvasWithDebugProps extends CanvasProps {
|
|
15
5
|
/** When falsy, the Canvas mounts untouched and no debug code is loaded. */
|