@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.
- package/AGENTS.md +99 -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-gamepad-device.ts +161 -0
- package/src/input-gamepad.ts +24 -0
- package/src/input-keyboard.ts +156 -0
- package/src/input-map.ts +467 -0
- package/src/input-pointer.ts +213 -0
- package/src/input.ts +12 -0
- package/src/types.d.ts +4 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// A pointer feed: one element's pointer events turned into gesture
|
|
2
|
+
// sources for an input map. The pointer is the one device that is not
|
|
3
|
+
// global - the tree holds UI and scene together, so a drag belongs to the
|
|
4
|
+
// element it started on (a slider beside a viewport must not turn the
|
|
5
|
+
// camera) - hence a feed per element: spread `feed.handlers` on the leaf
|
|
6
|
+
// (a Scene, View3d, SpriteLayer or View2d takes it as its `pointer` prop
|
|
7
|
+
// and spreads it on the built-in leaf) and bind its sources.
|
|
8
|
+
//
|
|
9
|
+
// The sources are gestures, not raw events, because a pointer only means
|
|
10
|
+
// something once a gesture is recognized: a move without a press is
|
|
11
|
+
// nothing, one finger dragging is `drag`, two fingers moving together is
|
|
12
|
+
// `pan`, spreading is `pinch`, turning is `twist`, and the wheel is its
|
|
13
|
+
// own thing. One merged recognizer (createTransform) serves all of them,
|
|
14
|
+
// so they arbitrate in the app-wide gesture arena as one - a viewport
|
|
15
|
+
// inside a scroller never double-handles - and they never fight each
|
|
16
|
+
// other over the same fingers. `mouseDelta` is the pointer-lock path:
|
|
17
|
+
// raw mouse motion while the pointer is locked, when the cursor does not
|
|
18
|
+
// move and no drag can arm.
|
|
19
|
+
//
|
|
20
|
+
// Units are device-free (the input map's contract): drag and pan travel
|
|
21
|
+
// in the element's own heights (a drag across the element is 1 whatever
|
|
22
|
+
// the window size - Three's OrbitControls convention), pinch and wheel
|
|
23
|
+
// in octaves (log2 of the scale change, one notch about a fifth), twist
|
|
24
|
+
// in turns, mouse motion in view-height-equivalents through a fixed
|
|
25
|
+
// reference. The focal point of a pinch or a wheel rides along as a
|
|
26
|
+
// fraction of the element (0..1), the anchor a zoom-about-the-cursor
|
|
27
|
+
// consumer maps to its own space. Screen convention: x right, y down.
|
|
28
|
+
//
|
|
29
|
+
// Gesture brackets: `drag` begins when the first pointer lands and ends
|
|
30
|
+
// when the last lifts (or when a second lands, which begins `pan`,
|
|
31
|
+
// `pinch` and `twist` until the count drops back to one); the wheel
|
|
32
|
+
// sends no brackets, so a consumer can tell a notch from a finger.
|
|
33
|
+
// Brackets fire on the press itself, before the recognizer's slop, so a
|
|
34
|
+
// glide stops the moment a finger lands.
|
|
35
|
+
//
|
|
36
|
+
// The element's laid-out box normalizes the travel: read at the press
|
|
37
|
+
// through getLayoutBox (the untransformed read, so a designSize fit or
|
|
38
|
+
// an ancestor transform never skews it), or from `layout` when given -
|
|
39
|
+
// required for a detached d-* leaf, which has no layout box (the feed
|
|
40
|
+
// throws rather than treat pixels as heights), and handy for a headless
|
|
41
|
+
// check with no tree. A zero-height box passes pixels through unscaled.
|
|
42
|
+
//
|
|
43
|
+
// Create a feed in an owned scope (a component body): the recognizer
|
|
44
|
+
// registers its cleanup with the owner.
|
|
45
|
+
|
|
46
|
+
import { createTransform } from "./transform"
|
|
47
|
+
import { getLayoutBox } from "./core"
|
|
48
|
+
import { pointerLocked } from "./window"
|
|
49
|
+
import type { PointerEvent, WheelEvent } from "./types"
|
|
50
|
+
import type { DeltaSink, InputSource } from "./input-map"
|
|
51
|
+
import type { Vec2 } from "./input-axes"
|
|
52
|
+
|
|
53
|
+
// Octaves per wheel-delta unit: the zoom exponent both cameras used
|
|
54
|
+
// (0.0015 per unit) taken in log2, so a 100-unit notch zooms about a
|
|
55
|
+
// fifth of an octave and a fast scroll of ten notches doubles.
|
|
56
|
+
const WHEEL_OCTAVES = 0.0015 / Math.LN2
|
|
57
|
+
// Mouse travel per delta unit under pointer lock, in logical pixels: one
|
|
58
|
+
// unit is what a drag across the element's height means to a consumer (a
|
|
59
|
+
// half turn of look in the first-person control), and 1500 px per half
|
|
60
|
+
// turn is the ~0.002 rad/px of Three's PointerLockControls.
|
|
61
|
+
const MOUSE_PX_PER_UNIT = 1500
|
|
62
|
+
|
|
63
|
+
export interface PointerFeed {
|
|
64
|
+
/** Spread on the element whose pointer events feed the sources. */
|
|
65
|
+
handlers: {
|
|
66
|
+
onPointerDown(event: PointerEvent): void
|
|
67
|
+
onPointerMove(event: PointerEvent): void
|
|
68
|
+
onPointerUp(event: PointerEvent): void
|
|
69
|
+
onPointerLeave(event: PointerEvent): void
|
|
70
|
+
onWheel(event: WheelEvent): void
|
|
71
|
+
}
|
|
72
|
+
/** One pointer's travel, in element heights; bracketed. */
|
|
73
|
+
drag: InputSource<"vec2">
|
|
74
|
+
/** Two or more pointers' focal travel, in element heights; bracketed. */
|
|
75
|
+
pan: InputSource<"vec2">
|
|
76
|
+
/** Span change of two pointers, in octaves (spreading positive), with
|
|
77
|
+
* the focal point; bracketed. */
|
|
78
|
+
pinch: InputSource<"axis">
|
|
79
|
+
/** Rotation of the pointer pair, in turns, with the focal point; bracketed. */
|
|
80
|
+
twist: InputSource<"axis">
|
|
81
|
+
/** Wheel notches in octaves (wheel up positive), with the cursor as the
|
|
82
|
+
* focal point; unbracketed. */
|
|
83
|
+
wheel: InputSource<"axis">
|
|
84
|
+
/** Raw mouse motion while the pointer is locked, in view-height
|
|
85
|
+
* equivalents (see MOUSE_PX_PER_UNIT); unbracketed. */
|
|
86
|
+
mouseDelta: InputSource<"vec2">
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PointerFeedOptions {
|
|
90
|
+
/** The element's size, when the app knows it - required for a detached
|
|
91
|
+
* d-* leaf, which has no layout box; default reads getLayoutBox at each
|
|
92
|
+
* press. */
|
|
93
|
+
layout?: () => { width: number; height: number } | null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type Sinks = Set<DeltaSink>
|
|
97
|
+
|
|
98
|
+
function source<K extends "axis" | "vec2">(kind: K, label: string, sinks: Sinks): InputSource<K> {
|
|
99
|
+
return {
|
|
100
|
+
kind,
|
|
101
|
+
label,
|
|
102
|
+
deltas(sink) {
|
|
103
|
+
sinks.add(sink)
|
|
104
|
+
return () => {
|
|
105
|
+
sinks.delete(sink)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed {
|
|
112
|
+
let sinks = {
|
|
113
|
+
drag: new Set<DeltaSink>(),
|
|
114
|
+
pan: new Set<DeltaSink>(),
|
|
115
|
+
pinch: new Set<DeltaSink>(),
|
|
116
|
+
twist: new Set<DeltaSink>(),
|
|
117
|
+
wheel: new Set<DeltaSink>(),
|
|
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))
|
|
123
|
+
|
|
124
|
+
// The element's box, read at each press (see the header). A detached
|
|
125
|
+
// leaf has no layout box and must bring `layout`: normalizing by
|
|
126
|
+
// nothing would turn a 100 px drag into 100 element heights, so that
|
|
127
|
+
// is a throw (the dev validation policy), while a zero-size box is a
|
|
128
|
+
// degenerate layout and passes pixels through.
|
|
129
|
+
let size = (event: { currentTarget: number }): { width: number; height: number } => {
|
|
130
|
+
let box = options.layout ? options.layout() : getLayoutBox({ id: event.currentTarget })
|
|
131
|
+
if (box === null && !options.layout) {
|
|
132
|
+
throw new Error("createPointerFeed: the element has no layout box (a detached d-* leaf, or before its first layout); create the feed with { layout: () => ({ width, height }) } for it")
|
|
133
|
+
}
|
|
134
|
+
return box && box.width > 0 && box.height > 0 ? { width: box.width, height: box.height } : { width: 0, height: 0 }
|
|
135
|
+
}
|
|
136
|
+
let box = { width: 0, height: 0 }
|
|
137
|
+
let unit = () => (box.height > 0 ? box.height : 1)
|
|
138
|
+
let focalOf = (x: number, y: number): Vec2 | undefined => (box.width > 0 && box.height > 0 ? [x / box.width, y / box.height] : undefined)
|
|
139
|
+
|
|
140
|
+
let transform = createTransform({
|
|
141
|
+
onTransformMove: t => {
|
|
142
|
+
let h = unit()
|
|
143
|
+
let travel: Vec2 = [t.dx / h, t.dy / h]
|
|
144
|
+
if (t.pointers >= 2) {
|
|
145
|
+
if (t.dx !== 0 || t.dy !== 0) delta(sinks.pan, travel)
|
|
146
|
+
let focal = focalOf(t.x, t.y)
|
|
147
|
+
if (t.scale !== 1) delta(sinks.pinch, Math.log2(t.scale), focal)
|
|
148
|
+
if (t.rotation !== 0) delta(sinks.twist, t.rotation / (2 * Math.PI), focal)
|
|
149
|
+
} else if (t.dx !== 0 || t.dy !== 0) {
|
|
150
|
+
delta(sinks.drag, travel)
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Pointers down on the element, for the gesture brackets: the first
|
|
156
|
+
// press opens drag, a second closes it and opens the two-pointer
|
|
157
|
+
// gestures, and the count coming back down reverses that.
|
|
158
|
+
let downs = new Set<number>()
|
|
159
|
+
let landed = (id: number) => {
|
|
160
|
+
if (downs.has(id)) return
|
|
161
|
+
downs.add(id)
|
|
162
|
+
if (downs.size === 1) begin(sinks.drag)
|
|
163
|
+
if (downs.size === 2) {
|
|
164
|
+
end(sinks.drag)
|
|
165
|
+
begin(sinks.pan)
|
|
166
|
+
begin(sinks.pinch)
|
|
167
|
+
begin(sinks.twist)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let lifted = (id: number) => {
|
|
171
|
+
if (!downs.delete(id)) return
|
|
172
|
+
if (downs.size === 1) {
|
|
173
|
+
end(sinks.pan)
|
|
174
|
+
end(sinks.pinch)
|
|
175
|
+
end(sinks.twist)
|
|
176
|
+
begin(sinks.drag)
|
|
177
|
+
}
|
|
178
|
+
if (downs.size === 0) end(sinks.drag)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
handlers: {
|
|
183
|
+
onPointerDown(e) {
|
|
184
|
+
if (e.button != null && e.button !== 0) return
|
|
185
|
+
if (downs.size === 0) box = size(e)
|
|
186
|
+
landed(e.pointerId)
|
|
187
|
+
transform.handlers.onPointerDown(e)
|
|
188
|
+
},
|
|
189
|
+
onPointerMove(e) {
|
|
190
|
+
if (pointerLocked() && e.pointerType === "mouse" && (e.movementX !== 0 || e.movementY !== 0)) {
|
|
191
|
+
delta(sinks.mouseDelta, [e.movementX / MOUSE_PX_PER_UNIT, e.movementY / MOUSE_PX_PER_UNIT])
|
|
192
|
+
}
|
|
193
|
+
transform.handlers.onPointerMove(e)
|
|
194
|
+
},
|
|
195
|
+
onPointerUp(e) {
|
|
196
|
+
transform.handlers.onPointerUp(e)
|
|
197
|
+
lifted(e.pointerId)
|
|
198
|
+
},
|
|
199
|
+
onPointerLeave() {},
|
|
200
|
+
onWheel(e) {
|
|
201
|
+
if (e.deltaY === 0) return
|
|
202
|
+
box = size(e)
|
|
203
|
+
delta(sinks.wheel, -e.deltaY * WHEEL_OCTAVES, focalOf(e.localX, e.localY))
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
drag: source("vec2", "pointer drag", sinks.drag),
|
|
207
|
+
pan: source("vec2", "pointer two-finger pan", sinks.pan),
|
|
208
|
+
pinch: source("axis", "pointer pinch", sinks.pinch),
|
|
209
|
+
twist: source("axis", "pointer twist", sinks.twist),
|
|
210
|
+
wheel: source("axis", "pointer wheel", sinks.wheel),
|
|
211
|
+
mouseDelta: source("vec2", "mouse motion (pointer locked)", sinks.mouseDelta),
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/input.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The runtime-free half of the input surface, importable as
|
|
2
|
+
// `@solidrt/core/input` by modules that must stay headless (the camera
|
|
3
|
+
// controls and their checks run on the bare flux binary, which has no
|
|
4
|
+
// event bus). The map, the control-side axes contract and the keyboard
|
|
5
|
+
// device are pure; the gamepad device and the pointer feed need the
|
|
6
|
+
// runtime and live on the main entry.
|
|
7
|
+
export { createInputMap, invert, scale } from "./input-map"
|
|
8
|
+
export type { ActionKind, ActionValue, ActionsDecl, Binding, DeltaSink, GestureListener, InputMap, InputSource } from "./input-map"
|
|
9
|
+
export { createAxes, combineRates } from "./input-axes"
|
|
10
|
+
export type { Axes, AxesDecl, AxesHooks, AxisKind, AxisValue, Vec2 } from "./input-axes"
|
|
11
|
+
export { keyboard } from "./input-keyboard"
|
|
12
|
+
export type { KeyboardDevice, KeyboardVec2Keys } from "./input-keyboard"
|
package/src/types.d.ts
CHANGED
|
@@ -590,9 +590,10 @@ export interface TransitionProps {
|
|
|
590
590
|
* `all` covers every animatable property the element has, and a bare
|
|
591
591
|
* string is shorthand for it: `transition="300ms ease-out"`. Only
|
|
592
592
|
* properties the element carries animate (a d-rect has x, a view's x is
|
|
593
|
-
* its transform); the initial
|
|
594
|
-
*
|
|
595
|
-
*
|
|
593
|
+
* its transform); the initial values (every write before the element's
|
|
594
|
+
* first paint) never animate unless the entry sets `from` (an enter
|
|
595
|
+
* animation), and a non-numeric write (e.g. null) cancels the running
|
|
596
|
+
* animation and snaps. `null` clears the
|
|
596
597
|
* declaration; already-running animations finish. A spec built in a
|
|
597
598
|
* conditional widens `curve` to `string` for TypeScript; write
|
|
598
599
|
* `satisfies Transition` on the branch.
|