@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
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
7
7
 
8
8
  export { add, createGroup, getRotation, getTransform, lookAt, remove, setTransform, setTransition, setVisible, worldPosition } from "./node.ts"
9
- export type { SceneNode, ScenePointerEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
9
+ export type { NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
10
10
  export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, removeInstance, setCastShadow, setCulling, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, INSTANCE_FLOATS } from "./mesh.ts"
11
11
  export type { InstancedMesh as InstancedMeshNode, InstancedMeshOptions, InstanceNode, InstanceSlots, InstanceStyle, Mesh as MeshNode, MeshInstances, RecordMesh as RecordMeshNode, RecordMeshOptions } from "./mesh.ts"
12
12
  export { createDirectionalLight, createHemisphereLight, createPointLight, createSpotLight, setLight, MAX_SHADOWS } from "./light.ts"
@@ -26,9 +26,10 @@ export type {
26
26
  SpotShadowOptions,
27
27
  } from "./light.ts"
28
28
  export { createScene } from "./scene.ts"
29
- export type { Capsule, EnvironmentOptions, FogOptions, Hit, Impact, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, View, ViewOptions, Volume } from "./scene.ts"
29
+ export { feedPointer } from "./scene-pointer.ts"
30
+ export type { Capsule, EnvironmentOptions, FogOptions, Hit, Impact, MoveOptions, MoveResult, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, ViewHandle, ViewOptions, Volume } from "./scene.ts"
30
31
  export { moveAndSlide } from "./collision.ts"
31
- export type { MoveOptions, MoveResult, MoveScene } from "./collision.ts"
32
+ export type { MoveScene } from "./collision.ts"
32
33
  export { equirectToCube, loadEnvironment } from "./environment.ts"
33
34
  export { linearColor, linearToSrgb, srgbToLinear } from "./color.ts"
34
35
  export type { CameraState, CameraUpdate, OrthoExtent } from "./camera.ts"
@@ -42,8 +43,9 @@ export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
42
43
  export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
43
44
  export { lit, shaderMaterial, shaderMaterialClass, sprite, standard, unlit } from "./material.ts"
44
45
  export type { LitOptions, Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, StandardOptions, UnlitOptions } from "./material.ts"
45
- export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedMesh, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, useScene } from "./components.tsx"
46
+ export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedMesh, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, View3d, useScene } from "./components/index.ts"
46
47
  export type {
48
+ CameraTarget,
47
49
  DirectionalLightProps,
48
50
  FirstPersonCameraProps,
49
51
  HemisphereLightProps,
@@ -55,13 +57,13 @@ export type {
55
57
  PointerEventProps,
56
58
  PointLightProps,
57
59
  RecordMeshProps,
58
- SceneInput,
59
- SceneInputListener,
60
+ ScenePointerProps,
60
61
  SceneProps,
61
62
  SpotLightProps,
62
63
  SpriteProps,
63
64
  TransformProps,
64
- } from "./components.tsx"
65
+ View3dProps,
66
+ } from "./components/index.ts"
65
67
  export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
66
68
  export type { ModelChannel, ModelClip, ModelData, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
67
69
  export { decodeModel, encodeModel } from "./model-file.ts"
@@ -71,9 +73,11 @@ export { createMixer } from "./mixer.ts"
71
73
  export type { Mixer, MixerOptions, MixerPlayOptions } from "./mixer.ts"
72
74
  export { sampleChannel } from "./clip.ts"
73
75
  export { createOrbitCamera } from "./orbit.ts"
74
- export type { OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
76
+ export type { OrbitAxes, OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
75
77
  export { createFirstPersonCamera } from "./first-person.ts"
76
- export type { FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonTarget } from "./first-person.ts"
78
+ export type { FirstPersonAxes, FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonTarget } from "./first-person.ts"
79
+ export { firstPersonActions, firstPersonBindings, orbitActions, orbitBindings } from "./input.ts"
80
+ export type { CameraDevices } from "./input.ts"
77
81
  // math's lookAt (the camera view matrix) stays on the /math subpath: the
78
82
  // root's lookAt is the scene verb, the same split as `add`.
79
83
  export { rayBoxDistance, compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, orthographic, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
package/src/input.ts ADDED
@@ -0,0 +1,70 @@
1
+ // The standard input wiring for the 3d camera controls: the action
2
+ // declarations each control consumes (its axes, by the shared vocabulary)
3
+ // and a preset per control that binds the devices an app hands in. A
4
+ // preset is plain data - the bindings the app could write by hand -
5
+ // applied with `input.bind(orbitBindings({ pointer, gamepad }))` and
6
+ // adjusted afterwards with unbind; nothing binds unless the app says so
7
+ // (ARCHITECTURE.md).
8
+ //
9
+ // Devices are the nouns: `pointer` is the feed of the element showing the
10
+ // scene (`createPointerFeed()`, handed to `<Scene pointer>`), `gamepad` a
11
+ // core gamepad device (`gamepad(0)`, or `gamepad()` for any pad), and
12
+ // `keyboard` the core keyboard device, whose key events reach the map
13
+ // through `input.handlers` spread on the window.
14
+ //
15
+ // Keys and sticks move the CAMERA where a drag moves the content (the
16
+ // stick right orbits the eye to the right; a drag right spins the scene
17
+ // to the right), so the rotate and pan bindings of those devices go
18
+ // through invert() and each axis keeps its one meaning. `look` is the
19
+ // exception by nature: a drag and a stick both turn the eye.
20
+
21
+ import { invert } from "@solidrt/core/input"
22
+ import type { Binding, GamepadDevice, KeyboardDevice, PointerFeed } from "@solidrt/core"
23
+
24
+ export type CameraDevices = {
25
+ pointer?: PointerFeed
26
+ gamepad?: GamepadDevice
27
+ keyboard?: KeyboardDevice
28
+ }
29
+
30
+ /** The orbit control's actions: `rotate`, `zoom`, `pan`. */
31
+ export let orbitActions = { rotate: "vec2", zoom: "axis", pan: "vec2" } as const
32
+
33
+ /**
34
+ * The orbit camera's standard bindings: a drag rotates, a pinch and the
35
+ * wheel zoom, two fingers pan; the right stick rotates, the triggers
36
+ * zoom, the left stick pans; the arrow keys rotate and minus/equals zoom.
37
+ */
38
+ export function orbitBindings(devices: CameraDevices): Binding[] {
39
+ let out: Binding[] = []
40
+ let { pointer, gamepad, keyboard } = devices
41
+ if (pointer) {
42
+ out.push({ action: "rotate", source: pointer.drag }, { action: "zoom", source: pointer.pinch }, { action: "zoom", source: pointer.wheel }, { action: "pan", source: pointer.pan })
43
+ }
44
+ if (gamepad) {
45
+ out.push({ action: "rotate", source: invert(gamepad.rightStick) }, { action: "zoom", source: gamepad.triggers }, { action: "pan", source: invert(gamepad.leftStick) })
46
+ }
47
+ if (keyboard) {
48
+ out.push({ action: "rotate", source: invert(keyboard.arrows) }, { action: "zoom", source: keyboard.axis("Minus", "Equal") })
49
+ }
50
+ return out
51
+ }
52
+
53
+ /** The first-person control's actions: `look`, `move`, `rise`. */
54
+ export let firstPersonActions = { look: "vec2", move: "vec2", rise: "axis" } as const
55
+
56
+ /**
57
+ * The first-person camera's standard bindings: a drag looks around and so
58
+ * does mouse motion while the pointer is locked (the app locks it: see
59
+ * examples/first-person.tsx); the right stick looks, the left stick
60
+ * walks, the shoulders rise and sink in fly mode; WASD and the arrows
61
+ * walk, Q/E rise and sink.
62
+ */
63
+ export function firstPersonBindings(devices: CameraDevices): Binding[] {
64
+ let out: Binding[] = []
65
+ let { pointer, gamepad, keyboard } = devices
66
+ if (pointer) out.push({ action: "look", source: pointer.drag }, { action: "look", source: pointer.mouseDelta })
67
+ if (gamepad) out.push({ action: "look", source: gamepad.rightStick }, { action: "move", source: gamepad.leftStick }, { action: "rise", source: gamepad.shoulders })
68
+ if (keyboard) out.push({ action: "move", source: keyboard.wasd }, { action: "move", source: keyboard.arrows }, { action: "rise", source: keyboard.axis("KeyQ", "KeyE") })
69
+ return out
70
+ }
package/src/node.ts CHANGED
@@ -10,12 +10,14 @@ import * as spatial from "flux:spatial"
10
10
  import type { NodeId, NodeTransition } from "flux:spatial"
11
11
  import { on } from "srt:events"
12
12
  import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
13
+ import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
13
14
  // The scene's lookAt() aims a node; math's builds a camera's view matrix -
14
15
  // the same pairing (and the same name) as Three's Object3D/Matrix4.
15
16
  import { compose, eulerFromQuat, identity, mat4, multiply, quat, quatFromFrame, transformPoint, updateRotation, updateScale } from "./math.ts"
16
17
  import type { Mat4, Quat, TransformUpdate, Vec3, Vec4 } from "./math.ts"
17
18
  import type { CastingLight, Light } from "./light.ts"
18
19
  import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
20
+ import type { Scene, ViewHandle } from "./scene.ts"
19
21
 
20
22
  // lookAt()'s default roll reference. Read-only: quatFromFrame never
21
23
  // writes its inputs, so one shared vector is safe.
@@ -107,15 +109,23 @@ export type SceneNode = {
107
109
  visible: boolean
108
110
  /** Pointer event handlers - plain fields, assign freely (they touch no
109
111
  * GPU state, so they need no setTransform-style write path; components
110
- * sync their props here). Down/move/up dispatch on the hit mesh and
111
- * bubble through its ancestors (stopPropagation stops the walk);
112
- * enter/leave fire on the mesh alone. Events flow once the element
113
- * showing the scene carries `scene.handlers`. */
114
- onPointerDown?: (event: ScenePointerEvent) => void
115
- onPointerMove?: (event: ScenePointerEvent) => void
116
- onPointerUp?: (event: ScenePointerEvent) => void
117
- onPointerEnter?: (event: ScenePointerEvent) => void
118
- onPointerLeave?: (event: ScenePointerEvent) => void
112
+ * sync their props here). Down/move/up/wheel/tap dispatch on the
113
+ * nearest hit - the struck instance of an instanced mesh, else the mesh
114
+ * - bubble through its ancestors and end at the scene's listeners
115
+ * (stopPropagation stops the walk, and a stopped down claims the whole
116
+ * press); enter/leave fire on the struck node alone. Events flow once
117
+ * the element showing the scene carries `scene.handlers`. */
118
+ onPointerDown?: (event: NodePointerEvent) => void
119
+ onPointerMove?: (event: NodePointerEvent) => void
120
+ onPointerUp?: (event: NodePointerEvent) => void
121
+ onPointerEnter?: (event: NodePointerEvent) => void
122
+ onPointerLeave?: (event: NodePointerEvent) => void
123
+ /** The wheel over the node (NodeWheelEvent: `deltaX`/`deltaY`), bubbling
124
+ * like down/move/up. */
125
+ onWheel?: (event: NodeWheelEvent) => void
126
+ /** A press released on this node within the slop, alone for its whole
127
+ * press (NodeTapEvent: `tapCount`); bubbles like down/move/up. */
128
+ onTap?: (event: NodeTapEvent) => void
119
129
  /** A declared transition (setTransition) settled naturally on one
120
130
  * component; a cancel, snap or scene leave never fires. */
121
131
  onTransitionEnd?: (event: TransitionEndEvent) => void
@@ -146,26 +156,19 @@ export type TransitionEndEvent = {
146
156
  component: "position" | "rotation" | "scale"
147
157
  }
148
158
 
149
- /**
150
- * The event a mesh, instance or ancestor group handler receives: the
151
- * element pointer vocabulary carried over, plus the 3D fields.
152
- * `point`/`distance` are null exactly when the ray misses the dispatch
153
- * target - which happens only during a captured drag or on a leave.
154
- */
155
- export type ScenePointerEvent = {
156
- /** The mesh the event is about (the hit, or the captured mesh during a
157
- * drag) - constant while the event bubbles. */
158
- mesh: Mesh
159
+ /** What every scene pointer event carries, whichever handler sees it:
160
+ * the element pointer vocabulary carried over, plus the 3D fields. */
161
+ export type SceneEventBase = {
159
162
  /** The instance struck when `mesh` is an instanced mesh (the walk
160
163
  * starts there), null otherwise - constant while the event bubbles. */
161
164
  instance: InstanceNode | null
162
- /** Node whose handler is running; changes as the event bubbles. */
163
- currentTarget: SceneNode
164
- /** World-space hit point on `mesh`, or null when the ray misses it. */
165
+ /** World-space hit point on `mesh`, or null when there is no mesh or
166
+ * the ray misses it (a captured drag off the mesh, a leave). */
165
167
  point: Vec3 | null
166
168
  /** Camera-ray distance to `point` in world units, or null with it. */
167
169
  distance: number | null
168
- /** Pointer position in scene pixels - project()'s coordinate space. */
170
+ /** Pointer position in scene pixels - project()'s coordinate space
171
+ * (a view's own pixels under a view leaf). */
169
172
  x: number
170
173
  y: number
171
174
  pointerId: number
@@ -175,10 +178,77 @@ export type ScenePointerEvent = {
175
178
  ctrlKey: boolean
176
179
  altKey: boolean
177
180
  metaKey: boolean
178
- /** Stops the bubble walk after the current handler. */
181
+ /**
182
+ * The element event the scene's leaf received, in the leaf's own frame
183
+ * (localX/localY, clientX/clientY, movementX/Y): what core's
184
+ * recognizers consume, so a mesh drags itself through `createPan` and
185
+ * a camera control's feed rides the same events the walk carries.
186
+ */
187
+ native: ElementPointerEvent
188
+ /**
189
+ * Stops the walk after the current handler: no ancestor and none of the
190
+ * scene's listeners see the event. Stopping a DOWN claims the whole
191
+ * press - that pointer's move, up and tap never reach the scene either,
192
+ * so a mesh that drags itself stops its down once and an orbit control
193
+ * fed at the root never turns under it.
194
+ */
179
195
  stopPropagation(): void
180
196
  }
181
197
 
198
+ /**
199
+ * The event a mesh, instance or ancestor group handler receives: `mesh`
200
+ * is the hit (or the captured mesh during a drag), constant while the
201
+ * event bubbles; `currentTarget` the node whose handler is running.
202
+ */
203
+ export type NodePointerEvent = SceneEventBase & {
204
+ mesh: Mesh
205
+ currentTarget: SceneNode
206
+ }
207
+
208
+ /**
209
+ * The event as the SCENE's listeners see it (scene.listen, the `<Scene>`
210
+ * pointer props; a view's under its own leaf), the last stop of the walk:
211
+ * `mesh` is the hit it bubbled from, or null over empty space, where the
212
+ * scene is the only target.
213
+ */
214
+ export type ScenePointerEvent = SceneEventBase & {
215
+ mesh: Mesh | null
216
+ currentTarget: Scene | ViewHandle
217
+ }
218
+
219
+ type WheelFields = {
220
+ /** The wheel delta as the element event reports it. */
221
+ deltaX: number
222
+ deltaY: number
223
+ native: ElementWheelEvent
224
+ }
225
+ type TapFields = {
226
+ /** 1 for a tap, 2 for the second of a double tap (the same target,
227
+ * within the repeat interval and distance), and so on - DOM's `detail`,
228
+ * Unity's `clickCount`. */
229
+ tapCount: number
230
+ }
231
+
232
+ export type NodeWheelEvent = NodePointerEvent & WheelFields
233
+ export type SceneWheelEvent = ScenePointerEvent & WheelFields
234
+ /**
235
+ * A press that released on the target it pressed without travelling past
236
+ * the slop, the only pointer down for its whole press (a pinch never
237
+ * taps). Dispatched after the up, bubbling the same way; `x`/`y` and
238
+ * `point` are the release point.
239
+ */
240
+ export type NodeTapEvent = NodePointerEvent & TapFields
241
+ export type SceneTapEvent = ScenePointerEvent & TapFields
242
+
243
+ /** A listener at the scene's (or a view's) root; see Scene.listen. */
244
+ export type ScenePointerListener = {
245
+ onPointerDown?: (event: ScenePointerEvent) => void
246
+ onPointerMove?: (event: ScenePointerEvent) => void
247
+ onPointerUp?: (event: ScenePointerEvent) => void
248
+ onWheel?: (event: SceneWheelEvent) => void
249
+ onTap?: (event: SceneTapEvent) => void
250
+ }
251
+
182
252
  /** A bare node record (internal: the mesh and light constructors build on it). */
183
253
  export function makeNode(kind: SceneNode["kind"]): SceneNode {
184
254
  return {
@@ -245,7 +315,10 @@ export function enterScene(node: SceneNode, scene: SceneHooks): void {
245
315
  }
246
316
  }
247
317
  if (node._cullBounds !== null) spatial.setCullBounds(node._node, node._cullBounds)
248
- if (node.kind === "mesh") scene._attach(node as Mesh)
318
+ if (node.kind === "mesh") {
319
+ spatial.setLayers(node._node, (node as Mesh).layers)
320
+ scene._attach(node as Mesh)
321
+ }
249
322
  else if (node.kind === "light") scene._attachLight(node as Light)
250
323
  else if (node.kind === "instance") scene._attachInstance(node as InstanceNode)
251
324
  for (let c of node.children) enterScene(c, scene)