@solidrt/3d 0.0.55 → 0.0.56
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 +1083 -183
- package/README.md +15 -7
- package/examples/README.md +79 -5
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +250 -0
- package/examples/first-person.tsx +119 -0
- package/examples/fleet.tsx +194 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +32 -6
- package/examples/pick.tsx +2 -4
- package/examples/probe.tsx +101 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +32 -3
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +86 -0
- package/examples/skybox.tsx +128 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +132 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +199 -0
- package/src/color.ts +32 -0
- package/src/components.tsx +763 -82
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +306 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +57 -9
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +506 -0
- package/src/orbit.ts +97 -48
- package/src/scene-pointer.ts +166 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1242 -1557
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
package/src/orbit.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
// clamps - the standard interactive-viewer camera, extracted so apps stop
|
|
4
4
|
// rebuilding it.
|
|
5
5
|
//
|
|
6
|
+
// Rotation is viewport-relative when the control knows its viewport
|
|
7
|
+
// (OrbitCameraOptions.viewport): dragging one viewport height sweeps one
|
|
8
|
+
// full turn, Three's OrbitControls convention, so the same drag feels the
|
|
9
|
+
// same on a phone and a 4k window. Without a viewport it falls back to a
|
|
10
|
+
// fixed angle per pixel.
|
|
11
|
+
//
|
|
6
12
|
// Input goes through core's merged transform recognizer (createTransform), so
|
|
7
13
|
// the drag participates in the gesture arena: on a viewport embedded in a
|
|
8
14
|
// scrollable layout, an orbit drag and the ancestor scroller's pan arbitrate
|
|
@@ -21,9 +27,8 @@
|
|
|
21
27
|
// pinch focal / wheel cursor to a world point, and the zoom scales the pose
|
|
22
28
|
// about that point instead - the spot under the fingers stays under the
|
|
23
29
|
// fingers, with the target sliding toward it. Only the app can own that
|
|
24
|
-
// mapping: screen-to-ray needs the projection
|
|
25
|
-
//
|
|
26
|
-
// not here.
|
|
30
|
+
// mapping: screen-to-ray needs the projection (fov, aspect), which lives
|
|
31
|
+
// in the app's camera, not here.
|
|
27
32
|
//
|
|
28
33
|
// Anchored zoom leaves the target wherever the zoom carried it - possibly a
|
|
29
34
|
// point in empty air near the eye, and a drag orbiting THAT swings the scene
|
|
@@ -34,6 +39,13 @@
|
|
|
34
39
|
// target's depth - the eye and the picture do not change, only what a drag
|
|
35
40
|
// swings around.
|
|
36
41
|
//
|
|
42
|
+
// Every pointer coordinate here is in the input element's own frame: the
|
|
43
|
+
// recognizer measures drag deltas in the element's parent frame and the
|
|
44
|
+
// pinch focal in its local frame, and the wheel uses localX/localY, so a
|
|
45
|
+
// viewport under a designSize fit or any scaled ancestor behaves the same
|
|
46
|
+
// as one filling the window. `viewport().height` is therefore the
|
|
47
|
+
// element's laid-out height, which the OrbitCamera component passes.
|
|
48
|
+
//
|
|
37
49
|
// Pose is plain mutable state advanced by update(dt) from the app's own
|
|
38
50
|
// onFrame; the control registers no frame loop of its own, so whether
|
|
39
51
|
// anything animates stays the app's decision (a paused orbit with no other
|
|
@@ -44,37 +56,60 @@
|
|
|
44
56
|
// the scene's own shared write carries uViewProj and uCamPos), and reports
|
|
45
57
|
// that, so per-frame dependents (reprojecting HUD overlays via
|
|
46
58
|
// scene.project) can follow the camera without recomputing every frame.
|
|
59
|
+
//
|
|
60
|
+
// Options are read where they apply, not copied out: the clamps, the rates
|
|
61
|
+
// (auto-orbit, rotate, zoom, pan), `viewport` and the anchor callbacks are
|
|
62
|
+
// re-read from the options object on every input or update, so a caller
|
|
63
|
+
// may change a field (or hand in an object of getters, which is what
|
|
64
|
+
// `<OrbitCamera>` does with its props) and the next gesture or frame sees
|
|
65
|
+
// it. Only the initial pose is copied at creation; later pose changes go
|
|
66
|
+
// through set().
|
|
47
67
|
|
|
48
68
|
import { createSignal } from "@solidjs/signals"
|
|
49
69
|
import { createTransform } from "@solidrt/core"
|
|
50
70
|
import type { PointerEvent } from "@solidrt/core"
|
|
51
|
-
import type { CameraUpdate } from "./
|
|
71
|
+
import type { CameraUpdate } from "./camera.ts"
|
|
52
72
|
import type { Vec3 } from "./math.ts"
|
|
53
73
|
|
|
54
|
-
// Baseline sensitivities at rotateSpeed/zoomSpeed 1
|
|
55
|
-
//
|
|
74
|
+
// Baseline sensitivities at rotateSpeed/zoomSpeed 1. With a `viewport` the
|
|
75
|
+
// rotation is viewport-relative: dragging one viewport height sweeps
|
|
76
|
+
// DRAG_TURNS full turns on either axis (Three's OrbitControls convention),
|
|
77
|
+
// so the feel survives any window size or form factor. Without one it
|
|
78
|
+
// falls back to fixed radians per dragged pixel - the relative rule frozen
|
|
79
|
+
// at a ~785 px viewport. Wheel zoom is an exponent per wheel-delta unit.
|
|
80
|
+
const DRAG_TURNS = 1
|
|
56
81
|
const DRAG_AZIMUTH = 0.008
|
|
57
82
|
const DRAG_ELEVATION = 0.006
|
|
58
83
|
const WHEEL_ZOOM = 0.0015
|
|
84
|
+
// Default clamps: the distance floor keeps the eye off the target; the
|
|
85
|
+
// elevation limits (radians) stop just short of the poles.
|
|
86
|
+
const MIN_DISTANCE = 0.01
|
|
87
|
+
const ELEVATION_LIMIT = 1.55
|
|
59
88
|
|
|
60
89
|
/** What an orbit camera drives: a Scene, or one of its Views. */
|
|
61
90
|
export type OrbitTarget = { setCamera(update: CameraUpdate): void }
|
|
62
91
|
|
|
92
|
+
/** The pose fields (target, azimuth, elevation, distance) are initial
|
|
93
|
+
* values, copied at creation and changed through set() afterwards. Every
|
|
94
|
+
* other field is live: read from this object where it applies, so a
|
|
95
|
+
* change takes effect on the next gesture or update. */
|
|
63
96
|
export type OrbitCameraOptions = {
|
|
64
|
-
/**
|
|
97
|
+
/** Initial point the camera orbits and looks at (default origin). */
|
|
65
98
|
target?: Vec3
|
|
66
99
|
/** Initial pose, radians and world units. */
|
|
67
100
|
azimuth?: number
|
|
68
101
|
elevation?: number
|
|
69
102
|
distance?: number
|
|
103
|
+
/** Distance clamps, world units. */
|
|
70
104
|
minDistance?: number
|
|
71
105
|
maxDistance?: number
|
|
72
106
|
/** Elevation clamps, radians; the defaults stop just short of the poles. */
|
|
73
107
|
minElevation?: number
|
|
74
108
|
maxElevation?: number
|
|
75
109
|
/** Auto-orbit rate in radians/second (default 0: none). Runs while
|
|
76
|
-
* `orbiting()` and no drag/pinch is in progress;
|
|
77
|
-
* set({ orbiting }).
|
|
110
|
+
* `orbiting()` and no drag/pinch is in progress; `orbiting` starts on
|
|
111
|
+
* when the initial rate is positive and toggles with set({ orbiting }).
|
|
112
|
+
* A new rate applies from the next update. */
|
|
78
113
|
orbitSpeed?: number
|
|
79
114
|
/** Multipliers over the built-in drag and zoom (wheel + pinch) sensitivities. */
|
|
80
115
|
rotateSpeed?: number
|
|
@@ -82,21 +117,25 @@ export type OrbitCameraOptions = {
|
|
|
82
117
|
/** Multiplier over the two-finger pan (1 = the scene tracks the fingers
|
|
83
118
|
* exactly at the target's depth). */
|
|
84
119
|
panSpeed?: number
|
|
85
|
-
/** The viewport the
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
120
|
+
/** The viewport the drag lives in: the input element's own height (its
|
|
121
|
+
* laid-out size, the frame the recognizer's deltas arrive in), and the
|
|
122
|
+
* camera's vertical fov in degrees; return null while neither is known
|
|
123
|
+
* yet. Providing it makes rotation viewport-relative (a drag across the
|
|
124
|
+
* viewport height is one full turn, whatever the window size) and
|
|
125
|
+
* enables two-finger pan; without it rotation is a fixed angle per pixel
|
|
126
|
+
* and two-finger translation rotates. */
|
|
127
|
+
viewport?: () => { height: number; fov: number } | null
|
|
90
128
|
/** Constrain where a pan may put the target - return the target to use.
|
|
91
129
|
* The typical use: keep the pivot within a few radii of the subject so
|
|
92
130
|
* panning cannot strand the camera. Zoom and rotation do not consult it. */
|
|
93
131
|
clampTarget?: (target: Vec3) => Vec3
|
|
94
|
-
/** Map a
|
|
95
|
-
* the world point a zoom there should keep
|
|
96
|
-
* gesture (at its first span change - the
|
|
97
|
-
* gesture, see the jitter note at
|
|
98
|
-
*
|
|
99
|
-
*
|
|
132
|
+
/** Map a point in the input element's own pixels (the frame
|
|
133
|
+
* localX/localY report in) to the world point a zoom there should keep
|
|
134
|
+
* pinned. Called once per pinch gesture (at its first span change - the
|
|
135
|
+
* anchor then holds for the whole gesture, see the jitter note at
|
|
136
|
+
* `pinchAnchor`) and once per wheel event, with the pose the zoom is
|
|
137
|
+
* about to apply to. Return null to zoom toward the target as usual
|
|
138
|
+
* (also the default). */
|
|
100
139
|
zoomAnchor?: (x: number, y: number, view: { eye: Vec3; target: Vec3 }) => Vec3 | null
|
|
101
140
|
/** The world point rotation should pivot about - called when a drag or
|
|
102
141
|
* pinch starts. It is projected onto the view axis and becomes the new
|
|
@@ -126,6 +165,10 @@ export type OrbitCamera = {
|
|
|
126
165
|
/** Whether the auto-orbit is running. Reactive (signal-backed), so HUD
|
|
127
166
|
* text can read it. */
|
|
128
167
|
orbiting(): boolean
|
|
168
|
+
/** Whether update(dt) still has work: the auto-orbit on with a non-zero
|
|
169
|
+
* `orbitSpeed`. The frame-loop gate, reactive like orbiting() - the
|
|
170
|
+
* same predicate the 2d camera and the first-person camera expose. */
|
|
171
|
+
active(): boolean
|
|
129
172
|
/** Advance the auto-orbit and push any pose change to the driven camera.
|
|
130
173
|
* Call from onFrame with the frame's dt in seconds; returns whether the
|
|
131
174
|
* pose changed. */
|
|
@@ -136,9 +179,10 @@ export type OrbitCamera = {
|
|
|
136
179
|
onPointerDown(e: PointerEvent): void
|
|
137
180
|
onPointerMove(e: PointerEvent): void
|
|
138
181
|
onPointerUp(e: PointerEvent): void
|
|
139
|
-
/** Position is optional so a bare
|
|
140
|
-
*
|
|
141
|
-
|
|
182
|
+
/** Position (the element's own pixels) is optional so a bare
|
|
183
|
+
* `{ deltaY }` still zooms; without it a zoomAnchor cannot apply and
|
|
184
|
+
* the zoom falls back to the target. */
|
|
185
|
+
onWheel(e: { deltaY: number; localX?: number; localY?: number }): void
|
|
142
186
|
}
|
|
143
187
|
}
|
|
144
188
|
|
|
@@ -147,38 +191,38 @@ let clampNum = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo,
|
|
|
147
191
|
/**
|
|
148
192
|
* Create an orbit camera driving `camera`'s position and target, where
|
|
149
193
|
* `camera` is a Scene or one of its Views (fov, near, and far stay yours via
|
|
150
|
-
* its setCamera). The initial pose applies immediately. In a component
|
|
151
|
-
*
|
|
152
|
-
*
|
|
194
|
+
* its setCamera). The initial pose applies immediately. In a component
|
|
195
|
+
* tree, prefer the `<OrbitCamera>` component: it wires all of this -
|
|
196
|
+
* scene, input, viewport, frame loop - through the Scene context.
|
|
153
197
|
*/
|
|
154
198
|
export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptions = {}): OrbitCamera {
|
|
155
199
|
let target: Vec3 = options.target ? [options.target[0], options.target[1], options.target[2]] : [0, 0, 0]
|
|
156
200
|
let azimuth = options.azimuth ?? 0
|
|
157
201
|
let elevation = options.elevation ?? 0
|
|
158
202
|
let distance = options.distance ?? 5
|
|
159
|
-
|
|
160
|
-
let
|
|
161
|
-
let
|
|
162
|
-
let
|
|
163
|
-
let
|
|
164
|
-
let
|
|
165
|
-
let
|
|
166
|
-
let
|
|
203
|
+
// Everything below the pose is read from `options` where it applies.
|
|
204
|
+
let minDistance = () => options.minDistance ?? MIN_DISTANCE
|
|
205
|
+
let maxDistance = () => options.maxDistance ?? Infinity
|
|
206
|
+
let minElevation = () => options.minElevation ?? -ELEVATION_LIMIT
|
|
207
|
+
let maxElevation = () => options.maxElevation ?? ELEVATION_LIMIT
|
|
208
|
+
let orbitSpeed = () => options.orbitSpeed ?? 0
|
|
209
|
+
let rotateSpeed = () => options.rotateSpeed ?? 1
|
|
210
|
+
let zoomSpeed = () => options.zoomSpeed ?? 1
|
|
167
211
|
|
|
168
|
-
|
|
212
|
+
// The auto-orbit switch: starts on when the initial rate is positive.
|
|
213
|
+
let [orbiting, setOrbiting] = createSignal(orbitSpeed() > 0)
|
|
169
214
|
let interacting = false
|
|
170
215
|
let dirty = false
|
|
171
216
|
|
|
172
217
|
let clampPose = () => {
|
|
173
|
-
elevation = clampNum(elevation, minElevation, maxElevation)
|
|
174
|
-
distance = clampNum(distance, minDistance, maxDistance)
|
|
218
|
+
elevation = clampNum(elevation, minElevation(), maxElevation())
|
|
219
|
+
distance = clampNum(distance, minDistance(), maxDistance())
|
|
175
220
|
}
|
|
176
221
|
// Slide eye and target together along the camera's right/up so the scene
|
|
177
222
|
// tracks the fingers: dragged pixels map to world units through the
|
|
178
223
|
// frustum height at the target's depth. Screen +y is down, so the up-axis
|
|
179
224
|
// term is added (fingers down -> camera up -> scene follows down).
|
|
180
|
-
let pan = (dx: number, dy: number) => {
|
|
181
|
-
let vp = options.viewport!()
|
|
225
|
+
let pan = (dx: number, dy: number, vp: { height: number; fov: number }) => {
|
|
182
226
|
let wpp = ((2 * Math.tan((vp.fov * Math.PI) / 360) * distance) / vp.height) * (options.panSpeed ?? 1)
|
|
183
227
|
let sa = Math.sin(azimuth)
|
|
184
228
|
let ca = Math.cos(azimuth)
|
|
@@ -214,7 +258,7 @@ export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptio
|
|
|
214
258
|
// Bounds widen to the current distance so a pose already outside them
|
|
215
259
|
// (a rotateAnchor re-seat may land anywhere) zooms back toward range
|
|
216
260
|
// instead of snap-jumping into it.
|
|
217
|
-
distance = clampNum(distance * ratio, Math.min(minDistance, prev), Math.max(maxDistance, prev))
|
|
261
|
+
distance = clampNum(distance * ratio, Math.min(minDistance(), prev), Math.max(maxDistance(), prev))
|
|
218
262
|
if (!anchor) return
|
|
219
263
|
let s = distance / prev
|
|
220
264
|
target = [
|
|
@@ -269,11 +313,14 @@ export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptio
|
|
|
269
313
|
}
|
|
270
314
|
},
|
|
271
315
|
onTransformMove: (t) => {
|
|
272
|
-
|
|
273
|
-
|
|
316
|
+
let vp = options.viewport?.() ?? null
|
|
317
|
+
if (t.pointers >= 2 && vp !== null) {
|
|
318
|
+
pan(t.dx, t.dy, vp)
|
|
274
319
|
} else {
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
// Viewport-relative when the height is known, per-pixel otherwise.
|
|
321
|
+
let rel = vp !== null ? (DRAG_TURNS * 2 * Math.PI) / vp.height : null
|
|
322
|
+
azimuth -= t.dx * (rel ?? DRAG_AZIMUTH) * rotateSpeed()
|
|
323
|
+
elevation = clampNum(elevation + t.dy * (rel ?? DRAG_ELEVATION) * rotateSpeed(), minElevation(), maxElevation())
|
|
277
324
|
}
|
|
278
325
|
if (t.scale !== 1) {
|
|
279
326
|
// Fingers spreading (scale > 1) zooms in: the distance shrinks by the
|
|
@@ -283,7 +330,7 @@ export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptio
|
|
|
283
330
|
pinchSeen = true
|
|
284
331
|
pinchAnchor = anchorAt(t.x, t.y)
|
|
285
332
|
}
|
|
286
|
-
zoomAbout(1 / Math.pow(t.scale,
|
|
333
|
+
zoomAbout(1 / Math.pow(t.scale, zoomSpeed()), pinchAnchor)
|
|
287
334
|
}
|
|
288
335
|
dirty = true
|
|
289
336
|
},
|
|
@@ -298,6 +345,7 @@ export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptio
|
|
|
298
345
|
eye,
|
|
299
346
|
pose: () => ({ azimuth, elevation, distance }),
|
|
300
347
|
orbiting,
|
|
348
|
+
active: () => orbiting() && orbitSpeed() !== 0,
|
|
301
349
|
set(pose) {
|
|
302
350
|
if (pose.azimuth !== undefined) azimuth = pose.azimuth
|
|
303
351
|
if (pose.elevation !== undefined) elevation = pose.elevation
|
|
@@ -308,8 +356,9 @@ export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptio
|
|
|
308
356
|
dirty = true
|
|
309
357
|
},
|
|
310
358
|
update(dt) {
|
|
311
|
-
|
|
312
|
-
|
|
359
|
+
let rate = orbitSpeed()
|
|
360
|
+
if (rate !== 0 && !interacting && orbiting()) {
|
|
361
|
+
azimuth += dt * rate
|
|
313
362
|
dirty = true
|
|
314
363
|
}
|
|
315
364
|
if (!dirty) return false
|
|
@@ -320,7 +369,7 @@ export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptio
|
|
|
320
369
|
handlers: {
|
|
321
370
|
...transform.handlers,
|
|
322
371
|
onWheel(e) {
|
|
323
|
-
zoomAbout(Math.exp(e.deltaY *
|
|
372
|
+
zoomAbout(Math.exp(e.deltaY * WHEEL_ZOOM * zoomSpeed()), anchorAt(e.localX, e.localY))
|
|
324
373
|
dirty = true
|
|
325
374
|
},
|
|
326
375
|
},
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Pointer dispatch behind scene.handlers: element pointer events in,
|
|
2
|
+
// instance/mesh/group handler calls out - nearest hit wins, down/move/up
|
|
3
|
+
// bubble from the struck instance (or the mesh) up through the ancestors,
|
|
4
|
+
// pointer-down captures until up, enter/leave pair on hover changes. One
|
|
5
|
+
// system per scene; everything it needs of the scene is `pick` and the
|
|
6
|
+
// target size (makePointerInput's deps), the capture/hover bookkeeping is
|
|
7
|
+
// its own.
|
|
8
|
+
|
|
9
|
+
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
10
|
+
import type { SceneNode, ScenePointerEvent } from "./node.ts"
|
|
11
|
+
import type { InstanceNode, Mesh } from "./mesh.ts"
|
|
12
|
+
import type { Hit, SceneHandlers } from "./scene.ts"
|
|
13
|
+
|
|
14
|
+
type BubbleName = "onPointerDown" | "onPointerMove" | "onPointerUp"
|
|
15
|
+
type InternalEvent = ScenePointerEvent & { _stopped: boolean }
|
|
16
|
+
// What a press captures and a hover tracks: the struck mesh, and the
|
|
17
|
+
// instance when the mesh is instanced.
|
|
18
|
+
type Target = { mesh: Mesh; instance: InstanceNode | null }
|
|
19
|
+
|
|
20
|
+
export type PointerInputDeps = {
|
|
21
|
+
/** The scene's pick: camera ray through a scene pixel, hits nearest
|
|
22
|
+
* first. */
|
|
23
|
+
pick(x: number, y: number): Hit[]
|
|
24
|
+
/** The scene target's current size (handlersFor's layout scaling). */
|
|
25
|
+
targetSize(): { width: number; height: number }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type PointerInput = {
|
|
29
|
+
/** scene.handlers: for a leaf laid out at the target size. */
|
|
30
|
+
handlers: SceneHandlers
|
|
31
|
+
/** scene.handlersFor: for a leaf whose layout size differs (the
|
|
32
|
+
* supersampling pattern); `layout` is read per event. */
|
|
33
|
+
handlersFor(layout: () => { width: number; height: number }): SceneHandlers
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
37
|
+
let capture = new Map<number, Target>()
|
|
38
|
+
let hover = new Map<number, Target>()
|
|
39
|
+
|
|
40
|
+
let targetOf = (hit: Hit): Target => ({ mesh: hit.mesh, instance: hit.instance ?? null })
|
|
41
|
+
let sameTarget = (hit: Hit, target: Target): boolean => hit.mesh === target.mesh && (hit.instance ?? null) === target.instance
|
|
42
|
+
// The node the walk starts at, and the one enter/leave fire on.
|
|
43
|
+
let nodeOf = (target: Target): SceneNode => target.instance ?? target.mesh
|
|
44
|
+
|
|
45
|
+
let makeEvent = (e: ElementPointerEvent, target: Target, x: number, y: number, hit: Hit | null): InternalEvent => {
|
|
46
|
+
let event: InternalEvent = {
|
|
47
|
+
mesh: target.mesh,
|
|
48
|
+
instance: target.instance,
|
|
49
|
+
currentTarget: nodeOf(target),
|
|
50
|
+
point: hit !== null ? hit.point : null,
|
|
51
|
+
distance: hit !== null ? hit.distance : null,
|
|
52
|
+
x,
|
|
53
|
+
y,
|
|
54
|
+
pointerId: e.pointerId,
|
|
55
|
+
pointerType: e.pointerType,
|
|
56
|
+
button: e.button,
|
|
57
|
+
shiftKey: e.shiftKey,
|
|
58
|
+
ctrlKey: e.ctrlKey,
|
|
59
|
+
altKey: e.altKey,
|
|
60
|
+
metaKey: e.metaKey,
|
|
61
|
+
_stopped: false,
|
|
62
|
+
stopPropagation() {
|
|
63
|
+
event._stopped = true
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
return event
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The struck instance first (its chain passes through the mesh), then
|
|
70
|
+
// the mesh, then its ancestors.
|
|
71
|
+
let bubble = (name: BubbleName, event: InternalEvent): void => {
|
|
72
|
+
for (let n: SceneNode | null = event.instance ?? event.mesh; n !== null && !event._stopped; n = n.parent) {
|
|
73
|
+
let handler = n[name]
|
|
74
|
+
if (handler) {
|
|
75
|
+
event.currentTarget = n
|
|
76
|
+
handler(event)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The captured target's own hit, if the ray still strikes it.
|
|
82
|
+
let hitOn = (target: Target, x: number, y: number): Hit | null => {
|
|
83
|
+
for (let h of deps.pick(x, y)) if (sameTarget(h, target)) return h
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// localX/localY arrive in the leaf's LAYOUT frame (the hit test undoes
|
|
88
|
+
// every transform above it, design-size fits included), so a leaf laid out at
|
|
89
|
+
// the target size - the built-in <Scene> leaf, a d-texture at natural
|
|
90
|
+
// size - is already in scene pixels. Only a leaf deliberately laid out at
|
|
91
|
+
// a DIFFERENT size (the supersampling pattern) needs the ratio, and only
|
|
92
|
+
// the app knows that layout: handlersFor takes it.
|
|
93
|
+
let makeHandlers = (layout: (() => { width: number; height: number }) | null): SceneHandlers => {
|
|
94
|
+
let eventX = 0
|
|
95
|
+
let eventY = 0
|
|
96
|
+
let toScene = (e: ElementPointerEvent): void => {
|
|
97
|
+
if (layout === null) {
|
|
98
|
+
eventX = e.localX
|
|
99
|
+
eventY = e.localY
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
let l = layout()
|
|
103
|
+
let size = deps.targetSize()
|
|
104
|
+
eventX = e.localX * (l.width > 0 ? size.width / l.width : 1)
|
|
105
|
+
eventY = e.localY * (l.height > 0 ? size.height / l.height : 1)
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
onPointerDown(e) {
|
|
109
|
+
toScene(e)
|
|
110
|
+
let hit = deps.pick(eventX, eventY)[0]
|
|
111
|
+
if (hit === undefined) return
|
|
112
|
+
let target = targetOf(hit)
|
|
113
|
+
capture.set(e.pointerId, target)
|
|
114
|
+
bubble("onPointerDown", makeEvent(e, target, eventX, eventY, hit))
|
|
115
|
+
},
|
|
116
|
+
onPointerMove(e) {
|
|
117
|
+
toScene(e)
|
|
118
|
+
let captured = capture.get(e.pointerId)
|
|
119
|
+
if (captured !== undefined) {
|
|
120
|
+
bubble("onPointerMove", makeEvent(e, captured, eventX, eventY, hitOn(captured, eventX, eventY)))
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
let hit = deps.pick(eventX, eventY)[0]
|
|
124
|
+
let prev = hover.get(e.pointerId)
|
|
125
|
+
let changed = prev === undefined ? hit !== undefined : hit === undefined || !sameTarget(hit, prev)
|
|
126
|
+
if (changed) {
|
|
127
|
+
if (prev !== undefined) {
|
|
128
|
+
hover.delete(e.pointerId)
|
|
129
|
+
nodeOf(prev).onPointerLeave?.(makeEvent(e, prev, eventX, eventY, null))
|
|
130
|
+
}
|
|
131
|
+
if (hit !== undefined) {
|
|
132
|
+
let target = targetOf(hit)
|
|
133
|
+
hover.set(e.pointerId, target)
|
|
134
|
+
nodeOf(target).onPointerEnter?.(makeEvent(e, target, eventX, eventY, hit))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (hit !== undefined) {
|
|
138
|
+
bubble("onPointerMove", makeEvent(e, targetOf(hit), eventX, eventY, hit))
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
onPointerUp(e) {
|
|
142
|
+
toScene(e)
|
|
143
|
+
let captured = capture.get(e.pointerId)
|
|
144
|
+
if (captured !== undefined) {
|
|
145
|
+
capture.delete(e.pointerId)
|
|
146
|
+
bubble("onPointerUp", makeEvent(e, captured, eventX, eventY, hitOn(captured, eventX, eventY)))
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
let hit = deps.pick(eventX, eventY)[0]
|
|
150
|
+
if (hit !== undefined) {
|
|
151
|
+
bubble("onPointerUp", makeEvent(e, targetOf(hit), eventX, eventY, hit))
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
onPointerLeave(e) {
|
|
155
|
+
let prev = hover.get(e.pointerId)
|
|
156
|
+
if (prev !== undefined) {
|
|
157
|
+
hover.delete(e.pointerId)
|
|
158
|
+
toScene(e)
|
|
159
|
+
nodeOf(prev).onPointerLeave?.(makeEvent(e, prev, eventX, eventY, null))
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { handlers: makeHandlers(null), handlersFor: makeHandlers }
|
|
166
|
+
}
|