@solidrt/core 0.0.56 → 0.0.58
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/AGENTS.md +109 -2
- package/examples/README.md +3 -0
- package/examples/gamepad.tsx +94 -0
- package/package.json +3 -2
- package/src/index.ts +12 -0
- package/src/input-axes.ts +180 -0
- package/src/input-chord.ts +44 -0
- package/src/input-gamepad-device.ts +161 -0
- package/src/input-gamepad.ts +24 -0
- package/src/input-keyboard.ts +145 -0
- package/src/input-map.ts +468 -0
- package/src/input-pointer.ts +306 -0
- package/src/input.ts +12 -0
- package/src/transform.ts +4 -1
- package/src/types.d.ts +4 -3
package/src/input-map.ts
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// The input map: named, typed actions between devices and consumers -
|
|
2
|
+
// Godot's InputMap and Unity's action maps, in reactive form. An app
|
|
3
|
+
// declares its actions once (`look: "vec2"`, `jump: "button"`), binds
|
|
4
|
+
// device sources to them (a stick, a key composite, a pointer drag), and
|
|
5
|
+
// consumers read the action, never the device. Anything can then drive a
|
|
6
|
+
// consumer: a second pad on a second map for split screen, a debug
|
|
7
|
+
// command or a network peer through the by-name injection, a replay on
|
|
8
|
+
// the action stream, a settings screen over bindings().
|
|
9
|
+
//
|
|
10
|
+
// Each axis or vec2 action carries two channels, which is the part Godot
|
|
11
|
+
// (no mouse motion in actions) and Unity (mouse delta and stick share one
|
|
12
|
+
// value, consumers scale by dt themselves) each got half of:
|
|
13
|
+
// - the RATE: the sum of every bound source's current value, clamped (an
|
|
14
|
+
// axis to -1..1, a vec2 to unit length), reactive, sampled per frame by
|
|
15
|
+
// the consumer and integrated over dt at its own speed;
|
|
16
|
+
// - the DELTAS: immediate, device-free amounts a gesture produced (a drag
|
|
17
|
+
// in view heights, a wheel notch in octaves), delivered as they happen
|
|
18
|
+
// with begin/end brackets around a finger's gesture.
|
|
19
|
+
// Values use the screen convention: x right, y down.
|
|
20
|
+
//
|
|
21
|
+
// A button action reads pressed while any bound button source is, and
|
|
22
|
+
// may also bind to an axis action (1 while pressed). Kinds are checked at
|
|
23
|
+
// bind: a vec2 source on an axis action throws.
|
|
24
|
+
//
|
|
25
|
+
// Reactivity: rates are memos over reactive sources, so a consumer's
|
|
26
|
+
// active() gate wakes on a stick or a key. Create the map in an owned
|
|
27
|
+
// scope (a component body): onPress/onRelease run effects under it.
|
|
28
|
+
//
|
|
29
|
+
// Consumers with the Axes contract (createAxes; the camera controls) are
|
|
30
|
+
// connected by name with drive(): each axis takes the action of the same
|
|
31
|
+
// name, or the name given, for both channels.
|
|
32
|
+
//
|
|
33
|
+
// Contexts (a menu open, a cutscene) switch actions off and on by name:
|
|
34
|
+
// enable/disable. A disabled action reads neutral, drops its deltas and
|
|
35
|
+
// closes the gesture it had open, while its sources keep their state, so
|
|
36
|
+
// a key still held when the action comes back reads at once. A set is a
|
|
37
|
+
// list of names, which a preset's action object already is
|
|
38
|
+
// (`input.disable(...Object.keys(gameActions))`): Unity's per-map enable
|
|
39
|
+
// and Unreal's stacked contexts, on one map, with no extra concept.
|
|
40
|
+
|
|
41
|
+
import { createEffect, createRoot, createSignal, untrack } from "@solidjs/signals"
|
|
42
|
+
import type { KeyEvent } from "./types"
|
|
43
|
+
import { combineRates } from "./input-axes"
|
|
44
|
+
import type { Axes, AxesDecl, AxisValue, Vec2 } from "./input-axes"
|
|
45
|
+
|
|
46
|
+
export type ActionKind = "button" | "axis" | "vec2"
|
|
47
|
+
export type ActionValue<K extends ActionKind> = K extends "button" ? boolean : K extends "axis" ? number : Vec2
|
|
48
|
+
export type ActionsDecl = Record<string, ActionKind>
|
|
49
|
+
|
|
50
|
+
/** Where a delta source delivers: the gesture brackets and the deltas. */
|
|
51
|
+
export interface DeltaSink {
|
|
52
|
+
begin(): void
|
|
53
|
+
delta(value: number | Vec2, focal?: Vec2): void
|
|
54
|
+
end(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One device control as an input source. `rate` reads its current value
|
|
59
|
+
* (reactive); `deltas` subscribes a sink to the amounts a gesture
|
|
60
|
+
* produces; `key`/`blur` receive the key events a map forwards
|
|
61
|
+
* (InputMap.handlers). A source may carry any subset.
|
|
62
|
+
*/
|
|
63
|
+
export interface InputSource<K extends ActionKind = ActionKind> {
|
|
64
|
+
readonly kind: K
|
|
65
|
+
/** For bindings listings and settings screens: "keyboard W/A/S/D". */
|
|
66
|
+
readonly label: string
|
|
67
|
+
rate?: () => ActionValue<K>
|
|
68
|
+
deltas?: (sink: DeltaSink) => () => void
|
|
69
|
+
key?: (event: KeyEvent, down: boolean) => void
|
|
70
|
+
blur?: () => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type Binding = { action: string; source: InputSource }
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A source with its values negated - Unity's Invert processor. The one
|
|
77
|
+
* convention split every engine has: a drag moves the CONTENT under the
|
|
78
|
+
* finger, while keys and sticks move the CAMERA, so a preset binds the
|
|
79
|
+
* arrows or a stick to `pan` or `rotate` through invert() and the axis
|
|
80
|
+
* keeps one meaning.
|
|
81
|
+
*/
|
|
82
|
+
export function invert<K extends "axis" | "vec2">(source: InputSource<K>): InputSource<K> {
|
|
83
|
+
checkSource(source)
|
|
84
|
+
if ((source.kind as ActionKind) === "button") throw new Error(`invert: "${source.label}" is a button`)
|
|
85
|
+
let neg = (v: number | Vec2): number | Vec2 => (typeof v === "number" ? -v : [-v[0], -v[1]])
|
|
86
|
+
return {
|
|
87
|
+
kind: source.kind,
|
|
88
|
+
label: `${source.label} (inverted)`,
|
|
89
|
+
rate: source.rate ? () => neg(source.rate!() as number | Vec2) as never : undefined,
|
|
90
|
+
deltas: source.deltas
|
|
91
|
+
? sink => source.deltas!({ begin: sink.begin, end: sink.end, delta: (value, focal) => sink.delta(neg(value), focal) })
|
|
92
|
+
: undefined,
|
|
93
|
+
key: source.key,
|
|
94
|
+
blur: source.blur,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A source scaled by `factor` (rates and deltas alike) - Unity's Scale
|
|
99
|
+
* processor: a slower stick, a finer wheel. */
|
|
100
|
+
export function scale<K extends "axis" | "vec2">(source: InputSource<K>, factor: number): InputSource<K> {
|
|
101
|
+
checkSource(source)
|
|
102
|
+
if ((source.kind as ActionKind) === "button") throw new Error(`scale: "${source.label}" is a button`)
|
|
103
|
+
if (!Number.isFinite(factor)) throw new Error(`scale: factor must be a finite number, got ${factor}`)
|
|
104
|
+
let mul = (v: number | Vec2): number | Vec2 => (typeof v === "number" ? v * factor : [v[0] * factor, v[1] * factor])
|
|
105
|
+
return {
|
|
106
|
+
kind: source.kind,
|
|
107
|
+
label: `${source.label} (x${factor})`,
|
|
108
|
+
rate: source.rate ? () => mul(source.rate!() as number | Vec2) as never : undefined,
|
|
109
|
+
deltas: source.deltas
|
|
110
|
+
? sink => source.deltas!({ begin: sink.begin, end: sink.end, delta: (value, focal) => sink.delta(mul(value), focal) })
|
|
111
|
+
: undefined,
|
|
112
|
+
key: source.key,
|
|
113
|
+
blur: source.blur,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface GestureListener<K extends "axis" | "vec2" = "axis" | "vec2"> {
|
|
118
|
+
begin?: () => void
|
|
119
|
+
delta?: (value: AxisValue<K>, focal: Vec2 | undefined) => void
|
|
120
|
+
end?: () => void
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type AxisActions<A extends ActionsDecl> = { [N in keyof A]: A[N] extends "button" ? never : N }[keyof A] & string
|
|
124
|
+
type ButtonActions<A extends ActionsDecl> = { [N in keyof A]: A[N] extends "button" ? N : never }[keyof A] & string
|
|
125
|
+
|
|
126
|
+
export interface InputMap<A extends ActionsDecl> {
|
|
127
|
+
readonly actions: A
|
|
128
|
+
/** Bind sources to an action (kinds checked), or a list of bindings (a
|
|
129
|
+
* preset's return). Returns the remover for what this call bound. */
|
|
130
|
+
bind<N extends keyof A & string>(action: N, ...sources: InputSource[]): () => void
|
|
131
|
+
bind(bindings: Binding[]): () => void
|
|
132
|
+
unbind(action: keyof A & string, source: InputSource): void
|
|
133
|
+
/** Current bindings, of one action or all, in binding order. */
|
|
134
|
+
bindings(action?: keyof A & string): Binding[]
|
|
135
|
+
/** The action's rate now (reactive): pressed for a button, -1..1 for an
|
|
136
|
+
* axis, a unit-clamped [x, y] for a vec2. */
|
|
137
|
+
value<N extends keyof A & string>(action: N): ActionValue<A[N]>
|
|
138
|
+
/** Reactive: whether a button action is pressed. */
|
|
139
|
+
pressed(action: ButtonActions<A>): boolean
|
|
140
|
+
/** Edge callbacks on a button action; return the unsubscribe. */
|
|
141
|
+
onPress(action: ButtonActions<A>, callback: () => void): () => void
|
|
142
|
+
onRelease(action: ButtonActions<A>, callback: () => void): () => void
|
|
143
|
+
/** Subscribe to an axis or vec2 action's delta channel. */
|
|
144
|
+
onGesture<N extends AxisActions<A>>(action: N, listener: GestureListener<A[N] extends "axis" ? "axis" : "vec2">): () => void
|
|
145
|
+
/** Injection by name, for scripts, debug commands, peers: `set` holds
|
|
146
|
+
* a rate until set again (a value source of the map's own); `press`/
|
|
147
|
+
* `release` do the same for a button; `nudge`, `begin` and `end` feed
|
|
148
|
+
* the delta channel. */
|
|
149
|
+
set<N extends keyof A & string>(action: N, value: ActionValue<A[N]>): void
|
|
150
|
+
press(action: ButtonActions<A>): void
|
|
151
|
+
release(action: ButtonActions<A>): void
|
|
152
|
+
nudge<N extends AxisActions<A>>(action: N, delta: ActionValue<A[N]>, focal?: Vec2): void
|
|
153
|
+
begin(action: AxisActions<A>): void
|
|
154
|
+
end(action: AxisActions<A>): void
|
|
155
|
+
/** Switch actions on and off by name - a context. A disabled action
|
|
156
|
+
* reads neutral (false, 0, [0, 0]), drops its deltas and closes the
|
|
157
|
+
* gesture it had open (its listeners see the end); its sources keep
|
|
158
|
+
* their state, so a key still held when the action comes back reads at
|
|
159
|
+
* once. Edge callbacks see the switch as a release or a press. All
|
|
160
|
+
* actions start enabled. */
|
|
161
|
+
enable(...actions: (keyof A & string)[]): void
|
|
162
|
+
disable(...actions: (keyof A & string)[]): void
|
|
163
|
+
/** Reactive: whether the action is enabled. */
|
|
164
|
+
enabled(action: keyof A & string): boolean
|
|
165
|
+
/** Key events for the bound keyboard sources: spread on the window
|
|
166
|
+
* (app-global) or on the leaf that should hold focus for them. */
|
|
167
|
+
handlers: {
|
|
168
|
+
onKeyDown(event: KeyEvent): void
|
|
169
|
+
onKeyUp(event: KeyEvent): void
|
|
170
|
+
onBlur(): void
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Drive a control's axes (createAxes) from this map: each axis reads
|
|
174
|
+
* the action of the same name, or the one `names` gives it (null skips
|
|
175
|
+
* an axis). Both channels: rates as a source, deltas and brackets as
|
|
176
|
+
* nudges. Returns the disconnect.
|
|
177
|
+
*/
|
|
178
|
+
drive<X extends AxesDecl>(axes: Axes<X>, names?: Partial<Record<keyof X & string, keyof A & string | null>>): () => void
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Per action: the bound sources and the script value are plain state (a
|
|
182
|
+
// write and a read in the same synchronous block must agree, which a
|
|
183
|
+
// signal's deferred write does not give); `version` notifies the reactive
|
|
184
|
+
// readers of a change.
|
|
185
|
+
type ActionState = {
|
|
186
|
+
kind: ActionKind
|
|
187
|
+
sources: InputSource[]
|
|
188
|
+
script: boolean | number | Vec2 | null
|
|
189
|
+
enabled: boolean
|
|
190
|
+
/** Gesture brackets delivered to the listeners and not yet closed. */
|
|
191
|
+
depth: number
|
|
192
|
+
version: () => number
|
|
193
|
+
bump: () => void
|
|
194
|
+
value: () => boolean | number | Vec2
|
|
195
|
+
gesture: Set<GestureListener>
|
|
196
|
+
/** Live delta subscriptions per bound source. */
|
|
197
|
+
live: Map<InputSource, () => void>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let compatible = (source: ActionKind, action: ActionKind): boolean => source === action || (source === "button" && action === "axis")
|
|
201
|
+
|
|
202
|
+
let neutral = (kind: ActionKind): boolean | number | Vec2 => (kind === "button" ? false : kind === "axis" ? 0 : [0, 0])
|
|
203
|
+
|
|
204
|
+
function checkValue(what: string, kind: ActionKind, v: unknown): void {
|
|
205
|
+
if (kind === "button") {
|
|
206
|
+
if (typeof v !== "boolean") throw new Error(`${what}: expected a boolean, got ${String(v)}`)
|
|
207
|
+
} else if (kind === "axis") {
|
|
208
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`${what}: expected a finite number, got ${String(v)}`)
|
|
209
|
+
} else if (!Array.isArray(v) || v.length !== 2 || !Number.isFinite(v[0]) || !Number.isFinite(v[1])) {
|
|
210
|
+
throw new Error(`${what}: expected [x, y] finite numbers, got ${JSON.stringify(v)}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function checkSource(source: unknown): InputSource {
|
|
215
|
+
let s = source as InputSource
|
|
216
|
+
// A pointer gesture is a callable source (`pointer.drag("Ctrl")` is its chord variant).
|
|
217
|
+
if (!s || (typeof s !== "object" && typeof s !== "function") || (s.kind !== "button" && s.kind !== "axis" && s.kind !== "vec2") || typeof s.label !== "string") {
|
|
218
|
+
throw new Error(`createInputMap: not an input source: ${String(source)}`)
|
|
219
|
+
}
|
|
220
|
+
return s
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function createInputMap<A extends ActionsDecl>(actions: A): InputMap<A> {
|
|
224
|
+
if (!actions || typeof actions !== "object") throw new Error("createInputMap: expected an object of action kinds")
|
|
225
|
+
let states = new Map<string, ActionState>()
|
|
226
|
+
for (let [name, kind] of Object.entries(actions)) {
|
|
227
|
+
if (kind !== "button" && kind !== "axis" && kind !== "vec2") throw new Error(`createInputMap: action "${name}" has kind "${String(kind)}", expected "button", "axis" or "vec2"`)
|
|
228
|
+
// ownedWrite: bind() and set() run from component bodies.
|
|
229
|
+
let [version, setVersion] = createSignal(0, { ownedWrite: true })
|
|
230
|
+
let state: ActionState = {
|
|
231
|
+
kind,
|
|
232
|
+
sources: [],
|
|
233
|
+
script: null,
|
|
234
|
+
enabled: true,
|
|
235
|
+
depth: 0,
|
|
236
|
+
version,
|
|
237
|
+
bump: () => setVersion(untrack(version) + 1),
|
|
238
|
+
value: (): boolean | number | Vec2 => {
|
|
239
|
+
version()
|
|
240
|
+
if (!state.enabled) return neutral(kind)
|
|
241
|
+
let s = state.script
|
|
242
|
+
if (kind === "button") {
|
|
243
|
+
if (s === true) return true
|
|
244
|
+
return state.sources.some(src => src.rate?.() === true)
|
|
245
|
+
}
|
|
246
|
+
let values: (number | Vec2)[] = []
|
|
247
|
+
if (s !== null) values.push(s as number | Vec2)
|
|
248
|
+
for (let src of state.sources) {
|
|
249
|
+
if (!src.rate) continue
|
|
250
|
+
let v = src.rate()
|
|
251
|
+
if (src.kind === "button") values.push(v ? 1 : 0)
|
|
252
|
+
else values.push(v as number | Vec2)
|
|
253
|
+
}
|
|
254
|
+
return combineRates(kind, values)
|
|
255
|
+
},
|
|
256
|
+
gesture: new Set(),
|
|
257
|
+
live: new Map(),
|
|
258
|
+
}
|
|
259
|
+
states.set(name, state)
|
|
260
|
+
}
|
|
261
|
+
let state = (name: string): ActionState => {
|
|
262
|
+
let s = states.get(name)
|
|
263
|
+
if (!s) throw new Error(`createInputMap: no action "${name}" (declared: ${[...states.keys()].join(", ")})`)
|
|
264
|
+
return s
|
|
265
|
+
}
|
|
266
|
+
let axisState = (name: string, what: string): ActionState => {
|
|
267
|
+
let s = state(name)
|
|
268
|
+
if (s.kind === "button") throw new Error(`createInputMap: ${what} needs an axis or vec2 action, "${name}" is a button`)
|
|
269
|
+
return s
|
|
270
|
+
}
|
|
271
|
+
let buttonState = (name: string, what: string): ActionState => {
|
|
272
|
+
let s = state(name)
|
|
273
|
+
if (s.kind !== "button") throw new Error(`createInputMap: ${what} needs a button action, "${name}" is a ${s.kind}`)
|
|
274
|
+
return s
|
|
275
|
+
}
|
|
276
|
+
// The delta channel fan-out for one action. Brackets are counted as
|
|
277
|
+
// delivered, so a disable can close what is open and an end whose begin
|
|
278
|
+
// was dropped (or never delivered) reaches no listener.
|
|
279
|
+
let closeGesture = (s: ActionState): void => {
|
|
280
|
+
s.depth--
|
|
281
|
+
s.gesture.forEach(g => g.end?.())
|
|
282
|
+
}
|
|
283
|
+
let sink = (s: ActionState): DeltaSink => ({
|
|
284
|
+
begin: () => {
|
|
285
|
+
if (!s.enabled) return
|
|
286
|
+
s.depth++
|
|
287
|
+
s.gesture.forEach(g => g.begin?.())
|
|
288
|
+
},
|
|
289
|
+
delta: (value, focal) => {
|
|
290
|
+
if (!s.enabled) return
|
|
291
|
+
s.gesture.forEach(g => g.delta?.(value as never, focal))
|
|
292
|
+
},
|
|
293
|
+
end: () => {
|
|
294
|
+
if (s.depth > 0) closeGesture(s)
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
let switchActions = (names: string[], on: boolean, what: string): void => {
|
|
298
|
+
if (names.length === 0) throw new Error(`createInputMap: ${what}() needs at least one action`)
|
|
299
|
+
for (let name of names) {
|
|
300
|
+
let s = state(name)
|
|
301
|
+
if (s.enabled === on) continue
|
|
302
|
+
s.enabled = on
|
|
303
|
+
while (!on && s.depth > 0) closeGesture(s)
|
|
304
|
+
s.bump()
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let bindOne = (name: string, source: InputSource): void => {
|
|
309
|
+
let s = state(name)
|
|
310
|
+
checkSource(source)
|
|
311
|
+
if (!compatible(source.kind, s.kind)) throw new Error(`createInputMap: cannot bind ${source.kind} source "${source.label}" to ${s.kind} action "${name}"`)
|
|
312
|
+
if (s.sources.includes(source)) return
|
|
313
|
+
s.sources.push(source)
|
|
314
|
+
s.bump()
|
|
315
|
+
if (source.deltas && s.kind !== "button") s.live.set(source, source.deltas(sink(s)))
|
|
316
|
+
}
|
|
317
|
+
let unbindOne = (name: string, source: InputSource): void => {
|
|
318
|
+
let s = state(name)
|
|
319
|
+
let i = s.sources.indexOf(source)
|
|
320
|
+
if (i < 0) return
|
|
321
|
+
s.sources.splice(i, 1)
|
|
322
|
+
s.bump()
|
|
323
|
+
let stop = s.live.get(source)
|
|
324
|
+
if (stop) {
|
|
325
|
+
s.live.delete(source)
|
|
326
|
+
stop()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
let edge = (name: string, want: boolean, callback: () => void): (() => void) => {
|
|
330
|
+
let s = buttonState(name, want ? "onPress" : "onRelease")
|
|
331
|
+
if (typeof callback !== "function") throw new Error(`createInputMap: ${want ? "onPress" : "onRelease"}("${name}") expects a function`)
|
|
332
|
+
return createRoot(dispose => {
|
|
333
|
+
// The callback is a side effect that may read state of its own (a
|
|
334
|
+
// focus, a pose): a snapshot, not a dependency, so untracked.
|
|
335
|
+
createEffect(
|
|
336
|
+
() => s.value() as boolean,
|
|
337
|
+
(pressed, prev) => {
|
|
338
|
+
if (pressed === want && prev !== want) untrack(callback)
|
|
339
|
+
},
|
|
340
|
+
{ defer: true },
|
|
341
|
+
)
|
|
342
|
+
return dispose
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let map: InputMap<A> = {
|
|
347
|
+
actions,
|
|
348
|
+
bind(actionOrList: string | Binding[], ...sources: InputSource[]) {
|
|
349
|
+
let list: Binding[] = Array.isArray(actionOrList) ? actionOrList : sources.map(source => ({ action: actionOrList, source }))
|
|
350
|
+
if (list.length === 0) throw new Error("createInputMap: bind() needs at least one source")
|
|
351
|
+
for (let b of list) bindOne(b.action, b.source)
|
|
352
|
+
return () => {
|
|
353
|
+
for (let b of list) unbindOne(b.action, b.source)
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
unbind: unbindOne,
|
|
357
|
+
bindings(action) {
|
|
358
|
+
let out: Binding[] = []
|
|
359
|
+
for (let [name, s] of states) {
|
|
360
|
+
if (action !== undefined && name !== action) continue
|
|
361
|
+
for (let source of s.sources) out.push({ action: name, source })
|
|
362
|
+
}
|
|
363
|
+
if (action !== undefined) state(action)
|
|
364
|
+
return out
|
|
365
|
+
},
|
|
366
|
+
value(action) {
|
|
367
|
+
return state(action).value() as never
|
|
368
|
+
},
|
|
369
|
+
pressed(action) {
|
|
370
|
+
return buttonState(action, "pressed").value() as boolean
|
|
371
|
+
},
|
|
372
|
+
onPress: (action, callback) => edge(action, true, callback),
|
|
373
|
+
onRelease: (action, callback) => edge(action, false, callback),
|
|
374
|
+
onGesture(action, listener) {
|
|
375
|
+
let s = axisState(action, "onGesture")
|
|
376
|
+
if (!listener || typeof listener !== "object") throw new Error(`createInputMap: onGesture("${action}") expects a listener object`)
|
|
377
|
+
s.gesture.add(listener as GestureListener)
|
|
378
|
+
return () => {
|
|
379
|
+
s.gesture.delete(listener as GestureListener)
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
set(action, value) {
|
|
383
|
+
let s = state(action)
|
|
384
|
+
checkValue(`set("${action}")`, s.kind, value)
|
|
385
|
+
s.script = value
|
|
386
|
+
s.bump()
|
|
387
|
+
},
|
|
388
|
+
press(action) {
|
|
389
|
+
let s = buttonState(action, "press")
|
|
390
|
+
s.script = true
|
|
391
|
+
s.bump()
|
|
392
|
+
},
|
|
393
|
+
release(action) {
|
|
394
|
+
let s = buttonState(action, "release")
|
|
395
|
+
s.script = false
|
|
396
|
+
s.bump()
|
|
397
|
+
},
|
|
398
|
+
nudge(action, delta, focal) {
|
|
399
|
+
let s = axisState(action, "nudge")
|
|
400
|
+
checkValue(`nudge("${action}")`, s.kind, delta)
|
|
401
|
+
if (focal !== undefined) checkValue(`nudge("${action}") focal`, "vec2", focal)
|
|
402
|
+
sink(s).delta(delta as number | Vec2, focal)
|
|
403
|
+
},
|
|
404
|
+
begin(action) {
|
|
405
|
+
sink(axisState(action, "begin")).begin()
|
|
406
|
+
},
|
|
407
|
+
end(action) {
|
|
408
|
+
sink(axisState(action, "end")).end()
|
|
409
|
+
},
|
|
410
|
+
enable: (...actions) => switchActions(actions, true, "enable"),
|
|
411
|
+
disable: (...actions) => switchActions(actions, false, "disable"),
|
|
412
|
+
enabled(action) {
|
|
413
|
+
let s = state(action)
|
|
414
|
+
s.version()
|
|
415
|
+
return s.enabled
|
|
416
|
+
},
|
|
417
|
+
handlers: {
|
|
418
|
+
onKeyDown: event => forwardKey(event, true),
|
|
419
|
+
onKeyUp: event => forwardKey(event, false),
|
|
420
|
+
onBlur: () => {
|
|
421
|
+
let seen = new Set<InputSource>()
|
|
422
|
+
for (let s of states.values()) {
|
|
423
|
+
for (let source of s.sources) {
|
|
424
|
+
if (seen.has(source)) continue
|
|
425
|
+
seen.add(source)
|
|
426
|
+
source.blur?.()
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
drive(axes, names) {
|
|
432
|
+
let stops: (() => void)[] = []
|
|
433
|
+
for (let [axis, kind] of Object.entries(axes.kinds)) {
|
|
434
|
+
let mapped = names && axis in names ? names[axis] : axis
|
|
435
|
+
if (mapped === null) continue
|
|
436
|
+
if (mapped === undefined || !states.has(mapped)) {
|
|
437
|
+
throw new Error(`createInputMap: drive() has no action for axis "${axis}" (declared: ${[...states.keys()].join(", ")}); declare it, map it with names, or skip it with null`)
|
|
438
|
+
}
|
|
439
|
+
let s = state(mapped)
|
|
440
|
+
if (s.kind !== kind) throw new Error(`createInputMap: drive() maps ${kind} axis "${axis}" to ${s.kind} action "${mapped}"`)
|
|
441
|
+
stops.push(axes.add(axis, () => s.value() as never))
|
|
442
|
+
stops.push(
|
|
443
|
+
map.onGesture(mapped as never, {
|
|
444
|
+
begin: () => axes.begin(axis),
|
|
445
|
+
delta: (value, focal) => axes.nudge(axis, value as never, focal),
|
|
446
|
+
end: () => axes.end(axis),
|
|
447
|
+
}),
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
return () => {
|
|
451
|
+
for (let stop of stops) stop()
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
}
|
|
455
|
+
// A key event reaches each bound keyboard source once, however many
|
|
456
|
+
// actions it is bound to.
|
|
457
|
+
let forwardKey = (event: KeyEvent, down: boolean): void => {
|
|
458
|
+
let seen = new Set<InputSource>()
|
|
459
|
+
for (let s of states.values()) {
|
|
460
|
+
for (let source of s.sources) {
|
|
461
|
+
if (seen.has(source) || !source.key) continue
|
|
462
|
+
seen.add(source)
|
|
463
|
+
source.key(event, down)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return map
|
|
468
|
+
}
|