@aura3d/input 1.0.6
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/README.md +17 -0
- package/dist/ActionMap.d.ts +44 -0
- package/dist/ActionMap.d.ts.map +1 -0
- package/dist/ActionMap.js +74 -0
- package/dist/ActionMap.js.map +1 -0
- package/dist/GamepadDevice.d.ts +17 -0
- package/dist/GamepadDevice.d.ts.map +1 -0
- package/dist/GamepadDevice.js +27 -0
- package/dist/GamepadDevice.js.map +1 -0
- package/dist/GamepadInput.d.ts +3 -0
- package/dist/GamepadInput.d.ts.map +1 -0
- package/dist/GamepadInput.js +2 -0
- package/dist/GamepadInput.js.map +1 -0
- package/dist/GestureControls.d.ts +3 -0
- package/dist/GestureControls.d.ts.map +1 -0
- package/dist/GestureControls.js +2 -0
- package/dist/GestureControls.js.map +1 -0
- package/dist/GestureHapticsFixtures.d.ts +57 -0
- package/dist/GestureHapticsFixtures.d.ts.map +1 -0
- package/dist/GestureHapticsFixtures.js +115 -0
- package/dist/GestureHapticsFixtures.js.map +1 -0
- package/dist/GestureRecognizer.d.ts +19 -0
- package/dist/GestureRecognizer.d.ts.map +1 -0
- package/dist/GestureRecognizer.js +32 -0
- package/dist/GestureRecognizer.js.map +1 -0
- package/dist/InputActionBindingFixtures.d.ts +44 -0
- package/dist/InputActionBindingFixtures.d.ts.map +1 -0
- package/dist/InputActionBindingFixtures.js +86 -0
- package/dist/InputActionBindingFixtures.js.map +1 -0
- package/dist/InputReplay.d.ts +97 -0
- package/dist/InputReplay.d.ts.map +1 -0
- package/dist/InputReplay.js +189 -0
- package/dist/InputReplay.js.map +1 -0
- package/dist/InputSnapshot.d.ts +44 -0
- package/dist/InputSnapshot.d.ts.map +1 -0
- package/dist/InputSnapshot.js +44 -0
- package/dist/InputSnapshot.js.map +1 -0
- package/dist/InputSystem.d.ts +24 -0
- package/dist/InputSystem.d.ts.map +1 -0
- package/dist/InputSystem.js +77 -0
- package/dist/InputSystem.js.map +1 -0
- package/dist/InteractionSystem.d.ts +42 -0
- package/dist/InteractionSystem.d.ts.map +1 -0
- package/dist/InteractionSystem.js +80 -0
- package/dist/InteractionSystem.js.map +1 -0
- package/dist/KeyboardDevice.d.ts +18 -0
- package/dist/KeyboardDevice.d.ts.map +1 -0
- package/dist/KeyboardDevice.js +22 -0
- package/dist/KeyboardDevice.js.map +1 -0
- package/dist/PickingRay.d.ts +10 -0
- package/dist/PickingRay.d.ts.map +1 -0
- package/dist/PickingRay.js +27 -0
- package/dist/PickingRay.js.map +1 -0
- package/dist/PointerDevice.d.ts +51 -0
- package/dist/PointerDevice.d.ts.map +1 -0
- package/dist/PointerDevice.js +75 -0
- package/dist/PointerDevice.js.map +1 -0
- package/dist/VirtualTouchControls.d.ts +57 -0
- package/dist/VirtualTouchControls.d.ts.map +1 -0
- package/dist/VirtualTouchControls.js +127 -0
- package/dist/VirtualTouchControls.js.map +1 -0
- package/dist/WebXRSessionController.d.ts +108 -0
- package/dist/WebXRSessionController.d.ts.map +1 -0
- package/dist/WebXRSessionController.js +143 -0
- package/dist/WebXRSessionController.js.map +1 -0
- package/dist/XRFixtures.d.ts +65 -0
- package/dist/XRFixtures.d.ts.map +1 -0
- package/dist/XRFixtures.js +136 -0
- package/dist/XRFixtures.js.map +1 -0
- package/dist/controls/CameraRig.d.ts +12 -0
- package/dist/controls/CameraRig.d.ts.map +1 -0
- package/dist/controls/CameraRig.js +32 -0
- package/dist/controls/CameraRig.js.map +1 -0
- package/dist/controls/ControlTypes.d.ts +17 -0
- package/dist/controls/ControlTypes.d.ts.map +1 -0
- package/dist/controls/ControlTypes.js +4 -0
- package/dist/controls/ControlTypes.js.map +1 -0
- package/dist/controls/EditorFlyControls.d.ts +20 -0
- package/dist/controls/EditorFlyControls.d.ts.map +1 -0
- package/dist/controls/EditorFlyControls.js +44 -0
- package/dist/controls/EditorFlyControls.js.map +1 -0
- package/dist/controls/FirstPersonControls.d.ts +22 -0
- package/dist/controls/FirstPersonControls.d.ts.map +1 -0
- package/dist/controls/FirstPersonControls.js +44 -0
- package/dist/controls/FirstPersonControls.js.map +1 -0
- package/dist/controls/OrbitControls.d.ts +49 -0
- package/dist/controls/OrbitControls.d.ts.map +1 -0
- package/dist/controls/OrbitControls.js +131 -0
- package/dist/controls/OrbitControls.js.map +1 -0
- package/dist/controls/PointerLockControls.d.ts +11 -0
- package/dist/controls/PointerLockControls.d.ts.map +1 -0
- package/dist/controls/PointerLockControls.js +19 -0
- package/dist/controls/PointerLockControls.js.map +1 -0
- package/dist/controls/SceneCameraAdapter.d.ts +10 -0
- package/dist/controls/SceneCameraAdapter.d.ts.map +1 -0
- package/dist/controls/SceneCameraAdapter.js +90 -0
- package/dist/controls/SceneCameraAdapter.js.map +1 -0
- package/dist/controls/ThirdPersonFollowControls.d.ts +20 -0
- package/dist/controls/ThirdPersonFollowControls.d.ts.map +1 -0
- package/dist/controls/ThirdPersonFollowControls.js +33 -0
- package/dist/controls/ThirdPersonFollowControls.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -0
- package/src/ActionMap.ts +94 -0
- package/src/GamepadDevice.ts +48 -0
- package/src/GamepadInput.ts +2 -0
- package/src/GestureControls.ts +2 -0
- package/src/GestureHapticsFixtures.ts +179 -0
- package/src/GestureRecognizer.ts +43 -0
- package/src/InputActionBindingFixtures.ts +121 -0
- package/src/InputReplay.ts +275 -0
- package/src/InputSnapshot.ts +92 -0
- package/src/InputSystem.ts +92 -0
- package/src/InteractionSystem.ts +119 -0
- package/src/KeyboardDevice.ts +32 -0
- package/src/PickingRay.ts +49 -0
- package/src/PointerDevice.ts +102 -0
- package/src/VirtualTouchControls.ts +169 -0
- package/src/WebXRSessionController.ts +248 -0
- package/src/XRFixtures.ts +203 -0
- package/src/controls/CameraRig.ts +38 -0
- package/src/controls/ControlTypes.ts +21 -0
- package/src/controls/EditorFlyControls.ts +55 -0
- package/src/controls/FirstPersonControls.ts +60 -0
- package/src/controls/OrbitControls.ts +168 -0
- package/src/controls/PointerLockControls.ts +24 -0
- package/src/controls/SceneCameraAdapter.ts +100 -0
- package/src/controls/ThirdPersonFollowControls.ts +42 -0
- package/src/index.ts +72 -0
- package/tests/input.test.ts +69 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export type XRFixtureSessionMode = "inline" | "immersive-vr" | "immersive-ar";
|
|
2
|
+
export type XRFixtureLodLevel = "high" | "medium" | "low" | "culled";
|
|
3
|
+
|
|
4
|
+
export interface XRFixtureOptions {
|
|
5
|
+
readonly requestedMode?: XRFixtureSessionMode;
|
|
6
|
+
readonly gaze?: readonly [number, number];
|
|
7
|
+
readonly objectCount?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface XRRuntimeFixture {
|
|
11
|
+
readonly source: "origin-master-xr-session-input-foveated-adapted";
|
|
12
|
+
readonly requestedMode: XRFixtureSessionMode;
|
|
13
|
+
readonly fallbackMode: "inline";
|
|
14
|
+
readonly sessionSupported: boolean;
|
|
15
|
+
readonly fallbackUsed: boolean;
|
|
16
|
+
readonly webXRSessionClaimed: false;
|
|
17
|
+
readonly deviceRuntimeClaimed: false;
|
|
18
|
+
readonly evidence: {
|
|
19
|
+
readonly oldCodebasePort: true;
|
|
20
|
+
readonly sessionCapabilityNegotiation: true;
|
|
21
|
+
readonly gracefulInlineFallback: true;
|
|
22
|
+
readonly controllerInputTelemetry: true;
|
|
23
|
+
readonly handGestureTelemetry: true;
|
|
24
|
+
readonly gazeBasedLodTelemetry: true;
|
|
25
|
+
};
|
|
26
|
+
readonly capabilities: {
|
|
27
|
+
readonly supportedModes: readonly XRFixtureSessionMode[];
|
|
28
|
+
readonly requestedFeatures: readonly string[];
|
|
29
|
+
readonly optionalFeatures: readonly string[];
|
|
30
|
+
readonly unsupportedFeatures: readonly string[];
|
|
31
|
+
readonly referenceSpaces: readonly string[];
|
|
32
|
+
readonly depthSensing: false;
|
|
33
|
+
readonly handTracking: false;
|
|
34
|
+
readonly eyeTracking: false;
|
|
35
|
+
readonly domOverlay: false;
|
|
36
|
+
};
|
|
37
|
+
readonly input: {
|
|
38
|
+
readonly controllerCount: number;
|
|
39
|
+
readonly handedness: readonly ["left", "right"];
|
|
40
|
+
readonly triggerPressed: boolean;
|
|
41
|
+
readonly squeezePressed: boolean;
|
|
42
|
+
readonly thumbstickMagnitude: number;
|
|
43
|
+
readonly hapticsClaimed: false;
|
|
44
|
+
readonly pinchDetected: boolean;
|
|
45
|
+
readonly pinchStrength: number;
|
|
46
|
+
readonly pinchDistanceMeters: number;
|
|
47
|
+
readonly pointDetected: boolean;
|
|
48
|
+
readonly pointConfidence: number;
|
|
49
|
+
};
|
|
50
|
+
readonly gazeLod: {
|
|
51
|
+
readonly gaze: readonly [number, number];
|
|
52
|
+
readonly objectCount: number;
|
|
53
|
+
readonly performanceBudget: number;
|
|
54
|
+
readonly updatedObjects: number;
|
|
55
|
+
readonly highDetailCount: number;
|
|
56
|
+
readonly mediumDetailCount: number;
|
|
57
|
+
readonly lowDetailCount: number;
|
|
58
|
+
readonly culledCount: number;
|
|
59
|
+
readonly transitionsPerFrame: number;
|
|
60
|
+
readonly selectedLevels: readonly XRFixtureLodLevel[];
|
|
61
|
+
};
|
|
62
|
+
readonly blockedClaims: readonly string[];
|
|
63
|
+
readonly hash: string;
|
|
64
|
+
readonly claimBoundary: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sampleXRRuntimeFixture(options: XRFixtureOptions = {}): XRRuntimeFixture {
|
|
68
|
+
const requestedMode = options.requestedMode ?? "immersive-vr";
|
|
69
|
+
const gaze = options.gaze ?? [0.48, 0.44] as const;
|
|
70
|
+
const objectCount = Math.max(6, Math.min(48, Math.trunc(options.objectCount ?? 14)));
|
|
71
|
+
const supportedModes = ["inline"] as const;
|
|
72
|
+
const sessionSupported = supportedModes.includes(requestedMode as "inline");
|
|
73
|
+
const objects = Array.from({ length: objectCount }, (_, index) => {
|
|
74
|
+
const column = index % 7;
|
|
75
|
+
const row = Math.floor(index / 7);
|
|
76
|
+
const x = 0.12 + column * 0.13;
|
|
77
|
+
const y = 0.18 + row * 0.21;
|
|
78
|
+
const distance = Math.hypot(x - gaze[0], y - gaze[1]);
|
|
79
|
+
return { index, distance, lod: lodForDistance(distance) };
|
|
80
|
+
});
|
|
81
|
+
const performanceBudget = Math.min(10, objectCount);
|
|
82
|
+
const updatedObjects = objects
|
|
83
|
+
.slice()
|
|
84
|
+
.sort((left, right) => left.distance - right.distance)
|
|
85
|
+
.slice(0, performanceBudget);
|
|
86
|
+
const selectedLevels = uniqueLevels(objects.map((object) => object.lod));
|
|
87
|
+
const pinchDistanceMeters = 0.024;
|
|
88
|
+
const pointConfidence = 0.82;
|
|
89
|
+
const unsupportedFeatures = [
|
|
90
|
+
"local-floor",
|
|
91
|
+
"bounded-floor",
|
|
92
|
+
"hand-tracking",
|
|
93
|
+
"eye-tracking",
|
|
94
|
+
"depth-sensing",
|
|
95
|
+
"dom-overlay",
|
|
96
|
+
"hit-test",
|
|
97
|
+
"anchors",
|
|
98
|
+
"plane-detection",
|
|
99
|
+
"mesh-detection",
|
|
100
|
+
"light-estimation"
|
|
101
|
+
];
|
|
102
|
+
const blockedClaims = [
|
|
103
|
+
"real WebXR headset session evidence",
|
|
104
|
+
"AR hit-test anchor platform parity",
|
|
105
|
+
"device hand-tracking runtime parity",
|
|
106
|
+
"eye-tracked foveated rendering",
|
|
107
|
+
"Unity XR Interaction Toolkit parity",
|
|
108
|
+
"Unreal XR framework parity"
|
|
109
|
+
];
|
|
110
|
+
const hash = hashStrings([
|
|
111
|
+
requestedMode,
|
|
112
|
+
String(sessionSupported),
|
|
113
|
+
...selectedLevels,
|
|
114
|
+
String(updatedObjects.length),
|
|
115
|
+
String(pinchDistanceMeters),
|
|
116
|
+
String(pointConfidence),
|
|
117
|
+
...blockedClaims
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
source: "origin-master-xr-session-input-foveated-adapted",
|
|
122
|
+
requestedMode,
|
|
123
|
+
fallbackMode: "inline",
|
|
124
|
+
sessionSupported,
|
|
125
|
+
fallbackUsed: !sessionSupported,
|
|
126
|
+
webXRSessionClaimed: false,
|
|
127
|
+
deviceRuntimeClaimed: false,
|
|
128
|
+
evidence: {
|
|
129
|
+
oldCodebasePort: true,
|
|
130
|
+
sessionCapabilityNegotiation: true,
|
|
131
|
+
gracefulInlineFallback: true,
|
|
132
|
+
controllerInputTelemetry: true,
|
|
133
|
+
handGestureTelemetry: true,
|
|
134
|
+
gazeBasedLodTelemetry: true
|
|
135
|
+
},
|
|
136
|
+
capabilities: {
|
|
137
|
+
supportedModes,
|
|
138
|
+
requestedFeatures: ["local-floor"],
|
|
139
|
+
optionalFeatures: unsupportedFeatures.slice(2),
|
|
140
|
+
unsupportedFeatures,
|
|
141
|
+
referenceSpaces: ["viewer"],
|
|
142
|
+
depthSensing: false,
|
|
143
|
+
handTracking: false,
|
|
144
|
+
eyeTracking: false,
|
|
145
|
+
domOverlay: false
|
|
146
|
+
},
|
|
147
|
+
input: {
|
|
148
|
+
controllerCount: 2,
|
|
149
|
+
handedness: ["left", "right"],
|
|
150
|
+
triggerPressed: true,
|
|
151
|
+
squeezePressed: false,
|
|
152
|
+
thumbstickMagnitude: round(Math.hypot(0.42, -0.36)),
|
|
153
|
+
hapticsClaimed: false,
|
|
154
|
+
pinchDetected: pinchDistanceMeters <= 0.03,
|
|
155
|
+
pinchStrength: round((0.05 - pinchDistanceMeters) / 0.05),
|
|
156
|
+
pinchDistanceMeters,
|
|
157
|
+
pointDetected: true,
|
|
158
|
+
pointConfidence
|
|
159
|
+
},
|
|
160
|
+
gazeLod: {
|
|
161
|
+
gaze: [round(gaze[0]), round(gaze[1])],
|
|
162
|
+
objectCount,
|
|
163
|
+
performanceBudget,
|
|
164
|
+
updatedObjects: updatedObjects.length,
|
|
165
|
+
highDetailCount: objects.filter((object) => object.lod === "high").length,
|
|
166
|
+
mediumDetailCount: objects.filter((object) => object.lod === "medium").length,
|
|
167
|
+
lowDetailCount: objects.filter((object) => object.lod === "low").length,
|
|
168
|
+
culledCount: objects.filter((object) => object.lod === "culled").length,
|
|
169
|
+
transitionsPerFrame: updatedObjects.filter((object) => object.lod !== "medium").length,
|
|
170
|
+
selectedLevels
|
|
171
|
+
},
|
|
172
|
+
blockedClaims,
|
|
173
|
+
hash,
|
|
174
|
+
claimBoundary: "Bounded deterministic fixture adapted from old XR session/input/foveated concepts. It does not claim a real WebXR headset session, AR platform support, eye-tracked foveated rendering, Unity XR parity, or Unreal XR parity."
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function lodForDistance(distance: number): XRFixtureLodLevel {
|
|
179
|
+
if (distance <= 0.18) return "high";
|
|
180
|
+
if (distance <= 0.36) return "medium";
|
|
181
|
+
if (distance <= 0.58) return "low";
|
|
182
|
+
return "culled";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function uniqueLevels(levels: readonly XRFixtureLodLevel[]): readonly XRFixtureLodLevel[] {
|
|
186
|
+
const order: readonly XRFixtureLodLevel[] = ["high", "medium", "low", "culled"];
|
|
187
|
+
return order.filter((level) => levels.includes(level));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hashStrings(values: readonly string[]): string {
|
|
191
|
+
let hash = 0x811c9dc5;
|
|
192
|
+
for (const value of values) {
|
|
193
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
194
|
+
hash ^= value.charCodeAt(index);
|
|
195
|
+
hash = Math.imul(hash, 0x01000193);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function round(value: number): number {
|
|
202
|
+
return Number(value.toFixed(4));
|
|
203
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CameraTransformLike, Vec3Like } from "./ControlTypes";
|
|
2
|
+
|
|
3
|
+
export interface CameraRigState {
|
|
4
|
+
readonly position: Vec3Like;
|
|
5
|
+
readonly target?: Vec3Like;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class CameraRig {
|
|
9
|
+
constructor(private readonly camera: CameraTransformLike) {}
|
|
10
|
+
|
|
11
|
+
apply(state: CameraRigState): void {
|
|
12
|
+
this.camera.position.x = state.position.x;
|
|
13
|
+
this.camera.position.y = state.position.y;
|
|
14
|
+
this.camera.position.z = state.position.z;
|
|
15
|
+
if (state.target) {
|
|
16
|
+
this.camera.lookAt?.(state.target);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
blend(from: CameraRigState, to: CameraRigState, alpha: number): CameraRigState {
|
|
21
|
+
const t = Math.min(1, Math.max(0, alpha));
|
|
22
|
+
return {
|
|
23
|
+
position: {
|
|
24
|
+
x: from.position.x + (to.position.x - from.position.x) * t,
|
|
25
|
+
y: from.position.y + (to.position.y - from.position.y) * t,
|
|
26
|
+
z: from.position.z + (to.position.z - from.position.z) * t
|
|
27
|
+
},
|
|
28
|
+
target:
|
|
29
|
+
from.target && to.target
|
|
30
|
+
? {
|
|
31
|
+
x: from.target.x + (to.target.x - from.target.x) * t,
|
|
32
|
+
y: from.target.y + (to.target.y - from.target.y) * t,
|
|
33
|
+
z: from.target.z + (to.target.z - from.target.z) * t
|
|
34
|
+
}
|
|
35
|
+
: (to.target ?? from.target)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Vec3Like {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
z: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface EulerLike {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
z: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CameraTransformLike {
|
|
14
|
+
readonly position: Vec3Like;
|
|
15
|
+
readonly rotation?: EulerLike;
|
|
16
|
+
lookAt?(target: Vec3Like): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
20
|
+
return Math.min(max, Math.max(min, value));
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { InputSnapshot } from "../InputSnapshot";
|
|
2
|
+
import { type CameraTransformLike, clamp } from "./ControlTypes";
|
|
3
|
+
|
|
4
|
+
export interface EditorFlyControlsOptions {
|
|
5
|
+
readonly baseSpeed?: number;
|
|
6
|
+
readonly fastMultiplier?: number;
|
|
7
|
+
readonly lookSpeed?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class EditorFlyControls {
|
|
11
|
+
enabled = true;
|
|
12
|
+
private yaw = 0;
|
|
13
|
+
private pitch = 0;
|
|
14
|
+
private readonly baseSpeed: number;
|
|
15
|
+
private readonly fastMultiplier: number;
|
|
16
|
+
private readonly lookSpeed: number;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly camera: CameraTransformLike,
|
|
20
|
+
options: EditorFlyControlsOptions = {}
|
|
21
|
+
) {
|
|
22
|
+
this.baseSpeed = options.baseSpeed ?? 3;
|
|
23
|
+
this.fastMultiplier = options.fastMultiplier ?? 4;
|
|
24
|
+
this.lookSpeed = options.lookSpeed ?? 0.0025;
|
|
25
|
+
this.pitch = clamp(camera.rotation?.x ?? 0, -Math.PI / 2 + 0.001, Math.PI / 2 - 0.001);
|
|
26
|
+
this.yaw = camera.rotation?.y ?? 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
update(snapshot: InputSnapshot, deltaSeconds: number): void {
|
|
30
|
+
if (!this.enabled) return;
|
|
31
|
+
if (snapshot.button(1).down || snapshot.button(2).down) {
|
|
32
|
+
this.yaw -= snapshot.pointer.deltaX * this.lookSpeed;
|
|
33
|
+
this.pitch = clamp(this.pitch - snapshot.pointer.deltaY * this.lookSpeed, -Math.PI / 2 + 0.001, Math.PI / 2 - 0.001);
|
|
34
|
+
if (this.camera.rotation) {
|
|
35
|
+
this.camera.rotation.x = this.pitch;
|
|
36
|
+
this.camera.rotation.y = this.yaw;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const speed = this.baseSpeed * (snapshot.key("ShiftLeft").down || snapshot.key("ShiftRight").down ? this.fastMultiplier : 1);
|
|
41
|
+
const forward = (snapshot.key("KeyW").down ? 1 : 0) - (snapshot.key("KeyS").down ? 1 : 0);
|
|
42
|
+
const strafe = (snapshot.key("KeyD").down ? 1 : 0) - (snapshot.key("KeyA").down ? 1 : 0);
|
|
43
|
+
const lift = (snapshot.key("KeyE").down ? 1 : 0) - (snapshot.key("KeyQ").down ? 1 : 0);
|
|
44
|
+
const step = speed * deltaSeconds;
|
|
45
|
+
const sin = Math.sin(this.yaw);
|
|
46
|
+
const cos = Math.cos(this.yaw);
|
|
47
|
+
this.camera.position.x += (strafe * cos + forward * sin) * step;
|
|
48
|
+
this.camera.position.z += (forward * cos - strafe * sin) * step;
|
|
49
|
+
this.camera.position.y += lift * step;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dispose(): void {
|
|
53
|
+
this.enabled = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { InputSnapshot } from "../InputSnapshot";
|
|
2
|
+
import { clamp, type CameraTransformLike } from "./ControlTypes";
|
|
3
|
+
|
|
4
|
+
export interface FirstPersonControlsOptions {
|
|
5
|
+
readonly moveSpeed?: number;
|
|
6
|
+
readonly lookSpeed?: number;
|
|
7
|
+
readonly minPitch?: number;
|
|
8
|
+
readonly maxPitch?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class FirstPersonControls {
|
|
12
|
+
enabled = true;
|
|
13
|
+
|
|
14
|
+
private yaw = 0;
|
|
15
|
+
private pitch = 0;
|
|
16
|
+
private readonly moveSpeed: number;
|
|
17
|
+
private readonly lookSpeed: number;
|
|
18
|
+
private readonly minPitch: number;
|
|
19
|
+
private readonly maxPitch: number;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly camera: CameraTransformLike,
|
|
23
|
+
options: FirstPersonControlsOptions = {}
|
|
24
|
+
) {
|
|
25
|
+
this.moveSpeed = options.moveSpeed ?? 1;
|
|
26
|
+
this.lookSpeed = options.lookSpeed ?? 0.002;
|
|
27
|
+
this.minPitch = options.minPitch ?? -Math.PI / 2 + 0.001;
|
|
28
|
+
this.maxPitch = options.maxPitch ?? Math.PI / 2 - 0.001;
|
|
29
|
+
this.pitch = clamp(camera.rotation?.x ?? 0, this.minPitch, this.maxPitch);
|
|
30
|
+
this.yaw = camera.rotation?.y ?? 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
update(snapshot: InputSnapshot, deltaSeconds: number): void {
|
|
34
|
+
if (!this.enabled) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (snapshot.button(0).down) {
|
|
39
|
+
this.yaw -= snapshot.pointer.deltaX * this.lookSpeed;
|
|
40
|
+
this.pitch = clamp(this.pitch - snapshot.pointer.deltaY * this.lookSpeed, this.minPitch, this.maxPitch);
|
|
41
|
+
if (this.camera.rotation) {
|
|
42
|
+
this.camera.rotation.x = this.pitch;
|
|
43
|
+
this.camera.rotation.y = this.yaw;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const forward = (snapshot.key("KeyW").down ? 1 : 0) - (snapshot.key("KeyS").down ? 1 : 0);
|
|
48
|
+
const strafe = (snapshot.key("KeyD").down ? 1 : 0) - (snapshot.key("KeyA").down ? 1 : 0);
|
|
49
|
+
const step = this.moveSpeed * deltaSeconds;
|
|
50
|
+
const sin = Math.sin(this.yaw);
|
|
51
|
+
const cos = Math.cos(this.yaw);
|
|
52
|
+
|
|
53
|
+
this.camera.position.x += (strafe * cos + forward * sin) * step;
|
|
54
|
+
this.camera.position.z += (forward * cos - strafe * sin) * step;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
dispose(): void {
|
|
58
|
+
this.enabled = false;
|
|
59
|
+
}
|
|
60
|
+
}
|