@solidrt/2d 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 +16 -10
- package/examples/README.md +1 -1
- package/examples/anim.tsx +6 -5
- package/examples/camera-probe.tsx +1 -1
- package/examples/camera.tsx +1 -1
- package/examples/pick.tsx +1 -1
- package/examples/split-screen.tsx +1 -1
- package/examples/tiles.tsx +1 -1
- package/examples/views.tsx +1 -1
- package/package.json +6 -3
- package/src/animation.ts +32 -26
- package/src/camera.ts +1 -1
- package/src/camera2d.ts +1 -1
- package/src/components/tile-layer.tsx +1 -1
- package/src/dispatch.ts +6 -5
- package/src/frames.ts +1 -1
- package/src/layer.ts +6 -1
- package/src/oversample-math.ts +1 -1
- package/src/oversample.ts +5 -2
- package/src/pick.ts +2 -2
- package/src/tiles-math.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -110,7 +110,7 @@ being shared with e.g. a 3d scene costs nothing in JS.
|
|
|
110
110
|
|
|
111
111
|
### Spatial queries
|
|
112
112
|
|
|
113
|
-
Spatial queries (`
|
|
113
|
+
Spatial queries (`tests/collision.test.tsx` pins the contract): the
|
|
114
114
|
3d scene's trio one dimension down, Godot's PhysicsDirectSpaceState2D
|
|
115
115
|
and CharacterBody2D in their names. `layer.raycast(x, y, dx, dy,
|
|
116
116
|
opts?)` is every shown sprite the ray strikes, nearest first, with
|
|
@@ -350,7 +350,7 @@ the nearest view (the `<SpriteLayer>`'s own or the enclosing
|
|
|
350
350
|
axes, frames only while `active()`. A view's feed normalizes a drag by
|
|
351
351
|
the leaf's own box, so a leaf under a designSize fit pans and zooms
|
|
352
352
|
correctly. camera2d.ts imports `@solidrt/core/input` and
|
|
353
|
-
`@solidrt/core/camera-control` only, so
|
|
353
|
+
`@solidrt/core/camera-control` only, so tests/camera2d.test.ts pins
|
|
354
354
|
the clamp, anchoring, glides, the framing, the lanes, damped bounds,
|
|
355
355
|
inertia and the axes headless; examples/camera.tsx (function face) and
|
|
356
356
|
examples/pick.tsx (`<Camera2d>`) are the live guards. The shared
|
|
@@ -453,7 +453,7 @@ it: `createSpriteLayer(texture, { stagger })` / `layer.setStagger(ms)`.
|
|
|
453
453
|
Each natural settle calls the handle's `onTransitionEnd` (plain field,
|
|
454
454
|
or the `<Sprite>`/`<Group>` prop) with `{ component }` - target-only,
|
|
455
455
|
never on a cancel, snap or exit; the raw "spatialTransitionEnd" engine
|
|
456
|
-
event (
|
|
456
|
+
event (sol:events, node = sprite.node) stays for flux:spatial consumers.
|
|
457
457
|
See examples/springs.tsx (tap a sprite: it leaves through its `exit`).
|
|
458
458
|
|
|
459
459
|
### Frame-rate motion
|
|
@@ -468,13 +468,16 @@ re-runs an effect per sprite per frame for nothing.
|
|
|
468
468
|
### Frame animation
|
|
469
469
|
|
|
470
470
|
Frame animation (animation.ts): `createAnimation(frames, fps, { loop })`
|
|
471
|
-
is a clip with a shared
|
|
471
|
+
is a clip with a shared clock on the app's frame time (it freezes with
|
|
472
|
+
the dev clock and plays at its own speed under `sol render`) stepping
|
|
473
|
+
every attached sprite
|
|
472
474
|
(`anim.add(sprite)` / `remove`; `play`/`pause`; `frame()`/`playing()`
|
|
473
475
|
read the clock, methods like the 3d mixer's `playing()`; `loop: false`
|
|
474
476
|
holds the last frame and fires `onFinish`, the mixer's name for it in
|
|
475
|
-
@solidrt/3d). One
|
|
476
|
-
per sprite per STEP - an 8fps cycle is 8 publishes
|
|
477
|
-
display rate, and a paused clip costs
|
|
477
|
+
@solidrt/3d). One no-demand frame callback per playing clip, one
|
|
478
|
+
setSprite per sprite per STEP - an 8fps cycle is 8 publishes and 8
|
|
479
|
+
presents a second regardless of display rate, and a paused clip costs
|
|
480
|
+
nothing. A sprite belongs to at
|
|
478
481
|
most one animation (add detaches the previous); removed sprites prune
|
|
479
482
|
lazily on the next step. Works on both layer kinds; with `<Sprite>`,
|
|
480
483
|
attach via `ref` and leave the `frame` prop off - the clip owns that
|
|
@@ -485,7 +488,7 @@ keeps, so other props stay reactive).
|
|
|
485
488
|
|
|
486
489
|
frames.ts, pick.ts, camera.ts, camera-motion.ts, dispatch.ts,
|
|
487
490
|
oversample-math.ts and tiles-math.ts (the tile grid's chunk and slot
|
|
488
|
-
math,
|
|
491
|
+
math, tests/tiles.test.ts) are pure (no GPU imports) BY DESIGN so they
|
|
489
492
|
can be checked headless; keep them that way.
|
|
490
493
|
|
|
491
494
|
## The baked tile layer (tiles.ts)
|
|
@@ -625,6 +628,9 @@ is never both a tap and a pan), was the only pointer down for its whole
|
|
|
625
628
|
press (a pinch never taps), with `tapCount` counting repeats within 300
|
|
626
629
|
ms and 20 px on the same target (a double tap is `tapCount === 2`).
|
|
627
630
|
Wheel walks like a move with `deltaX/deltaY`. Every event carries
|
|
631
|
+
`timeStamp` and `predicted` as the element event has them (time input
|
|
632
|
+
with the first; a `predicted` move is one to draw with, not to measure
|
|
633
|
+
with - core's AGENTS.md has both), and
|
|
628
634
|
`native`, the leaf's element event, for core's recognizers (`createPan`
|
|
629
635
|
to drag a sprite with slop, `createTransform` under the camera); a plain
|
|
630
636
|
`onPointerMove` also fires on hover, so a sprite drag gates on its own
|
|
@@ -632,7 +638,7 @@ pressed flag from down to up (`native.button` is set on down/up only).
|
|
|
632
638
|
Root listeners all run, in registration order (the root is the last
|
|
633
639
|
stop, nothing is left to claim); a record layer has no groups, so the
|
|
634
640
|
chain is the sprite then the root. Event x/y are layer pixels with the
|
|
635
|
-
camera undone.
|
|
641
|
+
camera undone. tests/dispatch.test.ts pins the walk, claiming, capture,
|
|
636
642
|
hover, wheel and tap rules headless.
|
|
637
643
|
|
|
638
644
|
## Traps
|
|
@@ -782,7 +788,7 @@ hover, wheel and tap rules headless.
|
|
|
782
788
|
- Every rotation must agree on direction (clockwise, y-down):
|
|
783
789
|
`pointInSprite` in pick.ts with the vertex stage's `iRot`, and
|
|
784
790
|
`projectCamera` in camera.ts with `uCameraRot` and `<TileLayer>`'s view
|
|
785
|
-
transform. The differential
|
|
791
|
+
transform. The differential tests (tests/pick.test.ts, tests/camera.test.ts)
|
|
786
792
|
guard the JS math against oracles but NOT against the shader - if you
|
|
787
793
|
touch one rotation, touch all, then run examples/camera-probe.tsx (the
|
|
788
794
|
live guard: shader vs projectCamera, node/record parity, the pointer
|
package/examples/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @solidrt/2d examples
|
|
2
2
|
|
|
3
3
|
Single-concept sprite-layer patterns. Each file is a complete, runnable app
|
|
4
|
-
(`
|
|
4
|
+
(`bun run sol run <file>` from the repo root) demonstrating exactly one thing -
|
|
5
5
|
copy one and adapt it.
|
|
6
6
|
|
|
7
7
|
- `sprites.tsx` - the layer at its natural scale: 500 sprites bouncing at
|
package/examples/anim.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Frame animation via createAnimation: a row of sprites sharing one looping
|
|
2
2
|
// clip (one clock, one setSprite per sprite per STEP - an app's whole crowd
|
|
3
|
-
// animates off a handful of
|
|
3
|
+
// animates off a handful of no-demand frame callbacks, presenting only on
|
|
4
|
+
// a step), plus a one-shot clip
|
|
4
5
|
// that holds its last frame and fires onFinish. Self-asserting: samples the
|
|
5
6
|
// sprites' frames against the wall clock and logs ANIM-OK / ANIM-FAIL.
|
|
6
7
|
//
|
|
@@ -15,7 +16,7 @@ import logoBytes from "./logo.png" with { type: "binary" }
|
|
|
15
16
|
const W = 520
|
|
16
17
|
const H = 200
|
|
17
18
|
const SPRITE = 96
|
|
18
|
-
// Slow enough that timer
|
|
19
|
+
// Slow enough that a late timer cannot blur a sampled frame boundary: the
|
|
19
20
|
// checks sample mid-frame, half a period from either edge.
|
|
20
21
|
const LOOP_FPS = 2
|
|
21
22
|
const SHOT_FPS = 5
|
|
@@ -53,9 +54,9 @@ function App() {
|
|
|
53
54
|
return indexOf(getSprite(sprite)!.frame)
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// Sample mid-frame
|
|
57
|
-
//
|
|
58
|
-
//
|
|
57
|
+
// Sample mid-frame on timers. At LOOP_FPS = 2 a frame is 500ms and the
|
|
58
|
+
// clip steps on the display frame a boundary falls in, so mid-frame
|
|
59
|
+
// samples have ~250ms of margin either way.
|
|
59
60
|
let period = 1000 / LOOP_FPS
|
|
60
61
|
setTimeout(() => {
|
|
61
62
|
check(shownIndex(looped[0]!) === 0, "loop shows frame 0 in its first period")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Camera-mapping probe: the live guard that the three spellings of the
|
|
2
2
|
// camera mapping agree - projectCamera (camera.ts), the vertex stages'
|
|
3
3
|
// uCameraRot (shaders.ts), and pointer dispatch's unprojectCamera
|
|
4
|
-
// (layer.ts). The headless check (
|
|
4
|
+
// (layer.ts). The headless check (tests/camera.test.ts) ties
|
|
5
5
|
// projectCamera to the <view>-prop oracle but cannot see the shader; this
|
|
6
6
|
// probe closes that gap on a real client. Self-asserting: watch the logs
|
|
7
7
|
// for CAMERA lines ending in CAMERA-OK. Run it after touching ANY
|
package/examples/camera.tsx
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { createEffect, createInputMap, createPointerFeed, createSignal, decodeImage, displayScale, gamepad, onFrame, render, Show, windowSize } from "@solidrt/core"
|
|
24
24
|
import { addSprite, camera2dActions, camera2dBindings, createAtlas, createCamera2d, createSpriteLayer, feedPointer, fitOversample, grid, setSprite } from "@solidrt/2d"
|
|
25
25
|
import type { Camera2dHandle, SpriteHandle } from "@solidrt/2d"
|
|
26
|
-
import { registerDebug } from "
|
|
26
|
+
import { registerDebug } from "sol:dev"
|
|
27
27
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
28
28
|
|
|
29
29
|
const WORLD = { width: 2400, height: 1600 }
|
package/examples/pick.tsx
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { createInputMap, createPointerFeed, createSignal, decodeImage, render, For } from "@solidrt/core"
|
|
22
22
|
import { Camera2d, camera2dActions, camera2dBindings, createAtlas, grid, setSprite, Sprite, SpriteLayer, View2d } from "@solidrt/2d"
|
|
23
23
|
import type { Camera2dHandle, Frame, SpriteHandle } from "@solidrt/2d"
|
|
24
|
-
import { registerDebug } from "
|
|
24
|
+
import { registerDebug } from "sol:dev"
|
|
25
25
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
26
26
|
|
|
27
27
|
const TINTS: [number, number, number, number][] = [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { createInputMap, createPointerFeed, decodeImage, gamepad, onFrame, render, windowSize, For } from "@solidrt/core"
|
|
13
13
|
import { Camera2d, camera2dActions, camera2dBindings, createAtlas, grid, setSprite, Sprite, SpriteLayer, View2d } from "@solidrt/2d"
|
|
14
14
|
import type { Camera2dHandle, Frame, SpriteHandle } from "@solidrt/2d"
|
|
15
|
-
import { registerDebug } from "
|
|
15
|
+
import { registerDebug } from "sol:dev"
|
|
16
16
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
17
17
|
|
|
18
18
|
const WORLD = { width: 2400, height: 1600 }
|
package/examples/tiles.tsx
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { createSignal, decodeImage, onFrame, pct, render, windowSize } from "@solidrt/core"
|
|
24
24
|
import { createAtlas, grid, TileLayer } from "@solidrt/2d"
|
|
25
25
|
import type { TileCamera, TileLayerHandle } from "@solidrt/2d"
|
|
26
|
-
import { registerDebug } from "
|
|
26
|
+
import { registerDebug } from "sol:dev"
|
|
27
27
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
28
28
|
|
|
29
29
|
const COLS = 128
|
package/examples/views.tsx
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createEffect, createInputMap, createPointerFeed, createSignal, decodeImage, displayScale, gamepad, onFrame, render, windowSize } from "@solidrt/core"
|
|
19
19
|
import { addSprite, camera2dActions, camera2dBindings, createAtlas, createCamera2d, createSpriteLayer, feedPointer, fitOversample, grid, setSprite } from "@solidrt/2d"
|
|
20
20
|
import type { Camera2dHandle, SpriteHandle, ViewHandle } from "@solidrt/2d"
|
|
21
|
-
import { registerDebug } from "
|
|
21
|
+
import { registerDebug } from "sol:dev"
|
|
22
22
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
23
23
|
|
|
24
24
|
const WORLD = { width: 2400, height: 1600 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.66",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
"examples/",
|
|
15
15
|
"AGENTS.md"
|
|
16
16
|
],
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@solidrt/test": "0.0.66"
|
|
19
|
+
},
|
|
17
20
|
"peerDependencies": {
|
|
18
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
19
|
-
"@solidrt/core": "0.0.
|
|
21
|
+
"@solidjs/signals": "2.0.0-rc.13",
|
|
22
|
+
"@solidrt/core": "0.0.66"
|
|
20
23
|
}
|
|
21
24
|
}
|
package/src/animation.ts
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
// Sprite frame animation: a clip (frames + fps) with a shared clock that
|
|
2
|
-
// steps every attached sprite's frame through setSprite. One
|
|
3
|
-
// playing clip and one frame write per
|
|
4
|
-
// display frame), so an 8fps walk cycle
|
|
5
|
-
//
|
|
2
|
+
// steps every attached sprite's frame through setSprite. One frame callback
|
|
3
|
+
// per playing clip, registered without demand, and one frame write per
|
|
4
|
+
// attached sprite per STEP (not per display frame), so an 8fps walk cycle
|
|
5
|
+
// generates 8 publishes and 8 presents a second and a paused clip costs
|
|
6
|
+
// nothing - the demand-gate story unchanged. The clock is the app's frame
|
|
7
|
+
// time: the clip freezes and resumes with the dev tools' clock, plays at
|
|
8
|
+
// its own speed under `sol render`, and a step lands on the frame it is
|
|
9
|
+
// due in. The clip
|
|
6
10
|
// does not own its sprites (destroySprite prunes lazily on the next step),
|
|
7
11
|
// and a sprite belongs to at most one animation. Plain JS over setSprite,
|
|
8
12
|
// so it works on both layer kinds and composes with <Sprite> via ref (leave
|
|
9
13
|
// the frame prop off - the clip owns that field).
|
|
10
|
-
import { getOwner, onCleanup } from "@solidrt/core"
|
|
14
|
+
import { getOwner, onCleanup, onFrame, runWithOwner } from "@solidrt/core"
|
|
11
15
|
import type { Frame } from "./frames.ts"
|
|
12
16
|
import type { Sprite } from "./layer.ts"
|
|
13
17
|
import { setSprite } from "./layer.ts"
|
|
14
18
|
|
|
15
|
-
// Timer period as a fraction of the clip's frame duration: the step index
|
|
16
|
-
// is computed from the wall clock (drift-free), so the timer only decides
|
|
17
|
-
// how late after a frame boundary the step lands - half a frame bounds
|
|
18
|
-
// that lateness at half a frame, where a full-frame period could show a
|
|
19
|
-
// frame almost a whole frame late.
|
|
20
|
-
const TICK_RATIO = 0.5
|
|
21
|
-
|
|
22
19
|
export type AnimationOptions = {
|
|
23
20
|
/** Wrap around (default) or play once and hold the last frame. */
|
|
24
21
|
loop?: boolean
|
|
@@ -61,7 +58,7 @@ let attachedTo = new WeakMap<Sprite, SpriteAnimation>()
|
|
|
61
58
|
/**
|
|
62
59
|
* A frame animation clip: `frames` (usually a slice of a `grid()` result)
|
|
63
60
|
* played at `fps`. Sprites attach with `add`; the clip steps their `frame`
|
|
64
|
-
* on
|
|
61
|
+
* on the app's frame time, independent of display rate.
|
|
65
62
|
*/
|
|
66
63
|
export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOptions): SpriteAnimation {
|
|
67
64
|
if (frames.length === 0) throw new Error("createAnimation: frames is empty")
|
|
@@ -74,24 +71,30 @@ export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOp
|
|
|
74
71
|
let playing = true
|
|
75
72
|
let ended = false
|
|
76
73
|
let disposed = false
|
|
77
|
-
|
|
78
|
-
let
|
|
79
|
-
let
|
|
74
|
+
// The running clock's frame callback, null while stopped.
|
|
75
|
+
let stop: (() => void) | null = null
|
|
76
|
+
let elapsed = 0 // ms of clip time played so far
|
|
77
|
+
// The frame time the clip last advanced to, null until the running
|
|
78
|
+
// clock's first frame: that one only anchors, so a clip starts at the
|
|
79
|
+
// frame it was started in, whenever in it.
|
|
80
|
+
let lastTick: number | null = null
|
|
80
81
|
|
|
81
82
|
function startClock(): void {
|
|
82
|
-
if (
|
|
83
|
+
if (stop !== null || !playing || sprites.size === 0) return
|
|
83
84
|
// A looping single-frame clip never changes; a one-shot still needs
|
|
84
85
|
// the clock once, to end.
|
|
85
86
|
if (loop && frames.length === 1) return
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
lastTick = null
|
|
88
|
+
// No demand: the clip follows the frames the app runs anyway and asks
|
|
89
|
+
// for one only by writing a step. Under no owner: the clock is the
|
|
90
|
+
// clip's (stopClock ends it), not the scope's that attached a sprite.
|
|
91
|
+
stop = runWithOwner(null, () => onFrame(tick, { demand: false }))
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
function stopClock(): void {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
timer = null
|
|
95
|
+
if (stop === null) return
|
|
96
|
+
stop()
|
|
97
|
+
stop = null
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
function writeAll(): void {
|
|
@@ -112,8 +115,11 @@ export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOp
|
|
|
112
115
|
writeAll()
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
function tick(): void {
|
|
116
|
-
|
|
118
|
+
function tick(now: number): void {
|
|
119
|
+
// Never backward: across a hot reload the frame time restarts.
|
|
120
|
+
if (lastTick !== null) elapsed += Math.max(0, now - lastTick)
|
|
121
|
+
lastTick = now
|
|
122
|
+
let raw = Math.floor((elapsed * fps) / 1000)
|
|
117
123
|
if (!loop && raw >= frames.length) {
|
|
118
124
|
// The last frame has shown for its full duration: hold it and stop.
|
|
119
125
|
setIndex(frames.length - 1)
|
|
@@ -147,7 +153,7 @@ export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOp
|
|
|
147
153
|
if (disposed || playing) return
|
|
148
154
|
if (ended) {
|
|
149
155
|
ended = false
|
|
150
|
-
|
|
156
|
+
elapsed = 0
|
|
151
157
|
current = 0
|
|
152
158
|
writeAll()
|
|
153
159
|
}
|
package/src/camera.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The 2d camera vocabulary, pure: the one camera type both layers share
|
|
2
2
|
// and its world <-> screen mapping as plain functions - no GPU or GUI
|
|
3
|
-
// imports, so
|
|
3
|
+
// imports, so its test (tests/camera.test.ts) exercises this
|
|
4
4
|
// module headless on the flux binary. Three consumers implement or undo
|
|
5
5
|
// exactly this mapping and must agree with it: the vertex stages
|
|
6
6
|
// (shaders.ts, uCamera + uCameraRot), the tile layer's composite
|
package/src/camera2d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// business - an input map over the view's pointer feed, a pad, a debug
|
|
24
24
|
// command - and `camera2dBindings` in input.ts is the standard wiring
|
|
25
25
|
// (drag pans, pinch and wheel zoom, twist rolls). This module imports no
|
|
26
|
-
// GUI or runtime module, so
|
|
26
|
+
// GUI or runtime module, so tests/camera2d.test.ts runs it headless.
|
|
27
27
|
//
|
|
28
28
|
// Conventions, decided against Godot's Camera2D, Unity's Cinemachine and
|
|
29
29
|
// Three's MapControls (plus the map/whiteboard libraries for the canvas
|
|
@@ -136,7 +136,7 @@ export let TileLayer: VoidComponent<TileLayerProps> = props => {
|
|
|
136
136
|
// World -> screen: p maps to pivot + R(rotation) * zoom * (p - camera) -
|
|
137
137
|
// projectCamera (camera.ts) spelled with element transforms as origin at
|
|
138
138
|
// the camera point, rotate + scale there, then translate the camera point
|
|
139
|
-
// onto the pivot.
|
|
139
|
+
// onto the pivot. tests/camera.test.ts holds the two spellings together.
|
|
140
140
|
let camX = () => props.camera?.x ?? 0
|
|
141
141
|
let camY = () => props.camera?.y ?? 0
|
|
142
142
|
return (
|
package/src/dispatch.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// counted up for repeats (DOM's detail, Unity's clickCount).
|
|
18
18
|
//
|
|
19
19
|
// Pure BY DESIGN (types and camera.ts, nothing with GPU imports) so
|
|
20
|
-
//
|
|
20
|
+
// tests/dispatch.test.ts drives it headless with a fake pick.
|
|
21
21
|
|
|
22
22
|
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
23
23
|
import { unprojectCamera } from "./camera.ts"
|
|
@@ -47,8 +47,6 @@ export type DispatchDeps = {
|
|
|
47
47
|
root: ViewHandle
|
|
48
48
|
/** The root's listeners, in registration order. */
|
|
49
49
|
listeners: Set<LayerPointerListener>
|
|
50
|
-
/** The clock for tap repeats (default performance.now; checks inject). */
|
|
51
|
-
now?: () => number
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
type HandlerName = "onPointerDown" | "onPointerMove" | "onPointerUp" | "onWheel" | "onTap"
|
|
@@ -61,6 +59,8 @@ type InternalEvent = {
|
|
|
61
59
|
currentTarget: Sprite | SpriteGroup | ViewHandle
|
|
62
60
|
x: number
|
|
63
61
|
y: number
|
|
62
|
+
timeStamp: number
|
|
63
|
+
predicted: boolean
|
|
64
64
|
pointerId: number
|
|
65
65
|
pointerType: string
|
|
66
66
|
button?: number
|
|
@@ -89,7 +89,6 @@ type Press = {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export function spriteDispatch(deps: DispatchDeps): (layout: (() => { width: number; height: number }) | null) => LayerHandlers {
|
|
92
|
-
let now = deps.now ?? (() => performance.now())
|
|
93
92
|
let presses = new Map<number, Press>()
|
|
94
93
|
let hover = new Map<number, Sprite>()
|
|
95
94
|
let lastTap: { time: number; cx: number; cy: number; target: Sprite | null; count: number } | null = null
|
|
@@ -135,6 +134,8 @@ export function spriteDispatch(deps: DispatchDeps): (layout: (() => { width: num
|
|
|
135
134
|
currentTarget: sprite ?? deps.root,
|
|
136
135
|
x,
|
|
137
136
|
y,
|
|
137
|
+
timeStamp: e.timeStamp,
|
|
138
|
+
predicted: e.predicted,
|
|
138
139
|
pointerId: e.pointerId,
|
|
139
140
|
pointerType: e.pointerType,
|
|
140
141
|
button: e.button,
|
|
@@ -199,7 +200,7 @@ export function spriteDispatch(deps: DispatchDeps): (layout: (() => { width: num
|
|
|
199
200
|
// press, released over the target it pressed (empty space for the
|
|
200
201
|
// layer itself).
|
|
201
202
|
if (press.moved || !press.alone || topmost(x, y) !== press.target) return
|
|
202
|
-
let time =
|
|
203
|
+
let time = e.timeStamp
|
|
203
204
|
let repeat =
|
|
204
205
|
lastTap !== null &&
|
|
205
206
|
time - lastTap.time <= TAP_INTERVAL_MS &&
|
package/src/frames.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Atlas frame math, pure: names and grid coordinates to normalized UV rects.
|
|
2
|
-
// No GPU or GUI imports, so
|
|
2
|
+
// No GPU or GUI imports, so its test (tests/frames.test.ts) exercises
|
|
3
3
|
// this module headless on the flux binary.
|
|
4
4
|
|
|
5
5
|
/**
|
package/src/layer.ts
CHANGED
|
@@ -40,7 +40,7 @@ import type {
|
|
|
40
40
|
NodeTransitionSpec,
|
|
41
41
|
QueryFilter,
|
|
42
42
|
} from "flux:spatial"
|
|
43
|
-
import { on } from "
|
|
43
|
+
import { on } from "sol:events"
|
|
44
44
|
import type { CameraState, CameraUpdate } from "./camera.ts"
|
|
45
45
|
import type { Frame } from "./frames.ts"
|
|
46
46
|
import { FULL_FRAME, writeFrame } from "./frames.ts"
|
|
@@ -265,6 +265,11 @@ export type LayerEventBase = {
|
|
|
265
265
|
/** Pointer position in LAYER pixels (the camera mapping undone). */
|
|
266
266
|
x: number
|
|
267
267
|
y: number
|
|
268
|
+
/** When the event happened, ms: the element event's `timeStamp`. */
|
|
269
|
+
timeStamp: number
|
|
270
|
+
/** The element event's `predicted`: a move the pointer was never
|
|
271
|
+
* reported at. Draw with it, do not measure with it. */
|
|
272
|
+
predicted: boolean
|
|
268
273
|
pointerId: number
|
|
269
274
|
pointerType: string
|
|
270
275
|
button?: number
|
package/src/oversample-math.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The pure oversample math: arithmetic on plain numbers, no GPU or window
|
|
2
|
-
// imports, so the headless check (
|
|
2
|
+
// imports, so the headless check (tests/oversample.test.ts) drives it
|
|
3
3
|
// directly. oversample.ts wraps it with the device limits; components/
|
|
4
4
|
// adds the measured inputs (window box, display scale, camera).
|
|
5
5
|
|
package/src/oversample.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// instead of snapping to uneven widths). okf/backlog/2d-layer-display-scale.md.
|
|
7
7
|
// The pure math lives in oversample-math.ts (checkable headless); this
|
|
8
8
|
// module binds it to the device limits and adds the thrash sentinel.
|
|
9
|
+
import { frameTime } from "@solidrt/core"
|
|
9
10
|
import { limits } from "@solidrt/core/gpu"
|
|
10
11
|
import { fitOversampleWithin } from "./oversample-math.ts"
|
|
11
12
|
|
|
@@ -36,7 +37,9 @@ export function fitOversample(scale: number, targetW: number, targetH: number, b
|
|
|
36
37
|
* healthy layer settles in a change or two (mount, then the post-resize
|
|
37
38
|
* scale); reaching this count means something re-picks every frame. */
|
|
38
39
|
const THRASH_CHANGES = 4
|
|
39
|
-
/** The window those changes must land in, ms
|
|
40
|
+
/** The window those changes must land in, ms of the app's frame time: a
|
|
41
|
+
* warning about what happens per frame counts on the clock frames run on,
|
|
42
|
+
* so it means the same under `sol render` and a paused dev clock. */
|
|
40
43
|
const THRASH_WINDOW_MS = 1000
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -52,7 +55,7 @@ export function thrashSentinel(what: string): () => void {
|
|
|
52
55
|
let warned = false
|
|
53
56
|
return () => {
|
|
54
57
|
if (warned) return
|
|
55
|
-
let now =
|
|
58
|
+
let now = frameTime()
|
|
56
59
|
stamps.push(now)
|
|
57
60
|
while (stamps.length > 0 && now - stamps[0]! > THRASH_WINDOW_MS) stamps.shift()
|
|
58
61
|
if (stamps.length < THRASH_CHANGES) return
|
package/src/pick.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Picking narrowphase, pure: no GPU or GUI imports, so
|
|
2
|
-
// (
|
|
1
|
+
// Picking narrowphase, pure: no GPU or GUI imports, so its test
|
|
2
|
+
// (tests/pick.test.ts) exercises this module headless on the flux binary.
|
|
3
3
|
// layer.ts walks its draw order calling this per sprite (topmost first).
|
|
4
4
|
|
|
5
5
|
/** Exact containment test against a rotated rect (center, size, rotation). */
|
package/src/tiles-math.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The pure tile-grid math: cell and rect validation, a cell's chunk and
|
|
2
2
|
// record slot, and the walk of a rect chunk by chunk - plain arithmetic
|
|
3
|
-
// with no GPU imports, so the headless check (
|
|
3
|
+
// with no GPU imports, so the headless check (tests/tiles.test.ts)
|
|
4
4
|
// drives it directly; tiles.ts wraps it with the chunk textures.
|
|
5
5
|
|
|
6
6
|
/** Throw unless (col, row) is a cell of the cols x rows grid. */
|