@solidrt/core 0.0.56 → 0.0.57

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.
@@ -0,0 +1,156 @@
1
+ // The keyboard as input sources: a key as a button, a key pair as an
2
+ // axis, four keys as a vec2 (the WASD composite), each holding its own
3
+ // pressed state from the key events an input map forwards to it
4
+ // (InputMap.handlers, spread on the window or the focused leaf). Keys
5
+ // match on the physical code ("KeyW", layout-independent) or the logical
6
+ // key ("w", what a synthetic event or an odd layout reports), and a
7
+ // letter code matches its letter either way, so "KeyW" and "w" both take
8
+ // a shifted "W".
9
+ //
10
+ // A spec may name modifiers ahead of the key ("Shift+Tab", "Ctrl+KeyS";
11
+ // Shift, Ctrl, Alt, Meta): the down must carry them, while a bare spec
12
+ // ignores them (a shifted W still walks). Within one source the most
13
+ // specific matching spec wins a down, so `axis("Shift+Tab", "Tab")` reads
14
+ // -1, not 0, on Shift+Tab; and an up releases on the key alone, so a
15
+ // modifier let go first cannot leave the key stuck.
16
+ //
17
+ // Held state is a count behind a signal, so a source's rate is reactive
18
+ // (a held key wakes a control's frame loop) and a blur - the up that never
19
+ // arrives once focus has left - clears it. Composites clamp nothing: the
20
+ // map's combination does (W and D together read [1, -1] before the
21
+ // vec2 clamp to unit length, so diagonals walk at the same speed).
22
+ //
23
+ // Values use the screen convention (x right, y down): W reads [0, -1], as
24
+ // a stick pushed up does.
25
+
26
+ import { createSignal } from "@solidjs/signals"
27
+ import type { KeyEvent } from "./types"
28
+ import type { InputSource } from "./input-map"
29
+ import type { Vec2 } from "./input-axes"
30
+
31
+ type Modifier = "shiftKey" | "ctrlKey" | "altKey" | "metaKey"
32
+
33
+ const MODIFIERS: Record<string, Modifier> = { Shift: "shiftKey", Ctrl: "ctrlKey", Control: "ctrlKey", Alt: "altKey", Meta: "metaKey" }
34
+
35
+ // A parsed spec: the text as given (the held-state key and the label),
36
+ // the key it names, and the modifiers the down must carry.
37
+ type Spec = { text: string; key: string; mods: Modifier[] }
38
+
39
+ function parse(what: string, text: unknown): Spec {
40
+ if (typeof text !== "string" || text.length === 0) throw new Error(`keyboard.${what}: expected a key code or key name, got ${String(text)}`)
41
+ let parts = text.split("+")
42
+ let key = parts.pop()!
43
+ if (key.length === 0) throw new Error(`keyboard.${what}: "${text}" names no key`)
44
+ let mods: Modifier[] = []
45
+ for (let part of parts) {
46
+ let mod = MODIFIERS[part]
47
+ if (!mod) throw new Error(`keyboard.${what}: unknown modifier "${part}" in "${text}" (Shift, Ctrl, Alt or Meta)`)
48
+ if (!mods.includes(mod)) mods.push(mod)
49
+ }
50
+ return { text, key, mods }
51
+ }
52
+
53
+ // A key matches its physical code or its logical key; a letter code
54
+ // ("KeyW") also matches the logical letter ("w", "W") and "Space" the
55
+ // logical " ", so a synthetic event without a code (an agent's
56
+ // send_input, an odd layout) still walks.
57
+ let matches = (event: KeyEvent, key: string): boolean => {
58
+ if (event.code === key || event.key === key) return true
59
+ if (key === "Space" && event.key === " ") return true
60
+ if (event.key.length !== 1) return false
61
+ if (key.length === 1) return event.key.toLowerCase() === key.toLowerCase()
62
+ return key.length === 4 && key.startsWith("Key") && event.key.toLowerCase() === key[3]!.toLowerCase()
63
+ }
64
+
65
+ // A set of specs with a reactive "how many are held" count.
66
+ function held(specs: Spec[]) {
67
+ let down = new Set<string>()
68
+ // ownedWrite: a map forwards key events from wherever its handlers sit.
69
+ let [count, setCount] = createSignal(0, { ownedWrite: true })
70
+ return {
71
+ count,
72
+ key(event: KeyEvent, isDown: boolean) {
73
+ // Every spec on this key is released first: a down settles the key
74
+ // on its most specific matches (a repeat under a newly held Shift
75
+ // moves "Tab" to "Shift+Tab" and back), an up frees them all.
76
+ let onKey = specs.filter(s => matches(event, s.key))
77
+ for (let s of onKey) down.delete(s.text)
78
+ if (isDown) {
79
+ let hits = onKey.filter(s => s.mods.every(m => event[m]))
80
+ let most = hits.reduce((n, s) => Math.max(n, s.mods.length), 0)
81
+ for (let s of hits) if (s.mods.length === most) down.add(s.text)
82
+ }
83
+ setCount(down.size)
84
+ },
85
+ blur() {
86
+ down.clear()
87
+ setCount(0)
88
+ },
89
+ has(text: string) {
90
+ return down.has(text)
91
+ },
92
+ }
93
+ }
94
+
95
+ /** One key as a button source (pressed while held). */
96
+ function key(spec: string): InputSource<"button"> {
97
+ let state = held([parse("key", spec)])
98
+ return {
99
+ kind: "button",
100
+ label: `keyboard ${spec}`,
101
+ rate: () => state.count() > 0,
102
+ key: state.key,
103
+ blur: state.blur,
104
+ }
105
+ }
106
+
107
+ /** Two keys as an axis: `neg` reads -1, `pos` reads 1, both 0. */
108
+ function axis(neg: string, pos: string): InputSource<"axis"> {
109
+ let state = held([parse("axis", neg), parse("axis", pos)])
110
+ return {
111
+ kind: "axis",
112
+ label: `keyboard ${neg}/${pos}`,
113
+ rate: () => {
114
+ state.count()
115
+ return (state.has(pos) ? 1 : 0) - (state.has(neg) ? 1 : 0)
116
+ },
117
+ key: state.key,
118
+ blur: state.blur,
119
+ }
120
+ }
121
+
122
+ export type KeyboardVec2Keys = { up: string; down: string; left: string; right: string }
123
+
124
+ /** Four keys as a vec2 in the screen convention: up reads [0, -1], right
125
+ * reads [1, 0]; opposite keys cancel. */
126
+ function vec2(keys: KeyboardVec2Keys): InputSource<"vec2"> {
127
+ let state = held((["up", "down", "left", "right"] as const).map(side => parse(`vec2 ${side}`, keys[side])))
128
+ return {
129
+ kind: "vec2",
130
+ label: `keyboard ${keys.up}/${keys.left}/${keys.down}/${keys.right}`,
131
+ rate: (): Vec2 => {
132
+ state.count()
133
+ return [(state.has(keys.right) ? 1 : 0) - (state.has(keys.left) ? 1 : 0), (state.has(keys.down) ? 1 : 0) - (state.has(keys.up) ? 1 : 0)]
134
+ },
135
+ key: state.key,
136
+ blur: state.blur,
137
+ }
138
+ }
139
+
140
+ /**
141
+ * The keyboard device: `keyboard.key("Space")`, `keyboard.axis("KeyQ",
142
+ * "KeyE")`, `keyboard.vec2({ up, down, left, right })`, and the two
143
+ * composites every game binds, `keyboard.wasd` and `keyboard.arrows`.
144
+ * A spec may carry modifiers ("Shift+Tab", "Ctrl+KeyS"). Each call makes
145
+ * an independent source with its own held state; the composites are
146
+ * shared singletons.
147
+ */
148
+ export let keyboard = {
149
+ key,
150
+ axis,
151
+ vec2,
152
+ wasd: vec2({ up: "KeyW", down: "KeyS", left: "KeyA", right: "KeyD" }),
153
+ arrows: vec2({ up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight" }),
154
+ }
155
+
156
+ export type KeyboardDevice = typeof keyboard
@@ -0,0 +1,467 @@
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
+ if (!s || typeof s !== "object" || (s.kind !== "button" && s.kind !== "axis" && s.kind !== "vec2") || typeof s.label !== "string") {
217
+ throw new Error(`createInputMap: not an input source: ${String(source)}`)
218
+ }
219
+ return s
220
+ }
221
+
222
+ export function createInputMap<A extends ActionsDecl>(actions: A): InputMap<A> {
223
+ if (!actions || typeof actions !== "object") throw new Error("createInputMap: expected an object of action kinds")
224
+ let states = new Map<string, ActionState>()
225
+ for (let [name, kind] of Object.entries(actions)) {
226
+ if (kind !== "button" && kind !== "axis" && kind !== "vec2") throw new Error(`createInputMap: action "${name}" has kind "${String(kind)}", expected "button", "axis" or "vec2"`)
227
+ // ownedWrite: bind() and set() run from component bodies.
228
+ let [version, setVersion] = createSignal(0, { ownedWrite: true })
229
+ let state: ActionState = {
230
+ kind,
231
+ sources: [],
232
+ script: null,
233
+ enabled: true,
234
+ depth: 0,
235
+ version,
236
+ bump: () => setVersion(untrack(version) + 1),
237
+ value: (): boolean | number | Vec2 => {
238
+ version()
239
+ if (!state.enabled) return neutral(kind)
240
+ let s = state.script
241
+ if (kind === "button") {
242
+ if (s === true) return true
243
+ return state.sources.some(src => src.rate?.() === true)
244
+ }
245
+ let values: (number | Vec2)[] = []
246
+ if (s !== null) values.push(s as number | Vec2)
247
+ for (let src of state.sources) {
248
+ if (!src.rate) continue
249
+ let v = src.rate()
250
+ if (src.kind === "button") values.push(v ? 1 : 0)
251
+ else values.push(v as number | Vec2)
252
+ }
253
+ return combineRates(kind, values)
254
+ },
255
+ gesture: new Set(),
256
+ live: new Map(),
257
+ }
258
+ states.set(name, state)
259
+ }
260
+ let state = (name: string): ActionState => {
261
+ let s = states.get(name)
262
+ if (!s) throw new Error(`createInputMap: no action "${name}" (declared: ${[...states.keys()].join(", ")})`)
263
+ return s
264
+ }
265
+ let axisState = (name: string, what: string): ActionState => {
266
+ let s = state(name)
267
+ if (s.kind === "button") throw new Error(`createInputMap: ${what} needs an axis or vec2 action, "${name}" is a button`)
268
+ return s
269
+ }
270
+ let buttonState = (name: string, what: string): ActionState => {
271
+ let s = state(name)
272
+ if (s.kind !== "button") throw new Error(`createInputMap: ${what} needs a button action, "${name}" is a ${s.kind}`)
273
+ return s
274
+ }
275
+ // The delta channel fan-out for one action. Brackets are counted as
276
+ // delivered, so a disable can close what is open and an end whose begin
277
+ // was dropped (or never delivered) reaches no listener.
278
+ let closeGesture = (s: ActionState): void => {
279
+ s.depth--
280
+ s.gesture.forEach(g => g.end?.())
281
+ }
282
+ let sink = (s: ActionState): DeltaSink => ({
283
+ begin: () => {
284
+ if (!s.enabled) return
285
+ s.depth++
286
+ s.gesture.forEach(g => g.begin?.())
287
+ },
288
+ delta: (value, focal) => {
289
+ if (!s.enabled) return
290
+ s.gesture.forEach(g => g.delta?.(value as never, focal))
291
+ },
292
+ end: () => {
293
+ if (s.depth > 0) closeGesture(s)
294
+ },
295
+ })
296
+ let switchActions = (names: string[], on: boolean, what: string): void => {
297
+ if (names.length === 0) throw new Error(`createInputMap: ${what}() needs at least one action`)
298
+ for (let name of names) {
299
+ let s = state(name)
300
+ if (s.enabled === on) continue
301
+ s.enabled = on
302
+ while (!on && s.depth > 0) closeGesture(s)
303
+ s.bump()
304
+ }
305
+ }
306
+
307
+ let bindOne = (name: string, source: InputSource): void => {
308
+ let s = state(name)
309
+ checkSource(source)
310
+ if (!compatible(source.kind, s.kind)) throw new Error(`createInputMap: cannot bind ${source.kind} source "${source.label}" to ${s.kind} action "${name}"`)
311
+ if (s.sources.includes(source)) return
312
+ s.sources.push(source)
313
+ s.bump()
314
+ if (source.deltas && s.kind !== "button") s.live.set(source, source.deltas(sink(s)))
315
+ }
316
+ let unbindOne = (name: string, source: InputSource): void => {
317
+ let s = state(name)
318
+ let i = s.sources.indexOf(source)
319
+ if (i < 0) return
320
+ s.sources.splice(i, 1)
321
+ s.bump()
322
+ let stop = s.live.get(source)
323
+ if (stop) {
324
+ s.live.delete(source)
325
+ stop()
326
+ }
327
+ }
328
+ let edge = (name: string, want: boolean, callback: () => void): (() => void) => {
329
+ let s = buttonState(name, want ? "onPress" : "onRelease")
330
+ if (typeof callback !== "function") throw new Error(`createInputMap: ${want ? "onPress" : "onRelease"}("${name}") expects a function`)
331
+ return createRoot(dispose => {
332
+ // The callback is a side effect that may read state of its own (a
333
+ // focus, a pose): a snapshot, not a dependency, so untracked.
334
+ createEffect(
335
+ () => s.value() as boolean,
336
+ (pressed, prev) => {
337
+ if (pressed === want && prev !== want) untrack(callback)
338
+ },
339
+ { defer: true },
340
+ )
341
+ return dispose
342
+ })
343
+ }
344
+
345
+ let map: InputMap<A> = {
346
+ actions,
347
+ bind(actionOrList: string | Binding[], ...sources: InputSource[]) {
348
+ let list: Binding[] = Array.isArray(actionOrList) ? actionOrList : sources.map(source => ({ action: actionOrList, source }))
349
+ if (list.length === 0) throw new Error("createInputMap: bind() needs at least one source")
350
+ for (let b of list) bindOne(b.action, b.source)
351
+ return () => {
352
+ for (let b of list) unbindOne(b.action, b.source)
353
+ }
354
+ },
355
+ unbind: unbindOne,
356
+ bindings(action) {
357
+ let out: Binding[] = []
358
+ for (let [name, s] of states) {
359
+ if (action !== undefined && name !== action) continue
360
+ for (let source of s.sources) out.push({ action: name, source })
361
+ }
362
+ if (action !== undefined) state(action)
363
+ return out
364
+ },
365
+ value(action) {
366
+ return state(action).value() as never
367
+ },
368
+ pressed(action) {
369
+ return buttonState(action, "pressed").value() as boolean
370
+ },
371
+ onPress: (action, callback) => edge(action, true, callback),
372
+ onRelease: (action, callback) => edge(action, false, callback),
373
+ onGesture(action, listener) {
374
+ let s = axisState(action, "onGesture")
375
+ if (!listener || typeof listener !== "object") throw new Error(`createInputMap: onGesture("${action}") expects a listener object`)
376
+ s.gesture.add(listener as GestureListener)
377
+ return () => {
378
+ s.gesture.delete(listener as GestureListener)
379
+ }
380
+ },
381
+ set(action, value) {
382
+ let s = state(action)
383
+ checkValue(`set("${action}")`, s.kind, value)
384
+ s.script = value
385
+ s.bump()
386
+ },
387
+ press(action) {
388
+ let s = buttonState(action, "press")
389
+ s.script = true
390
+ s.bump()
391
+ },
392
+ release(action) {
393
+ let s = buttonState(action, "release")
394
+ s.script = false
395
+ s.bump()
396
+ },
397
+ nudge(action, delta, focal) {
398
+ let s = axisState(action, "nudge")
399
+ checkValue(`nudge("${action}")`, s.kind, delta)
400
+ if (focal !== undefined) checkValue(`nudge("${action}") focal`, "vec2", focal)
401
+ sink(s).delta(delta as number | Vec2, focal)
402
+ },
403
+ begin(action) {
404
+ sink(axisState(action, "begin")).begin()
405
+ },
406
+ end(action) {
407
+ sink(axisState(action, "end")).end()
408
+ },
409
+ enable: (...actions) => switchActions(actions, true, "enable"),
410
+ disable: (...actions) => switchActions(actions, false, "disable"),
411
+ enabled(action) {
412
+ let s = state(action)
413
+ s.version()
414
+ return s.enabled
415
+ },
416
+ handlers: {
417
+ onKeyDown: event => forwardKey(event, true),
418
+ onKeyUp: event => forwardKey(event, false),
419
+ onBlur: () => {
420
+ let seen = new Set<InputSource>()
421
+ for (let s of states.values()) {
422
+ for (let source of s.sources) {
423
+ if (seen.has(source)) continue
424
+ seen.add(source)
425
+ source.blur?.()
426
+ }
427
+ }
428
+ },
429
+ },
430
+ drive(axes, names) {
431
+ let stops: (() => void)[] = []
432
+ for (let [axis, kind] of Object.entries(axes.kinds)) {
433
+ let mapped = names && axis in names ? names[axis] : axis
434
+ if (mapped === null) continue
435
+ if (mapped === undefined || !states.has(mapped)) {
436
+ 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`)
437
+ }
438
+ let s = state(mapped)
439
+ if (s.kind !== kind) throw new Error(`createInputMap: drive() maps ${kind} axis "${axis}" to ${s.kind} action "${mapped}"`)
440
+ stops.push(axes.add(axis, () => s.value() as never))
441
+ stops.push(
442
+ map.onGesture(mapped as never, {
443
+ begin: () => axes.begin(axis),
444
+ delta: (value, focal) => axes.nudge(axis, value as never, focal),
445
+ end: () => axes.end(axis),
446
+ }),
447
+ )
448
+ }
449
+ return () => {
450
+ for (let stop of stops) stop()
451
+ }
452
+ },
453
+ }
454
+ // A key event reaches each bound keyboard source once, however many
455
+ // actions it is bound to.
456
+ let forwardKey = (event: KeyEvent, down: boolean): void => {
457
+ let seen = new Set<InputSource>()
458
+ for (let s of states.values()) {
459
+ for (let source of s.sources) {
460
+ if (seen.has(source) || !source.key) continue
461
+ seen.add(source)
462
+ source.key(event, down)
463
+ }
464
+ }
465
+ }
466
+ return map
467
+ }