@solidrt/3d 0.0.56 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +199 -117
  2. package/README.md +2 -1
  3. package/examples/README.md +13 -8
  4. package/examples/collision.tsx +17 -4
  5. package/examples/first-person.tsx +20 -10
  6. package/examples/fleet.tsx +11 -5
  7. package/examples/model.tsx +3 -3
  8. package/examples/pick.tsx +144 -34
  9. package/examples/probe.tsx +10 -4
  10. package/examples/scene-views.tsx +49 -58
  11. package/examples/sky-lit.tsx +10 -4
  12. package/examples/skybox.tsx +10 -4
  13. package/examples/standard.tsx +10 -4
  14. package/package.json +2 -2
  15. package/src/collision.ts +14 -193
  16. package/src/components/context.tsx +35 -0
  17. package/src/components/directional-light.tsx +48 -0
  18. package/src/components/first-person-camera.tsx +72 -0
  19. package/src/components/group.tsx +18 -0
  20. package/src/components/hemisphere-light.tsx +24 -0
  21. package/src/components/index.ts +46 -0
  22. package/src/components/instanced-mesh.tsx +105 -0
  23. package/src/components/mesh.tsx +92 -0
  24. package/src/components/node-props.ts +90 -0
  25. package/src/components/orbit-camera.tsx +75 -0
  26. package/src/components/perspective-camera.tsx +32 -0
  27. package/src/components/point-light.tsx +52 -0
  28. package/src/components/record-mesh.tsx +68 -0
  29. package/src/components/scene.tsx +281 -0
  30. package/src/components/spot-light.tsx +65 -0
  31. package/src/components/sprite.tsx +38 -0
  32. package/src/components/view3d.tsx +152 -0
  33. package/src/first-person.ts +140 -188
  34. package/src/index.ts +13 -9
  35. package/src/input.ts +70 -0
  36. package/src/node.ts +98 -25
  37. package/src/orbit.ts +266 -235
  38. package/src/scene-pointer.ts +229 -101
  39. package/src/scene.ts +250 -76
  40. package/src/components.tsx +0 -1101
@@ -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
- onPointerDown={() => flip(i)}
188
+ onTap={() => flip(i)}
183
189
  ref={node => (nodes[i] = node)}
184
190
  />
185
191
  )}
@@ -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 click hides that part, a
56
- // click on the body brings everything back.
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.onPointerDown = () => {
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: hover to tint (enter/leave), click to pop (down),
2
- // with a Group seeing its children's clicks through bubbling and one mesh
3
- // stopping the walk. The scene is STATIC - no onFrame - so it renders only
4
- // when an event changes something: picking is demand-driven like
5
- // everything else. Events flow because the built-in <Scene> leaf carries
6
- // scene.handlers (default on); hit testing runs over the scene's BVH, so a
7
- // pointer move costs O(log meshes).
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, ScenePointerEvent, Vec3 } from "@solidrt/3d"
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
- let popped = new Set<object>()
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
- * click. setMeshParams writes raw uniform values, so the tint premultiplies
24
- * here (alpha 1: the rgb passes through). */
25
- function Pickable(p: { name: string; color: Color; geometry: Geometry; position: Vec3; scale?: number; stop?: boolean }) {
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
- onPointerEnter={(e: ScenePointerEvent) => {
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: ScenePointerEvent) => {
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
- onPointerDown={(e: ScenePointerEvent) => {
49
- let pop = !popped.has(e.mesh)
50
- if (pop) popped.add(e.mesh)
51
- else popped.delete(e.mesh)
52
- setTransform(e.mesh, { scale: pop ? (p.scale ?? 1) * 1.25 : (p.scale ?? 1) })
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(`down ${p.name} at ${pt[0].toFixed(2)},${pt[1].toFixed(2)},${pt[2].toFixed(2)} d=${e.distance!.toFixed(2)}`)
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 clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
73
- <PerspectiveCamera fov={55} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
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 every child click that is not stopped. */}
76
- <Group onPointerDown={() => console.log("group saw down")}>
77
- <Pickable name="crate" color={[0.85, 0.3, 0.3]} geometry={crate} position={[-1.6, 0.4, 0]} scale={0.8} />
78
- <Pickable name="cone" color={[0.35, 0.65, 0.9]} geometry={spike} position={[1.6, 0.5, 0]} stop />
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, click pops - a static scene, rendered only on change
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>
@@ -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) => (
@@ -1,20 +1,21 @@
1
1
  // One scene, three renderings: the built-in leaf from a perspective
2
- // camera, plus two `scene.createView` targets - a top-down ORTHOGRAPHIC
3
- // map (`ortho` on setCamera) and a side silhouette drawn with an
2
+ // camera, plus two `<View3d>`s - a top-down ORTHOGRAPHIC map (`ortho` in
3
+ // the view's `camera`) and a side silhouette drawn with an
4
4
  // `overrideMaterial`. The group spins from ONE signal: each mesh is one
5
5
  // core node whose flush writes its world matrix into every target's entry
6
6
  // at once, so the extra views cost the app nothing per frame (and the
7
7
  // light set fans out to them too - the map is lit like the main view).
8
- // Views die with the scene.
8
+ // A view is a Scene child: it renders its own `<texture>` leaf where it
9
+ // sits, so the two stack in the column beside the scene's leaf, and it
10
+ // dies with the scene.
9
11
  //
10
12
  // Layers and per-view fog: the marker disc is on layer bit 2, so the
11
13
  // main render (mask 1) and the silhouette never draw it - only the map,
12
- // whose mask admits both bits. The scene is fogged; `fog: null` on the
14
+ // whose mask admits both bits. The scene is fogged; `fog={null}` on the
13
15
  // map keeps the top-down view clear (its fog names are view-owned, so
14
16
  // scene-wide fog writes never clobber them).
15
17
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
16
- import { box, circle, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
17
- import type { SceneHandle } from "@solidrt/3d"
18
+ import { box, circle, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit, View3d } from "@solidrt/3d"
18
19
 
19
20
  // Layer bits: the world everything defaults to, and the map-only markers.
20
21
  const WORLD_LAYER = 1
@@ -31,67 +32,16 @@ function App() {
31
32
  let floor = plane({ width: 6, height: 6, label: "floor" })
32
33
  let ball = sphere({ radius: 0.35 })
33
34
  let marker = circle({ radius: 0.3, label: "marker" })
34
- // `ref` runs before `output`, so the handle is set when the views are
35
- // created there; both callbacks run once, untracked - no signals needed.
36
- let scene!: SceneHandle
37
35
 
38
36
  return (
39
37
  <window>
40
- <view width={pct(100)} height={pct(100)} designSize={[MAIN + SIDE, MAIN]}>
38
+ <view width={pct(100)} height={pct(100)} designSize={[MAIN + SIDE, MAIN]} flexDirection="row">
41
39
  <Scene
42
40
  width={MAIN}
43
41
  height={MAIN}
44
42
  clearColor={[0.07, 0.07, 0.1, 1]}
45
43
  fog={{ color: [0.07, 0.07, 0.1], near: 2.5, far: 9 }}
46
44
  label="scene-views"
47
- ref={s => (scene = s)}
48
- output={tex => {
49
- // The map: straight down from y = 10, world -z toward the top of
50
- // the leaf, seven world units across at any depth. Its mask also
51
- // admits the marker layer, and `fog: null` keeps it clear while
52
- // the main view fades to the horizon.
53
- let map = scene.createView({
54
- width: SIDE,
55
- height: SIDE,
56
- clearColor: [0.05, 0.08, 0.06, 1],
57
- layers: WORLD_LAYER | MARKER_LAYER,
58
- fog: null,
59
- label: "map",
60
- })
61
- map.setCamera({
62
- position: [0, 10, 0],
63
- target: [0, 0, 0],
64
- up: [0, 0, -1],
65
- ortho: { left: -3.5, right: 3.5, top: 3.5, bottom: -3.5 },
66
- near: 0.1,
67
- far: 20,
68
- })
69
- // The silhouette: every mesh drawn flat amber, from the side. The
70
- // floor is edge-on here, a single line at the bottom.
71
- let side = scene.createView({
72
- width: SIDE,
73
- height: SIDE,
74
- clearColor: [0.1, 0.06, 0.05, 1],
75
- overrideMaterial: unlit({ color: [0.95, 0.75, 0.3] }),
76
- label: "silhouette",
77
- })
78
- side.setCamera({
79
- position: [6, 1, 0],
80
- target: [0, 0.6, 0],
81
- ortho: { left: -2.4, right: 2.4, top: 2.4, bottom: -2.4 },
82
- near: 0.1,
83
- far: 20,
84
- })
85
- return (
86
- <view flexDirection="row" width={MAIN + SIDE} height={MAIN}>
87
- <texture src={tex} width={MAIN} height={MAIN} />
88
- <view flexDirection="column" width={SIDE} height={MAIN}>
89
- <texture src={map.texture} width={SIDE} height={SIDE} />
90
- <texture src={side.texture} width={SIDE} height={SIDE} />
91
- </view>
92
- </view>
93
- )
94
- }}
95
45
  >
96
46
  <PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
97
47
  <HemisphereLight sky={[0.6, 0.7, 0.9]} ground={[0.25, 0.2, 0.15]} intensity={0.6} />
@@ -115,6 +65,47 @@ function App() {
115
65
  rotation={[-Math.PI / 2, 0, 0]}
116
66
  />
117
67
  </Group>
68
+ {/* The two views' leaves stack beside the scene's: a laid-out
69
+ element among Scene children renders where it sits, and the
70
+ views inside it stay Scene children through context. */}
71
+ <view flexDirection="column" width={SIDE} height={MAIN}>
72
+ {/* The map: straight down from y = 10, world -z toward the top
73
+ of the leaf, seven world units across at any depth. Its mask
74
+ also admits the marker layer, and `fog={null}` keeps it clear
75
+ while the main view fades to the horizon. */}
76
+ <View3d
77
+ width={SIDE}
78
+ height={SIDE}
79
+ clearColor={[0.05, 0.08, 0.06, 1]}
80
+ layers={WORLD_LAYER | MARKER_LAYER}
81
+ fog={null}
82
+ label="map"
83
+ camera={{
84
+ position: [0, 10, 0],
85
+ target: [0, 0, 0],
86
+ up: [0, 0, -1],
87
+ ortho: { left: -3.5, right: 3.5, top: 3.5, bottom: -3.5 },
88
+ near: 0.1,
89
+ far: 20,
90
+ }}
91
+ />
92
+ {/* The silhouette: every mesh drawn flat amber, from the side.
93
+ The floor is edge-on here, a single line at the bottom. */}
94
+ <View3d
95
+ width={SIDE}
96
+ height={SIDE}
97
+ clearColor={[0.1, 0.06, 0.05, 1]}
98
+ overrideMaterial={unlit({ color: [0.95, 0.75, 0.3] })}
99
+ label="silhouette"
100
+ camera={{
101
+ position: [6, 1, 0],
102
+ target: [0, 0.6, 0],
103
+ ortho: { left: -2.4, right: 2.4, top: 2.4, bottom: -2.4 },
104
+ near: 0.1,
105
+ far: 20,
106
+ }}
107
+ />
108
+ </view>
118
109
  </Scene>
119
110
  </view>
120
111
  </window>
@@ -9,9 +9,9 @@
9
9
  // gradient. The sky is a scene param (uSunDir), so the bake sees the same
10
10
  // sun the backdrop draws. Drag to look around.
11
11
 
12
- import { pct, render } from "@solidrt/core"
12
+ import { createInputMap, createPointerFeed, gamepad, pct, render } from "@solidrt/core"
13
13
  import { glsl } from "@solidrt/core/gpu"
14
- import { Mesh, OrbitCamera, Scene, sphere, standard } from "@solidrt/3d"
14
+ import { Mesh, OrbitCamera, orbitActions, orbitBindings, Scene, sphere, standard } from "@solidrt/3d"
15
15
  import type { SceneHandle } from "@solidrt/3d"
16
16
  import { normalize } from "@solidrt/3d/math"
17
17
  import type { Vec3 } from "@solidrt/3d/math"
@@ -55,6 +55,12 @@ const SKY = glsl`
55
55
  `
56
56
 
57
57
  function App() {
58
+ // The camera's input: the scene leaf's gestures and any pad, bound to the
59
+ // orbit control's actions - the app wires devices, the component takes
60
+ // the map (ARCHITECTURE.md).
61
+ let pointer = createPointerFeed()
62
+ let input = createInputMap(orbitActions)
63
+ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
58
64
  let spheres: { position: Vec3; material: ReturnType<typeof standard> }[] = []
59
65
  for (let i = 0; i < COUNT; i++) {
60
66
  let roughness = i / (COUNT - 1)
@@ -72,8 +78,8 @@ function App() {
72
78
  return (
73
79
  <window>
74
80
  <view width={pct(100)} height={pct(100)}>
75
- <Scene background={SKY} toneMapping="aces" samples={4} label="sky-lit" ref={lit}>
76
- <OrbitCamera target={[0, 0, 0]} azimuth={0.3} elevation={0.15} distance={7} />
81
+ <Scene background={SKY} toneMapping="aces" samples={4} label="sky-lit" ref={lit} pointer={pointer}>
82
+ <OrbitCamera input={input} target={[0, 0, 0]} azimuth={0.3} elevation={0.15} distance={7} />
77
83
  {spheres.map(s => (
78
84
  <Mesh geometry={geometry} material={s.material} position={s.position} />
79
85
  ))}
@@ -11,9 +11,9 @@
11
11
  // equirectToCube on a panorama. The sky turns slowly and the sun light
12
12
  // turns with it: `rotation` is a reactive prop the scene updates in
13
13
  // place, no recompile. Drag to look around, wheel to zoom.
14
- import { createSignal, onFrame, pct, render } from "@solidrt/core"
14
+ import { createInputMap, createPointerFeed, createSignal, gamepad, onFrame, pct, render } from "@solidrt/core"
15
15
  import { createCubeTexture } from "@solidrt/core/gpu"
16
- import { box, DirectionalLight, HemisphereLight, lit, Mesh, OrbitCamera, plane, Scene, sphere, torusKnot } from "@solidrt/3d"
16
+ import { box, DirectionalLight, HemisphereLight, lit, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, sphere, torusKnot } from "@solidrt/3d"
17
17
  import { normalize } from "@solidrt/3d/math"
18
18
  import type { Vec3 } from "@solidrt/3d/math"
19
19
 
@@ -92,6 +92,12 @@ function turnedSun(angle: number): Vec3 {
92
92
  }
93
93
 
94
94
  function App() {
95
+ // The camera's input: the scene leaf's gestures and any pad, bound to the
96
+ // orbit control's actions - the app wires devices, the component takes
97
+ // the map (ARCHITECTURE.md).
98
+ let pointer = createPointerFeed()
99
+ let input = createInputMap(orbitActions)
100
+ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
95
101
  let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
96
102
  let [turn, setTurn] = createSignal(0)
97
103
  let last = 0
@@ -111,8 +117,8 @@ function App() {
111
117
  return (
112
118
  <window>
113
119
  <view width={pct(100)} height={pct(100)}>
114
- <Scene background={{ cube, rotation: turn() }} environment={{ cube, rotation: turn() }} label="skybox-demo">
115
- <OrbitCamera azimuth={0.4} elevation={0.12} distance={7} />
120
+ <Scene background={{ cube, rotation: turn() }} environment={{ cube, rotation: turn() }} label="skybox-demo" pointer={pointer}>
121
+ <OrbitCamera input={input} azimuth={0.4} elevation={0.12} distance={7} />
116
122
  <HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} />
117
123
  <DirectionalLight direction={[-sun()[0], -sun()[1], -sun()[2]]} color={SUN_COLOR} intensity={0.9} />
118
124
  <Mesh geometry={plane({ width: 12, height: 12 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
@@ -12,9 +12,9 @@
12
12
  // Light intensities read as lit's: 1 lights a white matte surface to 1.
13
13
  // Drag to look around.
14
14
 
15
- import { pct, render } from "@solidrt/core"
15
+ import { createInputMap, createPointerFeed, gamepad, pct, render } from "@solidrt/core"
16
16
  import { createCubeTexture } from "@solidrt/core/gpu"
17
- import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, plane, Scene, sphere, standard } from "@solidrt/3d"
17
+ import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, sphere, standard } from "@solidrt/3d"
18
18
  import { normalize } from "@solidrt/3d/math"
19
19
  import type { Vec3 } from "@solidrt/3d/math"
20
20
 
@@ -91,6 +91,12 @@ function bakeSky(): Uint8Array[] {
91
91
  }
92
92
 
93
93
  function App() {
94
+ // The camera's input: the scene leaf's gestures and any pad, bound to the
95
+ // orbit control's actions - the app wires devices, the component takes
96
+ // the map (ARCHITECTURE.md).
97
+ let pointer = createPointerFeed()
98
+ let input = createInputMap(orbitActions)
99
+ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
94
100
  let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
95
101
  let ball = sphere({ radius: RADIUS, widthSegments: 48, heightSegments: 32 })
96
102
  let ground = standard({ color: [0.32, 0.33, 0.3], roughness: 0.9 })
@@ -109,8 +115,8 @@ function App() {
109
115
  return (
110
116
  <window>
111
117
  <view width={pct(100)} height={pct(100)}>
112
- <Scene background={{ cube }} environment={{ cube }} samples={4} label="standard-demo">
113
- <OrbitCamera target={center} azimuth={0.25} elevation={0.15} distance={9} />
118
+ <Scene background={{ cube }} environment={{ cube }} samples={4} label="standard-demo" pointer={pointer}>
119
+ <OrbitCamera input={input} target={center} azimuth={0.25} elevation={0.15} distance={9} />
114
120
  <HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} intensity={0.5} />
115
121
  <DirectionalLight
116
122
  position={[SUN[0] * SUN_DISTANCE, SUN[1] * SUN_DISTANCE, SUN[2] * SUN_DISTANCE]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.56",
3
+ "version": "0.0.57",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -19,6 +19,6 @@
19
19
  ],
20
20
  "peerDependencies": {
21
21
  "@solidjs/signals": "2.0.0-rc.6",
22
- "@solidrt/core": "0.0.56"
22
+ "@solidrt/core": "0.0.57"
23
23
  }
24
24
  }