@solidrt/core 0.0.57 → 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 +11 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/input-chord.ts +44 -0
- package/src/input-keyboard.ts +10 -21
- package/src/input-map.ts +2 -1
- package/src/input-pointer.ts +153 -60
- package/src/transform.ts +4 -1
package/AGENTS.md
CHANGED
|
@@ -361,7 +361,17 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
361
361
|
`<View2d pointer>` take a feed and spread it on their built-in leaf) and
|
|
362
362
|
bind its gestures `drag` (one pointer), `pan` (two), `pinch`, `twist`,
|
|
363
363
|
`wheel` and `mouseDelta` (raw motion while pointerLocked()). One
|
|
364
|
-
merged recognizer serves them, so they arbitrate in the arena as one.
|
|
364
|
+
merged recognizer serves them, so they arbitrate in the arena as one.
|
|
365
|
+
Each gesture takes a chord in the key-spec grammar: `pointer.drag("Ctrl")`
|
|
366
|
+
fires only when the event that opens the gesture carries Ctrl, and then
|
|
367
|
+
instead of the bare `drag` (the most specific bound chord wins, as a
|
|
368
|
+
key spec does), held for the whole gesture - so plain drag on `rotate`
|
|
369
|
+
and `drag("Ctrl")` on `pan` is one binding each, the every-viewport
|
|
370
|
+
default. A bracketed spec may end in its button (`drag("Right")`,
|
|
371
|
+
`drag("Shift+Middle")`; bare means Left), a discriminator rather than
|
|
372
|
+
a modifier: a right drag feeds only Right specs. Touch carries no
|
|
373
|
+
modifiers or buttons: a chord is the desktop path, the bare binding
|
|
374
|
+
stays for fingers. A
|
|
365
375
|
detached d-* leaf has no layout box: give the feed `{ layout }` or it
|
|
366
376
|
throws at the first press. `invert(source)` and `scale(source, k)` are
|
|
367
377
|
the two processors: keys and sticks move the CAMERA where a drag moves
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"AGENTS.md"
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@solidrt/flux-types": "0.0.
|
|
34
|
+
"@solidrt/flux-types": "0.0.58"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@solidjs/signals": "2.0.0-rc.6",
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type { KeyboardDevice, KeyboardVec2Keys } from "./input-keyboard"
|
|
|
37
37
|
export { gamepad } from "./input-gamepad"
|
|
38
38
|
export type { GamepadDevice } from "./input-gamepad"
|
|
39
39
|
export { createPointerFeed } from "./input-pointer"
|
|
40
|
-
export type { PointerFeed, PointerFeedOptions } from "./input-pointer"
|
|
40
|
+
export type { PointerFeed, PointerFeedOptions, PointerSource } from "./input-pointer"
|
|
41
41
|
export type {
|
|
42
42
|
Element,
|
|
43
43
|
LayoutProps,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Modifier chords, the vocabulary two devices share: a spec names
|
|
2
|
+
// modifiers ahead of what it qualifies ("Shift+Tab" on the keyboard,
|
|
3
|
+
// "Ctrl" on a pointer gesture), the event must carry them, a bare spec
|
|
4
|
+
// ignores them (a shifted W still walks, a Ctrl-drag still drags), and
|
|
5
|
+
// among the specs one event satisfies the most specific wins - Unreal's
|
|
6
|
+
// chord blocker, Three's OrbitControls picking PAN over ROTATE at
|
|
7
|
+
// mousedown; Godot and Unity fire both by default and every app writes
|
|
8
|
+
// the exclusion itself. Which event resolves a chord is the device's
|
|
9
|
+
// call: the keyboard settles each key down, the pointer feed the event
|
|
10
|
+
// that opens a gesture bracket.
|
|
11
|
+
|
|
12
|
+
export type Modifier = "shiftKey" | "ctrlKey" | "altKey" | "metaKey"
|
|
13
|
+
/** The modifier flags every key, pointer and wheel event carries. */
|
|
14
|
+
export type Modified = Record<Modifier, boolean>
|
|
15
|
+
|
|
16
|
+
const MODIFIERS: Record<string, Modifier> = { Shift: "shiftKey", Ctrl: "ctrlKey", Control: "ctrlKey", Alt: "altKey", Meta: "metaKey" }
|
|
17
|
+
// The canonical order a chord is spelled in, so one modifier set has one name.
|
|
18
|
+
const ORDER: Modifier[] = ["shiftKey", "ctrlKey", "altKey", "metaKey"]
|
|
19
|
+
const NAMES: Record<Modifier, string> = { shiftKey: "Shift", ctrlKey: "Ctrl", altKey: "Alt", metaKey: "Meta" }
|
|
20
|
+
|
|
21
|
+
/** The modifiers `parts` name (["Ctrl", "Shift"]), deduplicated, in
|
|
22
|
+
* canonical order; `what` and `text` name the caller and its spec in the
|
|
23
|
+
* throw on an unknown name. */
|
|
24
|
+
export function parseModifiers(what: string, text: string, parts: string[]): Modifier[] {
|
|
25
|
+
let mods = new Set<Modifier>()
|
|
26
|
+
for (let part of parts) {
|
|
27
|
+
let mod = MODIFIERS[part]
|
|
28
|
+
if (!mod) throw new Error(`${what}: unknown modifier "${part}" in "${text}" (Shift, Ctrl, Alt or Meta)`)
|
|
29
|
+
mods.add(mod)
|
|
30
|
+
}
|
|
31
|
+
return ORDER.filter(m => mods.has(m))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The canonical spelling of a modifier set: "Shift+Ctrl"; "" for none. */
|
|
35
|
+
export let chordName = (mods: Modifier[]): string => mods.map(m => NAMES[m]).join("+")
|
|
36
|
+
|
|
37
|
+
/** Of `items`, those whose modifiers the event carries, narrowed to the
|
|
38
|
+
* most specific (the longest modifier lists, all of them on a tie);
|
|
39
|
+
* empty when none match. */
|
|
40
|
+
export function mostSpecific<T>(items: T[], mods: (item: T) => Modifier[], event: Modified): T[] {
|
|
41
|
+
let hits = items.filter(item => mods(item).every(m => event[m]))
|
|
42
|
+
let most = hits.reduce((n, item) => Math.max(n, mods(item).length), 0)
|
|
43
|
+
return hits.filter(item => mods(item).length === most)
|
|
44
|
+
}
|
package/src/input-keyboard.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
// a shifted "W".
|
|
9
9
|
//
|
|
10
10
|
// A spec may name modifiers ahead of the key ("Shift+Tab", "Ctrl+KeyS";
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// -1, not 0, on Shift+Tab;
|
|
15
|
-
//
|
|
11
|
+
// the chord vocabulary of input-chord.ts, shared with the pointer feed):
|
|
12
|
+
// the down must carry them, while a bare spec ignores them (a shifted W
|
|
13
|
+
// still walks). Within one source the most specific matching spec wins
|
|
14
|
+
// a down, so `axis("Shift+Tab", "Tab")` reads -1, not 0, on Shift+Tab;
|
|
15
|
+
// and an up releases on the key alone, so a modifier let go first
|
|
16
|
+
// cannot leave the key stuck.
|
|
16
17
|
//
|
|
17
18
|
// Held state is a count behind a signal, so a source's rate is reactive
|
|
18
19
|
// (a held key wakes a control's frame loop) and a blur - the up that never
|
|
@@ -27,10 +28,8 @@ import { createSignal } from "@solidjs/signals"
|
|
|
27
28
|
import type { KeyEvent } from "./types"
|
|
28
29
|
import type { InputSource } from "./input-map"
|
|
29
30
|
import type { Vec2 } from "./input-axes"
|
|
30
|
-
|
|
31
|
-
type Modifier
|
|
32
|
-
|
|
33
|
-
const MODIFIERS: Record<string, Modifier> = { Shift: "shiftKey", Ctrl: "ctrlKey", Control: "ctrlKey", Alt: "altKey", Meta: "metaKey" }
|
|
31
|
+
import { mostSpecific, parseModifiers } from "./input-chord"
|
|
32
|
+
import type { Modifier } from "./input-chord"
|
|
34
33
|
|
|
35
34
|
// A parsed spec: the text as given (the held-state key and the label),
|
|
36
35
|
// the key it names, and the modifiers the down must carry.
|
|
@@ -41,13 +40,7 @@ function parse(what: string, text: unknown): Spec {
|
|
|
41
40
|
let parts = text.split("+")
|
|
42
41
|
let key = parts.pop()!
|
|
43
42
|
if (key.length === 0) throw new Error(`keyboard.${what}: "${text}" names no key`)
|
|
44
|
-
|
|
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 }
|
|
43
|
+
return { text, key, mods: parseModifiers(`keyboard.${what}`, text, parts) }
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
// A key matches its physical code or its logical key; a letter code
|
|
@@ -75,11 +68,7 @@ function held(specs: Spec[]) {
|
|
|
75
68
|
// moves "Tab" to "Shift+Tab" and back), an up frees them all.
|
|
76
69
|
let onKey = specs.filter(s => matches(event, s.key))
|
|
77
70
|
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
|
-
}
|
|
71
|
+
if (isDown) for (let s of mostSpecific(onKey, s => s.mods, event)) down.add(s.text)
|
|
83
72
|
setCount(down.size)
|
|
84
73
|
},
|
|
85
74
|
blur() {
|
package/src/input-map.ts
CHANGED
|
@@ -213,7 +213,8 @@ function checkValue(what: string, kind: ActionKind, v: unknown): void {
|
|
|
213
213
|
|
|
214
214
|
function checkSource(source: unknown): InputSource {
|
|
215
215
|
let s = source as InputSource
|
|
216
|
-
|
|
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") {
|
|
217
218
|
throw new Error(`createInputMap: not an input source: ${String(source)}`)
|
|
218
219
|
}
|
|
219
220
|
return s
|
package/src/input-pointer.ts
CHANGED
|
@@ -33,6 +33,30 @@
|
|
|
33
33
|
// Brackets fire on the press itself, before the recognizer's slop, so a
|
|
34
34
|
// glide stops the moment a finger lands.
|
|
35
35
|
//
|
|
36
|
+
// Chords: every source is also callable with a modifier spec, and
|
|
37
|
+
// `pointer.drag("Ctrl")` is the variant that fires only when the event
|
|
38
|
+
// opening the gesture carries Ctrl - and then instead of the bare `drag`,
|
|
39
|
+
// as the keyboard's most specific spec wins a key down (input-chord.ts,
|
|
40
|
+
// the one grammar for both devices). That is the every-viewport default,
|
|
41
|
+
// plain drag orbits and Ctrl-drag pans, as one binding each. A chord is
|
|
42
|
+
// resolved once per bracket, at the event that opens it (the down that
|
|
43
|
+
// starts a drag, the second down that starts pan/pinch/twist, the up
|
|
44
|
+
// that hands the gesture back to drag), and held for the bracket, so a
|
|
45
|
+
// modifier tapped mid-drag cannot split one gesture across two actions
|
|
46
|
+
// (Three decides at mousedown; Unreal's continuous chord splits). The
|
|
47
|
+
// unbracketed wheel and mouseDelta resolve per event. Touch carries no
|
|
48
|
+
// modifiers, so a chord binding is the desktop path and the bare one
|
|
49
|
+
// stays for fingers.
|
|
50
|
+
//
|
|
51
|
+
// Buttons: a bracketed spec may end in the button that opens it, as a
|
|
52
|
+
// key spec ends in its key - `pointer.drag("Right")`, `drag("Shift+Middle")`
|
|
53
|
+
// (Left, Middle, Right; bare means Left) - so Three's right-drag pan is
|
|
54
|
+
// a binding too. A button is a discriminator, not a modifier: a right
|
|
55
|
+
// drag feeds only Right specs, and the chord rule picks among those. One
|
|
56
|
+
// button per pointer: a second button pressed while the first is held
|
|
57
|
+
// joins nothing, and only the held button's release closes the gesture.
|
|
58
|
+
// The wheel and mouseDelta have no button; a button word on them throws.
|
|
59
|
+
//
|
|
36
60
|
// The element's laid-out box normalizes the travel: read at the press
|
|
37
61
|
// through getLayoutBox (the untransformed read, so a designSize fit or
|
|
38
62
|
// an ancestor transform never skews it), or from `layout` when given -
|
|
@@ -49,6 +73,8 @@ import { pointerLocked } from "./window"
|
|
|
49
73
|
import type { PointerEvent, WheelEvent } from "./types"
|
|
50
74
|
import type { DeltaSink, InputSource } from "./input-map"
|
|
51
75
|
import type { Vec2 } from "./input-axes"
|
|
76
|
+
import { chordName, mostSpecific, parseModifiers } from "./input-chord"
|
|
77
|
+
import type { Modified, Modifier } from "./input-chord"
|
|
52
78
|
|
|
53
79
|
// Octaves per wheel-delta unit: the zoom exponent both cameras used
|
|
54
80
|
// (0.0015 per unit) taken in log2, so a 100-unit notch zooms about a
|
|
@@ -60,6 +86,16 @@ const WHEEL_OCTAVES = 0.0015 / Math.LN2
|
|
|
60
86
|
// turn is the ~0.002 rad/px of Three's PointerLockControls.
|
|
61
87
|
const MOUSE_PX_PER_UNIT = 1500
|
|
62
88
|
|
|
89
|
+
/**
|
|
90
|
+
* A pointer gesture as a source. Bound as is, it fires on the primary
|
|
91
|
+
* button under any modifiers; called with a spec ("Ctrl", "Shift+Alt",
|
|
92
|
+
* "Right", "Shift+Middle") it is the variant that fires only when the
|
|
93
|
+
* event opening the gesture carries those modifiers and that button, and
|
|
94
|
+
* then instead of the bare one: the most specific bound chord wins. The
|
|
95
|
+
* same spec returns the same source, so unbind by identity works.
|
|
96
|
+
*/
|
|
97
|
+
export type PointerSource<K extends "axis" | "vec2"> = InputSource<K> & ((chord: string) => InputSource<K>)
|
|
98
|
+
|
|
63
99
|
export interface PointerFeed {
|
|
64
100
|
/** Spread on the element whose pointer events feed the sources. */
|
|
65
101
|
handlers: {
|
|
@@ -70,20 +106,20 @@ export interface PointerFeed {
|
|
|
70
106
|
onWheel(event: WheelEvent): void
|
|
71
107
|
}
|
|
72
108
|
/** One pointer's travel, in element heights; bracketed. */
|
|
73
|
-
drag:
|
|
109
|
+
drag: PointerSource<"vec2">
|
|
74
110
|
/** Two or more pointers' focal travel, in element heights; bracketed. */
|
|
75
|
-
pan:
|
|
111
|
+
pan: PointerSource<"vec2">
|
|
76
112
|
/** Span change of two pointers, in octaves (spreading positive), with
|
|
77
113
|
* the focal point; bracketed. */
|
|
78
|
-
pinch:
|
|
114
|
+
pinch: PointerSource<"axis">
|
|
79
115
|
/** Rotation of the pointer pair, in turns, with the focal point; bracketed. */
|
|
80
|
-
twist:
|
|
116
|
+
twist: PointerSource<"axis">
|
|
81
117
|
/** Wheel notches in octaves (wheel up positive), with the cursor as the
|
|
82
118
|
* focal point; unbracketed. */
|
|
83
|
-
wheel:
|
|
119
|
+
wheel: PointerSource<"axis">
|
|
84
120
|
/** Raw mouse motion while the pointer is locked, in view-height
|
|
85
121
|
* equivalents (see MOUSE_PX_PER_UNIT); unbracketed. */
|
|
86
|
-
mouseDelta:
|
|
122
|
+
mouseDelta: PointerSource<"vec2">
|
|
87
123
|
}
|
|
88
124
|
|
|
89
125
|
export interface PointerFeedOptions {
|
|
@@ -93,33 +129,86 @@ export interface PointerFeedOptions {
|
|
|
93
129
|
layout?: () => { width: number; height: number } | null
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
|
|
132
|
+
// Button names a bracketed spec may end in, to the event's button index.
|
|
133
|
+
const BUTTONS: Record<string, number> = { Left: 0, Middle: 1, Right: 2 }
|
|
134
|
+
const PRIMARY = 0
|
|
97
135
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
// One gesture's sinks, bucketed by spec (the bare source is the empty
|
|
137
|
+
// chord on the primary button). A bracket opens the buckets of the
|
|
138
|
+
// button that opened it whose modifiers the opening event carries,
|
|
139
|
+
// narrowed to the most specific among those with a sink bound, and
|
|
140
|
+
// delivers to them until it closes; an unbracketed send resolves the
|
|
141
|
+
// same way per event. Sinks are live sets, as before chords: a consumer
|
|
142
|
+
// bound mid-gesture joins it.
|
|
143
|
+
type Bucket = { mods: Modifier[]; button: number; sinks: Set<DeltaSink>; source: InputSource }
|
|
144
|
+
|
|
145
|
+
function gesture<K extends "axis" | "vec2">(kind: K, label: string, buttons: boolean) {
|
|
146
|
+
let buckets = new Map<string, Bucket>()
|
|
147
|
+
let open: Set<DeltaSink>[] = []
|
|
148
|
+
let variant = (mods: Modifier[], button: number): InputSource<K> => {
|
|
149
|
+
let name = [chordName(mods), button === PRIMARY ? "" : Object.keys(BUTTONS)[button]!].filter(Boolean).join("+")
|
|
150
|
+
let bucket = buckets.get(name)
|
|
151
|
+
if (!bucket) {
|
|
152
|
+
let sinks = new Set<DeltaSink>()
|
|
153
|
+
let source: InputSource<K> = {
|
|
154
|
+
kind,
|
|
155
|
+
label: name ? `${label} (${name})` : label,
|
|
156
|
+
deltas(sink) {
|
|
157
|
+
sinks.add(sink)
|
|
158
|
+
return () => {
|
|
159
|
+
sinks.delete(sink)
|
|
160
|
+
}
|
|
161
|
+
},
|
|
106
162
|
}
|
|
163
|
+
bucket = { mods, button, sinks, source }
|
|
164
|
+
buckets.set(name, bucket)
|
|
165
|
+
}
|
|
166
|
+
return bucket.source as InputSource<K>
|
|
167
|
+
}
|
|
168
|
+
let bare = variant([], PRIMARY)
|
|
169
|
+
let spec = (text: string): InputSource<K> => {
|
|
170
|
+
if (typeof text !== "string" || text.length === 0) throw new Error(`${label}: expected a spec such as "Ctrl", "Shift+Alt"${buttons ? ', "Right" or "Shift+Middle"' : ""}, got ${String(text)}`)
|
|
171
|
+
let parts = text.split("+")
|
|
172
|
+
let button = PRIMARY
|
|
173
|
+
if (parts[parts.length - 1]! in BUTTONS) {
|
|
174
|
+
if (!buttons) throw new Error(`${label}: has no button, but "${text}" names one`)
|
|
175
|
+
button = BUTTONS[parts.pop()!]!
|
|
176
|
+
}
|
|
177
|
+
return variant(parseModifiers(label, text, parts), button)
|
|
178
|
+
}
|
|
179
|
+
let source: PointerSource<K> = Object.assign(spec, { kind, label: bare.label, deltas: bare.deltas! })
|
|
180
|
+
let resolve = (event: Modified, button: number): Set<DeltaSink>[] =>
|
|
181
|
+
mostSpecific(
|
|
182
|
+
[...buckets.values()].filter(b => b.sinks.size > 0 && b.button === button),
|
|
183
|
+
b => b.mods,
|
|
184
|
+
event,
|
|
185
|
+
).map(b => b.sinks)
|
|
186
|
+
return {
|
|
187
|
+
source,
|
|
188
|
+
begin(event: Modified, button: number) {
|
|
189
|
+
open = resolve(event, button)
|
|
190
|
+
for (let sinks of open) sinks.forEach(k => k.begin())
|
|
191
|
+
},
|
|
192
|
+
delta(value: number | Vec2, focal?: Vec2) {
|
|
193
|
+
for (let sinks of open) sinks.forEach(k => k.delta(value, focal))
|
|
194
|
+
},
|
|
195
|
+
end() {
|
|
196
|
+
for (let sinks of open) sinks.forEach(k => k.end())
|
|
197
|
+
open = []
|
|
198
|
+
},
|
|
199
|
+
send(event: Modified, value: number | Vec2, focal?: Vec2) {
|
|
200
|
+
for (let sinks of resolve(event, PRIMARY)) sinks.forEach(k => k.delta(value, focal))
|
|
107
201
|
},
|
|
108
202
|
}
|
|
109
203
|
}
|
|
110
204
|
|
|
111
205
|
export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed {
|
|
112
|
-
let
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
mouseDelta: new Set<DeltaSink>(),
|
|
119
|
-
}
|
|
120
|
-
let begin = (s: Sinks) => s.forEach(k => k.begin())
|
|
121
|
-
let end = (s: Sinks) => s.forEach(k => k.end())
|
|
122
|
-
let delta = (s: Sinks, value: number | Vec2, focal?: Vec2) => s.forEach(k => k.delta(value, focal))
|
|
206
|
+
let drag = gesture("vec2", "pointer drag", true)
|
|
207
|
+
let pan = gesture("vec2", "pointer two-finger pan", true)
|
|
208
|
+
let pinch = gesture("axis", "pointer pinch", true)
|
|
209
|
+
let twist = gesture("axis", "pointer twist", true)
|
|
210
|
+
let wheel = gesture("axis", "pointer wheel", false)
|
|
211
|
+
let mouseDelta = gesture("vec2", "mouse motion (pointer locked)", false)
|
|
123
212
|
|
|
124
213
|
// The element's box, read at each press (see the header). A detached
|
|
125
214
|
// leaf has no layout box and must bring `layout`: normalizing by
|
|
@@ -138,76 +227,80 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
|
|
|
138
227
|
let focalOf = (x: number, y: number): Vec2 | undefined => (box.width > 0 && box.height > 0 ? [x / box.width, y / box.height] : undefined)
|
|
139
228
|
|
|
140
229
|
let transform = createTransform({
|
|
230
|
+
buttons: "any",
|
|
141
231
|
onTransformMove: t => {
|
|
142
232
|
let h = unit()
|
|
143
233
|
let travel: Vec2 = [t.dx / h, t.dy / h]
|
|
144
234
|
if (t.pointers >= 2) {
|
|
145
|
-
if (t.dx !== 0 || t.dy !== 0) delta(
|
|
235
|
+
if (t.dx !== 0 || t.dy !== 0) pan.delta(travel)
|
|
146
236
|
let focal = focalOf(t.x, t.y)
|
|
147
|
-
if (t.scale !== 1) delta(
|
|
148
|
-
if (t.rotation !== 0) delta(
|
|
237
|
+
if (t.scale !== 1) pinch.delta(Math.log2(t.scale), focal)
|
|
238
|
+
if (t.rotation !== 0) twist.delta(t.rotation / (2 * Math.PI), focal)
|
|
149
239
|
} else if (t.dx !== 0 || t.dy !== 0) {
|
|
150
|
-
delta(
|
|
240
|
+
drag.delta(travel)
|
|
151
241
|
}
|
|
152
242
|
},
|
|
153
243
|
})
|
|
154
244
|
|
|
155
|
-
// Pointers down on the element
|
|
156
|
-
// press opens drag, a second closes it
|
|
157
|
-
// gestures, and the count coming back down
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
245
|
+
// Pointers down on the element with the button that pressed them, for
|
|
246
|
+
// the gesture brackets: the first press opens drag, a second closes it
|
|
247
|
+
// and opens the two-pointer gestures, and the count coming back down
|
|
248
|
+
// reverses that. The event that opens a bracket resolves its chord,
|
|
249
|
+
// and the pointer it opens for supplies the button (the drag that
|
|
250
|
+
// resumes when a second pointer lifts belongs to the one still down).
|
|
251
|
+
let downs = new Map<number, number>()
|
|
252
|
+
let landed = (e: PointerEvent, button: number) => {
|
|
253
|
+
downs.set(e.pointerId, button)
|
|
254
|
+
if (downs.size === 1) drag.begin(e, button)
|
|
163
255
|
if (downs.size === 2) {
|
|
164
|
-
end(
|
|
165
|
-
begin(
|
|
166
|
-
begin(
|
|
167
|
-
begin(
|
|
256
|
+
drag.end()
|
|
257
|
+
pan.begin(e, button)
|
|
258
|
+
pinch.begin(e, button)
|
|
259
|
+
twist.begin(e, button)
|
|
168
260
|
}
|
|
169
261
|
}
|
|
170
|
-
let lifted = (
|
|
171
|
-
|
|
262
|
+
let lifted = (e: PointerEvent) => {
|
|
263
|
+
downs.delete(e.pointerId)
|
|
172
264
|
if (downs.size === 1) {
|
|
173
|
-
end(
|
|
174
|
-
end(
|
|
175
|
-
end(
|
|
176
|
-
begin(
|
|
265
|
+
pan.end()
|
|
266
|
+
pinch.end()
|
|
267
|
+
twist.end()
|
|
268
|
+
drag.begin(e, [...downs.values()][0]!)
|
|
177
269
|
}
|
|
178
|
-
if (downs.size === 0) end(
|
|
270
|
+
if (downs.size === 0) drag.end()
|
|
179
271
|
}
|
|
180
272
|
|
|
181
273
|
return {
|
|
182
274
|
handlers: {
|
|
183
275
|
onPointerDown(e) {
|
|
184
|
-
if (
|
|
276
|
+
if (downs.has(e.pointerId)) return
|
|
185
277
|
if (downs.size === 0) box = size(e)
|
|
186
|
-
landed(e.
|
|
278
|
+
landed(e, e.button ?? PRIMARY)
|
|
187
279
|
transform.handlers.onPointerDown(e)
|
|
188
280
|
},
|
|
189
281
|
onPointerMove(e) {
|
|
190
282
|
if (pointerLocked() && e.pointerType === "mouse" && (e.movementX !== 0 || e.movementY !== 0)) {
|
|
191
|
-
|
|
283
|
+
mouseDelta.send(e, [e.movementX / MOUSE_PX_PER_UNIT, e.movementY / MOUSE_PX_PER_UNIT])
|
|
192
284
|
}
|
|
193
285
|
transform.handlers.onPointerMove(e)
|
|
194
286
|
},
|
|
195
287
|
onPointerUp(e) {
|
|
288
|
+
if (downs.get(e.pointerId) !== (e.button ?? PRIMARY)) return
|
|
196
289
|
transform.handlers.onPointerUp(e)
|
|
197
|
-
lifted(e
|
|
290
|
+
lifted(e)
|
|
198
291
|
},
|
|
199
292
|
onPointerLeave() {},
|
|
200
293
|
onWheel(e) {
|
|
201
294
|
if (e.deltaY === 0) return
|
|
202
295
|
box = size(e)
|
|
203
|
-
|
|
296
|
+
wheel.send(e, -e.deltaY * WHEEL_OCTAVES, focalOf(e.localX, e.localY))
|
|
204
297
|
},
|
|
205
298
|
},
|
|
206
|
-
drag:
|
|
207
|
-
pan:
|
|
208
|
-
pinch:
|
|
209
|
-
twist:
|
|
210
|
-
wheel:
|
|
211
|
-
mouseDelta: source
|
|
299
|
+
drag: drag.source,
|
|
300
|
+
pan: pan.source,
|
|
301
|
+
pinch: pinch.source,
|
|
302
|
+
twist: twist.source,
|
|
303
|
+
wheel: wheel.source,
|
|
304
|
+
mouseDelta: mouseDelta.source,
|
|
212
305
|
}
|
|
213
306
|
}
|
package/src/transform.ts
CHANGED
|
@@ -47,6 +47,9 @@ export type TransformDelta = {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export interface TransformOptions {
|
|
50
|
+
/** Which mouse buttons open a gesture: the primary only (default), or
|
|
51
|
+
* any - the pointer feed's choice, which tells its buttons apart itself. */
|
|
52
|
+
buttons?: "primary" | "any"
|
|
50
53
|
onTransformStart?: () => void
|
|
51
54
|
/** Streams one delta per frame; compose them multiplicatively (scale) / additively (dx, dy, rotation). */
|
|
52
55
|
onTransformMove?: (t: TransformDelta) => void
|
|
@@ -251,7 +254,7 @@ export function createTransform(options: TransformOptions) {
|
|
|
251
254
|
|
|
252
255
|
let handlers = {
|
|
253
256
|
onPointerDown: (e: PointerEvent) => {
|
|
254
|
-
if (e.button != null && e.button !== 0) return
|
|
257
|
+
if (options.buttons !== "any" && e.button != null && e.button !== 0) return
|
|
255
258
|
if (pointers.has(e.pointerId)) return
|
|
256
259
|
if (active) {
|
|
257
260
|
// A finger joining an established gesture belongs to it outright; if
|