@solidrt/core 0.0.64 → 0.0.66
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 +93 -10
- package/README.md +1 -1
- package/agents/performance.md +9 -4
- package/docs/index.md +10 -1
- package/package.json +7 -5
- package/src/camera.ts +1 -1
- package/src/core.ts +7 -2
- package/src/double-tap.ts +2 -2
- package/src/environment.ts +3 -3
- package/src/gamepad.ts +33 -2
- package/src/gpu.ts +7 -6
- package/src/image.ts +10 -2
- package/src/index.ts +6 -4
- package/src/input-gamepad-device.ts +38 -14
- package/src/input-gamepad.ts +6 -4
- package/src/input-keyboard.ts +11 -2
- package/src/input-map.ts +12 -1
- package/src/input-pointer.ts +3 -3
- package/src/input-processors.ts +65 -25
- package/src/pan.ts +4 -3
- package/src/renderer.ts +9 -9
- package/src/runtime-modules.d.ts +75 -8
- package/src/swipe.ts +6 -6
- package/src/textures.ts +333 -0
- package/src/transform.ts +17 -8
- package/src/types.d.ts +45 -3
- package/src/velocity.ts +113 -50
- package/src/window.ts +44 -6
package/src/input-keyboard.ts
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Held state is a count behind a signal, so a source's rate is reactive
|
|
19
19
|
// (a held key wakes a control's frame loop) and a blur - the up that never
|
|
20
|
-
// arrives once focus has left - clears it.
|
|
20
|
+
// arrives once focus has left - clears it. A source also reports when its
|
|
21
|
+
// keys last changed (the key event's timeStamp; nothing after a blur,
|
|
22
|
+
// which is no release by the player). Composites clamp nothing: the
|
|
21
23
|
// map's combination does (W and D together read [1, -1] before the
|
|
22
24
|
// vec2 clamp to unit length, so diagonals walk at the same speed).
|
|
23
25
|
//
|
|
@@ -60,9 +62,11 @@ let matches = (event: KeyEvent, key: string): boolean => {
|
|
|
60
62
|
return key.length === 4 && key.startsWith("Key") && event.key.toLowerCase() === key[3]!.toLowerCase()
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
// A set of specs with a reactive "how many are held" count
|
|
65
|
+
// A set of specs with a reactive "how many are held" count, and the time
|
|
66
|
+
// of the key event that last changed which are held.
|
|
64
67
|
function held(specs: Spec[]) {
|
|
65
68
|
let down = new Set<string>()
|
|
69
|
+
let changedAt: number | null = null
|
|
66
70
|
// ownedWrite: a map forwards key events from wherever its handlers sit.
|
|
67
71
|
let [count, setCount] = createSignal(0, { ownedWrite: true })
|
|
68
72
|
return {
|
|
@@ -72,14 +76,18 @@ function held(specs: Spec[]) {
|
|
|
72
76
|
// on its most specific matches (a repeat under a newly held Shift
|
|
73
77
|
// moves "Tab" to "Shift+Tab" and back), an up frees them all.
|
|
74
78
|
let onKey = specs.filter(s => matches(event, s.key))
|
|
79
|
+
let before = [...down].join()
|
|
75
80
|
for (let s of onKey) down.delete(s.text)
|
|
76
81
|
if (isDown) for (let s of mostSpecific(onKey, s => s.mods, event)) down.add(s.text)
|
|
82
|
+
if ([...down].join() !== before) changedAt = event.timeStamp
|
|
77
83
|
setCount(down.size)
|
|
78
84
|
},
|
|
79
85
|
blur() {
|
|
86
|
+
if (down.size > 0) changedAt = null
|
|
80
87
|
down.clear()
|
|
81
88
|
setCount(0)
|
|
82
89
|
},
|
|
90
|
+
changedAt: () => changedAt,
|
|
83
91
|
has(text: string) {
|
|
84
92
|
return down.has(text)
|
|
85
93
|
},
|
|
@@ -95,6 +103,7 @@ function key(spec: string): InputSource<"button"> {
|
|
|
95
103
|
id: `${DEVICE}:key:${spec}`,
|
|
96
104
|
device: DEVICE,
|
|
97
105
|
rate: () => state.count() > 0,
|
|
106
|
+
changedAt: state.changedAt,
|
|
98
107
|
key: state.key,
|
|
99
108
|
blur: state.blur,
|
|
100
109
|
}
|
package/src/input-map.ts
CHANGED
|
@@ -76,7 +76,8 @@ export type DeviceName = "keyboard" | "gamepad" | "pointer"
|
|
|
76
76
|
* One device control as an input source. `rate` reads its current value
|
|
77
77
|
* (reactive); `deltas` subscribes a sink to the amounts a gesture
|
|
78
78
|
* produces; `key`/`blur` receive the key events a map forwards
|
|
79
|
-
* (InputMap.handlers)
|
|
79
|
+
* (InputMap.handlers); `changedAt` says when the rate last changed. A
|
|
80
|
+
* source may carry any subset.
|
|
80
81
|
*/
|
|
81
82
|
export interface InputSource<K extends ActionKind = ActionKind> {
|
|
82
83
|
readonly kind: K
|
|
@@ -90,6 +91,16 @@ export interface InputSource<K extends ActionKind = ActionKind> {
|
|
|
90
91
|
* may leave it out and never counts as the active device. */
|
|
91
92
|
readonly device?: DeviceName
|
|
92
93
|
rate?: () => ActionValue<K>
|
|
94
|
+
/**
|
|
95
|
+
* When `rate` last changed: the `timeStamp` (ms) of the input event that
|
|
96
|
+
* changed it, or null when the value was dropped and not let go by the
|
|
97
|
+
* player (a blur releases every held key with no event), and before any
|
|
98
|
+
* change. What `tap` and `doubleTap` measure with, so a button source
|
|
99
|
+
* has to carry it to sit under one: the time travels with the value,
|
|
100
|
+
* since whoever sees the change sees it after the event is gone. Not
|
|
101
|
+
* reactive.
|
|
102
|
+
*/
|
|
103
|
+
changedAt?: () => number | null
|
|
93
104
|
deltas?: (sink: DeltaSink) => () => void
|
|
94
105
|
key?: (event: KeyEvent, down: boolean) => void
|
|
95
106
|
blur?: () => void
|
package/src/input-pointer.ts
CHANGED
|
@@ -454,7 +454,7 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
|
|
|
454
454
|
if (downs.size === 1) {
|
|
455
455
|
opening = { id: e.pointerId, x: e.clientX, y: e.clientY, mods: mods(e), alone: true }
|
|
456
456
|
finger.reset()
|
|
457
|
-
finger.push(e.clientX, e.clientY)
|
|
457
|
+
finger.push(e.clientX, e.clientY, e.timeStamp)
|
|
458
458
|
} else if (opening) opening.alone = false
|
|
459
459
|
}
|
|
460
460
|
},
|
|
@@ -465,7 +465,7 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
|
|
|
465
465
|
transform.handlers.onPointerMove(e)
|
|
466
466
|
hold.move(e)
|
|
467
467
|
taps.move(e)
|
|
468
|
-
if (opening && opening.id === e.pointerId) finger.push(e.clientX, e.clientY)
|
|
468
|
+
if (opening && opening.id === e.pointerId && !e.predicted) finger.push(e.clientX, e.clientY, e.timeStamp)
|
|
469
469
|
},
|
|
470
470
|
onPointerUp(e) {
|
|
471
471
|
if (downs.get(e.pointerId) !== (e.button ?? PRIMARY)) return
|
|
@@ -475,7 +475,7 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
|
|
|
475
475
|
taps.up(e)
|
|
476
476
|
if (opening && opening.id === e.pointerId) {
|
|
477
477
|
if (opening.alone && downs.size === 0) {
|
|
478
|
-
let direction: SwipeDirection | null = classifySwipe(finger.velocity(), { dx: e.clientX - opening.x, dy: e.clientY - opening.y })
|
|
478
|
+
let direction: SwipeDirection | null = classifySwipe(finger.velocity(e.timeStamp), { dx: e.clientX - opening.x, dy: e.clientY - opening.y })
|
|
479
479
|
if (direction) swipe.fire(opening.mods, direction)
|
|
480
480
|
}
|
|
481
481
|
opening = null
|
package/src/input-processors.ts
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
// the plain press on another action, and the consumer reads one bool.
|
|
7
7
|
//
|
|
8
8
|
// Timing: an interaction watches its source's rate under a root of its
|
|
9
|
-
// own
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// own, so it works headless and needs no frame loop; the root is disposed
|
|
10
|
+
// with the creating scope (a component body) and lives for the session
|
|
11
|
+
// without one. `hold` keeps time with setTimeout. `tap` and `doubleTap`
|
|
12
|
+
// measure between the source's edges with the source's own time
|
|
13
|
+
// (`changedAt`, the timeStamp of the input event behind each edge): they
|
|
14
|
+
// see an edge at the flush, after the event is gone, so they read no
|
|
15
|
+
// clock of their own, and a source without `changedAt` cannot sit under
|
|
16
|
+
// them. They are edges, not levels: they read pressed for one task, long
|
|
17
|
+
// enough for a map's onPress to fire, then release on their own.
|
|
14
18
|
//
|
|
15
19
|
// Every processor composes the source's id (`invert(gamepad:leftStick)`,
|
|
16
20
|
// `hold(400,keyboard:key:Space)`), so a saved binding restores through
|
|
@@ -49,6 +53,13 @@ let checkButtonSource = (what: string, source: InputSource): void => {
|
|
|
49
53
|
if (source.kind !== "button") throw new Error(`${what}: "${source.label}" is a ${source.kind}, not a button`)
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
let checkTimedSource = (what: string, source: InputSource): void => {
|
|
57
|
+
checkButtonSource(what, source)
|
|
58
|
+
if (typeof source.changedAt !== "function") {
|
|
59
|
+
throw new Error(`${what}: "${source.label}" does not say when it changes (no changedAt), so its presses cannot be timed`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
let checkMs = (what: string, ms: number): void => {
|
|
53
64
|
if (!Number.isFinite(ms) || ms < 0) throw new Error(`${what}: ms must be a non-negative number, got ${String(ms)}`)
|
|
54
65
|
}
|
|
@@ -98,11 +109,13 @@ export function scale<K extends "axis" | "vec2">(source: InputSource<K>, factor:
|
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
// A button source derived from one button source's press and release
|
|
101
|
-
// edges: `edge(down,
|
|
102
|
-
// state and `later` for a timer that the disposal clears.
|
|
112
|
+
// edges: `edge(down, at)` runs on each, with `set` for the derived
|
|
113
|
+
// state and `later` for a timer that the disposal clears. `at` is the
|
|
114
|
+
// source's time for the edge (its changedAt), null when it has none: a
|
|
115
|
+
// source that carries no time, or a value that was dropped (a blur).
|
|
103
116
|
type EdgeHooks = { set: (pressed: boolean) => void; later: (ms: number, run: () => void) => void; clearLater: () => void }
|
|
104
117
|
|
|
105
|
-
function derived(source: InputSource<"button">, label: string, id: string, edge: (down: boolean,
|
|
118
|
+
function derived(source: InputSource<"button">, label: string, id: string, edge: (down: boolean, at: number | null, hooks: EdgeHooks) => void): InputSource<"button"> {
|
|
106
119
|
// ownedWrite: the edges land inside an effect and timers.
|
|
107
120
|
let [pressed, setPressed] = createSignal(false, { ownedWrite: true })
|
|
108
121
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -124,7 +137,7 @@ function derived(source: InputSource<"button">, label: string, id: string, edge:
|
|
|
124
137
|
createEffect(
|
|
125
138
|
() => source.rate!(),
|
|
126
139
|
(down, prev) => {
|
|
127
|
-
if (down !== prev) edge(down,
|
|
140
|
+
if (down !== prev) edge(down, source.changedAt?.() ?? null, hooks)
|
|
128
141
|
},
|
|
129
142
|
{ defer: true },
|
|
130
143
|
)
|
|
@@ -151,7 +164,7 @@ let pulse = (hooks: EdgeHooks): void => {
|
|
|
151
164
|
export function hold(source: InputSource<"button">, ms = HOLD_MS): InputSource<"button"> {
|
|
152
165
|
checkButtonSource("hold", source)
|
|
153
166
|
checkMs("hold", ms)
|
|
154
|
-
return derived(source, `${source.label} (hold ${ms} ms)`, `hold(${ms},${source.id})`, (down,
|
|
167
|
+
return derived(source, `${source.label} (hold ${ms} ms)`, `hold(${ms},${source.id})`, (down, _at, hooks) => {
|
|
155
168
|
if (down) hooks.later(ms, () => hooks.set(true))
|
|
156
169
|
else {
|
|
157
170
|
hooks.clearLater()
|
|
@@ -161,40 +174,55 @@ export function hold(source: InputSource<"button">, ms = HOLD_MS): InputSource<"
|
|
|
161
174
|
}
|
|
162
175
|
|
|
163
176
|
/** A press released within `ms` (default 200): pressed for one task on the
|
|
164
|
-
* release, so onPress fires once per tap and a long press is not one.
|
|
177
|
+
* release, so onPress fires once per tap and a long press is not one. A
|
|
178
|
+
* press the player did not let go (a blur dropped it) is no tap. The
|
|
179
|
+
* source has to say when it changes (`changedAt`). */
|
|
165
180
|
export function tap(source: InputSource<"button">, ms = TAP_MS): InputSource<"button"> {
|
|
166
|
-
|
|
181
|
+
checkTimedSource("tap", source)
|
|
167
182
|
checkMs("tap", ms)
|
|
168
|
-
let downAt =
|
|
169
|
-
return derived(source, `${source.label} (tap)`, `tap(${ms},${source.id})`, (down,
|
|
170
|
-
if (down) downAt =
|
|
171
|
-
else
|
|
183
|
+
let downAt: number | null = null
|
|
184
|
+
return derived(source, `${source.label} (tap)`, `tap(${ms},${source.id})`, (down, at, hooks) => {
|
|
185
|
+
if (down) downAt = at
|
|
186
|
+
else {
|
|
187
|
+
if (at !== null && downAt !== null && at - downAt <= ms) pulse(hooks)
|
|
188
|
+
downAt = null
|
|
189
|
+
}
|
|
172
190
|
})
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
/** Two taps (each within `tapMs`, default 200) at most `gapMs` (default
|
|
176
|
-
* 300) apart: pressed for one task on the second release.
|
|
194
|
+
* 300) apart: pressed for one task on the second release. A press the
|
|
195
|
+
* player did not let go (a blur dropped it) is no tap and ends the pair.
|
|
196
|
+
* The source has to say when it changes (`changedAt`). */
|
|
177
197
|
export function doubleTap(source: InputSource<"button">, gapMs = DOUBLE_TAP_GAP_MS, tapMs = TAP_MS): InputSource<"button"> {
|
|
178
|
-
|
|
198
|
+
checkTimedSource("doubleTap", source)
|
|
179
199
|
checkMs("doubleTap", gapMs)
|
|
180
200
|
checkMs("doubleTap", tapMs)
|
|
181
|
-
let downAt =
|
|
201
|
+
let downAt: number | null = null
|
|
182
202
|
let lastTap = -Infinity
|
|
183
|
-
return derived(source, `${source.label} (double tap)`, `doubleTap(${gapMs},${tapMs},${source.id})`, (down,
|
|
203
|
+
return derived(source, `${source.label} (double tap)`, `doubleTap(${gapMs},${tapMs},${source.id})`, (down, at, hooks) => {
|
|
184
204
|
if (down) {
|
|
185
|
-
downAt =
|
|
205
|
+
downAt = at
|
|
186
206
|
return
|
|
187
207
|
}
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
let pressedAt = downAt
|
|
209
|
+
downAt = null
|
|
210
|
+
if (at === null || pressedAt === null) {
|
|
211
|
+
lastTap = -Infinity
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (at - pressedAt > tapMs) return
|
|
215
|
+
if (at - lastTap <= gapMs) {
|
|
190
216
|
lastTap = -Infinity
|
|
191
217
|
pulse(hooks)
|
|
192
|
-
} else lastTap =
|
|
218
|
+
} else lastTap = at
|
|
193
219
|
})
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
/** Pressed while every source is: Shift+click on one action, a two-button
|
|
197
|
-
* combo on a pad. Key events reach each keyboard source among them.
|
|
223
|
+
* combo on a pad. Key events reach each keyboard source among them. It
|
|
224
|
+
* changes when the last of its sources to change did (`changedAt`, when
|
|
225
|
+
* each of them carries one). */
|
|
198
226
|
export function chord(...sources: InputSource<"button">[]): InputSource<"button"> {
|
|
199
227
|
if (sources.length < 2) throw new Error("chord: needs at least two button sources")
|
|
200
228
|
for (let s of sources) checkButtonSource("chord", s)
|
|
@@ -204,6 +232,18 @@ export function chord(...sources: InputSource<"button">[]): InputSource<"button"
|
|
|
204
232
|
id: `chord(${sources.map(s => s.id).join(",")})`,
|
|
205
233
|
device: sources[0]!.device,
|
|
206
234
|
rate: () => sources.every(s => s.rate?.() === true),
|
|
235
|
+
changedAt: sources.every(s => s.changedAt)
|
|
236
|
+
? () => {
|
|
237
|
+
let latest: number | null = null
|
|
238
|
+
for (let s of sources) {
|
|
239
|
+
let at = s.changedAt!()
|
|
240
|
+
// One source dropped (a blur): so was the chord.
|
|
241
|
+
if (at === null) return null
|
|
242
|
+
if (latest === null || at > latest) latest = at
|
|
243
|
+
}
|
|
244
|
+
return latest
|
|
245
|
+
}
|
|
246
|
+
: undefined,
|
|
207
247
|
key: (event, down) => {
|
|
208
248
|
for (let s of sources) s.key?.(event, down)
|
|
209
249
|
},
|
package/src/pan.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function createPan(options: PanOptions) {
|
|
|
108
108
|
armed = null
|
|
109
109
|
origin = { x: e.parentX, y: e.parentY }
|
|
110
110
|
tracker.reset()
|
|
111
|
-
tracker.push(e.parentX, e.parentY)
|
|
111
|
+
if (!e.predicted) tracker.push(e.parentX, e.parentY, e.timeStamp)
|
|
112
112
|
options.onPanStart?.()
|
|
113
113
|
} else {
|
|
114
114
|
// The arena is resolved against us; the drag belongs elsewhere.
|
|
@@ -117,7 +117,8 @@ export function createPan(options: PanOptions) {
|
|
|
117
117
|
return
|
|
118
118
|
}
|
|
119
119
|
if (active === e.pointerId && origin) {
|
|
120
|
-
|
|
120
|
+
// The pan follows a predicted position; its velocity does not.
|
|
121
|
+
if (!e.predicted) tracker.push(e.parentX, e.parentY, e.timeStamp)
|
|
121
122
|
options.onPanMove?.(e.parentX - origin.x, e.parentY - origin.y)
|
|
122
123
|
origin = { x: e.parentX, y: e.parentY }
|
|
123
124
|
}
|
|
@@ -126,7 +127,7 @@ export function createPan(options: PanOptions) {
|
|
|
126
127
|
if (active === e.pointerId) {
|
|
127
128
|
// The up is not a sample: it sits where the last move left the
|
|
128
129
|
// pointer, and its age is what tells a rest from a flick.
|
|
129
|
-
let velocity = flingVelocity(tracker.velocity())
|
|
130
|
+
let velocity = flingVelocity(tracker.velocity(e.timeStamp))
|
|
130
131
|
reset()
|
|
131
132
|
options.onPanEnd?.(velocity)
|
|
132
133
|
} else if (armed === e.pointerId) {
|
package/src/renderer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// createErrorBoundary comes from signals, not solid-js: solid-js marks it
|
|
2
2
|
// @internal (an app writes <Errored>, a renderer reaches for the primitive)
|
|
3
|
-
// and
|
|
3
|
+
// and leaves it out of its public types.
|
|
4
4
|
import { createErrorBoundary, createRoot, onCleanup, NotReadyError, untrack } from "@solidjs/signals"
|
|
5
5
|
import { createRenderer } from "@solidjs/universal"
|
|
6
6
|
import type { Element } from "solid-js"
|
|
@@ -86,7 +86,7 @@ function flushDestroy(): void {
|
|
|
86
86
|
function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
87
87
|
if (!node || !parent) return
|
|
88
88
|
|
|
89
|
-
// console.debug("[
|
|
89
|
+
// console.debug("[sol] removeNode", parent.id, node.id)
|
|
90
90
|
|
|
91
91
|
// Update JS tree references
|
|
92
92
|
let index = parent.children.indexOf(node)
|
|
@@ -113,7 +113,7 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
|
113
113
|
// bookkeeping on the hot create/insert paths, orphans are derived from the
|
|
114
114
|
// proxy map itself: parentless, not the window root, and not awaiting the
|
|
115
115
|
// destroy sweep. window.ts runs the scan on a rendered frame every few
|
|
116
|
-
// seconds; dev bundles only (
|
|
116
|
+
// seconds; dev bundles only (sol always defines import.meta.env.DEV, so a
|
|
117
117
|
// production bundle folds the check into a constant early return).
|
|
118
118
|
const SENTINEL_INTERVAL_MS = 5000
|
|
119
119
|
let sentinelDue = 0
|
|
@@ -208,7 +208,7 @@ function routeFor(name: string): number {
|
|
|
208
208
|
function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
209
209
|
if (!node) return
|
|
210
210
|
|
|
211
|
-
// console.debug("[
|
|
211
|
+
// console.debug("[sol] applyProp", node.id, name, value)
|
|
212
212
|
|
|
213
213
|
switch (routeFor(name)) {
|
|
214
214
|
case ROUTE_EVENT:
|
|
@@ -234,7 +234,7 @@ let renderer = createRenderer<ProxyNode>({
|
|
|
234
234
|
createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
|
|
235
235
|
let proxy = createProxyNode(elementType)
|
|
236
236
|
|
|
237
|
-
// console.debug("[
|
|
237
|
+
// console.debug("[sol] createElement", proxy.id, elementType)
|
|
238
238
|
|
|
239
239
|
if (elementType === "window") tree.createRoot(proxy.id)
|
|
240
240
|
else tree.createNode(proxy.id, elementType)
|
|
@@ -257,20 +257,20 @@ let renderer = createRenderer<ProxyNode>({
|
|
|
257
257
|
// no element form of its own (the DOM's "#text" node name).
|
|
258
258
|
createTextNode: (value: string): ProxyNode => {
|
|
259
259
|
let proxy = createProxyNode("#text")
|
|
260
|
-
// console.debug("[
|
|
260
|
+
// console.debug("[sol] createTextNode", proxy.id, value)
|
|
261
261
|
tree.createNode(proxy.id, "#text")
|
|
262
262
|
tree.setProperty(proxy.id, "text", "" + value)
|
|
263
263
|
return proxy
|
|
264
264
|
},
|
|
265
265
|
|
|
266
266
|
replaceText: (node: ProxyNode, value: string): void => {
|
|
267
|
-
// console.debug("[
|
|
267
|
+
// console.debug("[sol] replaceText", node.id, value)
|
|
268
268
|
tree.setProperty(node.id, "text", "" + value)
|
|
269
269
|
},
|
|
270
270
|
|
|
271
271
|
isTextNode: (node: ProxyNode): boolean => node?.elementType === "#text",
|
|
272
272
|
setProperty: <T>(node: ProxyNode, name: string, value: T): void => {
|
|
273
|
-
// console.debug("[
|
|
273
|
+
// console.debug("[sol] setProperty", node.id, name, value)
|
|
274
274
|
applyProp(node, name, value)
|
|
275
275
|
},
|
|
276
276
|
|
|
@@ -294,7 +294,7 @@ let renderer = createRenderer<ProxyNode>({
|
|
|
294
294
|
pendingDestroy.delete(node.id)
|
|
295
295
|
|
|
296
296
|
if (parent) {
|
|
297
|
-
// console.debug("[
|
|
297
|
+
// console.debug("[sol] insertNode", parent.id, node.id, anchor?.id ?? "")
|
|
298
298
|
|
|
299
299
|
// Native first: the tree refuses a laid-out element under a d-* parent
|
|
300
300
|
// (it throws, naming both tags), and the mirror must not record a child
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Lattice runner builtin modules (the "
|
|
1
|
+
// Lattice runner builtin modules (the "sol:*" surface). These are ambient
|
|
2
2
|
// module declarations, so they live in this global-script .d.ts (no top-level
|
|
3
3
|
// import/export) rather than in types.d.ts: an ambient `declare module` only
|
|
4
4
|
// becomes globally visible to consumers from a non-module declaration file.
|
|
@@ -51,7 +51,7 @@ declare module "*.glb" {
|
|
|
51
51
|
const bytes: Uint8Array
|
|
52
52
|
export default bytes
|
|
53
53
|
}
|
|
54
|
-
declare module "*.
|
|
54
|
+
declare module "*.sol3m" {
|
|
55
55
|
const bytes: Uint8Array
|
|
56
56
|
export default bytes
|
|
57
57
|
}
|
|
@@ -61,14 +61,15 @@ declare module "*.srtm" {
|
|
|
61
61
|
// stream ("pointerMove"/"pointerDown"/... consumed by window.ts),
|
|
62
62
|
// "pointerFrame" (the move-batch terminator: fires after all of a frame's
|
|
63
63
|
// pointer moves have dispatched, every pointer the same age - multi-pointer
|
|
64
|
-
// recognizers measure there), and "render" (the
|
|
65
|
-
|
|
64
|
+
// recognizers measure there; its `timeStamp` is theirs), and "render" (the
|
|
65
|
+
// per-frame signal).
|
|
66
|
+
declare module "sol:events" {
|
|
66
67
|
export function on(event: string, callback: (data: any) => void): () => void
|
|
67
68
|
export function once(event: string, callback: (data: any) => void): () => void
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// The running application's own surface (lattice), present in every build.
|
|
71
|
-
declare module "
|
|
72
|
+
declare module "sol:app" {
|
|
72
73
|
/**
|
|
73
74
|
* End the current app now, without running the quit hooks: back to the
|
|
74
75
|
* player in a dev client, quit when standalone or at the player root (on
|
|
@@ -99,7 +100,7 @@ declare module "srt:app" {
|
|
|
99
100
|
|
|
100
101
|
// Dev-server control surface (lattice). Present only in dev/go builds; in other
|
|
101
102
|
// builds `available` is false and the functions are no-ops.
|
|
102
|
-
declare module "
|
|
103
|
+
declare module "sol:dev" {
|
|
103
104
|
export const available: boolean
|
|
104
105
|
export const canDiscover: boolean
|
|
105
106
|
export const recents: string[]
|
|
@@ -138,7 +139,7 @@ declare module "srt:dev" {
|
|
|
138
139
|
// Installed-app management (lattice), the player's surface over the client's
|
|
139
140
|
// version store. Present only in go/dev client builds; elsewhere `available`
|
|
140
141
|
// is false, `list` returns [], and launch/remove are no-ops.
|
|
141
|
-
declare module "
|
|
142
|
+
declare module "sol:apps" {
|
|
142
143
|
export const available: boolean
|
|
143
144
|
/**
|
|
144
145
|
* An installed app: id, display name (the installed manifest's displayName,
|
|
@@ -232,6 +233,72 @@ declare module "srt:apps" {
|
|
|
232
233
|
// submits the display list. To schedule a future frame instead, use
|
|
233
234
|
// requestFrame() from "flux:rendertree". The tree-building surface itself is
|
|
234
235
|
// "flux:rendertree" (from @solidrt/flux-types).
|
|
235
|
-
declare module "
|
|
236
|
+
declare module "sol:render" {
|
|
236
237
|
export function renderFrame(): void
|
|
237
238
|
}
|
|
239
|
+
|
|
240
|
+
// The engine verbs of an app test (lattice, dev client only), which
|
|
241
|
+
// @solidrt/test builds its surface on; tests import that, not this.
|
|
242
|
+
// The verbs work only in an engine a test host built (`sol test`) and throw
|
|
243
|
+
// anywhere else. App time there is frame / frameRate and nothing else: no
|
|
244
|
+
// frame runs unless one is asked for.
|
|
245
|
+
declare module "sol:test" {
|
|
246
|
+
/**
|
|
247
|
+
* Runs one frame: timers due by its time fire, then frame callbacks and
|
|
248
|
+
* the flush, then the draw if anything demanded one. Fulfills once the
|
|
249
|
+
* frame has run. One frame at a time: throws while the previous one is
|
|
250
|
+
* still pending.
|
|
251
|
+
*/
|
|
252
|
+
export function frame(): Promise<void>
|
|
253
|
+
/** The frames per second this engine steps at (60 unless set). */
|
|
254
|
+
export function frameRate(): number
|
|
255
|
+
/** Another frame rate, a positive integer. Throws once a frame has run. */
|
|
256
|
+
export function setFrameRate(fps: number): void
|
|
257
|
+
/** App time as of the last frame, in milliseconds; 0 before the first. */
|
|
258
|
+
export function time(): number
|
|
259
|
+
/**
|
|
260
|
+
* Fulfills once the window's size has reached the engine, at once when
|
|
261
|
+
* it already has. No frame runs for it and no time passes: a mounted
|
|
262
|
+
* window builds its first frame on that size.
|
|
263
|
+
*/
|
|
264
|
+
export function windowReady(): Promise<void>
|
|
265
|
+
/**
|
|
266
|
+
* Expands `events` (JSON text, the control API's `/input` event shape)
|
|
267
|
+
* into the steps that are sent and returns what passes before each:
|
|
268
|
+
* `[ms, frames]` per step, at least that much time and that many frames.
|
|
269
|
+
* Throws on an invalid event, before anything is sent. The steps are then
|
|
270
|
+
* sent with `inputStep`, in order.
|
|
271
|
+
*/
|
|
272
|
+
export function inputPlan(events: string): [ms: number, frames: number][]
|
|
273
|
+
/**
|
|
274
|
+
* Sends step `index` of the current plan into the input pipeline: a
|
|
275
|
+
* down, an up, a key or a wheel is dispatched on arrival, ahead of the
|
|
276
|
+
* next frame; a move with that frame.
|
|
277
|
+
*/
|
|
278
|
+
export function inputStep(index: number): void
|
|
279
|
+
/**
|
|
280
|
+
* Runs frames until the app is at rest: nothing in flight, no timer due,
|
|
281
|
+
* no frame demanded. Work in flight is waited for with no app time
|
|
282
|
+
* passing. Rejects once `maxMs` of app time have passed without rest,
|
|
283
|
+
* with what is left in the message.
|
|
284
|
+
*/
|
|
285
|
+
export function settle(maxMs: number): Promise<void>
|
|
286
|
+
/**
|
|
287
|
+
* Delivers a link the way an OS-routed one arrives (the raw string, on
|
|
288
|
+
* the `link` event, ahead of the next frame). Returns whether anything
|
|
289
|
+
* listens for links.
|
|
290
|
+
*/
|
|
291
|
+
export function link(link: string): boolean
|
|
292
|
+
/**
|
|
293
|
+
* Calls a debug command the app registered with `registerDebug`. `args`
|
|
294
|
+
* and the result are JSON text (null for no args). Throws on an unknown
|
|
295
|
+
* name, a throwing command and a result JSON cannot carry.
|
|
296
|
+
*/
|
|
297
|
+
export function debug(name: string, args: string | null): string
|
|
298
|
+
/**
|
|
299
|
+
* The pixels a node's subtree paints: RGBA8, premultiplied, rows top to
|
|
300
|
+
* bottom, at the display scale. Drawn at once from the tree as it is; no
|
|
301
|
+
* frame runs and no app time passes.
|
|
302
|
+
*/
|
|
303
|
+
export function capture(node: number): { width: number; height: number; data: Uint8Array }
|
|
304
|
+
}
|
package/src/swipe.ts
CHANGED
|
@@ -80,8 +80,8 @@ export function createSwipe(options: SwipeOptions) {
|
|
|
80
80
|
// The down's window position and the finger's window-px velocity, over
|
|
81
81
|
// every event the wrapped pan sees.
|
|
82
82
|
let down: { id: number; x: number; y: number } | null = null
|
|
83
|
-
// The lift's window position, set just before the pan's up runs.
|
|
84
|
-
let lift: { x: number; y: number } | null = null
|
|
83
|
+
// The lift's window position and time, set just before the pan's up runs.
|
|
84
|
+
let lift: { x: number; y: number; at: number } | null = null
|
|
85
85
|
let finger = createVelocityTracker()
|
|
86
86
|
let pan = createPan({
|
|
87
87
|
get axis() {
|
|
@@ -92,7 +92,7 @@ export function createSwipe(options: SwipeOptions) {
|
|
|
92
92
|
onPanEnd: velocity => {
|
|
93
93
|
let direction: SwipeDirection | null = null
|
|
94
94
|
if (down && lift) {
|
|
95
|
-
direction = classifySwipe(finger.velocity(), { dx: lift.x - down.x, dy: lift.y - down.y }, directions())
|
|
95
|
+
direction = classifySwipe(finger.velocity(lift.at), { dx: lift.x - down.x, dy: lift.y - down.y }, directions())
|
|
96
96
|
}
|
|
97
97
|
if (direction) options.onSwipe?.(direction, velocity)
|
|
98
98
|
options.onSwipeEnd?.(velocity)
|
|
@@ -104,16 +104,16 @@ export function createSwipe(options: SwipeOptions) {
|
|
|
104
104
|
if (down === null && (e.button == null || e.button === 0)) {
|
|
105
105
|
down = { id: e.pointerId, x: e.clientX, y: e.clientY }
|
|
106
106
|
finger.reset()
|
|
107
|
-
finger.push(e.clientX, e.clientY)
|
|
107
|
+
finger.push(e.clientX, e.clientY, e.timeStamp)
|
|
108
108
|
}
|
|
109
109
|
pan.handlers.onPointerDown(e)
|
|
110
110
|
},
|
|
111
111
|
onPointerMove: (e: PointerEvent) => {
|
|
112
|
-
if (down && down.id === e.pointerId) finger.push(e.clientX, e.clientY)
|
|
112
|
+
if (down && down.id === e.pointerId && !e.predicted) finger.push(e.clientX, e.clientY, e.timeStamp)
|
|
113
113
|
pan.handlers.onPointerMove(e)
|
|
114
114
|
},
|
|
115
115
|
onPointerUp: (e: PointerEvent) => {
|
|
116
|
-
if (down && down.id === e.pointerId) lift = { x: e.clientX, y: e.clientY }
|
|
116
|
+
if (down && down.id === e.pointerId) lift = { x: e.clientX, y: e.clientY, at: e.timeStamp }
|
|
117
117
|
pan.handlers.onPointerUp(e)
|
|
118
118
|
if (down && down.id === e.pointerId) {
|
|
119
119
|
down = null
|