@opsydyn/effect-three 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +189 -2
- package/dist/index-01kwfekm.js +5118 -0
- package/dist/index-01kwfekm.js.map +40 -0
- package/dist/index-4045wme0.js +129 -0
- package/dist/index-4045wme0.js.map +10 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1361 -0
- package/dist/index.js.map +14 -0
- package/dist/kernel/animation.d.ts +14 -0
- package/dist/kernel/animation.d.ts.map +1 -0
- package/dist/kernel/behaviours.d.ts +16 -0
- package/dist/kernel/behaviours.d.ts.map +1 -0
- package/dist/kernel/bodies.d.ts +60 -0
- package/dist/kernel/bodies.d.ts.map +1 -0
- package/dist/kernel/bodySources.d.ts +47 -0
- package/dist/kernel/bodySources.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +564 -0
- package/dist/kernel/build.d.ts.map +1 -0
- package/dist/kernel/cameraMotion.d.ts +39 -0
- package/dist/kernel/cameraMotion.d.ts.map +1 -0
- package/dist/kernel/clock.d.ts +17 -0
- package/dist/kernel/clock.d.ts.map +1 -0
- package/dist/kernel/contactShadowPasses.d.ts +47 -0
- package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +1120 -0
- package/dist/kernel/description.d.ts.map +1 -0
- package/dist/kernel/diff.d.ts +90 -0
- package/dist/kernel/diff.d.ts.map +1 -0
- package/dist/kernel/environmentPresets.d.ts +30 -0
- package/dist/kernel/environmentPresets.d.ts.map +1 -0
- package/dist/kernel/equality.d.ts +64 -0
- package/dist/kernel/equality.d.ts.map +1 -0
- package/dist/kernel/frames.d.ts +55 -0
- package/dist/kernel/frames.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +37 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +335 -0
- package/dist/kernel/index.js.map +9 -0
- package/dist/kernel/instances.d.ts +26 -0
- package/dist/kernel/instances.d.ts.map +1 -0
- package/dist/kernel/labels.d.ts +51 -0
- package/dist/kernel/labels.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts +49 -0
- package/dist/kernel/loop.d.ts.map +1 -0
- package/dist/kernel/materials.d.ts +24 -0
- package/dist/kernel/materials.d.ts.map +1 -0
- package/dist/kernel/motion.d.ts +36 -0
- package/dist/kernel/motion.d.ts.map +1 -0
- package/dist/kernel/orbit.d.ts +127 -0
- package/dist/kernel/orbit.d.ts.map +1 -0
- package/dist/kernel/overlays.d.ts +16 -0
- package/dist/kernel/overlays.d.ts.map +1 -0
- package/dist/kernel/performance.d.ts +41 -0
- package/dist/kernel/performance.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +166 -0
- package/dist/kernel/physics.d.ts.map +1 -0
- package/dist/kernel/physicsDebug.d.ts +18 -0
- package/dist/kernel/physicsDebug.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +357 -0
- package/dist/kernel/pointer.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +49 -0
- package/dist/kernel/renderKit.d.ts.map +1 -0
- package/dist/kernel/renderer.d.ts +83 -0
- package/dist/kernel/renderer.d.ts.map +1 -0
- package/dist/kernel/resources.d.ts +112 -0
- package/dist/kernel/resources.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +155 -0
- package/dist/kernel/sceneGraph.d.ts.map +1 -0
- package/dist/kernel/sceneIndex.d.ts +90 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -0
- package/dist/kernel/stage.d.ts +87 -0
- package/dist/kernel/stage.d.ts.map +1 -0
- package/dist/kernel/textLayout.d.ts +29 -0
- package/dist/kernel/textLayout.d.ts.map +1 -0
- package/dist/kernel/tween.d.ts +35 -0
- package/dist/kernel/tween.d.ts.map +1 -0
- package/dist/kernel/xrInput.d.ts +107 -0
- package/dist/kernel/xrInput.d.ts.map +1 -0
- package/dist/kernel/xrLocomotion.d.ts +38 -0
- package/dist/kernel/xrLocomotion.d.ts.map +1 -0
- package/dist/runtime/FrameSource.d.ts +29 -0
- package/dist/runtime/FrameSource.d.ts.map +1 -0
- package/dist/runtime/ResourceRegistry.d.ts +18 -0
- package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
- package/dist/runtime/SceneRuntime.d.ts +68 -0
- package/dist/runtime/SceneRuntime.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +2536 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -0
- package/dist/runtime/errors.d.ts +19 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/webgl/contactShadows.d.ts +5 -0
- package/dist/webgl/contactShadows.d.ts.map +1 -0
- package/dist/webgl/index.d.ts +11 -0
- package/dist/webgl/index.d.ts.map +1 -0
- package/dist/webgl/index.js +239 -0
- package/dist/webgl/index.js.map +13 -0
- package/dist/webgl/shaderGrid.d.ts +27 -0
- package/dist/webgl/shaderGrid.d.ts.map +1 -0
- package/dist/webgl/sky.d.ts +4 -0
- package/dist/webgl/sky.d.ts.map +1 -0
- package/dist/webgpu/contactShadows.d.ts +5 -0
- package/dist/webgpu/contactShadows.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts +10 -0
- package/dist/webgpu/index.d.ts.map +1 -0
- package/dist/webgpu/index.js +216 -0
- package/dist/webgpu/index.js.map +13 -0
- package/dist/webgpu/shaderGrid.d.ts +5 -0
- package/dist/webgpu/shaderGrid.d.ts.map +1 -0
- package/dist/webgpu/sky.d.ts +4 -0
- package/dist/webgpu/sky.d.ts.map +1 -0
- package/package.json +82 -4
- package/src/index.ts +11 -0
- package/src/kernel/animation.ts +24 -0
- package/src/kernel/behaviours.ts +36 -0
- package/src/kernel/bodies.ts +338 -0
- package/src/kernel/bodySources.ts +203 -0
- package/src/kernel/build.ts +1325 -0
- package/src/kernel/cameraMotion.ts +202 -0
- package/src/kernel/clock.ts +35 -0
- package/src/kernel/contactShadowPasses.ts +207 -0
- package/src/kernel/description.ts +1197 -0
- package/src/kernel/diff.ts +383 -0
- package/src/kernel/environmentPresets.ts +38 -0
- package/src/kernel/equality.ts +604 -0
- package/src/kernel/frames.ts +124 -0
- package/src/kernel/index.ts +274 -0
- package/src/kernel/instances.ts +97 -0
- package/src/kernel/labels.ts +54 -0
- package/src/kernel/loop.ts +248 -0
- package/src/kernel/materials.ts +258 -0
- package/src/kernel/motion.ts +75 -0
- package/src/kernel/orbit.ts +554 -0
- package/src/kernel/overlays.ts +130 -0
- package/src/kernel/performance.ts +101 -0
- package/src/kernel/physics.ts +361 -0
- package/src/kernel/physicsDebug.ts +68 -0
- package/src/kernel/pointer.ts +785 -0
- package/src/kernel/renderKit.ts +54 -0
- package/src/kernel/renderer.ts +136 -0
- package/src/kernel/resources.ts +504 -0
- package/src/kernel/sceneGraph.ts +1442 -0
- package/src/kernel/sceneIndex.ts +219 -0
- package/src/kernel/stage.ts +157 -0
- package/src/kernel/textLayout.ts +102 -0
- package/src/kernel/tween.ts +80 -0
- package/src/kernel/xrInput.ts +230 -0
- package/src/kernel/xrLocomotion.ts +72 -0
- package/src/runtime/FrameSource.ts +41 -0
- package/src/runtime/ResourceRegistry.ts +24 -0
- package/src/runtime/SceneRuntime.ts +229 -0
- package/src/runtime/SceneSchema.ts +983 -0
- package/src/runtime/errors.ts +18 -0
- package/src/runtime/index.ts +10 -0
- package/src/webgl/contactShadows.ts +67 -0
- package/src/webgl/index.ts +19 -0
- package/src/webgl/shaderGrid.ts +196 -0
- package/src/webgl/sky.ts +42 -0
- package/src/webgpu/contactShadows.ts +83 -0
- package/src/webgpu/index.ts +19 -0
- package/src/webgpu/shaderGrid.ts +158 -0
- package/src/webgpu/sky.ts +35 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer hit testing and the pointer state machine.
|
|
3
|
+
*
|
|
4
|
+
* Input arrives in normalised device coordinates (NDC: x and y in [-1, 1], y up), never
|
|
5
|
+
* as DOM events, so this stays DOM-free. Hover is reported on transitions (enter, leave)
|
|
6
|
+
* plus a move for each pointer movement over an interactive mesh.
|
|
7
|
+
*
|
|
8
|
+
* Behavioural reference: React Three Fiber `packages/fiber/src/core/events.ts` at
|
|
9
|
+
* `d74622e`. As there, click, double-click and context-menu events go only to the mesh
|
|
10
|
+
* that was also under the pointer when it was pressed, and a click that hits nothing and
|
|
11
|
+
* moved at most two CSS pixels is a miss.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { type Object3D, Raycaster, Vector2, Vector3 } from 'three';
|
|
16
|
+
|
|
17
|
+
import type { Gestures, Vec3 } from './description.ts';
|
|
18
|
+
import type { SceneGraph } from './sceneGraph.ts';
|
|
19
|
+
import type { XrButtonName, XrHand, XrMesh, XrPlane, XrStickDirection } from './xrInput.ts';
|
|
20
|
+
|
|
21
|
+
/** Modifier keys held during an input, as on `MouseEvent`. */
|
|
22
|
+
export type Modifiers = {
|
|
23
|
+
readonly shift: boolean;
|
|
24
|
+
readonly control: boolean;
|
|
25
|
+
readonly meta: boolean;
|
|
26
|
+
readonly alt: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pointer input in normalised device coordinates. `button` follows `MouseEvent.button`.
|
|
31
|
+
* `pointer` is the input's `PointerEvent.pointerId` (0 when absent): with two pointers
|
|
32
|
+
* pressed (two fingers), the second only takes part in pinches.
|
|
33
|
+
*/
|
|
34
|
+
export type PointerInput =
|
|
35
|
+
| {
|
|
36
|
+
readonly _tag: 'PointerMove';
|
|
37
|
+
readonly x: number;
|
|
38
|
+
readonly y: number;
|
|
39
|
+
readonly pointer?: number;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
readonly _tag: 'PointerDown';
|
|
43
|
+
readonly x: number;
|
|
44
|
+
readonly y: number;
|
|
45
|
+
readonly button: number;
|
|
46
|
+
readonly pointer?: number;
|
|
47
|
+
readonly modifiers?: Modifiers;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
readonly _tag: 'PointerUp';
|
|
51
|
+
readonly x: number;
|
|
52
|
+
readonly y: number;
|
|
53
|
+
readonly button: number;
|
|
54
|
+
readonly pointer?: number;
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
readonly _tag: 'DoubleClick';
|
|
58
|
+
readonly x: number;
|
|
59
|
+
readonly y: number;
|
|
60
|
+
readonly button: number;
|
|
61
|
+
}
|
|
62
|
+
| { readonly _tag: 'ContextMenu'; readonly x: number; readonly y: number }
|
|
63
|
+
| {
|
|
64
|
+
readonly _tag: 'Wheel';
|
|
65
|
+
readonly x: number;
|
|
66
|
+
readonly y: number;
|
|
67
|
+
/** In CSS pixels: line and page deltas are converted, as three.js's controls do. */
|
|
68
|
+
readonly deltaX: number;
|
|
69
|
+
readonly deltaY: number;
|
|
70
|
+
readonly modifiers?: Modifiers;
|
|
71
|
+
}
|
|
72
|
+
| { readonly _tag: 'PointerLeave' };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A gesture on the canvas, reported when the scene claims gestures (`gestures: 'scene'`).
|
|
76
|
+
* Distances are CSS pixels, right and down positive, since the previous gesture event;
|
|
77
|
+
* `width` and `height` are the CSS size of the canvas, or of the view the gesture began
|
|
78
|
+
* in, for turning pixels into angles and distances. `view` names that view. `x` and `y`
|
|
79
|
+
* locate the gesture in it (−1 to 1, y up): where a drag was pressed, the wheel turned,
|
|
80
|
+
* or a pinch's midpoint is, for zooming toward the cursor.
|
|
81
|
+
*
|
|
82
|
+
* - `Drag`: the pointer moved while pressed, with the button and modifiers of the press.
|
|
83
|
+
* `key` names the interactive mesh the press landed on, if any, so a Model can leave
|
|
84
|
+
* the camera alone while that mesh is dragged.
|
|
85
|
+
* - `Wheel`: the wheel turned, anywhere over the canvas.
|
|
86
|
+
* - `Pinch`: two pointers moved: `scale` is their new distance over the old (above 1
|
|
87
|
+
* when spreading), and the deltas are how far their midpoint moved.
|
|
88
|
+
*/
|
|
89
|
+
export type Gesture =
|
|
90
|
+
| {
|
|
91
|
+
readonly _tag: 'Drag';
|
|
92
|
+
readonly button: number;
|
|
93
|
+
readonly modifiers: Modifiers;
|
|
94
|
+
readonly key?: string;
|
|
95
|
+
readonly view?: string;
|
|
96
|
+
readonly deltaX: number;
|
|
97
|
+
readonly deltaY: number;
|
|
98
|
+
readonly width: number;
|
|
99
|
+
readonly height: number;
|
|
100
|
+
readonly x: number;
|
|
101
|
+
readonly y: number;
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
readonly _tag: 'Wheel';
|
|
105
|
+
readonly modifiers: Modifiers;
|
|
106
|
+
readonly view?: string;
|
|
107
|
+
readonly deltaX: number;
|
|
108
|
+
readonly deltaY: number;
|
|
109
|
+
readonly width: number;
|
|
110
|
+
readonly height: number;
|
|
111
|
+
readonly x: number;
|
|
112
|
+
readonly y: number;
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
readonly _tag: 'Pinch';
|
|
116
|
+
readonly view?: string;
|
|
117
|
+
readonly scale: number;
|
|
118
|
+
readonly deltaX: number;
|
|
119
|
+
readonly deltaY: number;
|
|
120
|
+
readonly width: number;
|
|
121
|
+
readonly height: number;
|
|
122
|
+
readonly x: number;
|
|
123
|
+
readonly y: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** No modifier keys held. */
|
|
127
|
+
export const noModifiers: Modifiers = { shift: false, control: false, meta: false, alt: false };
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The nearest interactive mesh under a point.
|
|
131
|
+
*
|
|
132
|
+
* - `point`: world-space hit point. `distance`: from the camera, in world units.
|
|
133
|
+
* - `normal`: world-space normal of the face hit. `uv`: texture coordinates at the hit.
|
|
134
|
+
* Every kernel-built geometry has both; a geometry without them reports zeros.
|
|
135
|
+
*/
|
|
136
|
+
export type Hit = {
|
|
137
|
+
readonly key: string;
|
|
138
|
+
/** For an `instances` node, the key of the instance hit. */
|
|
139
|
+
readonly instance?: string;
|
|
140
|
+
/** For a `borrowed` node, the `name` of the object hit inside it, when it has one. */
|
|
141
|
+
readonly part?: string;
|
|
142
|
+
/** With several views, the key of the view the pointer was over. */
|
|
143
|
+
readonly view?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Keys of the hit mesh's ancestor groups, nearest first. Match on it to handle every
|
|
146
|
+
* mesh in a group with one handler: React Three Fiber's bubbling, as data.
|
|
147
|
+
*/
|
|
148
|
+
readonly path: ReadonlyArray<string>;
|
|
149
|
+
readonly point: Vec3;
|
|
150
|
+
readonly distance: number;
|
|
151
|
+
readonly normal: Vec3;
|
|
152
|
+
readonly uv: readonly [u: number, v: number];
|
|
153
|
+
/** The world-space ray through the pointer, as on every React Three Fiber event. */
|
|
154
|
+
readonly ray: Ray;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** A world-space ray from the camera through the pointer. `direction` is unit length. */
|
|
158
|
+
export type Ray = { readonly origin: Vec3; readonly direction: Vec3 };
|
|
159
|
+
|
|
160
|
+
/** A plane `normal · p + constant = 0`, as three.js's `Plane`. */
|
|
161
|
+
export type PlaneEquation = { readonly normal: Vec3; readonly constant: number };
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Where a ray meets a plane, or `undefined` when it runs parallel or the plane is behind
|
|
165
|
+
* it. Turns drag rays into points: `pointOnPlane(ray, { normal: [0, 1, 0], constant: 0 })`
|
|
166
|
+
* is the point on the ground.
|
|
167
|
+
*/
|
|
168
|
+
export const pointOnPlane = (ray: Ray, plane: PlaneEquation): Vec3 | undefined => {
|
|
169
|
+
const [nx, ny, nz] = plane.normal;
|
|
170
|
+
const [ox, oy, oz] = ray.origin;
|
|
171
|
+
const [dx, dy, dz] = ray.direction;
|
|
172
|
+
const denominator = nx * dx + ny * dy + nz * dz;
|
|
173
|
+
if (Math.abs(denominator) < 1e-12) return undefined;
|
|
174
|
+
const t = -(nx * ox + ny * oy + nz * oz + plane.constant) / denominator;
|
|
175
|
+
if (t < 0) return undefined;
|
|
176
|
+
return [ox + dx * t, oy + dy * t, oz + dz * t];
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** A hit with a tag and extra fields, flattened into one object type. */
|
|
180
|
+
type HitEvent<Tag extends string, Extra = unknown> = {
|
|
181
|
+
readonly [K in keyof ({ readonly _tag: Tag } & Hit & Extra)]: ({ readonly _tag: Tag } & Hit &
|
|
182
|
+
Extra)[K];
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Interaction with an interactive mesh, identified by its key, plus misses. */
|
|
186
|
+
export type SceneEvent =
|
|
187
|
+
| HitEvent<'PointerEnter'>
|
|
188
|
+
| { readonly _tag: 'PointerLeave'; readonly key: string }
|
|
189
|
+
| HitEvent<'PointerMove'>
|
|
190
|
+
| HitEvent<'PointerDown', { readonly button: number }>
|
|
191
|
+
| HitEvent<'PointerUp', { readonly button: number }>
|
|
192
|
+
| HitEvent<'Click', { readonly button: number }>
|
|
193
|
+
| HitEvent<'DoubleClick', { readonly button: number }>
|
|
194
|
+
| HitEvent<'ContextMenu'>
|
|
195
|
+
| HitEvent<'Wheel', { readonly deltaX: number; readonly deltaY: number }>
|
|
196
|
+
/** A click that hit no interactive mesh: useful for clearing a selection. */
|
|
197
|
+
| { readonly _tag: 'PointerMissed'; readonly button: number }
|
|
198
|
+
/**
|
|
199
|
+
* The pointer moved while pressed on mesh `key`, anywhere: the press captures the
|
|
200
|
+
* pointer, as React Three Fiber's `setPointerCapture` does, until release.
|
|
201
|
+
*/
|
|
202
|
+
| { readonly _tag: 'Drag'; readonly key: string; readonly button: number; readonly ray: Ray }
|
|
203
|
+
/** A drag on `key` ended: released anywhere, or the pointer left. Only after a `Drag`. */
|
|
204
|
+
| {
|
|
205
|
+
readonly _tag: 'DragEnd';
|
|
206
|
+
readonly key: string;
|
|
207
|
+
readonly button: number;
|
|
208
|
+
readonly ray: Ray;
|
|
209
|
+
}
|
|
210
|
+
/** A drag, wheel or pinch on the canvas, when the scene claims gestures. */
|
|
211
|
+
| { readonly _tag: 'Gesture'; readonly gesture: Gesture }
|
|
212
|
+
/**
|
|
213
|
+
* A gesture is about to begin on a camera that has turned by itself (`autoRotate`):
|
|
214
|
+
* where it got to, so the Model can keep it (`autoRotated(camera, detail)`) and the
|
|
215
|
+
* gesture moves on from there. `view` names a view's camera; without it, the scene's.
|
|
216
|
+
*/
|
|
217
|
+
| { readonly _tag: 'AutoRotated'; readonly view?: string; readonly position: Vec3 }
|
|
218
|
+
/** In an immersive session, a thumbstick pushed past halfway in a direction, or let back. */
|
|
219
|
+
| { readonly _tag: 'XrStick'; readonly hand: XrHand; readonly direction: XrStickDirection }
|
|
220
|
+
/** In an immersive session, a controller button pressed or released. */
|
|
221
|
+
| {
|
|
222
|
+
readonly _tag: 'XrButton';
|
|
223
|
+
readonly hand: XrHand;
|
|
224
|
+
readonly button: XrButtonName;
|
|
225
|
+
readonly pressed: boolean;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Gliding with the sticks (`xr({ glide })`) stopped: where the player stands, to keep
|
|
229
|
+
* in the Model's `xr({ origin, heading })`.
|
|
230
|
+
*/
|
|
231
|
+
| { readonly _tag: 'XrMoved'; readonly origin: Vec3; readonly heading: number }
|
|
232
|
+
/** The session's detected planes changed (`xr({ planes: true })`): all of them, now. */
|
|
233
|
+
| { readonly _tag: 'XrPlanes'; readonly planes: ReadonlyArray<XrPlane> }
|
|
234
|
+
/** The session's detected meshes changed (`xr({ meshes: true })`): all of them, now. */
|
|
235
|
+
| { readonly _tag: 'XrMeshes'; readonly meshes: ReadonlyArray<XrMesh> }
|
|
236
|
+
/** In AR, a select whose ray met a real surface (`xr({ hitTest: true })`). */
|
|
237
|
+
| {
|
|
238
|
+
readonly _tag: 'XrSurface';
|
|
239
|
+
readonly hand: XrHand;
|
|
240
|
+
readonly point: Vec3;
|
|
241
|
+
readonly normal: Vec3;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Two bodies began (`started`) or stopped touching, by their nodes' keys (and, for a body
|
|
245
|
+
* on an instance, the instance's): Rapier's collision events, as react-three-rapier's
|
|
246
|
+
* `onCollisionEnter` and `onCollisionExit`. Not a pointer event: it travels with them so
|
|
247
|
+
* one stream carries everything.
|
|
248
|
+
*/
|
|
249
|
+
| {
|
|
250
|
+
readonly _tag: 'Collision';
|
|
251
|
+
readonly a: string;
|
|
252
|
+
readonly aInstance?: string;
|
|
253
|
+
readonly aPart?: string;
|
|
254
|
+
readonly b: string;
|
|
255
|
+
readonly bInstance?: string;
|
|
256
|
+
readonly bPart?: string;
|
|
257
|
+
readonly started: boolean;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Two bodies pushed on each other harder than a body's `contactForces` threshold during
|
|
261
|
+
* a step: react-three-rapier's `onContactForce` payload, named as collisions are.
|
|
262
|
+
*/
|
|
263
|
+
| {
|
|
264
|
+
readonly _tag: 'ContactForce';
|
|
265
|
+
readonly a: string;
|
|
266
|
+
readonly aInstance?: string;
|
|
267
|
+
readonly aPart?: string;
|
|
268
|
+
readonly b: string;
|
|
269
|
+
readonly bInstance?: string;
|
|
270
|
+
readonly bPart?: string;
|
|
271
|
+
readonly totalForce: Vec3;
|
|
272
|
+
readonly totalForceMagnitude: number;
|
|
273
|
+
readonly maxForceDirection: Vec3;
|
|
274
|
+
readonly maxForceMagnitude: number;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* A measured label's laid-out size changed (`scene({ measure })`, after its glyphs are
|
|
278
|
+
* laid out): in its own units, before its scale, for sizing what goes behind it, as
|
|
279
|
+
* Drei's `Text` `onSync` reads troika's `textRenderInfo`.
|
|
280
|
+
*/
|
|
281
|
+
| {
|
|
282
|
+
readonly _tag: 'LabelMeasured';
|
|
283
|
+
readonly key: string;
|
|
284
|
+
readonly block: readonly [number, number, number, number];
|
|
285
|
+
readonly visible: readonly [number, number, number, number];
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* A measured node's world bounding box changed (`scene({ measure })`): Drei's `Bounds`
|
|
289
|
+
* `box.setFromObject`, reported rather than read. Not a pointer event either.
|
|
290
|
+
*/
|
|
291
|
+
| {
|
|
292
|
+
readonly _tag: 'Measured';
|
|
293
|
+
readonly key: string;
|
|
294
|
+
readonly min: Vec3;
|
|
295
|
+
readonly max: Vec3;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The frame-rate monitor moved the performance factor (0 to 1). Not a pointer event:
|
|
299
|
+
* it travels with them so one stream carries everything the scene reports.
|
|
300
|
+
*/
|
|
301
|
+
| { readonly _tag: 'PerformanceChanged'; readonly fps: number; readonly factor: number };
|
|
302
|
+
|
|
303
|
+
export type Picker = {
|
|
304
|
+
/** Hit-tests at NDC `(x, y)`. Updates world matrices first, so it works headless. */
|
|
305
|
+
readonly pick: (x: number, y: number) => Hit | undefined;
|
|
306
|
+
/** The world-space ray through NDC `(x, y)`. */
|
|
307
|
+
readonly ray: (x: number, y: number) => Ray;
|
|
308
|
+
/** Hit-tests along a world-space ray: an XR controller's, say. */
|
|
309
|
+
readonly pickRay: (ray: Ray) => Hit | undefined;
|
|
310
|
+
/** The picked graph's viewport in CSS pixels, for pixel thresholds. */
|
|
311
|
+
readonly viewport: () => { readonly width: number; readonly height: number };
|
|
312
|
+
/**
|
|
313
|
+
* The CSS size of what is under NDC `(x, y)`: the view there, with its key, or the
|
|
314
|
+
* whole canvas.
|
|
315
|
+
*/
|
|
316
|
+
readonly region: (x: number, y: number) => Region;
|
|
317
|
+
/** Whether the scene claims the pointer's gestures. */
|
|
318
|
+
readonly gestures: () => Gestures;
|
|
319
|
+
/** Holds the cameras' turning (`autoRotate`) while a gesture is pressed. */
|
|
320
|
+
readonly holdCameras: (held: boolean) => void;
|
|
321
|
+
/** Where turning has taken a view's camera (or the scene's), if it has turned. */
|
|
322
|
+
readonly turned: (view: string | undefined) => Vec3 | undefined;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** A part of the canvas in CSS pixels: a view, named by `view`, or the whole canvas. */
|
|
326
|
+
export type Region = {
|
|
327
|
+
readonly view?: string;
|
|
328
|
+
readonly width: number;
|
|
329
|
+
readonly height: number;
|
|
330
|
+
/** The point in the region's own normalised coordinates: −1 to 1, y up. */
|
|
331
|
+
readonly x: number;
|
|
332
|
+
readonly y: number;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const toVec3 = (vector: Vector3): Vec3 => [vector.x, vector.y, vector.z];
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The nearest object with a key: the object itself for kernel-built meshes, or the
|
|
339
|
+
* borrowed node wrapping an application object.
|
|
340
|
+
*/
|
|
341
|
+
const keyedOwner = (graph: SceneGraph, object: Object3D): Object3D | undefined => {
|
|
342
|
+
for (let current: Object3D | null = object; current !== null; current = current.parent) {
|
|
343
|
+
if (graph.keyOf(current) !== undefined) return current;
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** The name of the part hit inside a borrowed object, when the hit is inside one. */
|
|
349
|
+
const partOf = (owner: Object3D, hit: Object3D): { readonly part?: string } =>
|
|
350
|
+
owner === hit || hit.name === '' ? {} : { part: hit.name };
|
|
351
|
+
|
|
352
|
+
/** Keys of an object's keyed ancestors, nearest first. */
|
|
353
|
+
const pathOf = (graph: SceneGraph, object: Object3D): ReadonlyArray<string> => {
|
|
354
|
+
const path: Array<string> = [];
|
|
355
|
+
for (let parent = object.parent; parent !== null; parent = parent.parent) {
|
|
356
|
+
const key = graph.keyOf(parent);
|
|
357
|
+
if (key !== undefined) path.push(key);
|
|
358
|
+
}
|
|
359
|
+
return path;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** Creates a raycasting picker over a graph's interactive meshes and groups. */
|
|
363
|
+
export const createPicker = (graph: SceneGraph): Picker => {
|
|
364
|
+
const raycaster = new Raycaster();
|
|
365
|
+
const ndc = new Vector2();
|
|
366
|
+
const normal = new Vector3();
|
|
367
|
+
/**
|
|
368
|
+
* Aims the ray through canvas NDC `(x, y)`: with the main camera, or with the camera of
|
|
369
|
+
* the topmost view under the point, in that view's own NDC. Returns the view's key, or
|
|
370
|
+
* `false` when views exist but none is under the point.
|
|
371
|
+
*/
|
|
372
|
+
const aim = (x: number, y: number): string | undefined | false => {
|
|
373
|
+
graph.scene.updateMatrixWorld();
|
|
374
|
+
const views = graph.views();
|
|
375
|
+
if (views.length === 0) {
|
|
376
|
+
graph.camera.updateMatrixWorld();
|
|
377
|
+
ndc.set(x, y);
|
|
378
|
+
raycaster.setFromCamera(ndc, graph.camera);
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
const { width, height } = graph.viewport();
|
|
382
|
+
const px = ((x + 1) / 2) * width;
|
|
383
|
+
const py = ((1 - y) / 2) * height;
|
|
384
|
+
for (let i = views.length - 1; i >= 0; i--) {
|
|
385
|
+
const view = views[i];
|
|
386
|
+
if (view === undefined) continue;
|
|
387
|
+
const { area } = view;
|
|
388
|
+
if (px < area.x || px > area.x + area.width || py < area.y || py > area.y + area.height) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
view.camera.updateMatrixWorld();
|
|
392
|
+
ndc.set(((px - area.x) / area.width) * 2 - 1, -(((py - area.y) / area.height) * 2 - 1));
|
|
393
|
+
raycaster.setFromCamera(ndc, view.camera);
|
|
394
|
+
return view.key;
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
};
|
|
398
|
+
// A world-space ray's origin and direction, as `Raycaster.set` takes them.
|
|
399
|
+
const rayOrigin = new Vector3();
|
|
400
|
+
const rayDirection = new Vector3();
|
|
401
|
+
|
|
402
|
+
/** The nearest keyed hit along the raycaster's ray, reporting `view` when there is one. */
|
|
403
|
+
const nearestHit = (view: string | undefined): Hit | undefined => {
|
|
404
|
+
// Recursive, so an interactive group makes every mesh inside it pickable.
|
|
405
|
+
for (const intersection of raycaster.intersectObjects(graph.interactiveObjects(), true)) {
|
|
406
|
+
const owner = keyedOwner(graph, intersection.object);
|
|
407
|
+
const key = owner === undefined ? undefined : graph.keyOf(owner);
|
|
408
|
+
if (owner !== undefined && key !== undefined) {
|
|
409
|
+
normal.set(0, 0, 0);
|
|
410
|
+
if (intersection.face) {
|
|
411
|
+
normal.copy(intersection.face.normal).transformDirection(intersection.object.matrixWorld);
|
|
412
|
+
}
|
|
413
|
+
const instance =
|
|
414
|
+
intersection.instanceId === undefined
|
|
415
|
+
? undefined
|
|
416
|
+
: graph.instanceKeyOf(intersection.object, intersection.instanceId);
|
|
417
|
+
return {
|
|
418
|
+
key,
|
|
419
|
+
...(instance === undefined ? {} : { instance }),
|
|
420
|
+
...partOf(owner, intersection.object),
|
|
421
|
+
...(view === undefined ? {} : { view }),
|
|
422
|
+
path: pathOf(graph, owner),
|
|
423
|
+
point: toVec3(intersection.point),
|
|
424
|
+
distance: intersection.distance,
|
|
425
|
+
normal: toVec3(normal),
|
|
426
|
+
uv: [intersection.uv?.x ?? 0, intersection.uv?.y ?? 0],
|
|
427
|
+
ray: currentRay(),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return undefined;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const currentRay = (): Ray => ({
|
|
435
|
+
origin: toVec3(raycaster.ray.origin),
|
|
436
|
+
direction: toVec3(raycaster.ray.direction),
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
ray: (x, y) => {
|
|
440
|
+
aim(x, y);
|
|
441
|
+
return currentRay();
|
|
442
|
+
},
|
|
443
|
+
region: (x, y) => {
|
|
444
|
+
const { width, height } = graph.viewport();
|
|
445
|
+
const px = ((x + 1) / 2) * width;
|
|
446
|
+
const py = ((1 - y) / 2) * height;
|
|
447
|
+
const view = graph
|
|
448
|
+
.views()
|
|
449
|
+
.findLast(
|
|
450
|
+
({ area }) =>
|
|
451
|
+
px >= area.x && px <= area.x + area.width && py >= area.y && py <= area.y + area.height,
|
|
452
|
+
);
|
|
453
|
+
return view === undefined
|
|
454
|
+
? { width, height, x, y }
|
|
455
|
+
: {
|
|
456
|
+
view: view.key,
|
|
457
|
+
width: view.area.width,
|
|
458
|
+
height: view.area.height,
|
|
459
|
+
x: ((px - view.area.x) / view.area.width) * 2 - 1,
|
|
460
|
+
y: 1 - ((py - view.area.y) / view.area.height) * 2,
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
gestures: graph.gestures,
|
|
464
|
+
holdCameras: graph.holdCameras,
|
|
465
|
+
turned: graph.turned,
|
|
466
|
+
pick: (x, y) => {
|
|
467
|
+
if (graph.interactiveObjects().length === 0) return undefined;
|
|
468
|
+
const view = aim(x, y);
|
|
469
|
+
if (view === false) return undefined;
|
|
470
|
+
return nearestHit(view);
|
|
471
|
+
},
|
|
472
|
+
pickRay: (ray) => {
|
|
473
|
+
if (graph.interactiveObjects().length === 0) return undefined;
|
|
474
|
+
graph.scene.updateMatrixWorld();
|
|
475
|
+
raycaster.set(
|
|
476
|
+
rayOrigin.fromArray(ray.origin),
|
|
477
|
+
rayDirection.fromArray(ray.direction).normalize(),
|
|
478
|
+
);
|
|
479
|
+
return nearestHit(undefined);
|
|
480
|
+
},
|
|
481
|
+
viewport: graph.viewport,
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export type PointerTracker = {
|
|
486
|
+
/** Applies one input and returns the resulting events. */
|
|
487
|
+
readonly input: (input: PointerInput) => ReadonlyArray<SceneEvent>;
|
|
488
|
+
/**
|
|
489
|
+
* Re-tests hover at the last pointer position. Call once per frame: objects can move
|
|
490
|
+
* under a stationary pointer. Returns only enter and leave transitions.
|
|
491
|
+
*/
|
|
492
|
+
readonly frame: () => ReadonlyArray<SceneEvent>;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const none: ReadonlyArray<SceneEvent> = [];
|
|
496
|
+
|
|
497
|
+
/** A click moving further than this, in CSS pixels, is a drag and never a miss. */
|
|
498
|
+
const missTolerance = 2;
|
|
499
|
+
|
|
500
|
+
type Press = {
|
|
501
|
+
readonly key: string | undefined;
|
|
502
|
+
readonly button: number;
|
|
503
|
+
readonly x: number;
|
|
504
|
+
readonly y: number;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/** The press a gesture began with: its button, modifiers, mesh and region. */
|
|
508
|
+
type GesturePress = Region & {
|
|
509
|
+
readonly button: number;
|
|
510
|
+
readonly modifiers: Modifiers;
|
|
511
|
+
readonly key?: string;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
type Point = { readonly x: number; readonly y: number };
|
|
515
|
+
|
|
516
|
+
const pointerOf = (input: PointerInput): number =>
|
|
517
|
+
'pointer' in input && input.pointer !== undefined ? input.pointer : 0;
|
|
518
|
+
|
|
519
|
+
/** Creates the pointer state machine for one scene. */
|
|
520
|
+
export const createPointerTracker = (picker: Picker): PointerTracker => {
|
|
521
|
+
let position: { x: number; y: number } | undefined;
|
|
522
|
+
// Pressed pointers in NDC, for gestures. The first pressed is primary and drives the
|
|
523
|
+
// press, click and drag machinery; others only take part in pinches.
|
|
524
|
+
const pressedPointers = new Map<number, Point>();
|
|
525
|
+
let primary: number | undefined;
|
|
526
|
+
let gesturePress: GesturePress | undefined;
|
|
527
|
+
let hovered: string | undefined;
|
|
528
|
+
// The latest press, kept after release: double-click and context-menu events follow it.
|
|
529
|
+
let press: Press | undefined;
|
|
530
|
+
let pressing = false;
|
|
531
|
+
// Whether the current press has dragged: only then does its release end a drag.
|
|
532
|
+
let dragged = false;
|
|
533
|
+
|
|
534
|
+
const hover = (hit: Hit | undefined, events: Array<SceneEvent>): void => {
|
|
535
|
+
const next = hit?.key;
|
|
536
|
+
if (next === hovered) return;
|
|
537
|
+
if (hovered !== undefined) events.push({ _tag: 'PointerLeave', key: hovered });
|
|
538
|
+
if (hit !== undefined) events.push({ _tag: 'PointerEnter', ...hit });
|
|
539
|
+
hovered = next;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/** CSS pixels between the press and NDC `(x, y)`. */
|
|
543
|
+
const travel = (from: Press, x: number, y: number): number => {
|
|
544
|
+
const { width, height } = picker.viewport();
|
|
545
|
+
return Math.hypot(((x - from.x) * width) / 2, ((y - from.y) * height) / 2);
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const pressed = (hit: Hit): boolean => press !== undefined && press.key === hit.key;
|
|
549
|
+
|
|
550
|
+
const release = (
|
|
551
|
+
hit: Hit | undefined,
|
|
552
|
+
event: { readonly x: number; readonly y: number; readonly button: number },
|
|
553
|
+
events: Array<SceneEvent>,
|
|
554
|
+
): void => {
|
|
555
|
+
const from = pressing ? press : undefined;
|
|
556
|
+
pressing = false;
|
|
557
|
+
if (hit !== undefined) {
|
|
558
|
+
events.push({ _tag: 'PointerUp', button: event.button, ...hit });
|
|
559
|
+
if (from?.key === hit.key && from.button === event.button) {
|
|
560
|
+
events.push({ _tag: 'Click', button: event.button, ...hit });
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (from?.button === event.button && travel(from, event.x, event.y) <= missTolerance) {
|
|
565
|
+
events.push({ _tag: 'PointerMissed', button: event.button });
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/** The pressed mesh holding pointer capture, if any. */
|
|
570
|
+
const captured = (): { readonly key: string; readonly button: number } | undefined =>
|
|
571
|
+
pressing && press?.key !== undefined ? { key: press.key, button: press.button } : undefined;
|
|
572
|
+
|
|
573
|
+
/** Ends a drag that moved; a press released without moving was a click, not a drag. */
|
|
574
|
+
const endDrag = (
|
|
575
|
+
drag: { readonly key: string; readonly button: number } | undefined,
|
|
576
|
+
x: number,
|
|
577
|
+
y: number,
|
|
578
|
+
events: Array<SceneEvent>,
|
|
579
|
+
): void => {
|
|
580
|
+
if (drag !== undefined && dragged) {
|
|
581
|
+
events.push({ _tag: 'DragEnd', ...drag, ray: picker.ray(x, y) });
|
|
582
|
+
}
|
|
583
|
+
dragged = false;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/** NDC distance to CSS pixels on the canvas, down positive. */
|
|
587
|
+
const pixels = (from: Point, to: Point): Point => {
|
|
588
|
+
const { width, height } = picker.viewport();
|
|
589
|
+
return { x: ((to.x - from.x) * width) / 2, y: (-(to.y - from.y) * height) / 2 };
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const pixelDistance = (a: Point, b: Point): number => {
|
|
593
|
+
const { x, y } = pixels(a, b);
|
|
594
|
+
return Math.hypot(x, y);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
/** Reports where the camera under `region` has turned to, before a gesture on it. */
|
|
598
|
+
const reportTurned = (region: Region, events: Array<SceneEvent>): void => {
|
|
599
|
+
const position = picker.turned(region.view);
|
|
600
|
+
if (position === undefined) return;
|
|
601
|
+
events.push({
|
|
602
|
+
_tag: 'AutoRotated',
|
|
603
|
+
...(region.view === undefined ? {} : { view: region.view }),
|
|
604
|
+
position,
|
|
605
|
+
});
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
/** A gesture press begins: the first pointer down while the scene claims gestures. */
|
|
609
|
+
const beginGesture = (
|
|
610
|
+
event: Extract<PointerInput, { readonly _tag: 'PointerDown' }>,
|
|
611
|
+
id: number,
|
|
612
|
+
key: string | undefined,
|
|
613
|
+
events: Array<SceneEvent>,
|
|
614
|
+
): void => {
|
|
615
|
+
if (picker.gestures() !== 'scene') return;
|
|
616
|
+
pressedPointers.clear();
|
|
617
|
+
primary = id;
|
|
618
|
+
pressedPointers.set(id, { x: event.x, y: event.y });
|
|
619
|
+
const region = picker.region(event.x, event.y);
|
|
620
|
+
reportTurned(region, events);
|
|
621
|
+
// Cameras turning by themselves hold still while it is pressed, as `OrbitControls`.
|
|
622
|
+
picker.holdCameras(true);
|
|
623
|
+
gesturePress = {
|
|
624
|
+
...region,
|
|
625
|
+
button: event.button,
|
|
626
|
+
modifiers: event.modifiers ?? noModifiers,
|
|
627
|
+
...(key === undefined ? {} : { key }),
|
|
628
|
+
};
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/** Moves pointer `id` to `to`: a drag with one pointer pressed, a pinch with two. */
|
|
632
|
+
const moveGesture = (id: number, to: Point, events: Array<SceneEvent>): void => {
|
|
633
|
+
const from = pressedPointers.get(id);
|
|
634
|
+
if (from === undefined || gesturePress === undefined) return;
|
|
635
|
+
pressedPointers.set(id, to);
|
|
636
|
+
const { view, width, height, x, y } = gesturePress;
|
|
637
|
+
const region = { ...(view === undefined ? {} : { view }), width, height, x, y };
|
|
638
|
+
const moved = pixels(from, to);
|
|
639
|
+
const other = [...pressedPointers].find(([pointer]) => pointer !== id)?.[1];
|
|
640
|
+
if (other === undefined) {
|
|
641
|
+
if (id !== primary) return;
|
|
642
|
+
const { button, modifiers, key } = gesturePress;
|
|
643
|
+
events.push({
|
|
644
|
+
_tag: 'Gesture',
|
|
645
|
+
gesture: {
|
|
646
|
+
_tag: 'Drag',
|
|
647
|
+
button,
|
|
648
|
+
modifiers,
|
|
649
|
+
...(key === undefined ? {} : { key }),
|
|
650
|
+
...region,
|
|
651
|
+
deltaX: moved.x,
|
|
652
|
+
deltaY: moved.y,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const before = pixelDistance(from, other);
|
|
658
|
+
const after = pixelDistance(to, other);
|
|
659
|
+
const middle = picker.region((to.x + other.x) / 2, (to.y + other.y) / 2);
|
|
660
|
+
events.push({
|
|
661
|
+
_tag: 'Gesture',
|
|
662
|
+
gesture: {
|
|
663
|
+
_tag: 'Pinch',
|
|
664
|
+
...region,
|
|
665
|
+
x: middle.x,
|
|
666
|
+
y: middle.y,
|
|
667
|
+
scale: before === 0 ? 1 : after / before,
|
|
668
|
+
// The midpoint moves half as far as the one pointer that moved.
|
|
669
|
+
deltaX: moved.x / 2,
|
|
670
|
+
deltaY: moved.y / 2,
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const endGestures = (): void => {
|
|
676
|
+
if (gesturePress !== undefined) picker.holdCameras(false);
|
|
677
|
+
pressedPointers.clear();
|
|
678
|
+
primary = undefined;
|
|
679
|
+
gesturePress = undefined;
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Input from a pointer other than the primary one while that is pressed: it joins,
|
|
684
|
+
* pinches or leaves, and touches nothing else.
|
|
685
|
+
*/
|
|
686
|
+
const secondary = (event: PointerInput, id: number): ReadonlyArray<SceneEvent> => {
|
|
687
|
+
const events: Array<SceneEvent> = [];
|
|
688
|
+
if (event._tag === 'PointerDown' && gesturePress !== undefined) {
|
|
689
|
+
pressedPointers.set(id, { x: event.x, y: event.y });
|
|
690
|
+
} else if (event._tag === 'PointerMove') {
|
|
691
|
+
moveGesture(id, event, events);
|
|
692
|
+
} else if (event._tag === 'PointerUp') {
|
|
693
|
+
pressedPointers.delete(id);
|
|
694
|
+
}
|
|
695
|
+
return events;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const isSecondary = (event: PointerInput, id: number): boolean =>
|
|
699
|
+
primary !== undefined &&
|
|
700
|
+
id !== primary &&
|
|
701
|
+
(event._tag === 'PointerDown' ||
|
|
702
|
+
((event._tag === 'PointerMove' || event._tag === 'PointerUp') && pressedPointers.has(id)));
|
|
703
|
+
|
|
704
|
+
const input = (event: PointerInput): ReadonlyArray<SceneEvent> => {
|
|
705
|
+
const id = pointerOf(event);
|
|
706
|
+
if (isSecondary(event, id)) return secondary(event, id);
|
|
707
|
+
const events: Array<SceneEvent> = [];
|
|
708
|
+
if (event._tag === 'PointerLeave') {
|
|
709
|
+
endGestures();
|
|
710
|
+
// A cancelled or abandoned press still ends its drag, at the last known position.
|
|
711
|
+
if (position !== undefined) endDrag(captured(), position.x, position.y, events);
|
|
712
|
+
dragged = false;
|
|
713
|
+
position = undefined;
|
|
714
|
+
press = undefined;
|
|
715
|
+
pressing = false;
|
|
716
|
+
hover(undefined, events);
|
|
717
|
+
return events;
|
|
718
|
+
}
|
|
719
|
+
const drag = captured();
|
|
720
|
+
position = { x: event.x, y: event.y };
|
|
721
|
+
const hit = picker.pick(event.x, event.y);
|
|
722
|
+
hover(hit, events);
|
|
723
|
+
switch (event._tag) {
|
|
724
|
+
case 'PointerMove':
|
|
725
|
+
if (hit !== undefined) events.push({ _tag: 'PointerMove', ...hit });
|
|
726
|
+
if (drag !== undefined) {
|
|
727
|
+
dragged = true;
|
|
728
|
+
events.push({ _tag: 'Drag', ...drag, ray: picker.ray(event.x, event.y) });
|
|
729
|
+
}
|
|
730
|
+
moveGesture(id, event, events);
|
|
731
|
+
break;
|
|
732
|
+
case 'PointerDown':
|
|
733
|
+
press = { key: hit?.key, button: event.button, x: event.x, y: event.y };
|
|
734
|
+
pressing = true;
|
|
735
|
+
dragged = false;
|
|
736
|
+
if (hit !== undefined) events.push({ _tag: 'PointerDown', button: event.button, ...hit });
|
|
737
|
+
beginGesture(event, id, hit?.key, events);
|
|
738
|
+
break;
|
|
739
|
+
case 'PointerUp':
|
|
740
|
+
release(hit, event, events);
|
|
741
|
+
// Any release ends capture: a mouse's buttons share one pointer.
|
|
742
|
+
endDrag(drag, event.x, event.y, events);
|
|
743
|
+
endGestures();
|
|
744
|
+
break;
|
|
745
|
+
case 'DoubleClick':
|
|
746
|
+
if (hit !== undefined && pressed(hit)) {
|
|
747
|
+
events.push({ _tag: 'DoubleClick', button: event.button, ...hit });
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
case 'ContextMenu':
|
|
751
|
+
if (hit !== undefined && pressed(hit)) events.push({ _tag: 'ContextMenu', ...hit });
|
|
752
|
+
break;
|
|
753
|
+
case 'Wheel':
|
|
754
|
+
if (hit !== undefined) {
|
|
755
|
+
events.push({ _tag: 'Wheel', deltaX: event.deltaX, deltaY: event.deltaY, ...hit });
|
|
756
|
+
}
|
|
757
|
+
if (picker.gestures() === 'scene') {
|
|
758
|
+
const region = picker.region(event.x, event.y);
|
|
759
|
+
// The wheel does not hold turning still, as `OrbitControls` keeps turning on it.
|
|
760
|
+
reportTurned(region, events);
|
|
761
|
+
events.push({
|
|
762
|
+
_tag: 'Gesture',
|
|
763
|
+
gesture: {
|
|
764
|
+
_tag: 'Wheel',
|
|
765
|
+
modifiers: event.modifiers ?? noModifiers,
|
|
766
|
+
...region,
|
|
767
|
+
deltaX: event.deltaX,
|
|
768
|
+
deltaY: event.deltaY,
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
return events;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const frame = (): ReadonlyArray<SceneEvent> => {
|
|
778
|
+
if (position === undefined) return none;
|
|
779
|
+
const events: Array<SceneEvent> = [];
|
|
780
|
+
hover(picker.pick(position.x, position.y), events);
|
|
781
|
+
return events.length === 0 ? none : events;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
return { input, frame };
|
|
785
|
+
};
|