@aura3d/input 1.4.0 → 1.4.1
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/package.json +36 -4
- package/src/ActionMap.ts +0 -94
- package/src/GamepadDevice.ts +0 -48
- package/src/GamepadInput.ts +0 -2
- package/src/GestureControls.ts +0 -2
- package/src/GestureHapticsFixtures.ts +0 -179
- package/src/GestureRecognizer.ts +0 -43
- package/src/InputActionBindingFixtures.ts +0 -121
- package/src/InputReplay.ts +0 -275
- package/src/InputSnapshot.ts +0 -92
- package/src/InputSystem.ts +0 -92
- package/src/InteractionSystem.ts +0 -119
- package/src/KeyboardDevice.ts +0 -32
- package/src/PickingRay.ts +0 -49
- package/src/PointerDevice.ts +0 -102
- package/src/VirtualTouchControls.ts +0 -169
- package/src/WebXRSessionController.ts +0 -248
- package/src/XRFixtures.ts +0 -203
- package/src/controls/CameraRig.ts +0 -38
- package/src/controls/ControlTypes.ts +0 -21
- package/src/controls/EditorFlyControls.ts +0 -55
- package/src/controls/FirstPersonControls.ts +0 -60
- package/src/controls/OrbitControls.ts +0 -168
- package/src/controls/PointerLockControls.ts +0 -24
- package/src/controls/SceneCameraAdapter.ts +0 -100
- package/src/controls/ThirdPersonFollowControls.ts +0 -42
- package/src/index.ts +0 -72
- package/tests/input.test.ts +0 -69
- package/tsconfig.json +0 -7
package/src/PointerDevice.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import type { PointerTouch } from "./InputSnapshot";
|
|
2
|
-
|
|
3
|
-
export interface PointerEventLike {
|
|
4
|
-
readonly clientX: number;
|
|
5
|
-
readonly clientY: number;
|
|
6
|
-
readonly button?: number;
|
|
7
|
-
readonly buttons?: number;
|
|
8
|
-
readonly pointerId?: number;
|
|
9
|
-
preventDefault?(): void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface WheelEventLike {
|
|
13
|
-
readonly deltaX: number;
|
|
14
|
-
readonly deltaY: number;
|
|
15
|
-
preventDefault?(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class PointerDevice {
|
|
19
|
-
x = 0;
|
|
20
|
-
y = 0;
|
|
21
|
-
deltaX = 0;
|
|
22
|
-
deltaY = 0;
|
|
23
|
-
wheelX = 0;
|
|
24
|
-
wheelY = 0;
|
|
25
|
-
|
|
26
|
-
private readonly buttons = new Set<number>();
|
|
27
|
-
private previousButtons = new Set<number>();
|
|
28
|
-
private readonly touches = new Map<number, PointerTouch>();
|
|
29
|
-
private pixelRatio = 1;
|
|
30
|
-
|
|
31
|
-
setDevicePixelRatio(value: number): void {
|
|
32
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
33
|
-
throw new Error("Device pixel ratio must be positive");
|
|
34
|
-
}
|
|
35
|
-
this.pixelRatio = value;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
move(event: PointerEventLike): void {
|
|
39
|
-
const nextX = event.clientX * this.pixelRatio;
|
|
40
|
-
const nextY = event.clientY * this.pixelRatio;
|
|
41
|
-
this.deltaX += nextX - this.x;
|
|
42
|
-
this.deltaY += nextY - this.y;
|
|
43
|
-
this.x = nextX;
|
|
44
|
-
this.y = nextY;
|
|
45
|
-
|
|
46
|
-
if (event.pointerId !== undefined) {
|
|
47
|
-
this.touches.set(event.pointerId, { id: event.pointerId, x: this.x, y: this.y });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
down(event: PointerEventLike): void {
|
|
52
|
-
this.move(event);
|
|
53
|
-
this.buttons.add(event.button ?? 0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
up(event: PointerEventLike): void {
|
|
57
|
-
this.move(event);
|
|
58
|
-
this.buttons.delete(event.button ?? 0);
|
|
59
|
-
if (event.pointerId !== undefined) {
|
|
60
|
-
this.touches.delete(event.pointerId);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
wheel(event: WheelEventLike): void {
|
|
65
|
-
this.wheelX += event.deltaX;
|
|
66
|
-
this.wheelY += event.deltaY;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
blur(): void {
|
|
70
|
-
this.buttons.clear();
|
|
71
|
-
this.touches.clear();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
snapshotData() {
|
|
75
|
-
const buttonMap = new Map<number, { down: boolean; pressed: boolean; released: boolean }>();
|
|
76
|
-
for (const button of this.buttons) {
|
|
77
|
-
buttonMap.set(button, { down: true, pressed: false, released: false });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
pointer: {
|
|
82
|
-
x: this.x,
|
|
83
|
-
y: this.y,
|
|
84
|
-
deltaX: this.deltaX,
|
|
85
|
-
deltaY: this.deltaY,
|
|
86
|
-
wheelX: this.wheelX,
|
|
87
|
-
wheelY: this.wheelY,
|
|
88
|
-
buttons: buttonMap,
|
|
89
|
-
touches: [...this.touches.values()]
|
|
90
|
-
},
|
|
91
|
-
previousPointerButtons: new Set(this.previousButtons)
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
endFrame(): void {
|
|
96
|
-
this.previousButtons = new Set(this.buttons);
|
|
97
|
-
this.deltaX = 0;
|
|
98
|
-
this.deltaY = 0;
|
|
99
|
-
this.wheelX = 0;
|
|
100
|
-
this.wheelY = 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
export interface VirtualJoystickConfig {
|
|
2
|
-
readonly center: readonly [number, number];
|
|
3
|
-
readonly radius: number;
|
|
4
|
-
readonly deadZone?: number;
|
|
5
|
-
readonly maxDistance?: number;
|
|
6
|
-
readonly fixed?: boolean;
|
|
7
|
-
readonly returnToCenter?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface VirtualTouchPoint {
|
|
11
|
-
readonly id: number;
|
|
12
|
-
readonly x: number;
|
|
13
|
-
readonly y: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface VirtualTouchJoystickSnapshot {
|
|
17
|
-
readonly source: "origin-master-touch-joystick-virtual-input-adapted";
|
|
18
|
-
readonly active: boolean;
|
|
19
|
-
readonly activeTouchId: number | null;
|
|
20
|
-
readonly center: readonly [number, number];
|
|
21
|
-
readonly stick: readonly [number, number];
|
|
22
|
-
readonly value: readonly [number, number];
|
|
23
|
-
readonly magnitude: number;
|
|
24
|
-
readonly deadZone: number;
|
|
25
|
-
readonly consumedTouches: number;
|
|
26
|
-
readonly evidence: {
|
|
27
|
-
readonly oldCodebasePort: true;
|
|
28
|
-
readonly virtualJoystick: boolean;
|
|
29
|
-
readonly deadZone: boolean;
|
|
30
|
-
readonly clampedMagnitude: boolean;
|
|
31
|
-
readonly returnToCenter: boolean;
|
|
32
|
-
readonly floatingCenter: boolean;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class VirtualTouchJoystick {
|
|
37
|
-
private readonly initialCenter: readonly [number, number];
|
|
38
|
-
private readonly radius: number;
|
|
39
|
-
private readonly deadZone: number;
|
|
40
|
-
private readonly maxDistance: number;
|
|
41
|
-
private readonly fixed: boolean;
|
|
42
|
-
private readonly returnToCenter: boolean;
|
|
43
|
-
private centerRef: readonly [number, number];
|
|
44
|
-
private stickRef: readonly [number, number];
|
|
45
|
-
private valueRef: readonly [number, number] = [0, 0];
|
|
46
|
-
private activeTouchId: number | null = null;
|
|
47
|
-
private active = false;
|
|
48
|
-
private consumedTouches = 0;
|
|
49
|
-
|
|
50
|
-
constructor(config: VirtualJoystickConfig) {
|
|
51
|
-
this.initialCenter = config.center;
|
|
52
|
-
this.centerRef = config.center;
|
|
53
|
-
this.stickRef = config.center;
|
|
54
|
-
this.radius = positive(config.radius, 64);
|
|
55
|
-
this.deadZone = clamp(config.deadZone ?? 0.15, 0, 0.95);
|
|
56
|
-
this.maxDistance = positive(config.maxDistance, this.radius * 0.8);
|
|
57
|
-
this.fixed = config.fixed ?? true;
|
|
58
|
-
this.returnToCenter = config.returnToCenter ?? true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
touchStart(touch: VirtualTouchPoint): boolean {
|
|
62
|
-
if (this.activeTouchId !== null) return false;
|
|
63
|
-
if (distance([touch.x, touch.y], this.centerRef) > this.radius) return false;
|
|
64
|
-
this.activeTouchId = touch.id;
|
|
65
|
-
this.active = true;
|
|
66
|
-
this.consumedTouches += 1;
|
|
67
|
-
if (!this.fixed) this.centerRef = [touch.x, touch.y];
|
|
68
|
-
this.updateStick([touch.x, touch.y]);
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
touchMove(touch: VirtualTouchPoint): boolean {
|
|
73
|
-
if (touch.id !== this.activeTouchId) return false;
|
|
74
|
-
this.updateStick([touch.x, touch.y]);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
touchEnd(touch: VirtualTouchPoint): boolean {
|
|
79
|
-
if (touch.id !== this.activeTouchId) return false;
|
|
80
|
-
this.activeTouchId = null;
|
|
81
|
-
this.active = false;
|
|
82
|
-
if (this.returnToCenter) {
|
|
83
|
-
this.stickRef = this.centerRef;
|
|
84
|
-
this.valueRef = [0, 0];
|
|
85
|
-
}
|
|
86
|
-
if (!this.fixed) this.centerRef = this.initialCenter;
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
snapshot(): VirtualTouchJoystickSnapshot {
|
|
91
|
-
return {
|
|
92
|
-
source: "origin-master-touch-joystick-virtual-input-adapted",
|
|
93
|
-
active: this.active,
|
|
94
|
-
activeTouchId: this.activeTouchId,
|
|
95
|
-
center: [round(this.centerRef[0]), round(this.centerRef[1])],
|
|
96
|
-
stick: [round(this.stickRef[0]), round(this.stickRef[1])],
|
|
97
|
-
value: [round(this.valueRef[0]), round(this.valueRef[1])],
|
|
98
|
-
magnitude: round(Math.hypot(this.valueRef[0], this.valueRef[1])),
|
|
99
|
-
deadZone: this.deadZone,
|
|
100
|
-
consumedTouches: this.consumedTouches,
|
|
101
|
-
evidence: {
|
|
102
|
-
oldCodebasePort: true,
|
|
103
|
-
virtualJoystick: true,
|
|
104
|
-
deadZone: true,
|
|
105
|
-
clampedMagnitude: Math.hypot(this.valueRef[0], this.valueRef[1]) <= 1.0001,
|
|
106
|
-
returnToCenter: this.returnToCenter,
|
|
107
|
-
floatingCenter: !this.fixed
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private updateStick(touch: readonly [number, number]): void {
|
|
113
|
-
const dx = touch[0] - this.centerRef[0];
|
|
114
|
-
const dy = touch[1] - this.centerRef[1];
|
|
115
|
-
const rawDistance = Math.hypot(dx, dy);
|
|
116
|
-
if (rawDistance === 0) {
|
|
117
|
-
this.stickRef = this.centerRef;
|
|
118
|
-
this.valueRef = [0, 0];
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const clampedDistance = Math.min(rawDistance, this.maxDistance);
|
|
122
|
-
const nx = dx / rawDistance;
|
|
123
|
-
const ny = dy / rawDistance;
|
|
124
|
-
this.stickRef = [this.centerRef[0] + nx * clampedDistance, this.centerRef[1] + ny * clampedDistance];
|
|
125
|
-
const normalized = clampedDistance / this.maxDistance;
|
|
126
|
-
if (normalized < this.deadZone) {
|
|
127
|
-
this.valueRef = [0, 0];
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const remapped = Math.min(1, (normalized - this.deadZone) / (1 - this.deadZone));
|
|
131
|
-
this.valueRef = [nx * remapped, ny * remapped];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function sampleVirtualTouchJoystickFixture(): {
|
|
136
|
-
readonly active: VirtualTouchJoystickSnapshot;
|
|
137
|
-
readonly released: VirtualTouchJoystickSnapshot;
|
|
138
|
-
} {
|
|
139
|
-
const joystick = new VirtualTouchJoystick({
|
|
140
|
-
center: [96, 420],
|
|
141
|
-
radius: 72,
|
|
142
|
-
deadZone: 0.2,
|
|
143
|
-
maxDistance: 56,
|
|
144
|
-
fixed: false,
|
|
145
|
-
returnToCenter: true
|
|
146
|
-
});
|
|
147
|
-
joystick.touchStart({ id: 7, x: 90, y: 416 });
|
|
148
|
-
joystick.touchMove({ id: 7, x: 154, y: 376 });
|
|
149
|
-
const active = joystick.snapshot();
|
|
150
|
-
joystick.touchEnd({ id: 7, x: 154, y: 376 });
|
|
151
|
-
return { active, released: joystick.snapshot() };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function positive(value: unknown, fallback: number): number {
|
|
155
|
-
const number = Number(value);
|
|
156
|
-
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function distance(a: readonly [number, number], b: readonly [number, number]): number {
|
|
160
|
-
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function clamp(value: number, min: number, max: number): number {
|
|
164
|
-
return Math.max(min, Math.min(max, value));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function round(value: number): number {
|
|
168
|
-
return Number(value.toFixed(4));
|
|
169
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
export type A3DXRSessionMode = "inline" | "immersive-vr" | "immersive-ar";
|
|
2
|
-
export type A3DXRReferenceSpaceType = "viewer" | "local" | "local-floor" | "bounded-floor" | "unbounded";
|
|
3
|
-
export type A3DXRHandedness = "none" | "left" | "right";
|
|
4
|
-
|
|
5
|
-
export interface A3DXRSystemLike {
|
|
6
|
-
isSessionSupported?(mode: A3DXRSessionMode): Promise<boolean>;
|
|
7
|
-
requestSession(mode: A3DXRSessionMode, options?: A3DXRSessionInit): Promise<A3DXRSessionLike>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface A3DXRSessionInit {
|
|
11
|
-
readonly requiredFeatures?: readonly string[];
|
|
12
|
-
readonly optionalFeatures?: readonly string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface A3DXRSessionLike {
|
|
16
|
-
readonly inputSources?: readonly A3DXRInputSourceLike[];
|
|
17
|
-
requestReferenceSpace?(type: A3DXRReferenceSpaceType): Promise<A3DXRReferenceSpaceLike>;
|
|
18
|
-
requestAnimationFrame?(callback: (time: number, frame: A3DXRFrameLike) => void): number;
|
|
19
|
-
end?(): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface A3DXRInputSourceLike {
|
|
23
|
-
readonly handedness?: A3DXRHandedness;
|
|
24
|
-
readonly targetRayMode?: "gaze" | "tracked-pointer" | "screen";
|
|
25
|
-
readonly profiles?: readonly string[];
|
|
26
|
-
readonly targetRaySpace?: unknown;
|
|
27
|
-
readonly gripSpace?: unknown;
|
|
28
|
-
readonly gamepad?: {
|
|
29
|
-
readonly buttons?: readonly { readonly pressed?: boolean; readonly touched?: boolean; readonly value?: number }[];
|
|
30
|
-
readonly axes?: readonly number[];
|
|
31
|
-
readonly hapticActuators?: readonly { pulse?(intensity: number, duration: number): Promise<boolean> | boolean }[];
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface A3DXRFrameLike {
|
|
36
|
-
getHitTestResults?(source: unknown): readonly A3DXRHitTestResultLike[];
|
|
37
|
-
getPose?(space: unknown, baseSpace: A3DXRReferenceSpaceLike): A3DXRPoseLike | undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface A3DXRReferenceSpaceLike {
|
|
41
|
-
readonly type?: A3DXRReferenceSpaceType;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface A3DXRHitTestResultLike {
|
|
45
|
-
readonly position?: readonly [number, number, number];
|
|
46
|
-
readonly normal?: readonly [number, number, number];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface A3DXRPoseLike {
|
|
50
|
-
readonly transform?: {
|
|
51
|
-
readonly matrix?: readonly number[];
|
|
52
|
-
readonly position?: readonly [number, number, number];
|
|
53
|
-
readonly orientation?: readonly [number, number, number, number];
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface WebXRSessionControllerOptions {
|
|
58
|
-
readonly xr?: A3DXRSystemLike;
|
|
59
|
-
readonly mode?: A3DXRSessionMode;
|
|
60
|
-
readonly requiredFeatures?: readonly string[];
|
|
61
|
-
readonly optionalFeatures?: readonly string[];
|
|
62
|
-
readonly referenceSpace?: A3DXRReferenceSpaceType;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface WebXRControllerSample {
|
|
66
|
-
readonly handedness: A3DXRHandedness;
|
|
67
|
-
readonly targetRayMode: "gaze" | "tracked-pointer" | "screen";
|
|
68
|
-
readonly profiles: readonly string[];
|
|
69
|
-
readonly triggerPressed: boolean;
|
|
70
|
-
readonly squeezePressed: boolean;
|
|
71
|
-
readonly primaryValue: number;
|
|
72
|
-
readonly axes: readonly number[];
|
|
73
|
-
readonly targetRayMatrix: readonly number[] | null;
|
|
74
|
-
readonly gripMatrix: readonly number[] | null;
|
|
75
|
-
readonly hapticActuatorCount: number;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface WebXRHitTestSample {
|
|
79
|
-
readonly position: readonly [number, number, number];
|
|
80
|
-
readonly normal: readonly [number, number, number];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface WebXRFrameSample {
|
|
84
|
-
readonly mode: A3DXRSessionMode;
|
|
85
|
-
readonly referenceSpace: A3DXRReferenceSpaceType;
|
|
86
|
-
readonly active: boolean;
|
|
87
|
-
readonly controllerCount: number;
|
|
88
|
-
readonly controllers: readonly WebXRControllerSample[];
|
|
89
|
-
readonly hitTestCount: number;
|
|
90
|
-
readonly hitTests: readonly WebXRHitTestSample[];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface WebXRSessionStartResult {
|
|
94
|
-
readonly mode: A3DXRSessionMode;
|
|
95
|
-
readonly referenceSpace: A3DXRReferenceSpaceType;
|
|
96
|
-
readonly supported: boolean;
|
|
97
|
-
readonly started: boolean;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export class WebXRSessionController {
|
|
101
|
-
readonly mode: A3DXRSessionMode;
|
|
102
|
-
readonly requiredFeatures: readonly string[];
|
|
103
|
-
readonly optionalFeatures: readonly string[];
|
|
104
|
-
readonly referenceSpaceType: A3DXRReferenceSpaceType;
|
|
105
|
-
|
|
106
|
-
private readonly xr?: A3DXRSystemLike;
|
|
107
|
-
private session: A3DXRSessionLike | null = null;
|
|
108
|
-
private referenceSpace: A3DXRReferenceSpaceLike | null = null;
|
|
109
|
-
|
|
110
|
-
constructor(options: WebXRSessionControllerOptions = {}) {
|
|
111
|
-
this.xr = options.xr ?? readNavigatorXR();
|
|
112
|
-
this.mode = options.mode ?? "immersive-vr";
|
|
113
|
-
this.requiredFeatures = options.requiredFeatures ?? [];
|
|
114
|
-
this.optionalFeatures = options.optionalFeatures ?? [];
|
|
115
|
-
this.referenceSpaceType = options.referenceSpace ?? (this.mode === "immersive-ar" ? "viewer" : "local-floor");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
get active(): boolean {
|
|
119
|
-
return this.session !== null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async isSupported(): Promise<boolean> {
|
|
123
|
-
if (!this.xr) return false;
|
|
124
|
-
if (!this.xr.isSessionSupported) return true;
|
|
125
|
-
return this.xr.isSessionSupported(this.mode);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async start(): Promise<WebXRSessionStartResult> {
|
|
129
|
-
if (!this.xr) {
|
|
130
|
-
return {
|
|
131
|
-
mode: this.mode,
|
|
132
|
-
referenceSpace: this.referenceSpaceType,
|
|
133
|
-
supported: false,
|
|
134
|
-
started: false
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
const supported = await this.isSupported();
|
|
138
|
-
if (!supported) {
|
|
139
|
-
return {
|
|
140
|
-
mode: this.mode,
|
|
141
|
-
referenceSpace: this.referenceSpaceType,
|
|
142
|
-
supported: false,
|
|
143
|
-
started: false
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
this.session = await this.xr.requestSession(this.mode, {
|
|
147
|
-
requiredFeatures: this.requiredFeatures,
|
|
148
|
-
optionalFeatures: this.optionalFeatures
|
|
149
|
-
});
|
|
150
|
-
this.referenceSpace = await this.session.requestReferenceSpace?.(this.referenceSpaceType) ?? { type: this.referenceSpaceType };
|
|
151
|
-
return {
|
|
152
|
-
mode: this.mode,
|
|
153
|
-
referenceSpace: this.referenceSpace?.type ?? this.referenceSpaceType,
|
|
154
|
-
supported: true,
|
|
155
|
-
started: true
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
sampleFrame(frame?: A3DXRFrameLike, hitTestSource?: unknown): WebXRFrameSample {
|
|
160
|
-
const referenceSpace = this.referenceSpace ?? { type: this.referenceSpaceType };
|
|
161
|
-
const controllers = Array.from(this.session?.inputSources ?? []).map((source) => sampleInputSource(source, frame, referenceSpace));
|
|
162
|
-
const hitTests = frame?.getHitTestResults && hitTestSource
|
|
163
|
-
? frame.getHitTestResults(hitTestSource).map(sampleHitTest)
|
|
164
|
-
: [];
|
|
165
|
-
return {
|
|
166
|
-
mode: this.mode,
|
|
167
|
-
referenceSpace: this.referenceSpace?.type ?? this.referenceSpaceType,
|
|
168
|
-
active: this.active,
|
|
169
|
-
controllerCount: controllers.length,
|
|
170
|
-
controllers,
|
|
171
|
-
hitTestCount: hitTests.length,
|
|
172
|
-
hitTests
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async end(): Promise<void> {
|
|
177
|
-
const session = this.session;
|
|
178
|
-
this.session = null;
|
|
179
|
-
this.referenceSpace = null;
|
|
180
|
-
await session?.end?.();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async pulseHaptics(intensity = 0.5, duration = 35): Promise<number> {
|
|
184
|
-
if (!Number.isFinite(intensity) || intensity < 0 || intensity > 1) throw new RangeError("WebXR haptic intensity must be in [0, 1].");
|
|
185
|
-
if (!Number.isFinite(duration) || duration < 0) throw new RangeError("WebXR haptic duration must be finite and non-negative.");
|
|
186
|
-
let pulses = 0;
|
|
187
|
-
for (const source of this.session?.inputSources ?? []) {
|
|
188
|
-
for (const actuator of source.gamepad?.hapticActuators ?? []) {
|
|
189
|
-
const accepted = await actuator.pulse?.(intensity, duration);
|
|
190
|
-
if (accepted !== false) pulses += 1;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return pulses;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function sampleInputSource(source: A3DXRInputSourceLike, frame: A3DXRFrameLike | undefined, referenceSpace: A3DXRReferenceSpaceLike): WebXRControllerSample {
|
|
198
|
-
const buttons = source.gamepad?.buttons ?? [];
|
|
199
|
-
const axes = source.gamepad?.axes ?? [];
|
|
200
|
-
const trigger = buttons[0];
|
|
201
|
-
const squeeze = buttons[1];
|
|
202
|
-
return {
|
|
203
|
-
handedness: source.handedness ?? "none",
|
|
204
|
-
targetRayMode: source.targetRayMode ?? "tracked-pointer",
|
|
205
|
-
profiles: source.profiles ?? [],
|
|
206
|
-
triggerPressed: Boolean(trigger?.pressed),
|
|
207
|
-
squeezePressed: Boolean(squeeze?.pressed),
|
|
208
|
-
primaryValue: finiteOrZero(trigger?.value),
|
|
209
|
-
axes: axes.map(finiteOrZero),
|
|
210
|
-
targetRayMatrix: samplePoseMatrix(frame, source.targetRaySpace, referenceSpace),
|
|
211
|
-
gripMatrix: samplePoseMatrix(frame, source.gripSpace, referenceSpace),
|
|
212
|
-
hapticActuatorCount: source.gamepad?.hapticActuators?.length ?? 0
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function samplePoseMatrix(frame: A3DXRFrameLike | undefined, space: unknown, referenceSpace: A3DXRReferenceSpaceLike): readonly number[] | null {
|
|
217
|
-
if (!frame?.getPose || !space) return null;
|
|
218
|
-
return sanitizeMatrix4(frame.getPose(space, referenceSpace)?.transform?.matrix);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function sampleHitTest(result: A3DXRHitTestResultLike): WebXRHitTestSample {
|
|
222
|
-
return {
|
|
223
|
-
position: sanitizeVec3(result.position, [0, 0, 0]),
|
|
224
|
-
normal: sanitizeVec3(result.normal, [0, 1, 0])
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function sanitizeVec3(value: readonly number[] | undefined, fallback: readonly [number, number, number]): readonly [number, number, number] {
|
|
229
|
-
if (!value || value.length < 3) return fallback;
|
|
230
|
-
const x = finiteOrZero(value[0]);
|
|
231
|
-
const y = finiteOrZero(value[1]);
|
|
232
|
-
const z = finiteOrZero(value[2]);
|
|
233
|
-
return [x, y, z];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function finiteOrZero(value: unknown): number {
|
|
237
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function sanitizeMatrix4(value: readonly number[] | undefined): readonly number[] | null {
|
|
241
|
-
if (!value || value.length < 16) return null;
|
|
242
|
-
const matrix = Array.from(value.slice(0, 16), finiteOrZero);
|
|
243
|
-
return matrix;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function readNavigatorXR(): A3DXRSystemLike | undefined {
|
|
247
|
-
return (globalThis.navigator as Navigator & { xr?: A3DXRSystemLike } | undefined)?.xr;
|
|
248
|
-
}
|