@opsydyn/effect-three-foldkit 0.0.0-stage → 0.1.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 +56 -0
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/assets.d.ts +53 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/dom/animationFrames.d.ts +12 -0
- package/dist/dom/animationFrames.d.ts.map +1 -0
- package/dist/dom/canvas.d.ts +26 -0
- package/dist/dom/canvas.d.ts.map +1 -0
- package/dist/dom/latch.d.ts +15 -0
- package/dist/dom/latch.d.ts.map +1 -0
- package/dist/dom/models.d.ts +10 -0
- package/dist/dom/models.d.ts.map +1 -0
- package/dist/dom/overlays.d.ts +24 -0
- package/dist/dom/overlays.d.ts.map +1 -0
- package/dist/dom/pointer.d.ts +49 -0
- package/dist/dom/pointer.d.ts.map +1 -0
- package/dist/dom/postprocessing.d.ts +22 -0
- package/dist/dom/postprocessing.d.ts.map +1 -0
- package/dist/dom/property.d.ts +15 -0
- package/dist/dom/property.d.ts.map +1 -0
- package/dist/dom/reconciled.d.ts +10 -0
- package/dist/dom/reconciled.d.ts.map +1 -0
- package/dist/dom/renderSettings.d.ts +58 -0
- package/dist/dom/renderSettings.d.ts.map +1 -0
- package/dist/dom/typedScenes.d.ts +14 -0
- package/dist/dom/typedScenes.d.ts.map +1 -0
- package/dist/dom/updates.d.ts +8 -0
- package/dist/dom/updates.d.ts.map +1 -0
- package/dist/dom/viewport.d.ts +40 -0
- package/dist/dom/viewport.d.ts.map +1 -0
- package/dist/dom/webgl.d.ts +12 -0
- package/dist/dom/webgl.d.ts.map +1 -0
- package/dist/dom/webgpu.d.ts +23 -0
- package/dist/dom/webgpu.d.ts.map +1 -0
- package/dist/dom/webgpuPostprocessing.d.ts +4 -0
- package/dist/dom/webgpuPostprocessing.d.ts.map +1 -0
- package/dist/dom/xr.d.ts +53 -0
- package/dist/dom/xr.d.ts.map +1 -0
- package/dist/dom/xrGlide.d.ts +49 -0
- package/dist/dom/xrGlide.d.ts.map +1 -0
- package/dist/dom/xrHub.d.ts +45 -0
- package/dist/dom/xrHub.d.ts.map +1 -0
- package/dist/dom/xrInputs.d.ts +40 -0
- package/dist/dom/xrInputs.d.ts.map +1 -0
- package/dist/dom/xrSpaces.d.ts +33 -0
- package/dist/dom/xrSpaces.d.ts.map +1 -0
- package/dist/dom/xrTarget.d.ts +36 -0
- package/dist/dom/xrTarget.d.ts.map +1 -0
- package/dist/element.d.ts +771 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/environment.d.ts +64 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2037 -0
- package/dist/index.js.map +38 -0
- package/dist/mount.d.ts +14 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/overlayView.d.ts +23 -0
- package/dist/overlayView.d.ts.map +1 -0
- package/dist/view.d.ts +179 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/xr.d.ts +34 -0
- package/dist/xr.d.ts.map +1 -0
- package/package.json +72 -4
- package/src/assets.ts +151 -0
- package/src/dom/animationFrames.ts +32 -0
- package/src/dom/canvas.ts +54 -0
- package/src/dom/latch.ts +36 -0
- package/src/dom/models.ts +37 -0
- package/src/dom/overlays.ts +63 -0
- package/src/dom/pointer.ts +163 -0
- package/src/dom/postprocessing.ts +116 -0
- package/src/dom/property.ts +43 -0
- package/src/dom/reconciled.ts +51 -0
- package/src/dom/renderSettings.ts +117 -0
- package/src/dom/typedScenes.ts +21 -0
- package/src/dom/updates.ts +14 -0
- package/src/dom/viewport.ts +97 -0
- package/src/dom/webgl.ts +216 -0
- package/src/dom/webgpu.ts +204 -0
- package/src/dom/webgpuPostprocessing.ts +95 -0
- package/src/dom/xr.ts +211 -0
- package/src/dom/xrGlide.ts +114 -0
- package/src/dom/xrHub.ts +118 -0
- package/src/dom/xrInputs.ts +309 -0
- package/src/dom/xrSpaces.ts +287 -0
- package/src/dom/xrTarget.ts +71 -0
- package/src/element.ts +114 -0
- package/src/environment.ts +78 -0
- package/src/index.ts +71 -0
- package/src/mount.ts +305 -0
- package/src/overlayView.ts +71 -0
- package/src/view.ts +254 -0
- package/src/xr.ts +36 -0
package/src/dom/xr.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presenting a scene in a WebXR session through three.js's `WebXRManager`.
|
|
3
|
+
*
|
|
4
|
+
* Behavioural references: React Three Fiber `core/configuration.ts` at `d74622e` (on
|
|
5
|
+
* `sessionstart` enable XR and hand frames to `xr.setAnimationLoop`; on `sessionend` take
|
|
6
|
+
* them back and draw again), and pmndrs/xr at `0e1acd8` (`store.ts` `setupXRManager`:
|
|
7
|
+
* reference space type and framebuffer scale before `setSession`; `init.ts`
|
|
8
|
+
* `buildXRSessionInit`: the reference space required, other features optional). The
|
|
9
|
+
* player's origin is an offset reference space, as three.js's teleport example moves
|
|
10
|
+
* the viewer. Controller selects become rays, as `@react-three/xr`'s pointers hit-test
|
|
11
|
+
* along each controller's target ray.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Xr, XrInput } from '@opsydyn/effect-three';
|
|
16
|
+
import { Group, type Object3D } from 'three';
|
|
17
|
+
|
|
18
|
+
import type { PresentableRenderer } from './renderSettings.ts';
|
|
19
|
+
import { floorFacing, type GlidePose, glideFrame, startGlider } from './xrGlide.ts';
|
|
20
|
+
import { rayOf, trackInputs, type XrInputs } from './xrInputs.ts';
|
|
21
|
+
import { type Follow, type LendGeometry, trackSpaces } from './xrSpaces.ts';
|
|
22
|
+
|
|
23
|
+
/** Where an immersive session can come from: `navigator.xr`, or an emulator's. */
|
|
24
|
+
export type XrSystem = Pick<XRSystem, 'requestSession' | 'isSessionSupported'>;
|
|
25
|
+
|
|
26
|
+
/** The immersive session modes. */
|
|
27
|
+
export type XrMode = 'immersive-vr' | 'immersive-ar';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The session request for a scene's settings: its reference space required, other
|
|
31
|
+
* floors, hand tracking and anchors optional, and plane detection, a DOM overlay (on
|
|
32
|
+
* `overlay`, the scene's element) and in AR hit testing when the scene asks for them
|
|
33
|
+
* (pmndrs/xr's `buildXRSessionInit` asks for every feature it may use, optionally, and
|
|
34
|
+
* roots the overlay on the element it is given). Anchors are asked for whether or not
|
|
35
|
+
* the scene has any yet: it may place them mid-session.
|
|
36
|
+
*/
|
|
37
|
+
export const sessionInit = (
|
|
38
|
+
settings: Xr,
|
|
39
|
+
mode: XrMode = 'immersive-vr',
|
|
40
|
+
overlay?: Element,
|
|
41
|
+
): XRSessionInit => ({
|
|
42
|
+
requiredFeatures: [settings.referenceSpace],
|
|
43
|
+
optionalFeatures: [
|
|
44
|
+
...['local-floor', 'bounded-floor', 'hand-tracking', 'anchors'].filter(
|
|
45
|
+
(feature) => feature !== settings.referenceSpace,
|
|
46
|
+
),
|
|
47
|
+
...(settings.planes ? ['plane-detection'] : []),
|
|
48
|
+
...(settings.meshes ? ['mesh-detection'] : []),
|
|
49
|
+
...(settings.domOverlay && overlay !== undefined ? ['dom-overlay'] : []),
|
|
50
|
+
...(mode === 'immersive-ar' && settings.hitTest ? ['hit-test'] : []),
|
|
51
|
+
],
|
|
52
|
+
...(settings.domOverlay && overlay !== undefined ? { domOverlay: { root: overlay } } : {}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The offset that puts the player at `origin` facing `heading`: a reference space whose
|
|
57
|
+
* poses are the device's, rotated by the heading and moved to the origin.
|
|
58
|
+
*/
|
|
59
|
+
const originOffset = (settings: GlidePose): XRRigidTransform => {
|
|
60
|
+
const [x, y, z] = settings.origin;
|
|
61
|
+
const half = -settings.heading / 2;
|
|
62
|
+
const cos = Math.cos(-settings.heading);
|
|
63
|
+
const sin = Math.sin(-settings.heading);
|
|
64
|
+
// The inverse of "rotate by heading, then move to origin": rotate by −heading the point −origin.
|
|
65
|
+
return new XRRigidTransform(
|
|
66
|
+
{ x: -x * cos - z * sin, y: -y, z: x * sin - z * cos },
|
|
67
|
+
{ x: 0, y: Math.sin(half), z: 0, w: Math.cos(half) },
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Forwards each source's select as a ray in the presented space (the scene's, since the
|
|
73
|
+
* reference space is offset to the player's origin); a select ending in AR also reports
|
|
74
|
+
* the surface its ray met. Returns a function that stops.
|
|
75
|
+
*/
|
|
76
|
+
const listenForSelects = (
|
|
77
|
+
session: XRSession,
|
|
78
|
+
space: () => XRReferenceSpace | null,
|
|
79
|
+
inputs: XrInputs,
|
|
80
|
+
input: (input: XrInput) => void,
|
|
81
|
+
): (() => void) => {
|
|
82
|
+
const forward = (tag: 'SelectStart' | 'SelectEnd') => (event: XRInputSourceEvent) => {
|
|
83
|
+
const reference = space();
|
|
84
|
+
const pose =
|
|
85
|
+
reference === null
|
|
86
|
+
? undefined
|
|
87
|
+
: event.frame.getPose(event.inputSource.targetRaySpace, reference);
|
|
88
|
+
if (pose !== undefined && pose !== null) {
|
|
89
|
+
input({ _tag: tag, source: inputs.idOf(event.inputSource), ray: rayOf(pose) });
|
|
90
|
+
}
|
|
91
|
+
if (tag === 'SelectEnd') inputs.selected(event.inputSource);
|
|
92
|
+
};
|
|
93
|
+
const started = forward('SelectStart');
|
|
94
|
+
const ended = forward('SelectEnd');
|
|
95
|
+
session.addEventListener('selectstart', started);
|
|
96
|
+
session.addEventListener('selectend', ended);
|
|
97
|
+
return () => {
|
|
98
|
+
session.removeEventListener('selectstart', started);
|
|
99
|
+
session.removeEventListener('selectend', ended);
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type Presentation = {
|
|
104
|
+
/** Applies new settings mid-session: a new origin or heading moves the player. */
|
|
105
|
+
readonly update: (settings: Xr) => void;
|
|
106
|
+
readonly session: XRSession;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** What a session presents: the scene, and how it is drawn, followed and told of input. */
|
|
110
|
+
export type Presented = {
|
|
111
|
+
readonly scene: Object3D;
|
|
112
|
+
readonly draw: () => void;
|
|
113
|
+
/** Places the groups that follow tracked spaces (`group({ follow })`). */
|
|
114
|
+
readonly follow: Follow;
|
|
115
|
+
/** Lends detected meshes' geometry to the scene (`xrMesh(id)`). */
|
|
116
|
+
readonly lend: LendGeometry;
|
|
117
|
+
readonly input: (input: XrInput) => void;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Presents into `session`: frames come from the session (each read for input and the
|
|
122
|
+
* spaces groups follow, then drawn) until it ends, when `ended` is called and the page's
|
|
123
|
+
* own frames take over again. Input sources are drawn into the scene while it presents.
|
|
124
|
+
*/
|
|
125
|
+
export const present = async (
|
|
126
|
+
renderer: PresentableRenderer,
|
|
127
|
+
session: XRSession,
|
|
128
|
+
settings: Xr,
|
|
129
|
+
presented: Presented,
|
|
130
|
+
ended: () => void,
|
|
131
|
+
): Promise<Presentation> => {
|
|
132
|
+
const { scene, draw, input } = presented;
|
|
133
|
+
const { xr } = renderer;
|
|
134
|
+
xr.enabled = true;
|
|
135
|
+
xr.setReferenceSpaceType(settings.referenceSpace);
|
|
136
|
+
xr.setFramebufferScaleFactor(settings.framebufferScale);
|
|
137
|
+
let inputs: XrInputs | undefined;
|
|
138
|
+
const spaces = trackSpaces(session, () => current, presented.follow, input, presented.lend);
|
|
139
|
+
let current = settings;
|
|
140
|
+
// Gliding (`xr({ glide })`) moves the player from the description's place; the
|
|
141
|
+
// description wins again whenever it changes.
|
|
142
|
+
let glider = startGlider({ origin: settings.origin, heading: settings.heading });
|
|
143
|
+
let lastTime: number | undefined;
|
|
144
|
+
let base: XRReferenceSpace | null = null;
|
|
145
|
+
const place = (pose: GlidePose): void => {
|
|
146
|
+
if (base !== null) xr.setReferenceSpace(base.getOffsetReferenceSpace(originOffset(pose)));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** One frame of gliding, when the description asks for it. */
|
|
150
|
+
const glideStep = (time: number, frame: XRFrame, space: XRReferenceSpace): void => {
|
|
151
|
+
const seconds = Math.min(0.1, Math.max(0, (time - (lastTime ?? time)) / 1000));
|
|
152
|
+
lastTime = time;
|
|
153
|
+
if (current.glide === null || inputs === undefined) return;
|
|
154
|
+
const head = frame.getViewerPose(space);
|
|
155
|
+
const facing = floorFacing(head?.transform.orientation ?? { x: 0, y: 0, z: 0, w: 1 });
|
|
156
|
+
const { left, right } = inputs.sticks();
|
|
157
|
+
const next = glideFrame(glider, current.glide, { move: left, turn: right }, facing, seconds);
|
|
158
|
+
if (next.glider.pose !== glider.pose) place(next.glider.pose);
|
|
159
|
+
glider = next.glider;
|
|
160
|
+
if (next.stopped !== undefined) input({ _tag: 'Moved', ...next.stopped });
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** One session frame: its input read, then drawn. Frames outside the session are skipped. */
|
|
164
|
+
function sessionFrame(time: number, frame?: XRFrame): void {
|
|
165
|
+
if (!xr.isPresenting) return;
|
|
166
|
+
const space = xr.getReferenceSpace();
|
|
167
|
+
if (frame !== undefined && space !== null) {
|
|
168
|
+
inputs?.frame(frame, space);
|
|
169
|
+
glideStep(time, frame, space);
|
|
170
|
+
// After gliding, so the spaces are where the player now stands.
|
|
171
|
+
spaces.frame(frame, xr.getReferenceSpace() ?? space);
|
|
172
|
+
}
|
|
173
|
+
draw();
|
|
174
|
+
}
|
|
175
|
+
// Set before the session starts (WebGPU's XR manager takes the loop then).
|
|
176
|
+
renderer.setXrLoop(sessionFrame);
|
|
177
|
+
await xr.setSession(session);
|
|
178
|
+
base = xr.getReferenceSpace();
|
|
179
|
+
place(glider.pose);
|
|
180
|
+
const shown = new Group();
|
|
181
|
+
shown.name = 'xr-inputs';
|
|
182
|
+
scene.add(shown);
|
|
183
|
+
const tracked = trackInputs(session, shown, () => current, input);
|
|
184
|
+
inputs = tracked;
|
|
185
|
+
const stopSelects = listenForSelects(session, () => xr.getReferenceSpace(), tracked, input);
|
|
186
|
+
session.addEventListener(
|
|
187
|
+
'end',
|
|
188
|
+
() => {
|
|
189
|
+
stopSelects();
|
|
190
|
+
tracked.stop();
|
|
191
|
+
spaces.stop();
|
|
192
|
+
shown.removeFromParent();
|
|
193
|
+
renderer.setXrLoop(null);
|
|
194
|
+
xr.enabled = false;
|
|
195
|
+
ended();
|
|
196
|
+
},
|
|
197
|
+
{ once: true },
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
session,
|
|
201
|
+
update: (next) => {
|
|
202
|
+
const moved =
|
|
203
|
+
next.origin.some((value, i) => value !== current.origin[i]) ||
|
|
204
|
+
next.heading !== current.heading;
|
|
205
|
+
current = next;
|
|
206
|
+
if (!moved) return;
|
|
207
|
+
glider = startGlider({ origin: next.origin, heading: next.heading });
|
|
208
|
+
place(glider.pose);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gliding with the thumbsticks while presenting (`xr({ glide })`), one frame at a time.
|
|
3
|
+
*
|
|
4
|
+
* Behavioural reference: pmndrs/xr at `0e1acd8`, `packages/xr/src/controller-locomotion.ts`:
|
|
5
|
+
* the left stick moves the player along the head's facing, flattened onto the floor, at
|
|
6
|
+
* `speed` metres per second at full stick (forward is the stick pushed away, negative y);
|
|
7
|
+
* the right stick turns, smoothly at `turnSpeed`, or by `snapDegrees` once per push past
|
|
8
|
+
* halfway, ready again once back under it. Unlike there, a still stick is a still player
|
|
9
|
+
* (a small dead zone keeps resting thumbsticks from drifting), and the frame that stops
|
|
10
|
+
* says where the player ended, for the Model.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { Vec3, XrGlide } from '@opsydyn/effect-three';
|
|
15
|
+
|
|
16
|
+
/** Where the player stands: the reference space's origin in the scene, and its heading. */
|
|
17
|
+
export type GlidePose = { readonly origin: Vec3; readonly heading: number };
|
|
18
|
+
|
|
19
|
+
/** A thumbstick, `[x, y]`, each −1 to 1 (`y` negative pushed away). */
|
|
20
|
+
export type Stick = readonly [x: number, y: number];
|
|
21
|
+
|
|
22
|
+
/** Gliding's state between frames. */
|
|
23
|
+
export type Glider = {
|
|
24
|
+
readonly pose: GlidePose;
|
|
25
|
+
/** Whether a snap turn is ready (the turning stick has been back under halfway). */
|
|
26
|
+
readonly snapReady: boolean;
|
|
27
|
+
/** Whether the pose moved since the last report. */
|
|
28
|
+
readonly moved: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Thumbsticks rest a little off centre; under this they are still. */
|
|
32
|
+
const deadZone = 0.15;
|
|
33
|
+
/** Past this the turning stick turns (pmndrs/xr's default dead zone, 0.5). */
|
|
34
|
+
const turnZone = 0.5;
|
|
35
|
+
|
|
36
|
+
export const startGlider = (pose: GlidePose): Glider => ({ pose, snapReady: true, moved: false });
|
|
37
|
+
|
|
38
|
+
/** The head's facing on the floor, from its orientation quaternion: −z turned, flattened. */
|
|
39
|
+
export const floorFacing = (q: {
|
|
40
|
+
readonly x: number;
|
|
41
|
+
readonly y: number;
|
|
42
|
+
readonly z: number;
|
|
43
|
+
readonly w: number;
|
|
44
|
+
}): readonly [x: number, z: number] => {
|
|
45
|
+
const x = -2 * (q.x * q.z + q.w * q.y);
|
|
46
|
+
const z = -(1 - 2 * (q.x * q.x + q.y * q.y));
|
|
47
|
+
const length = Math.hypot(x, z);
|
|
48
|
+
return length < 1e-6 ? [0, -1] : [x / length, z / length];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const still = (stick: Stick, zone: number): boolean =>
|
|
52
|
+
Math.abs(stick[0]) < zone && Math.abs(stick[1]) < zone;
|
|
53
|
+
|
|
54
|
+
/** One step of moving: along the head's facing and its right, by the stick. */
|
|
55
|
+
const moveBy = (
|
|
56
|
+
origin: Vec3,
|
|
57
|
+
stick: Stick,
|
|
58
|
+
facing: readonly [number, number],
|
|
59
|
+
distance: number,
|
|
60
|
+
): Vec3 => {
|
|
61
|
+
const [fx, fz] = facing;
|
|
62
|
+
// The right of the facing (fx, fz) on the floor, y up: (−fz, fx).
|
|
63
|
+
const forward = -stick[1] * distance;
|
|
64
|
+
const right = stick[0] * distance;
|
|
65
|
+
return [origin[0] + fx * forward - fz * right, origin[1], origin[2] + fz * forward + fx * right];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** The heading after the turning stick, and whether a snap is ready next frame. */
|
|
69
|
+
const turnBy = (
|
|
70
|
+
glider: Glider,
|
|
71
|
+
glide: XrGlide,
|
|
72
|
+
stick: Stick,
|
|
73
|
+
seconds: number,
|
|
74
|
+
): { readonly heading: number; readonly snapReady: boolean } => {
|
|
75
|
+
const { heading } = glider.pose;
|
|
76
|
+
const pushed = Math.abs(stick[0]) >= turnZone;
|
|
77
|
+
if (glide.turn === 'smooth') {
|
|
78
|
+
return {
|
|
79
|
+
heading: pushed ? heading - stick[0] * glide.turnSpeed * seconds : heading,
|
|
80
|
+
snapReady: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (!pushed) return { heading, snapReady: true };
|
|
84
|
+
if (!glider.snapReady) return { heading, snapReady: false };
|
|
85
|
+
// Pushed right turns right: the heading (about +y) goes down.
|
|
86
|
+
return {
|
|
87
|
+
heading: heading - (Math.sign(stick[0]) * (glide.snapDegrees * Math.PI)) / 180,
|
|
88
|
+
snapReady: false,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One frame of gliding. Returns the next state, and the pose to report when the sticks
|
|
94
|
+
* have just come to rest after moving it.
|
|
95
|
+
*/
|
|
96
|
+
export const glideFrame = (
|
|
97
|
+
glider: Glider,
|
|
98
|
+
glide: XrGlide,
|
|
99
|
+
sticks: { readonly move: Stick; readonly turn: Stick },
|
|
100
|
+
facing: readonly [number, number],
|
|
101
|
+
seconds: number,
|
|
102
|
+
): { readonly glider: Glider; readonly stopped: GlidePose | undefined } => {
|
|
103
|
+
const moving = !still(sticks.move, deadZone);
|
|
104
|
+
const origin = moving
|
|
105
|
+
? moveBy(glider.pose.origin, sticks.move, facing, glide.speed * seconds)
|
|
106
|
+
: glider.pose.origin;
|
|
107
|
+
const { heading, snapReady } = turnBy(glider, glide, sticks.turn, seconds);
|
|
108
|
+
const changed = moving || heading !== glider.pose.heading;
|
|
109
|
+
const resting = !moving && still(sticks.turn, turnZone);
|
|
110
|
+
const moved = glider.moved || changed;
|
|
111
|
+
const pose = { origin, heading };
|
|
112
|
+
if (resting && moved) return { glider: { pose, snapReady, moved: false }, stopped: pose };
|
|
113
|
+
return { glider: { pose, snapReady, moved }, stopped: undefined };
|
|
114
|
+
};
|
package/src/dom/xrHub.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's immersive sessions: which mounted scenes can be presented, and the one
|
|
3
|
+
* session (WebXR allows one at a time) presenting a scene.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { Xr, XrInput } from '@opsydyn/effect-three';
|
|
8
|
+
import type { Object3D } from 'three';
|
|
9
|
+
|
|
10
|
+
import { presentableRendererOf } from './renderSettings.ts';
|
|
11
|
+
import { type Presentation, present, sessionInit, type XrMode, type XrSystem } from './xr.ts';
|
|
12
|
+
import type { Follow, LendGeometry } from './xrSpaces.ts';
|
|
13
|
+
|
|
14
|
+
export type { XrMode } from './xr.ts';
|
|
15
|
+
|
|
16
|
+
/** A mounted scene that can be presented. */
|
|
17
|
+
export type XrTarget = {
|
|
18
|
+
readonly canvas: HTMLCanvasElement;
|
|
19
|
+
/** The scene's current `xr` description; `null` while it does not ask to be presented. */
|
|
20
|
+
readonly settings: () => Xr | null;
|
|
21
|
+
/** Draws one frame now (the session calls it for each of its frames). */
|
|
22
|
+
readonly draw: () => void;
|
|
23
|
+
/** The scene's three.js root, where input sources are drawn while it presents. */
|
|
24
|
+
readonly scene: Object3D;
|
|
25
|
+
/** The scene's element: the DOM overlay's root, when the scene asks for one. */
|
|
26
|
+
readonly element: Element;
|
|
27
|
+
/** Places the groups that follow tracked spaces. */
|
|
28
|
+
readonly follow: Follow;
|
|
29
|
+
/** Lends a geometry to the scene's store, for detected meshes. */
|
|
30
|
+
readonly lend: LendGeometry;
|
|
31
|
+
/** Input from the session: aims, selects, sticks, buttons and surfaces. */
|
|
32
|
+
readonly input: (input: XrInput) => void;
|
|
33
|
+
/** The session began presenting this scene in `mode`, or (with `null`) ended. */
|
|
34
|
+
readonly presenting: (mode: XrMode | null) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type XrHub = {
|
|
38
|
+
/** Makes a scene presentable; returns a function that withdraws it (ending its session). */
|
|
39
|
+
readonly register: (target: XrTarget) => () => void;
|
|
40
|
+
readonly supported: (mode: XrMode) => Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Requests a session and presents the first registered scene that describes `xr`.
|
|
43
|
+
* Call from a user gesture's handling: browsers require one to start a session.
|
|
44
|
+
*/
|
|
45
|
+
readonly enter: (mode: XrMode) => Promise<void>;
|
|
46
|
+
readonly exit: () => Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type Current = { readonly target: XrTarget; readonly presentation: Presentation };
|
|
50
|
+
|
|
51
|
+
/** Creates the hub for a page; `system` is `navigator.xr` when the browser has WebXR. */
|
|
52
|
+
export const createXrHub = (system: () => XrSystem | undefined): XrHub => {
|
|
53
|
+
const targets = new Set<XrTarget>();
|
|
54
|
+
let current: Current | undefined;
|
|
55
|
+
|
|
56
|
+
const presentable = (): { readonly target: XrTarget; readonly settings: Xr } | undefined => {
|
|
57
|
+
for (const target of targets) {
|
|
58
|
+
const settings = target.settings();
|
|
59
|
+
if (settings !== null) return { target, settings };
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const enter = async (mode: XrMode): Promise<void> => {
|
|
65
|
+
if (current !== undefined) return;
|
|
66
|
+
const xr = system();
|
|
67
|
+
if (xr === undefined) throw new Error('WebXR is not available in this browser');
|
|
68
|
+
const chosen = presentable();
|
|
69
|
+
if (chosen === undefined) throw new Error('no mounted scene describes xr');
|
|
70
|
+
const renderer = presentableRendererOf(chosen.target.canvas);
|
|
71
|
+
if (renderer === undefined) throw new Error('the scene is not drawn by a three.js renderer');
|
|
72
|
+
const session = await xr.requestSession(
|
|
73
|
+
mode,
|
|
74
|
+
sessionInit(chosen.settings, mode, chosen.target.element),
|
|
75
|
+
);
|
|
76
|
+
const { target } = chosen;
|
|
77
|
+
// Each frame: follow the description (a new origin moves the player), then draw.
|
|
78
|
+
const draw = (): void => {
|
|
79
|
+
const settings = target.settings();
|
|
80
|
+
if (settings !== null) current?.presentation.update(settings);
|
|
81
|
+
target.draw();
|
|
82
|
+
};
|
|
83
|
+
const ended = (): void => {
|
|
84
|
+
current = undefined;
|
|
85
|
+
target.presenting(null);
|
|
86
|
+
};
|
|
87
|
+
const presentation = await present(
|
|
88
|
+
renderer,
|
|
89
|
+
session,
|
|
90
|
+
chosen.settings,
|
|
91
|
+
{
|
|
92
|
+
scene: target.scene,
|
|
93
|
+
draw,
|
|
94
|
+
follow: target.follow,
|
|
95
|
+
lend: target.lend,
|
|
96
|
+
input: target.input,
|
|
97
|
+
},
|
|
98
|
+
ended,
|
|
99
|
+
);
|
|
100
|
+
current = { target, presentation };
|
|
101
|
+
target.presenting(mode);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
register: (target) => {
|
|
106
|
+
targets.add(target);
|
|
107
|
+
return () => {
|
|
108
|
+
targets.delete(target);
|
|
109
|
+
if (current?.target === target) void current.presentation.session.end();
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
supported: async (mode) => (await system()?.isSessionSupported(mode)) ?? false,
|
|
113
|
+
enter,
|
|
114
|
+
exit: async () => {
|
|
115
|
+
await current?.presentation.session.end();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
};
|