@aura3d/input 1.4.0 → 1.4.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aura3d/input",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -11,7 +11,39 @@
|
|
|
11
11
|
"main": "./dist/index.js",
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@aura3d/math": "1.4.
|
|
15
|
-
"@aura3d/scene": "1.4.
|
|
16
|
-
}
|
|
14
|
+
"@aura3d/math": "1.4.2",
|
|
15
|
+
"@aura3d/scene": "1.4.2"
|
|
16
|
+
},
|
|
17
|
+
"description": "Keyboard, pointer, gamepad, gesture, action-map, picking-ray, interaction, and camera-control runtime contracts for Aura3D.",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"aura3d",
|
|
20
|
+
"3d",
|
|
21
|
+
"browser-3d",
|
|
22
|
+
"web-3d",
|
|
23
|
+
"typescript",
|
|
24
|
+
"input",
|
|
25
|
+
"keyboard",
|
|
26
|
+
"pointer",
|
|
27
|
+
"gamepad",
|
|
28
|
+
"gestures",
|
|
29
|
+
"action-map",
|
|
30
|
+
"camera-controls"
|
|
31
|
+
],
|
|
32
|
+
"homepage": "https://aura3d.auraone.ai",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/auraoneai/aura3d.git",
|
|
36
|
+
"directory": "packages/input"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/auraoneai/aura3d/issues"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"README.md",
|
|
47
|
+
"dist"
|
|
48
|
+
]
|
|
17
49
|
}
|
package/src/ActionMap.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { InputSnapshot } from "./InputSnapshot";
|
|
2
|
-
|
|
3
|
-
export type ActionBinding =
|
|
4
|
-
| { readonly type: "keyboard"; readonly code: string }
|
|
5
|
-
| { readonly type: "keyboard-chord"; readonly codes: readonly string[] }
|
|
6
|
-
| { readonly type: "pointer"; readonly button: number }
|
|
7
|
-
| { readonly type: "gamepad-button"; readonly gamepad?: number; readonly button: number };
|
|
8
|
-
|
|
9
|
-
export type AxisBinding =
|
|
10
|
-
| { readonly type: "keyboard-axis"; readonly negative: string; readonly positive: string; readonly scale?: number }
|
|
11
|
-
| { readonly type: "pointer-axis"; readonly axis: "x" | "y" | "wheelX" | "wheelY"; readonly scale?: number }
|
|
12
|
-
| { readonly type: "gamepad-axis"; readonly gamepad?: number; readonly axis: number; readonly scale?: number };
|
|
13
|
-
|
|
14
|
-
export class ActionMap {
|
|
15
|
-
private readonly actions = new Map<string, readonly ActionBinding[]>();
|
|
16
|
-
private readonly axes = new Map<string, readonly AxisBinding[]>();
|
|
17
|
-
private snapshot = new InputSnapshot();
|
|
18
|
-
|
|
19
|
-
bind(name: string, bindings: readonly ActionBinding[]): void {
|
|
20
|
-
this.actions.set(name, [...bindings]);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
bindAxis(name: string, bindings: readonly AxisBinding[]): void {
|
|
24
|
-
this.axes.set(name, [...bindings]);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
useSnapshot(snapshot: InputSnapshot): void {
|
|
28
|
-
this.snapshot = snapshot;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
down(name: string, snapshot = this.snapshot): boolean {
|
|
32
|
-
return this.evaluate(name, snapshot, "down");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
pressed(name: string, snapshot = this.snapshot): boolean {
|
|
36
|
-
return this.evaluate(name, snapshot, "pressed");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
released(name: string, snapshot = this.snapshot): boolean {
|
|
40
|
-
return this.evaluate(name, snapshot, "released");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
axis(name: string, snapshot = this.snapshot): number {
|
|
44
|
-
let value = 0;
|
|
45
|
-
|
|
46
|
-
for (const binding of this.axes.get(name) ?? []) {
|
|
47
|
-
const scale = binding.scale ?? 1;
|
|
48
|
-
if (binding.type === "keyboard-axis") {
|
|
49
|
-
value += (snapshot.key(binding.positive).down ? 1 : 0) * scale;
|
|
50
|
-
value -= (snapshot.key(binding.negative).down ? 1 : 0) * scale;
|
|
51
|
-
} else if (binding.type === "pointer-axis") {
|
|
52
|
-
const pointerValue =
|
|
53
|
-
binding.axis === "x"
|
|
54
|
-
? snapshot.pointer.deltaX
|
|
55
|
-
: binding.axis === "y"
|
|
56
|
-
? snapshot.pointer.deltaY
|
|
57
|
-
: snapshot.pointer[binding.axis];
|
|
58
|
-
value += pointerValue * scale;
|
|
59
|
-
} else {
|
|
60
|
-
const gamepad = snapshot.gamepads.find((pad) => pad.index === (binding.gamepad ?? 0));
|
|
61
|
-
value += (gamepad?.axes[binding.axis] ?? 0) * scale;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return value;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private evaluate(name: string, snapshot: InputSnapshot, field: "down" | "pressed" | "released"): boolean {
|
|
69
|
-
return (this.actions.get(name) ?? []).some((binding) => {
|
|
70
|
-
if (binding.type === "keyboard") {
|
|
71
|
-
return snapshot.key(binding.code)[field];
|
|
72
|
-
}
|
|
73
|
-
if (binding.type === "keyboard-chord") {
|
|
74
|
-
return evaluateChord(binding.codes, snapshot, field);
|
|
75
|
-
}
|
|
76
|
-
if (binding.type === "pointer") {
|
|
77
|
-
return snapshot.button(binding.button)[field];
|
|
78
|
-
}
|
|
79
|
-
return snapshot.gamepadButton(binding.gamepad ?? 0, binding.button)[field];
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function evaluateChord(codes: readonly string[], snapshot: InputSnapshot, field: "down" | "pressed" | "released"): boolean {
|
|
85
|
-
if (codes.length === 0) return false;
|
|
86
|
-
const states = codes.map((code) => snapshot.key(code));
|
|
87
|
-
if (field === "down") {
|
|
88
|
-
return states.every((state) => state.down);
|
|
89
|
-
}
|
|
90
|
-
if (field === "pressed") {
|
|
91
|
-
return states.every((state) => state.down) && states.some((state) => state.pressed);
|
|
92
|
-
}
|
|
93
|
-
return states.some((state) => state.released);
|
|
94
|
-
}
|
package/src/GamepadDevice.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { ButtonState, GamepadSnapshot } from "./InputSnapshot";
|
|
2
|
-
|
|
3
|
-
export interface GamepadButtonLike {
|
|
4
|
-
readonly pressed: boolean;
|
|
5
|
-
readonly value: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface GamepadLike {
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly index: number;
|
|
11
|
-
readonly connected: boolean;
|
|
12
|
-
readonly axes: readonly number[];
|
|
13
|
-
readonly buttons: readonly GamepadButtonLike[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class GamepadDevice {
|
|
17
|
-
private previousButtons = new Map<string, readonly boolean[]>();
|
|
18
|
-
|
|
19
|
-
poll(gamepads: readonly (GamepadLike | null | undefined)[], deadZone = 0.1): readonly GamepadSnapshot[] {
|
|
20
|
-
const snapshots: GamepadSnapshot[] = [];
|
|
21
|
-
|
|
22
|
-
for (const gamepad of gamepads) {
|
|
23
|
-
if (!gamepad?.connected) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const key = `${gamepad.index}:${gamepad.id}`;
|
|
28
|
-
const previous = this.previousButtons.get(key) ?? [];
|
|
29
|
-
const buttons: ButtonState[] = gamepad.buttons.map((button, index) => {
|
|
30
|
-
const down = button.pressed || button.value > 0.5;
|
|
31
|
-
const wasDown = previous[index] ?? false;
|
|
32
|
-
return Object.freeze({ down, pressed: down && !wasDown, released: !down && wasDown });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
snapshots.push(
|
|
36
|
-
Object.freeze({
|
|
37
|
-
id: gamepad.id,
|
|
38
|
-
index: gamepad.index,
|
|
39
|
-
axes: Object.freeze(gamepad.axes.map((axis) => (Math.abs(axis) < deadZone ? 0 : axis))),
|
|
40
|
-
buttons: Object.freeze(buttons)
|
|
41
|
-
})
|
|
42
|
-
);
|
|
43
|
-
this.previousButtons.set(key, buttons.map((button) => button.down));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return snapshots;
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/GamepadInput.ts
DELETED
package/src/GestureControls.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
export type GestureHapticsGestureType = "tap" | "pan" | "pinch" | "swipe" | "rotate";
|
|
2
|
-
export type GestureHapticsPatternName = "tap" | "impact" | "damage" | "success" | "engine";
|
|
3
|
-
|
|
4
|
-
export interface GestureHapticsFixtureOptions {
|
|
5
|
-
readonly seed?: number;
|
|
6
|
-
readonly pointerDeltaX?: number;
|
|
7
|
-
readonly pointerDeltaY?: number;
|
|
8
|
-
readonly pinchStartDistance?: number;
|
|
9
|
-
readonly pinchEndDistance?: number;
|
|
10
|
-
readonly rotateDegrees?: number;
|
|
11
|
-
readonly gamepadConnected?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface GestureHapticsFixture {
|
|
15
|
-
readonly source: "origin-master-input-gesture-rumble-adapted";
|
|
16
|
-
readonly gestures: readonly {
|
|
17
|
-
readonly type: GestureHapticsGestureType;
|
|
18
|
-
readonly state: "possible" | "began" | "changed" | "ended";
|
|
19
|
-
readonly touchCount: number;
|
|
20
|
-
readonly center: readonly [number, number];
|
|
21
|
-
readonly distance?: number;
|
|
22
|
-
readonly velocity?: readonly [number, number];
|
|
23
|
-
readonly scale?: number;
|
|
24
|
-
readonly rotationDegrees?: number;
|
|
25
|
-
}[];
|
|
26
|
-
readonly gestureSummary: {
|
|
27
|
-
readonly tapDetected: boolean;
|
|
28
|
-
readonly panDistance: number;
|
|
29
|
-
readonly pinchScale: number;
|
|
30
|
-
readonly swipeDirection: "left" | "right" | "up" | "down";
|
|
31
|
-
readonly rotateDegrees: number;
|
|
32
|
-
};
|
|
33
|
-
readonly haptics: {
|
|
34
|
-
readonly gamepadConnected: boolean;
|
|
35
|
-
readonly hapticsClaimed: false;
|
|
36
|
-
readonly patterns: readonly {
|
|
37
|
-
readonly name: GestureHapticsPatternName;
|
|
38
|
-
readonly steps: number;
|
|
39
|
-
readonly totalDurationMs: number;
|
|
40
|
-
readonly peakWeak: number;
|
|
41
|
-
readonly peakStrong: number;
|
|
42
|
-
readonly loop: boolean;
|
|
43
|
-
}[];
|
|
44
|
-
readonly queuedPatterns: number;
|
|
45
|
-
readonly totalDurationMs: number;
|
|
46
|
-
readonly intensityMultiplier: number;
|
|
47
|
-
};
|
|
48
|
-
readonly productionReadiness: {
|
|
49
|
-
readonly gestureTelemetry: true;
|
|
50
|
-
readonly swipeRotateTelemetry: true;
|
|
51
|
-
readonly hapticPatternTelemetry: true;
|
|
52
|
-
readonly hapticClaimBoundaryTelemetry: true;
|
|
53
|
-
};
|
|
54
|
-
readonly blockedClaims: readonly string[];
|
|
55
|
-
readonly claimBoundary: string;
|
|
56
|
-
readonly hash: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function sampleGestureHapticsFixture(options: GestureHapticsFixtureOptions = {}): GestureHapticsFixture {
|
|
60
|
-
const seed = normalizeSeed(options.seed ?? 0x9e57);
|
|
61
|
-
const pointerDeltaX = finite(options.pointerDeltaX ?? 84, "pointerDeltaX");
|
|
62
|
-
const pointerDeltaY = finite(options.pointerDeltaY ?? 18, "pointerDeltaY");
|
|
63
|
-
const pinchStartDistance = positive(options.pinchStartDistance ?? 42, "pinchStartDistance");
|
|
64
|
-
const pinchEndDistance = positive(options.pinchEndDistance ?? 76, "pinchEndDistance");
|
|
65
|
-
const rotateDegrees = finite(options.rotateDegrees ?? 28, "rotateDegrees");
|
|
66
|
-
const panDistance = Number(Math.hypot(pointerDeltaX, pointerDeltaY).toFixed(3));
|
|
67
|
-
const pinchScale = Number((pinchEndDistance / pinchStartDistance).toFixed(3));
|
|
68
|
-
const swipeDirection = Math.abs(pointerDeltaX) >= Math.abs(pointerDeltaY)
|
|
69
|
-
? pointerDeltaX >= 0 ? "right" : "left"
|
|
70
|
-
: pointerDeltaY >= 0 ? "down" : "up";
|
|
71
|
-
const patterns = [
|
|
72
|
-
hapticPattern("tap", [{ weak: 0.3, strong: 0.2, duration: 50 }], false),
|
|
73
|
-
hapticPattern("impact", [{ weak: 0.7, strong: 0.8, duration: 100 }], false),
|
|
74
|
-
hapticPattern("damage", [
|
|
75
|
-
{ weak: 0.8, strong: 0.9, duration: 80 },
|
|
76
|
-
{ weak: 0, strong: 0, duration: 40 },
|
|
77
|
-
{ weak: 0.6, strong: 0.7, duration: 60 }
|
|
78
|
-
], false),
|
|
79
|
-
hapticPattern("success", [
|
|
80
|
-
{ weak: 0.3, strong: 0.3, duration: 80 },
|
|
81
|
-
{ weak: 0, strong: 0, duration: 50 },
|
|
82
|
-
{ weak: 0.4, strong: 0.4, duration: 100 }
|
|
83
|
-
], false),
|
|
84
|
-
hapticPattern("engine", [
|
|
85
|
-
{ weak: 0.3, strong: 0.2, duration: 50 },
|
|
86
|
-
{ weak: 0.35, strong: 0.25, duration: 50 }
|
|
87
|
-
], true)
|
|
88
|
-
] as const;
|
|
89
|
-
const fixture: Omit<GestureHapticsFixture, "hash"> = {
|
|
90
|
-
source: "origin-master-input-gesture-rumble-adapted",
|
|
91
|
-
gestures: [
|
|
92
|
-
{ type: "tap", state: "ended", touchCount: 1, center: [16, 22] },
|
|
93
|
-
{ type: "pan", state: "changed", touchCount: 1, center: [42, 38], distance: panDistance, velocity: [pointerDeltaX, pointerDeltaY] },
|
|
94
|
-
{ type: "pinch", state: "changed", touchCount: 2, center: [64, 64], distance: pinchEndDistance, scale: pinchScale },
|
|
95
|
-
{ type: "swipe", state: "ended", touchCount: 1, center: [120, 64], distance: panDistance, velocity: [pointerDeltaX * 8, pointerDeltaY * 8] },
|
|
96
|
-
{ type: "rotate", state: "changed", touchCount: 2, center: [64, 64], rotationDegrees: Number(rotateDegrees.toFixed(3)) }
|
|
97
|
-
],
|
|
98
|
-
gestureSummary: {
|
|
99
|
-
tapDetected: true,
|
|
100
|
-
panDistance,
|
|
101
|
-
pinchScale,
|
|
102
|
-
swipeDirection,
|
|
103
|
-
rotateDegrees: Number(rotateDegrees.toFixed(3))
|
|
104
|
-
},
|
|
105
|
-
haptics: {
|
|
106
|
-
gamepadConnected: options.gamepadConnected ?? true,
|
|
107
|
-
hapticsClaimed: false,
|
|
108
|
-
patterns,
|
|
109
|
-
queuedPatterns: 3 + Math.floor(seededUnit(seed) * 2),
|
|
110
|
-
totalDurationMs: patterns.reduce((sum, pattern) => sum + pattern.totalDurationMs, 0),
|
|
111
|
-
intensityMultiplier: Number((0.65 + seededUnit(seed ^ 0x54c) * 0.25).toFixed(3))
|
|
112
|
-
},
|
|
113
|
-
productionReadiness: {
|
|
114
|
-
gestureTelemetry: true,
|
|
115
|
-
swipeRotateTelemetry: true,
|
|
116
|
-
hapticPatternTelemetry: true,
|
|
117
|
-
hapticClaimBoundaryTelemetry: true
|
|
118
|
-
},
|
|
119
|
-
blockedClaims: [
|
|
120
|
-
"native mobile gesture recognizer parity",
|
|
121
|
-
"gamepad haptic actuator delivery guarantee",
|
|
122
|
-
"platform vibration permission handling",
|
|
123
|
-
"Unity Input System haptics parity",
|
|
124
|
-
"Unreal Enhanced Input haptics parity"
|
|
125
|
-
],
|
|
126
|
-
claimBoundary: "This fixture ports old swipe/rotate gesture and rumble-pattern concepts into deterministic local input telemetry. It does not vibrate hardware, guarantee platform haptic delivery, or claim Unity Input System or Unreal Enhanced Input parity."
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
...fixture,
|
|
130
|
-
hash: stableHash(JSON.stringify(fixture))
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function hapticPattern(
|
|
135
|
-
name: GestureHapticsPatternName,
|
|
136
|
-
steps: readonly { readonly weak: number; readonly strong: number; readonly duration: number }[],
|
|
137
|
-
loop: boolean
|
|
138
|
-
): GestureHapticsFixture["haptics"]["patterns"][number] {
|
|
139
|
-
return {
|
|
140
|
-
name,
|
|
141
|
-
steps: steps.length,
|
|
142
|
-
totalDurationMs: steps.reduce((sum, step) => sum + step.duration, 0),
|
|
143
|
-
peakWeak: Math.max(...steps.map((step) => step.weak)),
|
|
144
|
-
peakStrong: Math.max(...steps.map((step) => step.strong)),
|
|
145
|
-
loop
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function normalizeSeed(seed: number): number {
|
|
150
|
-
if (!Number.isInteger(seed) || seed < 0) throw new Error("seed must be a non-negative integer.");
|
|
151
|
-
return seed >>> 0;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function finite(value: number, name: string): number {
|
|
155
|
-
if (!Number.isFinite(value)) throw new Error(`${name} must be finite.`);
|
|
156
|
-
return value;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function positive(value: number, name: string): number {
|
|
160
|
-
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be positive.`);
|
|
161
|
-
return value;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function seededUnit(seed: number): number {
|
|
165
|
-
let value = seed >>> 0;
|
|
166
|
-
value ^= value << 13;
|
|
167
|
-
value ^= value >>> 17;
|
|
168
|
-
value ^= value << 5;
|
|
169
|
-
return (value >>> 0) / 0xffffffff;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function stableHash(value: string): string {
|
|
173
|
-
let hash = 2166136261;
|
|
174
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
175
|
-
hash ^= value.charCodeAt(index);
|
|
176
|
-
hash = Math.imul(hash, 16777619);
|
|
177
|
-
}
|
|
178
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
179
|
-
}
|
package/src/GestureRecognizer.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { InputSnapshot, PointerTouch } from "./InputSnapshot";
|
|
2
|
-
|
|
3
|
-
export type Gesture =
|
|
4
|
-
| { readonly type: "tap"; readonly x: number; readonly y: number }
|
|
5
|
-
| { readonly type: "pan"; readonly deltaX: number; readonly deltaY: number }
|
|
6
|
-
| { readonly type: "pinch"; readonly scale: number };
|
|
7
|
-
|
|
8
|
-
export class GestureRecognizer {
|
|
9
|
-
private previousTouches: readonly PointerTouch[] = [];
|
|
10
|
-
private downFrames = 0;
|
|
11
|
-
|
|
12
|
-
update(snapshot: InputSnapshot): readonly Gesture[] {
|
|
13
|
-
const gestures: Gesture[] = [];
|
|
14
|
-
const touches = snapshot.pointer.touches;
|
|
15
|
-
|
|
16
|
-
if (snapshot.button(0).down) {
|
|
17
|
-
this.downFrames += 1;
|
|
18
|
-
if (snapshot.pointer.deltaX !== 0 || snapshot.pointer.deltaY !== 0) {
|
|
19
|
-
gestures.push({ type: "pan", deltaX: snapshot.pointer.deltaX, deltaY: snapshot.pointer.deltaY });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (snapshot.button(0).released && this.downFrames <= 12) {
|
|
24
|
-
gestures.push({ type: "tap", x: snapshot.pointer.x, y: snapshot.pointer.y });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (touches.length >= 2 && this.previousTouches.length >= 2) {
|
|
28
|
-
const previousDistance = distance(this.previousTouches[0]!, this.previousTouches[1]!);
|
|
29
|
-
const currentDistance = distance(touches[0]!, touches[1]!);
|
|
30
|
-
if (previousDistance > 0) gestures.push({ type: "pinch", scale: currentDistance / previousDistance });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!snapshot.button(0).down) {
|
|
34
|
-
this.downFrames = 0;
|
|
35
|
-
}
|
|
36
|
-
this.previousTouches = touches;
|
|
37
|
-
return gestures;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function distance(a: PointerTouch, b: PointerTouch): number {
|
|
42
|
-
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
43
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { InputSnapshot } from "./InputSnapshot";
|
|
2
|
-
|
|
3
|
-
export type InputValueProcessor =
|
|
4
|
-
| { readonly type: "deadzone"; readonly threshold: number }
|
|
5
|
-
| { readonly type: "scale"; readonly factor: number }
|
|
6
|
-
| { readonly type: "invert" }
|
|
7
|
-
| { readonly type: "clamp"; readonly min: number; readonly max: number }
|
|
8
|
-
| { readonly type: "exponential"; readonly exponent: number };
|
|
9
|
-
|
|
10
|
-
export interface InputActionBindingFixture {
|
|
11
|
-
readonly source: "origin-master-input-action-binding-adapted";
|
|
12
|
-
readonly evidence: {
|
|
13
|
-
readonly oldCodebasePort: true;
|
|
14
|
-
readonly actionBindings: true;
|
|
15
|
-
readonly processors: true;
|
|
16
|
-
readonly holdTapDoubleTap: true;
|
|
17
|
-
readonly compositeAxis: true;
|
|
18
|
-
readonly modifierChord: true;
|
|
19
|
-
};
|
|
20
|
-
readonly actionCount: number;
|
|
21
|
-
readonly bindingCount: number;
|
|
22
|
-
readonly processorCount: number;
|
|
23
|
-
readonly processedAxis: number;
|
|
24
|
-
readonly deadzoneFilteredAxis: number;
|
|
25
|
-
readonly compositeX: number;
|
|
26
|
-
readonly compositeY: number;
|
|
27
|
-
readonly compositeMagnitude: number;
|
|
28
|
-
readonly holdTriggered: boolean;
|
|
29
|
-
readonly holdDurationSeconds: number;
|
|
30
|
-
readonly tapTriggered: boolean;
|
|
31
|
-
readonly doubleTapTriggered: boolean;
|
|
32
|
-
readonly modifierChordPressed: boolean;
|
|
33
|
-
readonly claimBoundary: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function processInputValue(value: number, processors: readonly InputValueProcessor[], deltaSeconds = 1 / 60): number {
|
|
37
|
-
let result = finite(value, 0);
|
|
38
|
-
for (const processor of processors) {
|
|
39
|
-
if (processor.type === "deadzone") {
|
|
40
|
-
result = Math.abs(result) < Math.max(0, processor.threshold) ? 0 : result;
|
|
41
|
-
} else if (processor.type === "scale") {
|
|
42
|
-
result *= finite(processor.factor, 1);
|
|
43
|
-
} else if (processor.type === "invert") {
|
|
44
|
-
result *= -1;
|
|
45
|
-
} else if (processor.type === "clamp") {
|
|
46
|
-
result = clamp(result, processor.min, processor.max);
|
|
47
|
-
} else {
|
|
48
|
-
const sign = Math.sign(result);
|
|
49
|
-
result = sign * Math.pow(Math.abs(result), Math.max(0.01, finite(processor.exponent, 1))) * Math.max(0.001, deltaSeconds / (1 / 60));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return Number(result.toFixed(4));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function sampleInputActionBindingFixture(): InputActionBindingFixture {
|
|
56
|
-
const activeSnapshot = new InputSnapshot({
|
|
57
|
-
keys: new Set(["ControlLeft", "KeyW", "KeyD"]),
|
|
58
|
-
previousKeys: new Set(["ControlLeft", "KeyW"]),
|
|
59
|
-
gamepads: [{
|
|
60
|
-
id: "fixture-pad",
|
|
61
|
-
index: 0,
|
|
62
|
-
axes: [0.42, -0.74],
|
|
63
|
-
buttons: [{ down: true, pressed: true, released: false }]
|
|
64
|
-
}]
|
|
65
|
-
});
|
|
66
|
-
const processedAxis = processInputValue(activeSnapshot.gamepads[0]?.axes[0] ?? 0, [
|
|
67
|
-
{ type: "deadzone", threshold: 0.2 },
|
|
68
|
-
{ type: "scale", factor: 1.8 },
|
|
69
|
-
{ type: "clamp", min: -1, max: 1 }
|
|
70
|
-
]);
|
|
71
|
-
const deadzoneFilteredAxis = processInputValue(0.08, [{ type: "deadzone", threshold: 0.2 }]);
|
|
72
|
-
const compositeX = (activeSnapshot.key("KeyD").down ? 1 : 0) - (activeSnapshot.key("KeyA").down ? 1 : 0);
|
|
73
|
-
const compositeY = (activeSnapshot.key("KeyW").down ? 1 : 0) - (activeSnapshot.key("KeyS").down ? 1 : 0);
|
|
74
|
-
const holdDurationSeconds = Number((5 * 0.12).toFixed(2));
|
|
75
|
-
const tapTriggered = evaluateTap(1.2, 1.34, 0.2);
|
|
76
|
-
const doubleTapTriggered = evaluateDoubleTap([1.2, 1.34], 0.25);
|
|
77
|
-
const modifierChordPressed = activeSnapshot.key("ControlLeft").down && activeSnapshot.key("KeyD").pressed;
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
source: "origin-master-input-action-binding-adapted",
|
|
81
|
-
evidence: {
|
|
82
|
-
oldCodebasePort: true,
|
|
83
|
-
actionBindings: true,
|
|
84
|
-
processors: true,
|
|
85
|
-
holdTapDoubleTap: true,
|
|
86
|
-
compositeAxis: true,
|
|
87
|
-
modifierChord: true
|
|
88
|
-
},
|
|
89
|
-
actionCount: 4,
|
|
90
|
-
bindingCount: 8,
|
|
91
|
-
processorCount: 3,
|
|
92
|
-
processedAxis,
|
|
93
|
-
deadzoneFilteredAxis,
|
|
94
|
-
compositeX,
|
|
95
|
-
compositeY,
|
|
96
|
-
compositeMagnitude: Number(Math.hypot(compositeX, compositeY).toFixed(4)),
|
|
97
|
-
holdTriggered: holdDurationSeconds >= 0.5,
|
|
98
|
-
holdDurationSeconds,
|
|
99
|
-
tapTriggered,
|
|
100
|
-
doubleTapTriggered,
|
|
101
|
-
modifierChordPressed,
|
|
102
|
-
claimBoundary: "Bounded deterministic current-input evidence for old InputAction/InputBinding processors and interactions; this does not claim full input action asset, rebinding UI, or Unity Input System parity."
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function evaluateTap(pressedAtSeconds: number, releasedAtSeconds: number, tapSpeedSeconds: number): boolean {
|
|
107
|
-
return releasedAtSeconds >= pressedAtSeconds && releasedAtSeconds - pressedAtSeconds <= tapSpeedSeconds;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function evaluateDoubleTap(pressTimesSeconds: readonly number[], maxIntervalSeconds: number): boolean {
|
|
111
|
-
return pressTimesSeconds.length >= 2 && (pressTimesSeconds.at(-1) ?? 0) - (pressTimesSeconds.at(-2) ?? 0) <= maxIntervalSeconds;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function finite(value: unknown, fallback: number): number {
|
|
115
|
-
const number = Number(value);
|
|
116
|
-
return Number.isFinite(number) ? number : fallback;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function clamp(value: number, min: number, max: number): number {
|
|
120
|
-
return Math.max(min, Math.min(max, value));
|
|
121
|
-
}
|