@aethervtt/shard-input 0.0.1 → 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/dist/devices.d.ts +14 -1
- package/dist/devices.js +11 -0
- package/dist/gestures.d.ts +226 -0
- package/dist/gestures.js +535 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +24 -0
- package/dist/plugin.d.ts +10 -1
- package/dist/plugin.js +48 -4
- package/package.json +5 -5
package/dist/devices.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MouseButton } from '@aethervtt/shard-platform';
|
|
1
|
+
import type { MouseButton, RawInputEvent } from '@aethervtt/shard-platform';
|
|
2
2
|
/** Pressed/just-pressed/just-released bookkeeping for a set of named buttons. */
|
|
3
3
|
declare class ButtonSet<K extends string> {
|
|
4
4
|
private readonly down;
|
|
@@ -74,4 +74,17 @@ export declare class TouchesState {
|
|
|
74
74
|
/** @internal */
|
|
75
75
|
beginFrame(): void;
|
|
76
76
|
}
|
|
77
|
+
/** A raw event the gesture recognizer reads: a pointer, the wheel, or focus. */
|
|
78
|
+
export type PointerInput = Extract<RawInputEvent, {
|
|
79
|
+
type: 'pointer' | 'wheel' | 'focus';
|
|
80
|
+
}>;
|
|
81
|
+
/** Pointer events in CSS pixels (0060), for gestures. Mouse and Touches hold the device state. */
|
|
82
|
+
export declare class PointersState {
|
|
83
|
+
/** This frame's pointer, wheel and focus events, in order. Read-only: they're the raw events. */
|
|
84
|
+
readonly events: PointerInput[];
|
|
85
|
+
/** The last pointer position seen, in CSS pixels: where a wheel without one zooms about. */
|
|
86
|
+
readonly position: [number, number];
|
|
87
|
+
/** @internal */
|
|
88
|
+
beginFrame(): void;
|
|
89
|
+
}
|
|
77
90
|
export {};
|
package/dist/devices.js
CHANGED
|
@@ -126,6 +126,16 @@ class TouchesState {
|
|
|
126
126
|
this.ended.length = 0;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
class PointersState {
|
|
130
|
+
/** This frame's pointer, wheel and focus events, in order. Read-only: they're the raw events. */
|
|
131
|
+
events = [];
|
|
132
|
+
/** The last pointer position seen, in CSS pixels: where a wheel without one zooms about. */
|
|
133
|
+
position = [0, 0];
|
|
134
|
+
/** @internal */
|
|
135
|
+
beginFrame() {
|
|
136
|
+
this.events.length = 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
129
139
|
export {
|
|
130
140
|
GAMEPAD_AXES,
|
|
131
141
|
GAMEPAD_BUTTONS,
|
|
@@ -133,5 +143,6 @@ export {
|
|
|
133
143
|
GamepadsState,
|
|
134
144
|
KeyboardState,
|
|
135
145
|
MouseState,
|
|
146
|
+
PointersState,
|
|
136
147
|
TouchesState
|
|
137
148
|
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
import { type MouseButton, type PointerKind, type PointerPolicy, type RawInputEvent } from '@aethervtt/shard-platform';
|
|
3
|
+
import { type Plugin } from '@aethervtt/shard-runtime';
|
|
4
|
+
export type GestureKind = 'tap' | 'double-tap' | 'long-press' | 'drag-start' | 'drag' | 'drag-end' | 'pinch' | 'twist' | 'pan2' | 'wheel';
|
|
5
|
+
export interface GestureEvent {
|
|
6
|
+
kind: GestureKind;
|
|
7
|
+
/**
|
|
8
|
+
* The gesture this event belongs to: a drag's events share it, and so do a two-finger gesture's.
|
|
9
|
+
* Claims are by id. 0 for the wheel (and a ctrl-wheel pinch), which nothing claims.
|
|
10
|
+
*/
|
|
11
|
+
id: number;
|
|
12
|
+
pointer: PointerKind;
|
|
13
|
+
/** The button the gesture started with ('left' for touch and the wheel). */
|
|
14
|
+
button: MouseButton;
|
|
15
|
+
/** `MODIFIERS` bits held when it started (the wheel: when it turned). */
|
|
16
|
+
modifiers: number;
|
|
17
|
+
/** Where it is now, in CSS pixels; a two-finger gesture's is the fingers' center. */
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
/** Where it started. */
|
|
21
|
+
startX: number;
|
|
22
|
+
startY: number;
|
|
23
|
+
/** Movement since this gesture's previous event (`wheel`: the wheel's, in pixels). */
|
|
24
|
+
dx: number;
|
|
25
|
+
dy: number;
|
|
26
|
+
/** `pinch`: this event's scale factor, above 1 as the fingers spread. 1 otherwise. */
|
|
27
|
+
scale: number;
|
|
28
|
+
/** `twist`: this event's turn in radians, clockwise on screen. 0 otherwise. */
|
|
29
|
+
angle: number;
|
|
30
|
+
/** `drag-end`: the drag was cancelled (Escape, focus loss, a pointercancel). */
|
|
31
|
+
cancelled: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare const Gesture: import("@aethervtt/shard-core").EventDef<GestureEvent>;
|
|
34
|
+
/** Thresholds the recognizer uses. Change them on `world.resource(Gestures).settings`. */
|
|
35
|
+
export interface GestureSettings {
|
|
36
|
+
/** A press that moves at least this far (CSS px) is a drag. */
|
|
37
|
+
dragPx: number;
|
|
38
|
+
/** A second tap within this long and this near the first is a double tap. */
|
|
39
|
+
doubleTapMs: number;
|
|
40
|
+
doubleTapPx: number;
|
|
41
|
+
/** A press held this long without dragging is a long press. */
|
|
42
|
+
longPressMs: number;
|
|
43
|
+
}
|
|
44
|
+
type TrackState = 'pending' | 'pressed' | 'dragging' | 'two' | 'done';
|
|
45
|
+
interface Track {
|
|
46
|
+
pointerId: number;
|
|
47
|
+
gesture: number;
|
|
48
|
+
pointer: PointerKind;
|
|
49
|
+
button: MouseButton;
|
|
50
|
+
modifiers: number;
|
|
51
|
+
startX: number;
|
|
52
|
+
startY: number;
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
downAt: number;
|
|
56
|
+
state: TrackState;
|
|
57
|
+
}
|
|
58
|
+
interface TwoFinger {
|
|
59
|
+
gesture: number;
|
|
60
|
+
a: Track;
|
|
61
|
+
b: Track;
|
|
62
|
+
startX: number;
|
|
63
|
+
startY: number;
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
distance: number;
|
|
67
|
+
angle: number;
|
|
68
|
+
}
|
|
69
|
+
interface ClaimState {
|
|
70
|
+
owner: string | undefined;
|
|
71
|
+
held: number;
|
|
72
|
+
pointers: number[];
|
|
73
|
+
ended: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The gesture recognizer's state, and who owns which gesture. Consumers read `Gesture` events,
|
|
77
|
+
* claim the ones they use on their first event (`claim`), and ignore the rest.
|
|
78
|
+
*/
|
|
79
|
+
export declare class GesturesState {
|
|
80
|
+
readonly settings: GestureSettings;
|
|
81
|
+
/** @internal */ readonly tracks: Map<number, Track>;
|
|
82
|
+
/** @internal */ two: TwoFinger | undefined;
|
|
83
|
+
/** @internal */ readonly claims: Map<number, ClaimState>;
|
|
84
|
+
/** @internal */ lastTap: {
|
|
85
|
+
at: number;
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
button: MouseButton;
|
|
89
|
+
} | undefined;
|
|
90
|
+
/** @internal */ nextId: number;
|
|
91
|
+
/** @internal */ readonly policies: Map<string, PointerPolicy>;
|
|
92
|
+
/** @internal */ policyKey: string;
|
|
93
|
+
/** @internal A policy changed since the source last heard. */
|
|
94
|
+
policyDirty: boolean;
|
|
95
|
+
/** @internal */ world: World | undefined;
|
|
96
|
+
/** @internal Cancels the next frame applies (`cancel()` between frames). */
|
|
97
|
+
cancelRequested: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Takes gesture `id` for `owner`, from its first event on: its pointers are the scene's (the DOM
|
|
100
|
+
* source stops their default handling and captures them). False when someone else owns it.
|
|
101
|
+
*/
|
|
102
|
+
claim(id: number, owner: string): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Takes gesture `id` for `owner` even from another owner (whose next look at `owner(id)` tells
|
|
105
|
+
* it). PlaneDrag takes a drag a fallback control took while the host was still deciding.
|
|
106
|
+
*/
|
|
107
|
+
take(id: number, owner: string): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Says a consumer is deciding about gesture `id` (an async pick): fallback consumers (a control
|
|
110
|
+
* panning on a drag over empty floor) leave it alone until it's claimed or every hold is let go.
|
|
111
|
+
*/
|
|
112
|
+
hold(id: number): void;
|
|
113
|
+
/** Lets go of a `hold`, or of a claim `owner` made. */
|
|
114
|
+
release(id: number, owner?: string): void;
|
|
115
|
+
/** Who owns gesture `id`, if anyone. */
|
|
116
|
+
owner(id: number): string | undefined;
|
|
117
|
+
/** Whether a fallback consumer may claim gesture `id`: it's live, unowned and nobody holds it. */
|
|
118
|
+
free(id: number): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Where gesture `id`'s pointer is now (a two-finger gesture: the center), in CSS pixels. False
|
|
121
|
+
* once it ended. A consumer that claims late (after an async pick) catches up from here.
|
|
122
|
+
*/
|
|
123
|
+
position(id: number, out: {
|
|
124
|
+
[index: number]: number;
|
|
125
|
+
}): boolean;
|
|
126
|
+
/** Whether gesture `id` is still going (its pointers are down). */
|
|
127
|
+
live(id: number): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Ends every active drag and two-finger gesture, with `drag-end { cancelled: true }` for drags,
|
|
130
|
+
* at the start of the next frame. Their pointers are ignored until they lift. The `cancel` action
|
|
131
|
+
* (Escape) calls it.
|
|
132
|
+
*/
|
|
133
|
+
cancel(): void;
|
|
134
|
+
/**
|
|
135
|
+
* What `owner` takes from the pointer while enabled; `undefined` removes it. The DOM source gets
|
|
136
|
+
* the union of every owner's, so it stops default handling for those and nothing else.
|
|
137
|
+
*/
|
|
138
|
+
setPolicy(owner: string, policy: PointerPolicy | undefined): void;
|
|
139
|
+
/** Active pointers, gestures and claims. For agents. */
|
|
140
|
+
describe(): {
|
|
141
|
+
settings: {
|
|
142
|
+
/** A press that moves at least this far (CSS px) is a drag. */
|
|
143
|
+
dragPx: number;
|
|
144
|
+
/** A second tap within this long and this near the first is a double tap. */
|
|
145
|
+
doubleTapMs: number;
|
|
146
|
+
doubleTapPx: number;
|
|
147
|
+
/** A press held this long without dragging is a long press. */
|
|
148
|
+
longPressMs: number;
|
|
149
|
+
};
|
|
150
|
+
pointers: {
|
|
151
|
+
pointer: PointerKind;
|
|
152
|
+
button: MouseButton;
|
|
153
|
+
state: TrackState;
|
|
154
|
+
gesture: number;
|
|
155
|
+
position: number[];
|
|
156
|
+
}[];
|
|
157
|
+
claims: {
|
|
158
|
+
id: number;
|
|
159
|
+
owner: string | null;
|
|
160
|
+
held: boolean;
|
|
161
|
+
}[];
|
|
162
|
+
policy: PointerPolicy;
|
|
163
|
+
};
|
|
164
|
+
private own;
|
|
165
|
+
}
|
|
166
|
+
export declare const Gestures: import("@aethervtt/shard-core").ResourceDef<GesturesState>;
|
|
167
|
+
/** The built-in `cancel` action: Escape ends a drag. Rebind it with `rebindAction`. */
|
|
168
|
+
export declare const GestureActions: import("./actions.js").ActionMapDef<"cancel">;
|
|
169
|
+
/** Turns this frame's pointer events into gestures, after `input/actions` (for Escape). */
|
|
170
|
+
export declare const updateGestures: import("@aethervtt/shard-core").SystemDef<undefined>;
|
|
171
|
+
/**
|
|
172
|
+
* Gestures (0060): `Gesture` events from pointers and the wheel, claims, and the `cancel` action
|
|
173
|
+
* (Escape). Needs `inputPlugin`.
|
|
174
|
+
*/
|
|
175
|
+
export declare const gesturesPlugin: Plugin;
|
|
176
|
+
type Point = readonly [number, number];
|
|
177
|
+
/** A gesture to simulate (`simulateGestures`, the protocol's `input.simulate`), in CSS pixels. */
|
|
178
|
+
export type SimulatedGesture = {
|
|
179
|
+
drag: {
|
|
180
|
+
from: Point;
|
|
181
|
+
to: Point;
|
|
182
|
+
button?: MouseButton;
|
|
183
|
+
pointer?: PointerKind;
|
|
184
|
+
modifiers?: number;
|
|
185
|
+
/** Frames the move takes. Default 10. */
|
|
186
|
+
frames?: number;
|
|
187
|
+
/** Ends with Escape instead of lifting (a cancelled drag). */
|
|
188
|
+
cancel?: boolean;
|
|
189
|
+
};
|
|
190
|
+
} | {
|
|
191
|
+
pinch: {
|
|
192
|
+
center: Point;
|
|
193
|
+
/** Distance between the fingers at the start and the end, px. */
|
|
194
|
+
from: number;
|
|
195
|
+
to: number;
|
|
196
|
+
/** Turn over the gesture, radians clockwise. Default 0. */
|
|
197
|
+
twist?: number;
|
|
198
|
+
/** The center's movement over the gesture (two-finger pan). Default none. */
|
|
199
|
+
pan?: Point;
|
|
200
|
+
frames?: number;
|
|
201
|
+
};
|
|
202
|
+
} | {
|
|
203
|
+
wheel: {
|
|
204
|
+
at: Point;
|
|
205
|
+
dy: number;
|
|
206
|
+
dx?: number;
|
|
207
|
+
modifiers?: number;
|
|
208
|
+
};
|
|
209
|
+
} | {
|
|
210
|
+
tap: Point;
|
|
211
|
+
button?: MouseButton;
|
|
212
|
+
pointer?: PointerKind;
|
|
213
|
+
} | {
|
|
214
|
+
wait: number;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Raw input frames that play `gestures` one after another, a step per frame. Pointers start at
|
|
218
|
+
* id 1,000,000 so they never collide with a live one.
|
|
219
|
+
*/
|
|
220
|
+
export declare function gestureFrames(gestures: readonly SimulatedGesture[]): RawInputEvent[][];
|
|
221
|
+
/**
|
|
222
|
+
* Plays gestures through the input stream, a step per frame from the next one on, so gameplay
|
|
223
|
+
* tests and agents drive controls and drags headless. Returns how many frames they take.
|
|
224
|
+
*/
|
|
225
|
+
export declare function simulateGestures(world: World, gestures: readonly SimulatedGesture[]): number;
|
|
226
|
+
export {};
|
package/dist/gestures.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEvent,
|
|
3
|
+
defineResource,
|
|
4
|
+
defineSystem,
|
|
5
|
+
First,
|
|
6
|
+
ShardError
|
|
7
|
+
} from "@aethervtt/shard-core";
|
|
8
|
+
import {
|
|
9
|
+
MODIFIERS
|
|
10
|
+
} from "@aethervtt/shard-platform";
|
|
11
|
+
import { definePlugin, FrameDemand, Time } from "@aethervtt/shard-runtime";
|
|
12
|
+
import { defineActions } from "./actions.js";
|
|
13
|
+
import {
|
|
14
|
+
addActions,
|
|
15
|
+
InputQueue,
|
|
16
|
+
Mouse,
|
|
17
|
+
Pointers,
|
|
18
|
+
scheduleInput,
|
|
19
|
+
Touches,
|
|
20
|
+
updateActions
|
|
21
|
+
} from "./plugin.js";
|
|
22
|
+
const Gesture = defineEvent("input/Gesture", {
|
|
23
|
+
description: "Taps, long presses, drags, pinches, twists, two-finger pans and the wheel, in CSS pixels (0060)."
|
|
24
|
+
});
|
|
25
|
+
class GesturesState {
|
|
26
|
+
settings = {
|
|
27
|
+
dragPx: 4,
|
|
28
|
+
doubleTapMs: 300,
|
|
29
|
+
doubleTapPx: 6,
|
|
30
|
+
longPressMs: 500
|
|
31
|
+
};
|
|
32
|
+
/** @internal */
|
|
33
|
+
tracks = /* @__PURE__ */ new Map();
|
|
34
|
+
/** @internal */
|
|
35
|
+
two = void 0;
|
|
36
|
+
/** @internal */
|
|
37
|
+
claims = /* @__PURE__ */ new Map();
|
|
38
|
+
/** @internal */
|
|
39
|
+
lastTap;
|
|
40
|
+
/** @internal */
|
|
41
|
+
nextId = 1;
|
|
42
|
+
/** @internal */
|
|
43
|
+
policies = /* @__PURE__ */ new Map();
|
|
44
|
+
/** @internal */
|
|
45
|
+
policyKey = "";
|
|
46
|
+
/** @internal A policy changed since the source last heard. */
|
|
47
|
+
policyDirty = false;
|
|
48
|
+
/** @internal */
|
|
49
|
+
world = void 0;
|
|
50
|
+
/** @internal Cancels the next frame applies (`cancel()` between frames). */
|
|
51
|
+
cancelRequested = false;
|
|
52
|
+
/**
|
|
53
|
+
* Takes gesture `id` for `owner`, from its first event on: its pointers are the scene's (the DOM
|
|
54
|
+
* source stops their default handling and captures them). False when someone else owns it.
|
|
55
|
+
*/
|
|
56
|
+
claim(id, owner) {
|
|
57
|
+
const c = this.claims.get(id);
|
|
58
|
+
if (!c || c.ended) return false;
|
|
59
|
+
if (c.owner !== void 0 && c.owner !== owner) return false;
|
|
60
|
+
this.own(c, owner);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Takes gesture `id` for `owner` even from another owner (whose next look at `owner(id)` tells
|
|
65
|
+
* it). PlaneDrag takes a drag a fallback control took while the host was still deciding.
|
|
66
|
+
*/
|
|
67
|
+
take(id, owner) {
|
|
68
|
+
const c = this.claims.get(id);
|
|
69
|
+
if (!c || c.ended) return false;
|
|
70
|
+
this.own(c, owner);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Says a consumer is deciding about gesture `id` (an async pick): fallback consumers (a control
|
|
75
|
+
* panning on a drag over empty floor) leave it alone until it's claimed or every hold is let go.
|
|
76
|
+
*/
|
|
77
|
+
hold(id) {
|
|
78
|
+
const c = this.claims.get(id);
|
|
79
|
+
if (c && !c.ended) c.held++;
|
|
80
|
+
}
|
|
81
|
+
/** Lets go of a `hold`, or of a claim `owner` made. */
|
|
82
|
+
release(id, owner) {
|
|
83
|
+
const c = this.claims.get(id);
|
|
84
|
+
if (!c) return;
|
|
85
|
+
if (owner === void 0) c.held = Math.max(0, c.held - 1);
|
|
86
|
+
else if (c.owner === owner) c.owner = void 0;
|
|
87
|
+
}
|
|
88
|
+
/** Who owns gesture `id`, if anyone. */
|
|
89
|
+
owner(id) {
|
|
90
|
+
return this.claims.get(id)?.owner;
|
|
91
|
+
}
|
|
92
|
+
/** Whether a fallback consumer may claim gesture `id`: it's live, unowned and nobody holds it. */
|
|
93
|
+
free(id) {
|
|
94
|
+
const c = this.claims.get(id);
|
|
95
|
+
return c !== void 0 && !c.ended && c.owner === void 0 && c.held === 0;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Where gesture `id`'s pointer is now (a two-finger gesture: the center), in CSS pixels. False
|
|
99
|
+
* once it ended. A consumer that claims late (after an async pick) catches up from here.
|
|
100
|
+
*/
|
|
101
|
+
position(id, out) {
|
|
102
|
+
for (const t of this.tracks.values()) {
|
|
103
|
+
if (t.gesture !== id || t.state === "done") continue;
|
|
104
|
+
out[0] = t.x;
|
|
105
|
+
out[1] = t.y;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (this.two?.gesture === id) {
|
|
109
|
+
out[0] = this.two.x;
|
|
110
|
+
out[1] = this.two.y;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/** Whether gesture `id` is still going (its pointers are down). */
|
|
116
|
+
live(id) {
|
|
117
|
+
const c = this.claims.get(id);
|
|
118
|
+
return c !== void 0 && !c.ended;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Ends every active drag and two-finger gesture, with `drag-end { cancelled: true }` for drags,
|
|
122
|
+
* at the start of the next frame. Their pointers are ignored until they lift. The `cancel` action
|
|
123
|
+
* (Escape) calls it.
|
|
124
|
+
*/
|
|
125
|
+
cancel() {
|
|
126
|
+
this.cancelRequested = true;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* What `owner` takes from the pointer while enabled; `undefined` removes it. The DOM source gets
|
|
130
|
+
* the union of every owner's, so it stops default handling for those and nothing else.
|
|
131
|
+
*/
|
|
132
|
+
setPolicy(owner, policy) {
|
|
133
|
+
if (policy) this.policies.set(owner, policy);
|
|
134
|
+
else if (!this.policies.delete(owner)) return;
|
|
135
|
+
this.policyDirty = true;
|
|
136
|
+
}
|
|
137
|
+
/** Active pointers, gestures and claims. For agents. */
|
|
138
|
+
describe() {
|
|
139
|
+
return {
|
|
140
|
+
settings: { ...this.settings },
|
|
141
|
+
pointers: [...this.tracks.values()].map((t) => ({
|
|
142
|
+
pointer: t.pointer,
|
|
143
|
+
button: t.button,
|
|
144
|
+
state: t.state,
|
|
145
|
+
gesture: t.gesture,
|
|
146
|
+
position: [t.x, t.y]
|
|
147
|
+
})),
|
|
148
|
+
claims: [...this.claims].filter(([, c]) => !c.ended).map(([id, c]) => ({ id, owner: c.owner ?? null, held: c.held > 0 })),
|
|
149
|
+
policy: union(this.policies)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
own(c, owner) {
|
|
153
|
+
c.owner = owner;
|
|
154
|
+
const source = this.world?.tryResource(InputQueue)?.source;
|
|
155
|
+
for (const p of c.pointers) source?.claimPointer?.(p);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const Gestures = defineResource("input/Gestures", {
|
|
159
|
+
description: "Gesture recognizer (0060): thresholds, active pointers, and who claimed which gesture."
|
|
160
|
+
});
|
|
161
|
+
const GestureActions = defineActions(
|
|
162
|
+
"input/GestureActions",
|
|
163
|
+
{ cancel: { kind: "button", bindings: ["Key:Escape"] } },
|
|
164
|
+
{ context: "any" }
|
|
165
|
+
);
|
|
166
|
+
const BUTTON_ORDER = ["left", "middle", "right", "back", "forward"];
|
|
167
|
+
function union(policies) {
|
|
168
|
+
const buttons = /* @__PURE__ */ new Set();
|
|
169
|
+
let wheel2 = false;
|
|
170
|
+
let touch = false;
|
|
171
|
+
for (const p of policies.values()) {
|
|
172
|
+
for (const b of p.buttons) buttons.add(b);
|
|
173
|
+
wheel2 ||= p.wheel;
|
|
174
|
+
touch ||= p.touch;
|
|
175
|
+
}
|
|
176
|
+
return { buttons: BUTTON_ORDER.filter((b) => buttons.has(b)), wheel: wheel2, touch };
|
|
177
|
+
}
|
|
178
|
+
function send(world, kind, id, t, x, y, startX, startY, dx, dy, scale = 1, angle = 0, cancelled = false) {
|
|
179
|
+
world.send(Gesture, {
|
|
180
|
+
kind,
|
|
181
|
+
id,
|
|
182
|
+
pointer: t.pointer,
|
|
183
|
+
button: t.button,
|
|
184
|
+
modifiers: t.modifiers,
|
|
185
|
+
x,
|
|
186
|
+
y,
|
|
187
|
+
startX,
|
|
188
|
+
startY,
|
|
189
|
+
dx,
|
|
190
|
+
dy,
|
|
191
|
+
scale,
|
|
192
|
+
angle,
|
|
193
|
+
cancelled
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function endDrag(world, s, t, cancelled) {
|
|
197
|
+
if (t.state === "dragging")
|
|
198
|
+
send(world, "drag-end", t.gesture, t, t.x, t.y, t.startX, t.startY, 0, 0, 1, 0, cancelled);
|
|
199
|
+
end(s, t.gesture);
|
|
200
|
+
}
|
|
201
|
+
function end(s, id) {
|
|
202
|
+
const c = s.claims.get(id);
|
|
203
|
+
if (c) c.ended = true;
|
|
204
|
+
}
|
|
205
|
+
function cancelAll(world, s) {
|
|
206
|
+
for (const t of s.tracks.values()) {
|
|
207
|
+
if (t.state === "dragging") endDrag(world, s, t, true);
|
|
208
|
+
else if (t.state !== "done") end(s, t.gesture);
|
|
209
|
+
t.state = "done";
|
|
210
|
+
}
|
|
211
|
+
if (s.two) {
|
|
212
|
+
end(s, s.two.gesture);
|
|
213
|
+
s.two = void 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function startTwo(world, s, a, b) {
|
|
217
|
+
for (const t of [a, b]) {
|
|
218
|
+
if (t.state === "dragging") endDrag(world, s, t, false);
|
|
219
|
+
else end(s, t.gesture);
|
|
220
|
+
t.state = "two";
|
|
221
|
+
}
|
|
222
|
+
const id = s.nextId++;
|
|
223
|
+
s.claims.set(id, {
|
|
224
|
+
owner: void 0,
|
|
225
|
+
held: 0,
|
|
226
|
+
pointers: [a.pointerId, b.pointerId],
|
|
227
|
+
ended: false
|
|
228
|
+
});
|
|
229
|
+
const x = (a.x + b.x) / 2;
|
|
230
|
+
const y = (a.y + b.y) / 2;
|
|
231
|
+
s.two = {
|
|
232
|
+
gesture: id,
|
|
233
|
+
a,
|
|
234
|
+
b,
|
|
235
|
+
startX: x,
|
|
236
|
+
startY: y,
|
|
237
|
+
x,
|
|
238
|
+
y,
|
|
239
|
+
distance: Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2),
|
|
240
|
+
angle: Math.atan2(b.y - a.y, b.x - a.x)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function moveTwo(world, two) {
|
|
244
|
+
const { a, b } = two;
|
|
245
|
+
const x = (a.x + b.x) / 2;
|
|
246
|
+
const y = (a.y + b.y) / 2;
|
|
247
|
+
const distance = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
|
|
248
|
+
const angle = Math.atan2(b.y - a.y, b.x - a.x);
|
|
249
|
+
if (x !== two.x || y !== two.y)
|
|
250
|
+
send(world, "pan2", two.gesture, a, x, y, two.startX, two.startY, x - two.x, y - two.y);
|
|
251
|
+
if (distance > 0 && two.distance > 0 && distance !== two.distance)
|
|
252
|
+
send(
|
|
253
|
+
world,
|
|
254
|
+
"pinch",
|
|
255
|
+
two.gesture,
|
|
256
|
+
a,
|
|
257
|
+
x,
|
|
258
|
+
y,
|
|
259
|
+
two.startX,
|
|
260
|
+
two.startY,
|
|
261
|
+
0,
|
|
262
|
+
0,
|
|
263
|
+
distance / two.distance
|
|
264
|
+
);
|
|
265
|
+
let turn = angle - two.angle;
|
|
266
|
+
if (turn > Math.PI) turn -= 2 * Math.PI;
|
|
267
|
+
else if (turn < -Math.PI) turn += 2 * Math.PI;
|
|
268
|
+
if (turn !== 0) send(world, "twist", two.gesture, a, x, y, two.startX, two.startY, 0, 0, 1, turn);
|
|
269
|
+
two.x = x;
|
|
270
|
+
two.y = y;
|
|
271
|
+
two.distance = distance;
|
|
272
|
+
two.angle = angle;
|
|
273
|
+
}
|
|
274
|
+
function down(world, s, e, now, ui) {
|
|
275
|
+
const old = s.tracks.get(e.id);
|
|
276
|
+
if (old) up(world, s, old, now, true);
|
|
277
|
+
const id = s.nextId++;
|
|
278
|
+
const t = {
|
|
279
|
+
pointerId: e.id,
|
|
280
|
+
gesture: id,
|
|
281
|
+
pointer: e.pointer,
|
|
282
|
+
button: e.pointer === "touch" ? "left" : e.button,
|
|
283
|
+
modifiers: e.modifiers,
|
|
284
|
+
startX: e.x,
|
|
285
|
+
startY: e.y,
|
|
286
|
+
x: e.x,
|
|
287
|
+
y: e.y,
|
|
288
|
+
downAt: now,
|
|
289
|
+
state: "pending"
|
|
290
|
+
};
|
|
291
|
+
s.tracks.set(e.id, t);
|
|
292
|
+
s.claims.set(id, { owner: ui ? "ui" : void 0, held: 0, pointers: [e.id], ended: false });
|
|
293
|
+
if (e.pointer !== "touch") return;
|
|
294
|
+
let touching = 0;
|
|
295
|
+
let other;
|
|
296
|
+
for (const u of s.tracks.values()) {
|
|
297
|
+
if (u.pointer !== "touch") continue;
|
|
298
|
+
touching++;
|
|
299
|
+
if (u !== t && u.state !== "done") other = u;
|
|
300
|
+
}
|
|
301
|
+
if (touching === 2 && other && !s.two) startTwo(world, s, other, t);
|
|
302
|
+
else if (touching > 2) t.state = "done";
|
|
303
|
+
}
|
|
304
|
+
function move(world, s, t, x, y) {
|
|
305
|
+
const dx = x - t.x;
|
|
306
|
+
const dy = y - t.y;
|
|
307
|
+
t.x = x;
|
|
308
|
+
t.y = y;
|
|
309
|
+
if (t.state === "two") {
|
|
310
|
+
if (s.two && (s.two.a === t || s.two.b === t)) moveTwo(world, s.two);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (t.state === "dragging") {
|
|
314
|
+
send(world, "drag", t.gesture, t, x, y, t.startX, t.startY, dx, dy);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (t.state !== "pending" && t.state !== "pressed") return;
|
|
318
|
+
const ox = x - t.startX;
|
|
319
|
+
const oy = y - t.startY;
|
|
320
|
+
if (ox * ox + oy * oy < s.settings.dragPx * s.settings.dragPx) return;
|
|
321
|
+
t.state = "dragging";
|
|
322
|
+
send(world, "drag-start", t.gesture, t, x, y, t.startX, t.startY, ox, oy);
|
|
323
|
+
}
|
|
324
|
+
function up(world, s, t, now, cancelled) {
|
|
325
|
+
s.tracks.delete(t.pointerId);
|
|
326
|
+
if (t.state === "dragging") {
|
|
327
|
+
endDrag(world, s, t, cancelled);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (t.state === "two") {
|
|
331
|
+
const two = s.two;
|
|
332
|
+
if (two && (two.a === t || two.b === t)) {
|
|
333
|
+
end(s, two.gesture);
|
|
334
|
+
const rest = two.a === t ? two.b : two.a;
|
|
335
|
+
rest.state = "done";
|
|
336
|
+
s.two = void 0;
|
|
337
|
+
}
|
|
338
|
+
end(s, t.gesture);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
end(s, t.gesture);
|
|
342
|
+
if (t.state !== "pending" || cancelled) return;
|
|
343
|
+
send(world, "tap", t.gesture, t, t.x, t.y, t.startX, t.startY, 0, 0);
|
|
344
|
+
const last = s.lastTap;
|
|
345
|
+
const { doubleTapMs, doubleTapPx } = s.settings;
|
|
346
|
+
if (last && last.button === t.button && now - last.at <= doubleTapMs && (t.x - last.x) ** 2 + (t.y - last.y) ** 2 <= doubleTapPx * doubleTapPx) {
|
|
347
|
+
send(world, "double-tap", t.gesture, t, t.x, t.y, t.startX, t.startY, 0, 0);
|
|
348
|
+
s.lastTap = void 0;
|
|
349
|
+
} else {
|
|
350
|
+
s.lastTap = { at: now, x: t.x, y: t.y, button: t.button };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const WHEEL = { pointer: "mouse", button: "left", modifiers: 0 };
|
|
354
|
+
function wheel(world, e, fx, fy) {
|
|
355
|
+
const x = e.x ?? fx;
|
|
356
|
+
const y = e.y ?? fy;
|
|
357
|
+
WHEEL.modifiers = e.modifiers ?? 0;
|
|
358
|
+
if ((WHEEL.modifiers & MODIFIERS.ctrl) !== 0)
|
|
359
|
+
send(world, "pinch", 0, WHEEL, x, y, x, y, 0, 0, Math.exp(-e.dy * 0.01));
|
|
360
|
+
else send(world, "wheel", 0, WHEEL, x, y, x, y, e.dx, e.dy);
|
|
361
|
+
}
|
|
362
|
+
function onUi(world, e) {
|
|
363
|
+
if (e.pointer === "touch") return world.tryResource(Touches)?.captured.has(e.id) ?? false;
|
|
364
|
+
return world.tryResource(Mouse)?.captured ?? false;
|
|
365
|
+
}
|
|
366
|
+
function apply(world, s, e, now, fx, fy) {
|
|
367
|
+
if (e.type === "wheel") {
|
|
368
|
+
wheel(world, e, fx, fy);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (e.type === "focus") {
|
|
372
|
+
if (!e.focused) {
|
|
373
|
+
cancelAll(world, s);
|
|
374
|
+
s.tracks.clear();
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (e.phase === "down") {
|
|
379
|
+
down(world, s, e, now, onUi(world, e));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const t = s.tracks.get(e.id);
|
|
383
|
+
if (!t) return;
|
|
384
|
+
if (e.phase === "move") move(world, s, t, e.x, e.y);
|
|
385
|
+
else {
|
|
386
|
+
if (e.x !== t.x || e.y !== t.y) move(world, s, t, e.x, e.y);
|
|
387
|
+
up(world, s, t, now, e.phase === "cancel");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const updateGestures = defineSystem({
|
|
391
|
+
name: "input/gestures",
|
|
392
|
+
description: "Recognizes taps, presses, drags and two-finger gestures from pointer events.",
|
|
393
|
+
run: (_, world) => {
|
|
394
|
+
const s = world.resource(Gestures);
|
|
395
|
+
if (s.claims.size > 0) {
|
|
396
|
+
for (const [id, c] of s.claims) if (c.ended) s.claims.delete(id);
|
|
397
|
+
}
|
|
398
|
+
const now = world.resource(Time).elapsed * 1e3;
|
|
399
|
+
const actions = world.tryResource(GestureActions.resource);
|
|
400
|
+
if (s.cancelRequested) {
|
|
401
|
+
s.cancelRequested = false;
|
|
402
|
+
cancelAll(world, s);
|
|
403
|
+
}
|
|
404
|
+
const pointers = world.resource(Pointers);
|
|
405
|
+
const fx = pointers.position[0];
|
|
406
|
+
const fy = pointers.position[1];
|
|
407
|
+
for (const e of pointers.events) apply(world, s, e, now, fx, fy);
|
|
408
|
+
if (actions?.justPressed("cancel")) cancelAll(world, s);
|
|
409
|
+
let soonest = Number.POSITIVE_INFINITY;
|
|
410
|
+
if (s.tracks.size > 0)
|
|
411
|
+
for (const t of s.tracks.values()) {
|
|
412
|
+
if (t.state !== "pending") continue;
|
|
413
|
+
const left = t.downAt + s.settings.longPressMs - now;
|
|
414
|
+
if (left <= 0) {
|
|
415
|
+
t.state = "pressed";
|
|
416
|
+
send(world, "long-press", t.gesture, t, t.x, t.y, t.startX, t.startY, 0, 0);
|
|
417
|
+
} else if (left < soonest) soonest = left;
|
|
418
|
+
}
|
|
419
|
+
if (soonest !== Number.POSITIVE_INFINITY) world.resource(FrameDemand).after(soonest);
|
|
420
|
+
syncPolicy(world, s);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
function syncPolicy(world, s) {
|
|
424
|
+
if (!s.policyDirty) return;
|
|
425
|
+
s.policyDirty = false;
|
|
426
|
+
const policy = union(s.policies);
|
|
427
|
+
const key = `${policy.buttons.join(",")}|${policy.wheel}|${policy.touch}`;
|
|
428
|
+
if (key === s.policyKey) return;
|
|
429
|
+
s.policyKey = key;
|
|
430
|
+
world.resource(InputQueue).source?.setPointerPolicy?.(policy);
|
|
431
|
+
}
|
|
432
|
+
const gesturesPlugin = definePlugin({
|
|
433
|
+
name: "input/gestures",
|
|
434
|
+
dependencies: ["input"],
|
|
435
|
+
provides: [Gestures, Gesture, GestureActions.resource, updateGestures],
|
|
436
|
+
build(app) {
|
|
437
|
+
const state = new GesturesState();
|
|
438
|
+
state.world = app.world;
|
|
439
|
+
app.insertResource(Gestures, state);
|
|
440
|
+
addActions(app.world, GestureActions);
|
|
441
|
+
app.addSystems(First, updateGestures.after(updateActions));
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
const SIM_ID = 1e6;
|
|
445
|
+
function gestureFrames(gestures) {
|
|
446
|
+
const frames = [];
|
|
447
|
+
let id = SIM_ID;
|
|
448
|
+
const p = (pid, phase, x, y, pointer = "mouse", button = "left", modifiers = 0) => ({ type: "pointer", id: pid, phase, pointer, button, x, y, modifiers });
|
|
449
|
+
for (const g of gestures) {
|
|
450
|
+
if ("drag" in g) {
|
|
451
|
+
const d = g.drag;
|
|
452
|
+
const n = Math.max(1, d.frames ?? 10);
|
|
453
|
+
const pid = id++;
|
|
454
|
+
const kind = d.pointer ?? "mouse";
|
|
455
|
+
const button = d.button ?? "left";
|
|
456
|
+
frames.push([p(pid, "down", d.from[0], d.from[1], kind, button, d.modifiers)]);
|
|
457
|
+
for (let i = 1; i <= n; i++) {
|
|
458
|
+
const t = i / n;
|
|
459
|
+
const x = d.from[0] + (d.to[0] - d.from[0]) * t;
|
|
460
|
+
const y = d.from[1] + (d.to[1] - d.from[1]) * t;
|
|
461
|
+
frames.push([p(pid, "move", x, y, kind, button, d.modifiers)]);
|
|
462
|
+
}
|
|
463
|
+
if (d.cancel) {
|
|
464
|
+
frames.push([{ type: "key", code: "Escape", pressed: true }]);
|
|
465
|
+
frames.push([{ type: "key", code: "Escape", pressed: false }]);
|
|
466
|
+
}
|
|
467
|
+
frames.push([p(pid, "up", d.to[0], d.to[1], kind, button, d.modifiers)]);
|
|
468
|
+
} else if ("pinch" in g) {
|
|
469
|
+
const q = g.pinch;
|
|
470
|
+
const n = Math.max(1, q.frames ?? 10);
|
|
471
|
+
const a = id++;
|
|
472
|
+
const b = id++;
|
|
473
|
+
const at = (t) => {
|
|
474
|
+
const r = (q.from + (q.to - q.from) * t) / 2;
|
|
475
|
+
const turn = (q.twist ?? 0) * t;
|
|
476
|
+
const cx = q.center[0] + (q.pan?.[0] ?? 0) * t;
|
|
477
|
+
const cy = q.center[1] + (q.pan?.[1] ?? 0) * t;
|
|
478
|
+
const c = Math.cos(turn) * r;
|
|
479
|
+
const s = Math.sin(turn) * r;
|
|
480
|
+
return [cx - c, cy - s, cx + c, cy + s];
|
|
481
|
+
};
|
|
482
|
+
const start = at(0);
|
|
483
|
+
frames.push([
|
|
484
|
+
p(a, "down", start[0], start[1], "touch"),
|
|
485
|
+
p(b, "down", start[2], start[3], "touch")
|
|
486
|
+
]);
|
|
487
|
+
for (let i = 1; i <= n; i++) {
|
|
488
|
+
const f = at(i / n);
|
|
489
|
+
frames.push([p(a, "move", f[0], f[1], "touch"), p(b, "move", f[2], f[3], "touch")]);
|
|
490
|
+
}
|
|
491
|
+
const end2 = at(1);
|
|
492
|
+
frames.push([p(a, "up", end2[0], end2[1], "touch"), p(b, "up", end2[2], end2[3], "touch")]);
|
|
493
|
+
} else if ("wheel" in g) {
|
|
494
|
+
const w = g.wheel;
|
|
495
|
+
frames.push([
|
|
496
|
+
{
|
|
497
|
+
type: "wheel",
|
|
498
|
+
dx: w.dx ?? 0,
|
|
499
|
+
dy: w.dy,
|
|
500
|
+
x: w.at[0],
|
|
501
|
+
y: w.at[1],
|
|
502
|
+
modifiers: w.modifiers ?? 0
|
|
503
|
+
}
|
|
504
|
+
]);
|
|
505
|
+
} else if ("tap" in g) {
|
|
506
|
+
const pid = id++;
|
|
507
|
+
const kind = g.pointer ?? "mouse";
|
|
508
|
+
const button = g.button ?? "left";
|
|
509
|
+
frames.push([p(pid, "down", g.tap[0], g.tap[1], kind, button)]);
|
|
510
|
+
frames.push([p(pid, "up", g.tap[0], g.tap[1], kind, button)]);
|
|
511
|
+
} else if ("wait" in g) {
|
|
512
|
+
for (let i = 0; i < g.wait; i++) frames.push([]);
|
|
513
|
+
} else {
|
|
514
|
+
throw new ShardError("input/unknown-gesture", `Can't simulate ${JSON.stringify(g)}`, {
|
|
515
|
+
hint: "Simulate { drag }, { pinch }, { wheel }, { tap } or { wait }."
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return frames;
|
|
520
|
+
}
|
|
521
|
+
function simulateGestures(world, gestures) {
|
|
522
|
+
const frames = gestureFrames(gestures);
|
|
523
|
+
scheduleInput(world, frames);
|
|
524
|
+
return frames.length;
|
|
525
|
+
}
|
|
526
|
+
export {
|
|
527
|
+
Gesture,
|
|
528
|
+
GestureActions,
|
|
529
|
+
Gestures,
|
|
530
|
+
GesturesState,
|
|
531
|
+
gestureFrames,
|
|
532
|
+
gesturesPlugin,
|
|
533
|
+
simulateGestures,
|
|
534
|
+
updateGestures
|
|
535
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { type ActionDef, type ActionMapDef, ActionState, type Axis1dAction, type Axis2dAction, actionMapSchema, type ButtonAction, defineActions, type Interaction, } from './actions.js';
|
|
2
2
|
export { type ParsedBinding, parseBinding } from './bindings.js';
|
|
3
|
-
export { GAMEPAD_AXES, GAMEPAD_BUTTONS, type GamepadAxis, type GamepadButton, GamepadState, GamepadsState, KeyboardState, MouseState, type Touch, TouchesState, } from './devices.js';
|
|
4
|
-
export {
|
|
3
|
+
export { GAMEPAD_AXES, GAMEPAD_BUTTONS, type GamepadAxis, type GamepadButton, GamepadState, GamepadsState, KeyboardState, MouseState, type PointerInput, PointersState, type Touch, TouchesState, } from './devices.js';
|
|
4
|
+
export { Gesture, GestureActions, type GestureEvent, type GestureKind, type GestureSettings, Gestures, GesturesState, gestureFrames, gesturesPlugin, type SimulatedGesture, simulateGestures, updateGestures, } from './gestures.js';
|
|
5
|
+
export { addActions, describeInput, Gamepads, type InjectedInput, InputContext, type InputContextValue, type InputPluginOptions, InputQueue, injectInput, inputPlugin, Keyboard, Mouse, Pointers, rebindAction, scheduleInput, startRecording, startReplay, stopRecording, Touches, updateActions, updateInput, } from './plugin.js';
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,19 @@ import {
|
|
|
11
11
|
GamepadsState,
|
|
12
12
|
KeyboardState,
|
|
13
13
|
MouseState,
|
|
14
|
+
PointersState,
|
|
14
15
|
TouchesState
|
|
15
16
|
} from "./devices.js";
|
|
17
|
+
import {
|
|
18
|
+
Gesture,
|
|
19
|
+
GestureActions,
|
|
20
|
+
Gestures,
|
|
21
|
+
GesturesState,
|
|
22
|
+
gestureFrames,
|
|
23
|
+
gesturesPlugin,
|
|
24
|
+
simulateGestures,
|
|
25
|
+
updateGestures
|
|
26
|
+
} from "./gestures.js";
|
|
16
27
|
import {
|
|
17
28
|
addActions,
|
|
18
29
|
describeInput,
|
|
@@ -23,7 +34,9 @@ import {
|
|
|
23
34
|
inputPlugin,
|
|
24
35
|
Keyboard,
|
|
25
36
|
Mouse,
|
|
37
|
+
Pointers,
|
|
26
38
|
rebindAction,
|
|
39
|
+
scheduleInput,
|
|
27
40
|
startRecording,
|
|
28
41
|
startReplay,
|
|
29
42
|
stopRecording,
|
|
@@ -38,25 +51,36 @@ export {
|
|
|
38
51
|
GamepadState,
|
|
39
52
|
Gamepads,
|
|
40
53
|
GamepadsState,
|
|
54
|
+
Gesture,
|
|
55
|
+
GestureActions,
|
|
56
|
+
Gestures,
|
|
57
|
+
GesturesState,
|
|
41
58
|
InputContext,
|
|
42
59
|
InputQueue,
|
|
43
60
|
Keyboard,
|
|
44
61
|
KeyboardState,
|
|
45
62
|
Mouse,
|
|
46
63
|
MouseState,
|
|
64
|
+
Pointers,
|
|
65
|
+
PointersState,
|
|
47
66
|
Touches,
|
|
48
67
|
TouchesState,
|
|
49
68
|
actionMapSchema,
|
|
50
69
|
addActions,
|
|
51
70
|
defineActions,
|
|
52
71
|
describeInput,
|
|
72
|
+
gestureFrames,
|
|
73
|
+
gesturesPlugin,
|
|
53
74
|
injectInput,
|
|
54
75
|
inputPlugin,
|
|
55
76
|
parseBinding,
|
|
56
77
|
rebindAction,
|
|
78
|
+
scheduleInput,
|
|
79
|
+
simulateGestures,
|
|
57
80
|
startRecording,
|
|
58
81
|
startReplay,
|
|
59
82
|
stopRecording,
|
|
60
83
|
updateActions,
|
|
84
|
+
updateGestures,
|
|
61
85
|
updateInput
|
|
62
86
|
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { type World } from '@aethervtt/shard-core';
|
|
|
2
2
|
import type { InputSource, RawInputEvent } from '@aethervtt/shard-platform';
|
|
3
3
|
import { type Plugin } from '@aethervtt/shard-runtime';
|
|
4
4
|
import type { ActionMapDef, ActionState } from './actions.js';
|
|
5
|
-
import { GamepadsState, KeyboardState, MouseState, TouchesState } from './devices.js';
|
|
5
|
+
import { GamepadsState, KeyboardState, MouseState, PointersState, TouchesState } from './devices.js';
|
|
6
6
|
export declare const Keyboard: import("@aethervtt/shard-core").ResourceDef<KeyboardState>;
|
|
7
7
|
export declare const Mouse: import("@aethervtt/shard-core").ResourceDef<MouseState>;
|
|
8
8
|
export declare const Gamepads: import("@aethervtt/shard-core").ResourceDef<GamepadsState>;
|
|
9
9
|
export declare const Touches: import("@aethervtt/shard-core").ResourceDef<TouchesState>;
|
|
10
|
+
export declare const Pointers: import("@aethervtt/shard-core").ResourceDef<PointersState>;
|
|
10
11
|
interface InputQueueState {
|
|
11
12
|
source: InputSource | undefined;
|
|
12
13
|
/** Injected events, applied at the start of the next frame. */
|
|
@@ -21,6 +22,8 @@ interface InputQueueState {
|
|
|
21
22
|
frame: number;
|
|
22
23
|
length: number;
|
|
23
24
|
} | undefined;
|
|
25
|
+
/** Simulated input still to come (`simulateInput`), one entry per frame, merged with live input. */
|
|
26
|
+
scheduled: RawInputEvent[][];
|
|
24
27
|
}
|
|
25
28
|
export declare const InputQueue: import("@aethervtt/shard-core").ResourceDef<InputQueueState>;
|
|
26
29
|
export interface InputPluginOptions {
|
|
@@ -66,6 +69,11 @@ export type InjectedInput = RawInputEvent | {
|
|
|
66
69
|
* input (so it's recorded and replayed too). Actions are named `"<map>.<action>"`.
|
|
67
70
|
*/
|
|
68
71
|
export declare function injectInput(world: World, input: InjectedInput): void;
|
|
72
|
+
/**
|
|
73
|
+
* Queues raw input over the next frames, one entry per frame (empty entries wait a frame),
|
|
74
|
+
* after anything already queued. Gesture simulation (`simulateGestures`) builds on it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function scheduleInput(world: World, frames: readonly (readonly RawInputEvent[])[]): void;
|
|
69
77
|
export declare function startRecording(world: World): void;
|
|
70
78
|
/** Stops recording and returns it as JSON lines: `{"f":<frame>,"e":[...events]}` per non-empty frame. */
|
|
71
79
|
export declare function stopRecording(world: World): string;
|
|
@@ -79,6 +87,7 @@ export declare function describeInput(world: World): {
|
|
|
79
87
|
keysHeld: string[];
|
|
80
88
|
gamepads: number;
|
|
81
89
|
touches: number;
|
|
90
|
+
scheduledFrames: number;
|
|
82
91
|
context: string;
|
|
83
92
|
pointerCaptured: boolean;
|
|
84
93
|
actionMaps: {
|
package/dist/plugin.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defineResource, defineSystem, First, ShardError } from "@aethervtt/shard-core";
|
|
2
|
-
import { definePlugin, Time } from "@aethervtt/shard-runtime";
|
|
2
|
+
import { definePlugin, FrameDemand, Time } from "@aethervtt/shard-runtime";
|
|
3
3
|
import {
|
|
4
4
|
GAMEPAD_AXES,
|
|
5
5
|
GAMEPAD_BUTTONS,
|
|
6
6
|
GamepadsState,
|
|
7
7
|
KeyboardState,
|
|
8
8
|
MouseState,
|
|
9
|
+
PointersState,
|
|
9
10
|
TouchesState
|
|
10
11
|
} from "./devices.js";
|
|
11
12
|
import * as pluginModule from "./plugin.js";
|
|
@@ -21,6 +22,9 @@ const Gamepads = defineResource("input/Gamepads", {
|
|
|
21
22
|
const Touches = defineResource("input/Touches", {
|
|
22
23
|
description: "Active touches."
|
|
23
24
|
});
|
|
25
|
+
const Pointers = defineResource("input/Pointers", {
|
|
26
|
+
description: "This frame's pointer and wheel events in CSS pixels, for gestures (0060)."
|
|
27
|
+
});
|
|
24
28
|
const InputQueue = defineResource("input/Queue", {
|
|
25
29
|
description: "Raw input buffer: live source, injected events, recording, replay."
|
|
26
30
|
});
|
|
@@ -32,6 +36,15 @@ function devices(world) {
|
|
|
32
36
|
touches: world.resource(Touches)
|
|
33
37
|
};
|
|
34
38
|
}
|
|
39
|
+
function pointers(world, e) {
|
|
40
|
+
if (e.type !== "pointer" && e.type !== "wheel" && e.type !== "focus") return;
|
|
41
|
+
const p = world.resource(Pointers);
|
|
42
|
+
p.events.push(e);
|
|
43
|
+
if (e.type === "pointer") {
|
|
44
|
+
p.position[0] = e.x;
|
|
45
|
+
p.position[1] = e.y;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
35
48
|
function apply(world, d, e) {
|
|
36
49
|
switch (e.type) {
|
|
37
50
|
case "key":
|
|
@@ -84,6 +97,8 @@ function apply(world, d, e) {
|
|
|
84
97
|
}
|
|
85
98
|
break;
|
|
86
99
|
}
|
|
100
|
+
case "pointer":
|
|
101
|
+
break;
|
|
87
102
|
case "focus":
|
|
88
103
|
if (!e.focused) {
|
|
89
104
|
d.keyboard.releaseAll();
|
|
@@ -124,6 +139,7 @@ const updateInput = defineSystem({
|
|
|
124
139
|
d.mouse.beginFrame();
|
|
125
140
|
d.gamepads.beginFrame();
|
|
126
141
|
d.touches.beginFrame();
|
|
142
|
+
world.resource(Pointers).beginFrame();
|
|
127
143
|
const events = q.scratch;
|
|
128
144
|
events.length = 0;
|
|
129
145
|
if (q.replay) {
|
|
@@ -136,7 +152,14 @@ const updateInput = defineSystem({
|
|
|
136
152
|
}
|
|
137
153
|
for (const e of q.pending) events.push(e);
|
|
138
154
|
q.pending.length = 0;
|
|
139
|
-
|
|
155
|
+
if (q.scheduled.length > 0) {
|
|
156
|
+
for (const e of q.scheduled.shift()) events.push(e);
|
|
157
|
+
world.resource(FrameDemand).set(SIMULATE_DEMAND, q.scheduled.length > 0);
|
|
158
|
+
}
|
|
159
|
+
for (const e of events) {
|
|
160
|
+
apply(world, d, e);
|
|
161
|
+
pointers(world, e);
|
|
162
|
+
}
|
|
140
163
|
if (q.recording) q.recording.push(events.map((e) => ({ ...e })));
|
|
141
164
|
}
|
|
142
165
|
});
|
|
@@ -152,6 +175,7 @@ const updateActions = defineSystem({
|
|
|
152
175
|
}
|
|
153
176
|
});
|
|
154
177
|
function inputPlugin(options = {}) {
|
|
178
|
+
let unsubscribe;
|
|
155
179
|
return definePlugin({
|
|
156
180
|
name: "input",
|
|
157
181
|
provides: [pluginModule],
|
|
@@ -163,12 +187,23 @@ function inputPlugin(options = {}) {
|
|
|
163
187
|
scratch: [],
|
|
164
188
|
maps: [],
|
|
165
189
|
recording: void 0,
|
|
166
|
-
replay: void 0
|
|
190
|
+
replay: void 0,
|
|
191
|
+
scheduled: []
|
|
167
192
|
};
|
|
168
|
-
app.insertResource(Keyboard, new KeyboardState()).insertResource(Mouse, new MouseState()).insertResource(Gamepads, new GamepadsState()).insertResource(Touches, new TouchesState()).insertResource(InputQueue, queue);
|
|
193
|
+
app.insertResource(Keyboard, new KeyboardState()).insertResource(Mouse, new MouseState()).insertResource(Gamepads, new GamepadsState()).insertResource(Touches, new TouchesState()).insertResource(Pointers, new PointersState()).insertResource(InputQueue, queue);
|
|
169
194
|
app.world.initResource(InputContext);
|
|
170
195
|
for (const map of options.actions ?? []) addActions(app.world, map);
|
|
171
196
|
app.addSystems(First, updateInput, updateActions.after(updateInput));
|
|
197
|
+
unsubscribe = options.source?.onInput?.(() => {
|
|
198
|
+
if (!app.disposed) app.requestFrame();
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
dispose(app) {
|
|
202
|
+
unsubscribe?.();
|
|
203
|
+
unsubscribe = void 0;
|
|
204
|
+
const queue = app.world.tryResource(InputQueue);
|
|
205
|
+
queue?.source?.dispose();
|
|
206
|
+
if (queue) queue.source = void 0;
|
|
172
207
|
}
|
|
173
208
|
});
|
|
174
209
|
}
|
|
@@ -206,6 +241,12 @@ function injectInput(world, input) {
|
|
|
206
241
|
value: input.value
|
|
207
242
|
});
|
|
208
243
|
}
|
|
244
|
+
const SIMULATE_DEMAND = "input/simulate";
|
|
245
|
+
function scheduleInput(world, frames) {
|
|
246
|
+
const q = world.resource(InputQueue);
|
|
247
|
+
for (const frame of frames) q.scheduled.push([...frame]);
|
|
248
|
+
if (q.scheduled.length > 0) world.resource(FrameDemand).hold(SIMULATE_DEMAND);
|
|
249
|
+
}
|
|
209
250
|
function startRecording(world) {
|
|
210
251
|
world.resource(InputQueue).recording = [];
|
|
211
252
|
}
|
|
@@ -238,6 +279,7 @@ function describeInput(world) {
|
|
|
238
279
|
keysHeld: world.resource(Keyboard).held(),
|
|
239
280
|
gamepads: world.resource(Gamepads).connected().length,
|
|
240
281
|
touches: world.resource(Touches).active.size,
|
|
282
|
+
scheduledFrames: q.scheduled.length,
|
|
241
283
|
context: world.resource(InputContext).active,
|
|
242
284
|
pointerCaptured: world.resource(Mouse).captured,
|
|
243
285
|
actionMaps: q.maps.map((m) => ({
|
|
@@ -256,12 +298,14 @@ export {
|
|
|
256
298
|
InputQueue,
|
|
257
299
|
Keyboard,
|
|
258
300
|
Mouse,
|
|
301
|
+
Pointers,
|
|
259
302
|
Touches,
|
|
260
303
|
addActions,
|
|
261
304
|
describeInput,
|
|
262
305
|
injectInput,
|
|
263
306
|
inputPlugin,
|
|
264
307
|
rebindAction,
|
|
308
|
+
scheduleInput,
|
|
265
309
|
startRecording,
|
|
266
310
|
startReplay,
|
|
267
311
|
stopRecording,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-input",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@aethervtt/shard-core": "0.0
|
|
20
|
-
"@aethervtt/shard-platform": "0.0
|
|
21
|
-
"@aethervtt/shard-runtime": "0.0
|
|
19
|
+
"@aethervtt/shard-core": "0.1.0",
|
|
20
|
+
"@aethervtt/shard-platform": "0.1.0",
|
|
21
|
+
"@aethervtt/shard-runtime": "0.1.0"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"provenance": true
|
|
31
31
|
},
|
|
32
32
|
"shard": {
|
|
33
|
-
"buildSha": "
|
|
33
|
+
"buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
|
|
34
34
|
}
|
|
35
35
|
}
|