@solidrt/3d 0.0.56 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1052 -639
- package/README.md +2 -1
- package/examples/README.md +19 -8
- package/examples/collision.tsx +17 -4
- package/examples/first-person.tsx +20 -10
- package/examples/fleet.tsx +11 -5
- package/examples/model.tsx +3 -3
- package/examples/pick.tsx +144 -34
- package/examples/probe.tsx +10 -4
- package/examples/scene-views.tsx +49 -58
- package/examples/sky-lit.tsx +10 -4
- package/examples/skybox.tsx +10 -4
- package/examples/standard.tsx +10 -4
- package/examples/wireframe.tsx +107 -0
- package/package.json +2 -2
- package/src/collision.ts +14 -193
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/first-person.ts +140 -188
- package/src/geometry-gpu.ts +45 -13
- package/src/geometry.ts +186 -5
- package/src/gltf.ts +97 -16
- package/src/index.ts +15 -10
- package/src/input.ts +79 -0
- package/src/material.ts +26 -8
- package/src/mesh.ts +5 -6
- package/src/model.ts +65 -33
- package/src/node.ts +100 -26
- package/src/orbit.ts +266 -235
- package/src/scene-pointer.ts +229 -101
- package/src/scene.ts +256 -79
- package/src/skeleton.ts +211 -0
- package/src/components.tsx +0 -1101
package/README.md
CHANGED
|
@@ -97,7 +97,8 @@ named channel to a geometry's interleave, `withColors` is the `aColor`
|
|
|
97
97
|
vec4 case - tint, baked AO, any four scalars - and materials read channels
|
|
98
98
|
by name), one perspective camera
|
|
99
99
|
with an orbit control (`<OrbitCamera>` as a Scene child, or
|
|
100
|
-
`createOrbitCamera` imperatively
|
|
100
|
+
`createOrbitCamera` imperatively; drag, pinch/wheel zoom and a pad reach
|
|
101
|
+
it through an input map the app binds, auto-orbit),
|
|
101
102
|
mesh picking with pointer events, scene backgrounds, transparency, and
|
|
102
103
|
lights (`lit` material with hemisphere ambient, up to four directional
|
|
103
104
|
light nodes, Blinn-Phong highlight and triplanar mapping).
|
package/examples/README.md
CHANGED
|
@@ -21,10 +21,14 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
21
21
|
target: `lookAt` for a +z solid, `quatFromTo` for aiming a y-axis cone,
|
|
22
22
|
and a `quatSlerp` damped follow that visibly lags; all driven from
|
|
23
23
|
onFrame through refs, no per-frame signals.
|
|
24
|
-
- `pick.tsx` - mesh pointer events
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
- `pick.tsx` - mesh pointer events under an `<OrbitCamera>`, the scene
|
|
25
|
+
as the root of the walk: hover tints (enter/leave), a tap pops, a drag
|
|
26
|
+
on the crate slides it over the floor without orbiting (its down claims
|
|
27
|
+
the press), a drag elsewhere orbits and a wheel zooms through the feed
|
|
28
|
+
listening at the root, the ring claims the wheel to spin, a tap on
|
|
29
|
+
nothing un-pops (the scene's own `onTap` with `mesh` null), and a
|
|
30
|
+
Group hears its children's downs through bubbling; a STATIC scene
|
|
31
|
+
rendered only when an event changes something.
|
|
28
32
|
- `scene-background.tsx` - a fragment-GLSL background drawn inside the
|
|
29
33
|
scene's own pass (`<Scene background>`): one target, no stacked
|
|
30
34
|
backdrop texture, no resize plumbing; the source is shader-target
|
|
@@ -85,10 +89,11 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
85
89
|
Drag to orbit; the `formation` and `state` debug commands drive it
|
|
86
90
|
headlessly.
|
|
87
91
|
- `first-person.tsx` - a first-person walk: `<FirstPersonCamera>` over
|
|
88
|
-
a walled courtyard of shadow-casting pillars,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
a walled courtyard of shadow-casting pillars, driven by an input map
|
|
93
|
+
the app binds (`firstPersonBindings`: WASD/arrows and the pad sticks
|
|
94
|
+
to walk, the scene's drag to look on touch, mouse motion while the
|
|
95
|
+
pointer is locked - click locks, Escape releases, the app's calls, not
|
|
96
|
+
the control's); `clampPosition` keeps the walker inside the walls, the whole of
|
|
92
97
|
the collision a camera control offers; the `pose` debug command reads
|
|
93
98
|
and sets the pose headlessly.
|
|
94
99
|
- `collision.tsx` - collision without a physics engine: the same walker
|
|
@@ -137,6 +142,12 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
137
142
|
transparent dome and a mesh without normals) parsed with `parseGltf`
|
|
138
143
|
from a binary import and built by `createModel` into a Group of named
|
|
139
144
|
parts; clicking a part hides it, clicking the body restores all.
|
|
145
|
+
- `wireframe.tsx` - the rover drawn solid, as a wireframe or as its
|
|
146
|
+
feature edges: `wireframeGeometry`/`edgesGeometry` build "lines"
|
|
147
|
+
geometry over each part's own vertices (one shared vertex upload, a
|
|
148
|
+
second index buffer) and `setGeometry`/`setMaterial` swap it onto the
|
|
149
|
+
live mesh, no node touched. Space, a tap on the rover, or the `wire`
|
|
150
|
+
debug command (`{ mode: "solid" | "wireframe" | "edges" }`) cycles.
|
|
140
151
|
- `model-load.tsx` - the same rover loaded ASYNC with `loadModel` from
|
|
141
152
|
`assets/` under a `<Loading>` boundary: the async read lives in a memo,
|
|
142
153
|
a second memo derives the scene JSX after that read, and the shell
|
package/examples/collision.tsx
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
// click locks the pointer, Escape releases it. Debug commands: `pose`
|
|
12
12
|
// (read/set), `walk` ({ dx, dz } through the collision), `jump`, `fall`
|
|
13
13
|
// ({ dt }: one airborne step by hand, for headless checks), `state`.
|
|
14
|
-
import { createSignal, lockPointer, onFrame, pct, pointerLocked, render } from "@solidrt/core"
|
|
14
|
+
import { createInputMap, createPointerFeed, createSignal, gamepad, keyboard, lockPointer, onFrame, pct, pointerLocked, render } from "@solidrt/core"
|
|
15
15
|
import type { PointerEvent } from "@solidrt/core"
|
|
16
16
|
import {
|
|
17
17
|
box,
|
|
18
18
|
cylinder,
|
|
19
19
|
DirectionalLight,
|
|
20
20
|
FirstPersonCamera,
|
|
21
|
+
firstPersonActions,
|
|
22
|
+
firstPersonBindings,
|
|
21
23
|
HemisphereLight,
|
|
22
24
|
lit,
|
|
23
25
|
Mesh,
|
|
@@ -68,6 +70,14 @@ let feetOf = (eye: Vec3): Vec3 => [eye[0], eye[1] - EYE, eye[2]]
|
|
|
68
70
|
function App() {
|
|
69
71
|
let scene!: SceneHandle
|
|
70
72
|
let camera!: FirstPersonCameraHandle
|
|
73
|
+
// The walker's input plus the game's own `jump` action: the standard
|
|
74
|
+
// first-person bindings, and Space or the pad's south button for the
|
|
75
|
+
// jump - one map, read by name (ARCHITECTURE.md).
|
|
76
|
+
let pointer = createPointerFeed()
|
|
77
|
+
let pad = gamepad()
|
|
78
|
+
let input = createInputMap({ ...firstPersonActions, jump: "button" })
|
|
79
|
+
input.bind(firstPersonBindings({ pointer, gamepad: pad, keyboard }))
|
|
80
|
+
input.bind("jump", keyboard.key("Space"), pad.button("south"))
|
|
71
81
|
let [hud, setHud] = createSignal("")
|
|
72
82
|
let [near, setNear] = createSignal<Set<number>>(new Set())
|
|
73
83
|
// The same set, readable synchronously (a signal read right after its
|
|
@@ -135,6 +145,7 @@ function App() {
|
|
|
135
145
|
grounded = false
|
|
136
146
|
fall()
|
|
137
147
|
}
|
|
148
|
+
input.onPress("jump", jump)
|
|
138
149
|
|
|
139
150
|
let lastHud = 0
|
|
140
151
|
onFrame(tick => {
|
|
@@ -204,9 +215,11 @@ function App() {
|
|
|
204
215
|
return (
|
|
205
216
|
<window
|
|
206
217
|
onKeyDown={e => {
|
|
218
|
+
input.handlers.onKeyDown(e)
|
|
207
219
|
if (e.key === "Escape") lockPointer(false)
|
|
208
|
-
if (e.key === " " || e.code === "Space") jump()
|
|
209
220
|
}}
|
|
221
|
+
onKeyUp={input.handlers.onKeyUp}
|
|
222
|
+
onBlur={input.handlers.onBlur}
|
|
210
223
|
>
|
|
211
224
|
<view
|
|
212
225
|
width={pct(100)}
|
|
@@ -215,9 +228,9 @@ function App() {
|
|
|
215
228
|
if (e.pointerType === "mouse") lockPointer(true)
|
|
216
229
|
}}
|
|
217
230
|
>
|
|
218
|
-
<Scene ref={s => (scene = s)} clearColor={[0.6, 0.72, 0.88, 1]} samples={4} label="collision">
|
|
231
|
+
<Scene ref={s => (scene = s)} clearColor={[0.6, 0.72, 0.88, 1]} samples={4} label="collision" pointer={pointer}>
|
|
219
232
|
<PerspectiveCamera fov={70} near={0.1} far={80} />
|
|
220
|
-
<FirstPersonCamera ref={c => (camera = c)} position={[0, EYE, 10]} clampPosition={clamp} />
|
|
233
|
+
<FirstPersonCamera input={input} ref={c => (camera = c)} position={[0, EYE, 10]} clampPosition={clamp} />
|
|
221
234
|
<HemisphereLight sky={[0.5, 0.56, 0.68]} ground={[0.25, 0.22, 0.18]} />
|
|
222
235
|
<DirectionalLight color={[1, 0.95, 0.85]} intensity={0.9} position={[8, 14, 6]} direction={[-8, -14, -6]} castShadow shadow={{ mapSize: 2048, normalBias: 0.02, cascades: 3, distance: 40 }} />
|
|
223
236
|
<Mesh geometry={plane({ width: FLOOR * 2 + 2, height: FLOOR * 2 + 2 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} layers={1 | COLLIDER} />
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// A first-person walk: `<FirstPersonCamera>` driving the scene camera from
|
|
2
|
-
// WASD/arrows and the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// key is held or a stick deflected - a still scene renders
|
|
2
|
+
// an input map the app wires - WASD/arrows and the sticks through the
|
|
3
|
+
// standard bindings, the scene leaf's drag to look around on touch, and
|
|
4
|
+
// mouse motion while the pointer is locked. Click the scene to lock the
|
|
5
|
+
// pointer, Escape releases it; the keys reach the map through the window
|
|
6
|
+
// (`input.handlers`), so nothing needs focus. Movement runs a frame loop
|
|
7
|
+
// only while a key is held or a stick deflected - a still scene renders
|
|
8
|
+
// nothing new.
|
|
8
9
|
// `clampPosition` keeps the walker inside the courtyard walls, the whole
|
|
9
10
|
// of the collision an app gets from a camera control. The `pose` debug
|
|
10
11
|
// command reads and sets the pose for headless checks.
|
|
11
|
-
import { createSignal, lockPointer, onFrame, pct, pointerLocked, render } from "@solidrt/core"
|
|
12
|
+
import { createInputMap, createPointerFeed, createSignal, gamepad, keyboard, lockPointer, onFrame, pct, pointerLocked, render } from "@solidrt/core"
|
|
12
13
|
import type { PointerEvent } from "@solidrt/core"
|
|
13
|
-
import { box, cylinder, DirectionalLight, FirstPersonCamera, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene } from "@solidrt/3d"
|
|
14
|
+
import { box, cylinder, DirectionalLight, FirstPersonCamera, firstPersonActions, firstPersonBindings, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene } from "@solidrt/3d"
|
|
14
15
|
import type { FirstPersonCameraHandle, Vec3 } from "@solidrt/3d"
|
|
15
16
|
import { registerDebug } from "srt:dev"
|
|
16
17
|
|
|
@@ -43,6 +44,12 @@ let inside = (p: Vec3): Vec3 => [clampNum(p[0], -COURT + WALL_GAP, COURT - WALL_
|
|
|
43
44
|
|
|
44
45
|
function App() {
|
|
45
46
|
let camera!: FirstPersonCameraHandle
|
|
47
|
+
// The walker's input: the scene leaf's gestures, the keyboard and any
|
|
48
|
+
// pad, bound to the control's actions (ARCHITECTURE.md: the app wires
|
|
49
|
+
// devices, the component takes the map).
|
|
50
|
+
let pointer = createPointerFeed()
|
|
51
|
+
let input = createInputMap(firstPersonActions)
|
|
52
|
+
input.bind(firstPersonBindings({ pointer, gamepad: gamepad(), keyboard }))
|
|
46
53
|
let [hud, setHud] = createSignal("")
|
|
47
54
|
let lastHud = 0
|
|
48
55
|
onFrame(tick => {
|
|
@@ -77,8 +84,11 @@ function App() {
|
|
|
77
84
|
return (
|
|
78
85
|
<window
|
|
79
86
|
onKeyDown={e => {
|
|
87
|
+
input.handlers.onKeyDown(e)
|
|
80
88
|
if (e.key === "Escape") lockPointer(false)
|
|
81
89
|
}}
|
|
90
|
+
onKeyUp={input.handlers.onKeyUp}
|
|
91
|
+
onBlur={input.handlers.onBlur}
|
|
82
92
|
>
|
|
83
93
|
<view
|
|
84
94
|
width={pct(100)}
|
|
@@ -87,9 +97,9 @@ function App() {
|
|
|
87
97
|
if (e.pointerType === "mouse") lockPointer(true)
|
|
88
98
|
}}
|
|
89
99
|
>
|
|
90
|
-
<Scene clearColor={[0.6, 0.72, 0.88, 1]} samples={4} label="first-person">
|
|
100
|
+
<Scene clearColor={[0.6, 0.72, 0.88, 1]} samples={4} label="first-person" pointer={pointer}>
|
|
91
101
|
<PerspectiveCamera fov={70} near={0.1} far={60} />
|
|
92
|
-
<FirstPersonCamera ref={c => (camera = c)} position={[0, EYE, 7]} clampPosition={inside} />
|
|
102
|
+
<FirstPersonCamera input={input} ref={c => (camera = c)} position={[0, EYE, 7]} clampPosition={inside} />
|
|
93
103
|
<HemisphereLight sky={[0.5, 0.56, 0.68]} ground={[0.25, 0.22, 0.18]} />
|
|
94
104
|
<DirectionalLight
|
|
95
105
|
color={[1, 0.95, 0.85]}
|
package/examples/fleet.tsx
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// the last tapped instance, how many are flipped) and `pixel` ({ i }: the
|
|
16
16
|
// scene pixel instance i projects to and what a pick there finds, for a
|
|
17
17
|
// synthetic tap).
|
|
18
|
-
import { createSignal, flush, For, pct, render } from "@solidrt/core"
|
|
19
|
-
import { box, DirectionalLight, HemisphereLight, Instance, InstancedMesh, lit, Mesh, OrbitCamera, plane, Scene, worldPosition } from "@solidrt/3d"
|
|
18
|
+
import { createInputMap, createPointerFeed, createSignal, flush, For, gamepad, pct, render } from "@solidrt/core"
|
|
19
|
+
import { box, DirectionalLight, HemisphereLight, Instance, InstancedMesh, lit, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, worldPosition } from "@solidrt/3d"
|
|
20
20
|
import type { InstanceNode, NodeTransition, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
21
21
|
import { registerDebug } from "srt:dev"
|
|
22
22
|
|
|
@@ -143,12 +143,18 @@ registerDebug("pixel", (args?: Record<string, unknown>) => {
|
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
function App() {
|
|
146
|
+
// The camera's input: the scene leaf's gestures and any pad, bound to the
|
|
147
|
+
// orbit control's actions - the app wires devices, the component takes
|
|
148
|
+
// the map (ARCHITECTURE.md).
|
|
149
|
+
let pointer = createPointerFeed()
|
|
150
|
+
let input = createInputMap(orbitActions)
|
|
151
|
+
input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
|
|
146
152
|
let place = (i: number): Vec3 => FORMATIONS[formation()]!.at(i)
|
|
147
153
|
return (
|
|
148
154
|
<window>
|
|
149
155
|
<view width={pct(100)} height={pct(100)}>
|
|
150
|
-
<Scene clearColor={[SKY[0], SKY[1], SKY[2], 1]} fog={{ color: SKY, near: 12, far: 30 }} label="fleet" ref={s => (scene = s)}>
|
|
151
|
-
<OrbitCamera target={[0, 2, 0]} distance={15} elevation={0.45} azimuth={0.6} />
|
|
156
|
+
<Scene clearColor={[SKY[0], SKY[1], SKY[2], 1]} fog={{ color: SKY, near: 12, far: 30 }} label="fleet" ref={s => (scene = s)} pointer={pointer}>
|
|
157
|
+
<OrbitCamera input={input} target={[0, 2, 0]} distance={15} elevation={0.45} azimuth={0.6} />
|
|
152
158
|
<HemisphereLight sky={[0.45, 0.47, 0.5]} ground={[0.12, 0.13, 0.11]} />
|
|
153
159
|
<DirectionalLight
|
|
154
160
|
color={[1, 0.95, 0.85]}
|
|
@@ -179,7 +185,7 @@ function App() {
|
|
|
179
185
|
position={place(i)}
|
|
180
186
|
style={tints[i]![0]()}
|
|
181
187
|
transition={SPRING}
|
|
182
|
-
|
|
188
|
+
onTap={() => flip(i)}
|
|
183
189
|
ref={node => (nodes[i] = node)}
|
|
184
190
|
/>
|
|
185
191
|
)}
|
package/examples/model.tsx
CHANGED
|
@@ -52,10 +52,10 @@ function App() {
|
|
|
52
52
|
let center: [number, number, number] = [(b[0]! + b[3]!) / 2, (b[1]! + b[4]!) / 2, (b[2]! + b[5]!) / 2]
|
|
53
53
|
let radius = Math.hypot(b[3]! - b[0]!, b[4]! - b[1]!, b[5]! - b[2]!) / 2
|
|
54
54
|
|
|
55
|
-
// Parts are named after the glTF nodes: a
|
|
56
|
-
//
|
|
55
|
+
// Parts are named after the glTF nodes: a tap hides that part, a tap
|
|
56
|
+
// on the body brings everything back.
|
|
57
57
|
for (let part of model.parts) {
|
|
58
|
-
part.mesh.
|
|
58
|
+
part.mesh.onTap = () => {
|
|
59
59
|
if (part.name === "body") for (let p of model.parts) setVisible(p.mesh, true)
|
|
60
60
|
else setVisible(part.mesh, false)
|
|
61
61
|
}
|
package/examples/pick.tsx
CHANGED
|
@@ -1,60 +1,136 @@
|
|
|
1
|
-
// Mesh pointer events
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// Mesh pointer events under an orbit control, the scene as the root of
|
|
2
|
+
// the walk: hover tints (enter/leave), a TAP pops (a down may be the start
|
|
3
|
+
// of an orbit drag, so the pop waits for the release), a drag on the
|
|
4
|
+
// crate slides it over the floor and never orbits - its down claims the
|
|
5
|
+
// press, so the orbit control's feed, listening at the root, sees none
|
|
6
|
+
// of it - while a drag anywhere else orbits and a wheel zooms; the ring
|
|
7
|
+
// claims the wheel to spin itself; a tap on empty space un-pops
|
|
8
|
+
// everything (the scene's own onTap with `mesh` null, the deselect
|
|
9
|
+
// idiom). The Group hears the cone's downs through bubbling (the crate's
|
|
10
|
+
// claim stops the walk at the crate). The scene is STATIC - no onFrame - so it renders only when an event changes
|
|
11
|
+
// something, and the orbit runs frames only while it glides; hit testing
|
|
12
|
+
// runs over the scene's BVH, so a pointer move costs O(log meshes).
|
|
8
13
|
//
|
|
9
14
|
// The volume tier is honest about its shape: hits test each mesh's
|
|
10
15
|
// bounding box, so hovering just outside the ball's silhouette (inside its
|
|
11
16
|
// box corners) still counts. Triangle-accurate hits are a later tier.
|
|
17
|
+
//
|
|
18
|
+
// Debug commands: `state` (the orbit pose, the crate's position, the
|
|
19
|
+
// ring's spin, the popped and hovered names) and `pixel` ({ name }: the
|
|
20
|
+
// WINDOW pixel a mesh's position projects to, for a synthetic tap).
|
|
12
21
|
|
|
13
|
-
import { createSignal, pct, render } from "@solidrt/core"
|
|
14
|
-
import { box, cone, Group, Mesh, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit } from "@solidrt/3d"
|
|
15
|
-
import type { Geometry,
|
|
22
|
+
import { createInputMap, createPointerFeed, createSignal, displayScale, pct, render } from "@solidrt/core"
|
|
23
|
+
import { box, cone, getRotation, Group, Mesh, OrbitCamera, orbitActions, orbitBindings, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit, useScene } from "@solidrt/3d"
|
|
24
|
+
import type { Geometry, MeshNode, NodePointerEvent, NodeTapEvent, NodeWheelEvent, OrbitCameraHandle, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
25
|
+
import { registerDebug } from "srt:dev"
|
|
16
26
|
|
|
17
27
|
type Color = [number, number, number]
|
|
18
28
|
|
|
29
|
+
// How much a popped mesh grows.
|
|
30
|
+
const POP_SCALE = 1.25
|
|
31
|
+
// Radians of ring spin per wheel pixel.
|
|
32
|
+
const WHEEL_SPIN = 0.01
|
|
33
|
+
// The orbit's starting pose: the old fixed camera at [0, 2.6, 5] looking
|
|
34
|
+
// at the scene's middle.
|
|
35
|
+
const ORBIT_TARGET: Vec3 = [0, 0.4, 0]
|
|
36
|
+
const ORBIT_DISTANCE = 5.5
|
|
37
|
+
const ORBIT_ELEVATION = 0.42
|
|
38
|
+
|
|
19
39
|
let [hovered, setHovered] = createSignal("nothing")
|
|
20
|
-
|
|
40
|
+
// Popped meshes and the scale they pop back to.
|
|
41
|
+
let popped = new Map<MeshNode, number>()
|
|
42
|
+
let meshes: Record<string, MeshNode> = {}
|
|
43
|
+
let orbit!: OrbitCameraHandle
|
|
44
|
+
let scene!: SceneHandle
|
|
45
|
+
|
|
46
|
+
let unpopAll = () => {
|
|
47
|
+
for (let [mesh, base] of popped) setTransform(mesh, { scale: base })
|
|
48
|
+
popped.clear()
|
|
49
|
+
}
|
|
21
50
|
|
|
22
51
|
/** A pickable mesh: unlit color that brightens on hover, scale-pop on
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
52
|
+
* tap; `drag` slides it over the floor (claiming the press), `spin`
|
|
53
|
+
* turns it on the wheel (claiming the wheel). setMeshParams writes raw
|
|
54
|
+
* uniform values, so the tint premultiplies here (alpha 1: the rgb passes
|
|
55
|
+
* through). */
|
|
56
|
+
function Pickable(p: { name: string; color: Color; geometry: Geometry; position: Vec3; scale?: number; drag?: boolean; spin?: boolean }) {
|
|
57
|
+
let scene = useScene().scene
|
|
26
58
|
let lift = (up: number): [number, number, number, number] => [
|
|
27
59
|
Math.min(p.color[0] + up, 1),
|
|
28
60
|
Math.min(p.color[1] + up, 1),
|
|
29
61
|
Math.min(p.color[2] + up, 1),
|
|
30
62
|
1,
|
|
31
63
|
]
|
|
64
|
+
// The floor point under the pointer at the mesh's height: the camera
|
|
65
|
+
// ray through the event's scene pixel meets the plane y = mesh y.
|
|
66
|
+
let floorPoint = (e: NodePointerEvent): [number, number] | null => {
|
|
67
|
+
let ray = scene.screenRay(e.x, e.y)
|
|
68
|
+
if (Math.abs(ray.direction[1]) < 1e-6) return null
|
|
69
|
+
let t = (e.mesh.position[1] - ray.origin[1]) / ray.direction[1]
|
|
70
|
+
if (t < 0) return null
|
|
71
|
+
return [ray.origin[0] + t * ray.direction[0], ray.origin[2] + t * ray.direction[2]]
|
|
72
|
+
}
|
|
73
|
+
// The mesh's offset from the grabbed floor point while a drag is on.
|
|
74
|
+
let grab: [number, number] | null = null
|
|
75
|
+
let spin = 0
|
|
32
76
|
return (
|
|
33
77
|
<Mesh
|
|
34
78
|
geometry={p.geometry}
|
|
35
79
|
material={unlit({ color: p.color })}
|
|
36
80
|
position={p.position}
|
|
37
81
|
scale={p.scale}
|
|
38
|
-
|
|
82
|
+
ref={m => (meshes[p.name] = m)}
|
|
83
|
+
onPointerEnter={(e: NodePointerEvent) => {
|
|
39
84
|
setHovered(p.name)
|
|
40
85
|
setMeshParams(e.mesh, { uColor: lift(0.25) })
|
|
41
|
-
console.log(`enter ${p.name}`)
|
|
42
86
|
}}
|
|
43
|
-
onPointerLeave={(e:
|
|
87
|
+
onPointerLeave={(e: NodePointerEvent) => {
|
|
44
88
|
setHovered("nothing")
|
|
45
89
|
setMeshParams(e.mesh, { uColor: lift(0) })
|
|
46
|
-
console.log(`leave ${p.name}`)
|
|
47
90
|
}}
|
|
48
|
-
|
|
49
|
-
let
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
onTap={(e: NodeTapEvent) => {
|
|
92
|
+
let base = popped.get(e.mesh)
|
|
93
|
+
if (base === undefined) {
|
|
94
|
+
popped.set(e.mesh, p.scale ?? 1)
|
|
95
|
+
setTransform(e.mesh, { scale: (p.scale ?? 1) * POP_SCALE })
|
|
96
|
+
} else {
|
|
97
|
+
popped.delete(e.mesh)
|
|
98
|
+
setTransform(e.mesh, { scale: base })
|
|
99
|
+
}
|
|
53
100
|
let pt = e.point!
|
|
54
|
-
console.log(`
|
|
55
|
-
// The cone demonstrates stopping the bubble: its group never hears it.
|
|
56
|
-
if (p.stop) e.stopPropagation()
|
|
101
|
+
console.log(`tap ${p.name} x${e.tapCount} at ${pt[0].toFixed(2)},${pt[1].toFixed(2)},${pt[2].toFixed(2)} d=${e.distance!.toFixed(2)}`)
|
|
57
102
|
}}
|
|
103
|
+
onPointerDown={
|
|
104
|
+
p.drag
|
|
105
|
+
? (e: NodePointerEvent) => {
|
|
106
|
+
let f = floorPoint(e)
|
|
107
|
+
if (f === null) return
|
|
108
|
+
grab = [e.mesh.position[0] - f[0], e.mesh.position[2] - f[1]]
|
|
109
|
+
// The claim: this press is the crate's, the orbit never sees it.
|
|
110
|
+
e.stopPropagation()
|
|
111
|
+
}
|
|
112
|
+
: undefined
|
|
113
|
+
}
|
|
114
|
+
onPointerMove={
|
|
115
|
+
p.drag
|
|
116
|
+
? (e: NodePointerEvent) => {
|
|
117
|
+
if (grab === null) return
|
|
118
|
+
let f = floorPoint(e)
|
|
119
|
+
if (f === null) return
|
|
120
|
+
setTransform(e.mesh, { position: [f[0] + grab[0], e.mesh.position[1], f[1] + grab[1]] })
|
|
121
|
+
}
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
124
|
+
onPointerUp={p.drag ? () => (grab = null) : undefined}
|
|
125
|
+
onWheel={
|
|
126
|
+
p.spin
|
|
127
|
+
? (e: NodeWheelEvent) => {
|
|
128
|
+
spin += e.deltaY * WHEEL_SPIN
|
|
129
|
+
setTransform(e.mesh, { rotation: [0, spin, 0] })
|
|
130
|
+
e.stopPropagation()
|
|
131
|
+
}
|
|
132
|
+
: undefined
|
|
133
|
+
}
|
|
58
134
|
/>
|
|
59
135
|
)
|
|
60
136
|
}
|
|
@@ -65,27 +141,61 @@ function App() {
|
|
|
65
141
|
let ball = sphere({ radius: 0.45 })
|
|
66
142
|
let spike = cone({ radius: 0.45, height: 1 })
|
|
67
143
|
let ring = torus({ radius: 0.4, tube: 0.14 })
|
|
144
|
+
// The orbit control's input: the scene leaf's pointer feed, bound to
|
|
145
|
+
// the standard orbit actions (drag rotates, wheel and pinch zoom).
|
|
146
|
+
let pointer = createPointerFeed()
|
|
147
|
+
let input = createInputMap(orbitActions)
|
|
148
|
+
input.bind(orbitBindings({ pointer }))
|
|
149
|
+
|
|
150
|
+
registerDebug("state", () => ({
|
|
151
|
+
pose: orbit.pose(),
|
|
152
|
+
crate: meshes.crate?.position,
|
|
153
|
+
ringSpin: meshes.ring ? getRotation(meshes.ring)[1] : null,
|
|
154
|
+
popped: Object.keys(meshes).filter(n => popped.has(meshes[n]!)),
|
|
155
|
+
hovered: hovered(),
|
|
156
|
+
}))
|
|
157
|
+
// The scene fills the window at device density, so a scene pixel over
|
|
158
|
+
// the display scale is a window pixel.
|
|
159
|
+
registerDebug("pixel", (args?: { name?: string }) => {
|
|
160
|
+
let mesh = meshes[args?.name ?? ""]
|
|
161
|
+
if (!mesh) return null
|
|
162
|
+
let p = scene.project(mesh.position)
|
|
163
|
+
if (p === null) return null
|
|
164
|
+
return { x: p.x / displayScale(), y: p.y / displayScale() }
|
|
165
|
+
})
|
|
68
166
|
|
|
69
167
|
return (
|
|
70
168
|
<window>
|
|
71
169
|
<view width={pct(100)} height={pct(100)}>
|
|
72
|
-
<Scene
|
|
73
|
-
|
|
170
|
+
<Scene
|
|
171
|
+
clearColor={[0.07, 0.07, 0.1, 1]}
|
|
172
|
+
label="pick"
|
|
173
|
+
pointer={pointer}
|
|
174
|
+
ref={s => (scene = s)}
|
|
175
|
+
onTap={e => {
|
|
176
|
+
if (e.mesh !== null) return
|
|
177
|
+
console.log(`tap on nothing at ${e.x.toFixed(0)},${e.y.toFixed(0)}: un-pop`)
|
|
178
|
+
unpopAll()
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<PerspectiveCamera fov={55} />
|
|
182
|
+
<OrbitCamera input={input} target={ORBIT_TARGET} distance={ORBIT_DISTANCE} elevation={ORBIT_ELEVATION} ref={o => (orbit = o)} />
|
|
74
183
|
<Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
75
|
-
{/* The group hears
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<Pickable name="
|
|
184
|
+
{/* The group hears the cone's downs through bubbling; the crate's
|
|
185
|
+
claim stops the walk at the crate, so the group never sees those. */}
|
|
186
|
+
<Group onPointerDown={e => console.log(`group saw down on ${e.mesh === meshes.crate ? "crate" : "cone"}`)}>
|
|
187
|
+
<Pickable name="crate" color={[0.85, 0.3, 0.3]} geometry={crate} position={[-1.6, 0.4, 0]} scale={0.8} drag />
|
|
188
|
+
<Pickable name="cone" color={[0.35, 0.65, 0.9]} geometry={spike} position={[1.6, 0.5, 0]} />
|
|
79
189
|
</Group>
|
|
80
190
|
<Pickable name="ball" color={[0.9, 0.8, 0.35]} geometry={ball} position={[0, 0.45, 0]} />
|
|
81
|
-
<Pickable name="ring" color={[0.5, 0.85, 0.5]} geometry={ring} position={[0.9, 0.35, -1.6]} />
|
|
191
|
+
<Pickable name="ring" color={[0.5, 0.85, 0.5]} geometry={ring} position={[0.9, 0.35, -1.6]} spin />
|
|
82
192
|
</Scene>
|
|
83
193
|
<view position="absolute" x={0} y={0} padding={16} gap={4}>
|
|
84
194
|
<text color="#eef4ff" fontSize={22} fontWeight={700}>
|
|
85
195
|
{`hover: ${hovered()}`}
|
|
86
196
|
</text>
|
|
87
197
|
<text color="#8fa6c8" fontSize={13}>
|
|
88
|
-
hover tints,
|
|
198
|
+
hover tints, tap pops, drag the crate; drag elsewhere orbits, wheel zooms (over the ring: spins it), tap nothing to un-pop
|
|
89
199
|
</text>
|
|
90
200
|
</view>
|
|
91
201
|
</view>
|
package/examples/probe.tsx
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// (the chain is the frame's largest GPU item; see AGENTS.md). Drag to
|
|
13
13
|
// look around.
|
|
14
14
|
|
|
15
|
-
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
16
|
-
import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, plane, Scene, sphere, standard, unlit } from "@solidrt/3d"
|
|
15
|
+
import { createInputMap, createPointerFeed, createSignal, gamepad, onFrame, pct, render } from "@solidrt/core"
|
|
16
|
+
import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, sphere, standard, unlit } from "@solidrt/3d"
|
|
17
17
|
import type { ReflectionProbe, SceneHandle } from "@solidrt/3d"
|
|
18
18
|
import type { Vec3 } from "@solidrt/3d/math"
|
|
19
19
|
|
|
@@ -58,6 +58,12 @@ const ORBITER_COLORS: [number, number, number][] = [
|
|
|
58
58
|
]
|
|
59
59
|
|
|
60
60
|
function App() {
|
|
61
|
+
// The camera's input: the scene leaf's gestures and any pad, bound to the
|
|
62
|
+
// orbit control's actions - the app wires devices, the component takes
|
|
63
|
+
// the map (ARCHITECTURE.md).
|
|
64
|
+
let pointer = createPointerFeed()
|
|
65
|
+
let input = createInputMap(orbitActions)
|
|
66
|
+
input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
|
|
61
67
|
let [t, setT] = createSignal(0)
|
|
62
68
|
let scene: SceneHandle | undefined
|
|
63
69
|
let probe: ReflectionProbe | undefined
|
|
@@ -81,8 +87,8 @@ function App() {
|
|
|
81
87
|
return (
|
|
82
88
|
<window>
|
|
83
89
|
<view width={pct(100)} height={pct(100)}>
|
|
84
|
-
<Scene clearColor={[0.05, 0.05, 0.06, 1]} samples={4} label="probe-demo" ref={s => (scene = s)}>
|
|
85
|
-
<OrbitCamera target={[0, 0, 0]} azimuth={0.5} elevation={0.2} distance={4} />
|
|
90
|
+
<Scene clearColor={[0.05, 0.05, 0.06, 1]} samples={4} label="probe-demo" ref={s => (scene = s)} pointer={pointer}>
|
|
91
|
+
<OrbitCamera input={input} target={[0, 0, 0]} azimuth={0.5} elevation={0.2} distance={4} />
|
|
86
92
|
<HemisphereLight sky={[0.6, 0.65, 0.7]} ground={[0.3, 0.28, 0.26]} intensity={0.6} />
|
|
87
93
|
<DirectionalLight direction={[0.4, -0.8, 0.3]} intensity={0.8} />
|
|
88
94
|
{WALLS.map((w, i) => (
|