@solidrt/3d 0.0.58 → 0.0.60

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 (70) hide show
  1. package/AGENTS.md +734 -264
  2. package/README.md +4 -2
  3. package/examples/README.md +50 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +24 -2
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +172 -39
  51. package/src/geometry.ts +1573 -239
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +363 -141
  54. package/src/index.ts +31 -21
  55. package/src/light.ts +7 -8
  56. package/src/lod.ts +148 -0
  57. package/src/material.ts +316 -202
  58. package/src/mesh.ts +490 -256
  59. package/src/mixer.ts +27 -14
  60. package/src/model-file.ts +54 -15
  61. package/src/model.ts +86 -21
  62. package/src/motion.ts +17 -0
  63. package/src/node.ts +421 -22
  64. package/src/orbit.ts +378 -116
  65. package/src/profile.ts +4 -25
  66. package/src/resolve.ts +181 -0
  67. package/src/scene-shadows.ts +94 -93
  68. package/src/scene.ts +605 -185
  69. package/src/sweep.ts +27 -8
  70. package/tools/model.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -18,7 +18,7 @@
18
18
  "AGENTS.md"
19
19
  ],
20
20
  "peerDependencies": {
21
- "@solidjs/signals": "2.0.0-rc.6",
22
- "@solidrt/core": "0.0.58"
21
+ "@solidjs/signals": "2.0.0-rc.8",
22
+ "@solidrt/core": "0.0.60"
23
23
  }
24
24
  }
package/src/clip.ts CHANGED
@@ -6,18 +6,31 @@ import { quatNormalize, quatSlerp } from "./math.ts"
6
6
  import type { Quat } from "./math.ts"
7
7
  import type { ModelChannel } from "./gltf.ts"
8
8
 
9
+ /**
10
+ * Floats per key value of a channel: 3 for position and scale, 4 for a
11
+ * rotation, and for a weights channel one per morph target - read off
12
+ * the data, since the key count and the cubic triple fix it.
13
+ */
14
+ export function channelElements(channel: ModelChannel): number {
15
+ if (channel.path === "rotation") return 4
16
+ if (channel.path !== "weights") return 3
17
+ let perKey = channel.interpolation === "cubic" ? 3 : 1
18
+ return channel.times.length === 0 ? 0 : channel.values.length / (channel.times.length * perKey)
19
+ }
20
+
9
21
  /**
10
22
  * Sample one channel at `time` (seconds, clamped to the key range) into
11
- * `out` - 3 floats for position/scale, 4 for rotation. Pure array math:
12
- * step holds the earlier key, linear lerps (rotation slerps the shortest
13
- * path), cubic evaluates the glTF CUBICSPLINE Hermite (rotation
14
- * renormalized, per spec).
23
+ * `out` - 3 floats for position/scale, 4 for rotation, one per target
24
+ * for weights (`channelElements`). Pure array math: step holds the
25
+ * earlier key, linear lerps (rotation slerps the shortest path), cubic
26
+ * evaluates the glTF CUBICSPLINE Hermite (rotation renormalized, per
27
+ * spec).
15
28
  */
16
29
  export function sampleChannel(channel: ModelChannel, time: number, out: number[]): void {
17
30
  let times = channel.times
18
31
  let values = channel.values
19
32
  let keys = times.length
20
- let elements = channel.path === "rotation" ? 4 : 3
33
+ let elements = channelElements(channel)
21
34
  let stride = channel.interpolation === "cubic" ? elements * 3 : elements
22
35
  // The value element offset within a key: cubic keys are [in, value, out].
23
36
  let mid = channel.interpolation === "cubic" ? elements : 0
package/src/color.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  // is sRGB-encoded, what a color picker shows, and the library decodes it to
5
5
  // linear light when it writes the uniform (Three's ColorManagement,
6
6
  // Godot's inspector colors, Unity's linear color space all do the same).
7
- // Shading then runs in linear light and the fragment's output stage
8
- // (OUTPUT in ./glsl) encodes the result back. Values written straight to
7
+ // Shading then runs in linear light into the scene's buffer, and the
8
+ // target's resolve pass (RESOLVE in ./glsl) encodes the result back. Values written straight to
9
9
  // a uniform (scene.setParams, setMeshParams) and vertex colors are linear,
10
10
  // as in every engine; encode or decode them here when they come from a
11
11
  // picker.
@@ -30,3 +30,12 @@ export function linearColor(color: readonly number[]): number[] {
30
30
  for (let i = 0; i < 3 && i < out.length; i++) out[i] = srgbToLinear(out[i]!)
31
31
  return out
32
32
  }
33
+
34
+ /** An [r, g, b] or [r, g, b, a] sRGB color as the premultiplied linear
35
+ * vec4 the shaders take (uColor, aColor, iColor): rgb decoded to linear
36
+ * light and multiplied by the alpha, which is 1 when absent. */
37
+ export function premultipliedColor(color: readonly number[]): number[] {
38
+ let a = color.length === 4 ? color[3]! : 1
39
+ let c = linearColor(color)
40
+ return [c[0]! * a, c[1]! * a, c[2]! * a, a]
41
+ }
@@ -5,8 +5,8 @@ import type { Scene as SceneHandle } from "../scene.ts"
5
5
  import type { CameraState, CameraUpdate } from "../camera.ts"
6
6
 
7
7
  /** The camera state a camera control drives: the scene, or a view (both
8
- * expose setCamera and camera()). */
9
- export type CameraTarget = { setCamera(update: CameraUpdate): void; camera(): CameraState }
8
+ * expose setCamera, camera() and size()). */
9
+ export type CameraTarget = { setCamera(update: CameraUpdate): void; camera(): CameraState; size(): { width: number; height: number } }
10
10
 
11
11
  export type SceneCtx = { scene: SceneHandle; parent: SceneNode; viewport: CameraTarget; pointer: PointerFeed | null }
12
12
  export let SceneContext = createContext<SceneCtx>()
@@ -33,3 +33,12 @@ export function provide(ctx: SceneCtx, parent: SceneNode, props: { children?: El
33
33
  if (!("children" in props)) return undefined
34
34
  return <SceneContext value={{ scene: ctx.scene, parent, viewport: ctx.viewport, pointer: ctx.pointer }}>{props.children}</SceneContext>
35
35
  }
36
+
37
+ /** What a `<Lod>` hands its children: a direct child with `lodSize`
38
+ * registers itself as a level. */
39
+ export type LodRegistry = {
40
+ group: SceneNode
41
+ register(node: SceneNode, size: number): void
42
+ unregister(node: SceneNode): void
43
+ }
44
+ export let LodContext = createContext<LodRegistry | undefined>(undefined)
@@ -3,7 +3,7 @@ import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
5
5
  import type { TransformProps } from "./node-props.ts"
6
- import { add, remove } from "../node.ts"
6
+ import { add, destroy } from "../node.ts"
7
7
  import { createDirectionalLight, setLight } from "../light.ts"
8
8
  import type { DirectionalLight as DirectionalLightNode, ShadowOptions } from "../light.ts"
9
9
  import type { Vec3 } from "../math.ts"
@@ -43,6 +43,6 @@ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
43
43
  ([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
44
44
  )
45
45
  untrack(() => props.ref)?.(light)
46
- onCleanup(() => remove(light))
46
+ onCleanup(() => destroy(light))
47
47
  return null
48
48
  }
@@ -15,8 +15,8 @@ export type FirstPersonCameraProps = FirstPersonCameraOptions & {
15
15
  input?: InputMap<any>
16
16
  /** Action names per axis when the map's differ (null skips an axis). */
17
17
  actions?: Partial<Record<keyof FirstPersonAxes, string | null>>
18
- /** The control's handle (pose()/set()/eye()/forward()/active(), the
19
- * verbs, `axes` - also the debug command shape). */
18
+ /** The control's handle (pose()/set()/glideTo()/eye()/forward()/
19
+ * active(), the verbs, `axes` - also the debug command shape). */
20
20
  ref?: (camera: FirstPersonCameraHandle) => void
21
21
  }
22
22
 
@@ -35,8 +35,8 @@ export type FirstPersonCameraProps = FirstPersonCameraOptions & {
35
35
  * `fly={flying()}` swaps walk and fly on the running control (pose
36
36
  * carries over, no remount), `moveSpeed`, `lookSpeed` and
37
37
  * `clampPosition` follow their props, and a pitch clamp change re-clamps
38
- * at once. A frame loop runs only while `active()` - a rate driving - so
39
- * a still scene stays demand-driven idle.
38
+ * at once. A frame loop runs only while `active()` - a rate driving, or
39
+ * a glide in flight - so a still scene stays demand-driven idle.
40
40
  */
41
41
  export let FirstPersonCamera: VoidComponent<FirstPersonCameraProps> = props => {
42
42
  let ctx = useContext(SceneContext)
@@ -2,17 +2,21 @@ import { onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { ParentComponent } from "@solidrt/core"
3
3
  import { SceneContext, provide } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
5
- import type { TransformProps, PointerEventProps } from "./node-props.ts"
6
- import { add, createGroup, remove } from "../node.ts"
5
+ import type { TransformProps, BubblingPointerEventProps } from "./node-props.ts"
6
+ import { add, createGroup, destroy } from "../node.ts"
7
7
  import type { SceneNode } from "../node.ts"
8
8
 
9
- /** A transform node: children inherit its position/rotation/scale. */
10
- export let Group: ParentComponent<TransformProps & PointerEventProps & { ref?: (node: SceneNode) => void }> = props => {
9
+ export type GroupProps = TransformProps & BubblingPointerEventProps & { ref?: (node: SceneNode) => void }
10
+
11
+ /** A transform node: children inherit its position/rotation/scale. Its
12
+ * pointer events are the ones bubbling up from a hit descendant; a group
13
+ * is never the struck node, so it takes no enter/leave. */
14
+ export let Group: ParentComponent<GroupProps> = props => {
11
15
  let ctx = useContext(SceneContext)
12
16
  let node = createGroup()
13
17
  add(ctx.parent, node)
14
18
  syncNode(node, props)
15
19
  untrack(() => props.ref)?.(node)
16
- onCleanup(() => remove(node))
20
+ onCleanup(() => destroy(node))
17
21
  return provide(ctx, node, props)
18
22
  }
@@ -1,7 +1,7 @@
1
1
  import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
- import { add, remove } from "../node.ts"
4
+ import { add, destroy } from "../node.ts"
5
5
  import { createHemisphereLight, setLight } from "../light.ts"
6
6
  import type { HemisphereLight as HemisphereLightNode } from "../light.ts"
7
7
  import type { Vec3 } from "../math.ts"
@@ -19,6 +19,6 @@ export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
19
19
  ([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
20
20
  )
21
21
  untrack(() => props.ref)?.(light)
22
- onCleanup(() => remove(light))
22
+ onCleanup(() => destroy(light))
23
23
  return null
24
24
  }
@@ -16,18 +16,21 @@
16
16
 
17
17
  export { useScene } from "./context.tsx"
18
18
  export type { CameraTarget } from "./context.tsx"
19
- export type { PointerEventProps, TransformProps } from "./node-props.ts"
19
+ export type { BubblingPointerEventProps, PointerEventProps, TransformProps } from "./node-props.ts"
20
20
  export { Scene } from "./scene.tsx"
21
21
  export type { ScenePointerProps, SceneProps } from "./scene.tsx"
22
22
  export { View3d } from "./view3d.tsx"
23
23
  export type { View3dProps } from "./view3d.tsx"
24
24
  export { Group } from "./group.tsx"
25
+ export type { GroupProps } from "./group.tsx"
25
26
  export { Mesh } from "./mesh.tsx"
26
27
  export type { MeshProps } from "./mesh.tsx"
27
28
  export { Sprite } from "./sprite.tsx"
28
29
  export type { SpriteProps } from "./sprite.tsx"
29
30
  export { Instance, InstancedMesh } from "./instanced-mesh.tsx"
30
31
  export type { InstancedMeshProps, InstanceProps } from "./instanced-mesh.tsx"
32
+ export { InstancedLod, Lod } from "./lod.tsx"
33
+ export type { InstancedLodProps, LodProps } from "./lod.tsx"
31
34
  export { RecordMesh } from "./record-mesh.tsx"
32
35
  export type { RecordMeshProps } from "./record-mesh.tsx"
33
36
  export { PerspectiveCamera } from "./perspective-camera.tsx"
@@ -5,8 +5,9 @@ import { syncNode } from "./node-props.ts"
5
5
  import type { TransformProps, PointerEventProps } from "./node-props.ts"
6
6
  import { syncMesh } from "./mesh.tsx"
7
7
  import type { PopulatedMeshProps } from "./mesh.tsx"
8
- import { add } from "../node.ts"
9
- import { addInstance, createInstancedMesh, disposeInstances, removeInstance, setCastShadow, setGeometry, setInstanceStyle } from "../mesh.ts"
8
+ import { add, destroy, setMorphWeights } from "../node.ts"
9
+ import type { MorphWeights } from "../node.ts"
10
+ import { addInstance, createInstancedMesh, disposeInstances, setCastShadow, setGeometry, setInstanceStyle } from "../mesh.ts"
10
11
  import type { InstancedMesh as InstancedMeshNode, InstanceNode } from "../mesh.ts"
11
12
  import type { Geometry } from "../geometry.ts"
12
13
  import type { Material } from "../material.ts"
@@ -15,7 +16,7 @@ export type InstancedMeshProps = PopulatedMeshProps & {
15
16
  geometry: Geometry
16
17
  /** An instanced material: a stock one with `instanced` (or
17
18
  * `instanceColors`, for a per-instance tint), or a class declaring
18
- * INSTANCE_MATRIX_ATTRIBUTES in slot 0 and any style layout in slot 1. */
19
+ * INSTANCE_MATRIX_ATTRIBUTES first and any style layout second. */
19
20
  material: Material
20
21
  /** Instance slots reserved up front (default 64; fixed at creation).
21
22
  * Past it the buffers double into replacements - amortized, but size it
@@ -35,7 +36,9 @@ export type InstancedMeshProps = PopulatedMeshProps & {
35
36
  ref?: (mesh: InstancedMeshNode) => void
36
37
  }
37
38
 
38
- let InstancedMeshContext = createContext<InstancedMeshNode | null>(null)
39
+ /** The enclosing population, what `<Instance>` adds itself to: an
40
+ * `<InstancedMesh>`, or an `<InstancedLod>`'s first level. */
41
+ export let InstancedMeshContext = createContext<InstancedMeshNode | null>(null)
39
42
 
40
43
  /**
41
44
  * One draw entry covering N instance NODES (createInstancedMesh as a
@@ -63,7 +66,10 @@ export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
63
66
  )
64
67
  syncMesh(mesh, props)
65
68
  untrack(() => props.ref)?.(mesh)
66
- onCleanup(() => disposeInstances(mesh))
69
+ onCleanup(() => {
70
+ destroy(mesh)
71
+ disposeInstances(mesh)
72
+ })
67
73
  return (
68
74
  <SceneContext value={{ scene: ctx.scene, parent: mesh, viewport: ctx.viewport, pointer: ctx.pointer }}>
69
75
  <InstancedMeshContext value={mesh}>{props.children}</InstancedMeshContext>
@@ -72,11 +78,17 @@ export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
72
78
  }
73
79
 
74
80
  export type InstanceProps = TransformProps & PointerEventProps & {
75
- /** The instance's style record (setInstanceStyle as a prop): the
76
- * floats of the material's slot-1 instance attributes - `[r, g, b, a]`
77
- * under a stock material's `instanceColors`. Reactive; absent, the
78
- * instance keeps the material's instanceStyle (white for a tint). */
81
+ /** The instance's style record (setInstanceStyle as a prop): one value
82
+ * per component of the material's second instance buffer, in order -
83
+ * `[r, g, b, a]` under a stock material's `instanceColors`. Reactive;
84
+ * absent, the instance keeps the material's instanceStyle (white for
85
+ * a tint). */
79
86
  style?: ArrayLike<number>
87
+ /** The instance's morph target weights (setMorphWeights as a prop),
88
+ * for a population over geometry with `morphs` under a `morph: true`
89
+ * material: by name (keys merge) or every weight in target order; a
90
+ * `weights` entry in `transition` animates each change. */
91
+ morphWeights?: MorphWeights
80
92
  ref?: (instance: InstanceNode) => void
81
93
  }
82
94
 
@@ -99,7 +111,13 @@ export let Instance: ParentComponent<InstanceProps> = props => {
99
111
  if (s !== undefined) setInstanceStyle(instance, s)
100
112
  },
101
113
  )
114
+ createEffect(
115
+ () => props.morphWeights,
116
+ w => {
117
+ if (w !== undefined) setMorphWeights(instance, w)
118
+ },
119
+ )
102
120
  untrack(() => props.ref)?.(instance)
103
- onCleanup(() => removeInstance(instance))
121
+ onCleanup(() => destroy(instance))
104
122
  return provide(ctx, instance, props)
105
123
  }
@@ -0,0 +1,102 @@
1
+ import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
+ import type { ParentComponent } from "@solidrt/core"
3
+ import { LodContext, SceneContext, provide } from "./context.tsx"
4
+ import type { LodRegistry } from "./context.tsx"
5
+ import { syncNode } from "./node-props.ts"
6
+ import type { TransformProps, BubblingPointerEventProps } from "./node-props.ts"
7
+ import { syncMesh } from "./mesh.tsx"
8
+ import type { PopulatedMeshProps } from "./mesh.tsx"
9
+ import { InstancedMeshContext } from "./instanced-mesh.tsx"
10
+ import { add, createGroup, destroy } from "../node.ts"
11
+ import type { SceneNode } from "../node.ts"
12
+ import { createInstancedLod, setLod } from "../lod.ts"
13
+ import type { InstancedLodLevel } from "../lod.ts"
14
+ import { disposeInstances, setCastShadow } from "../mesh.ts"
15
+ import type { InstancedMesh as InstancedMeshNode } from "../mesh.ts"
16
+
17
+ export type LodProps = TransformProps &
18
+ BubblingPointerEventProps & {
19
+ /** The cross-fade band fraction (createLod's `fade`); default 0. */
20
+ fade?: number
21
+ ref?: (node: SceneNode) => void
22
+ }
23
+
24
+ /**
25
+ * A LOD group (createLod as a component): its levels are the direct
26
+ * children carrying `lodSize` - the projected size below which each hands
27
+ * over - in any JSX order (levels sort by size, never by position, since
28
+ * attach order is not JSX order); a child without one is drawn always.
29
+ * Levels may come and go (`<Show>`): the group re-declares itself.
30
+ */
31
+ export let Lod: ParentComponent<LodProps> = props => {
32
+ let ctx = useContext(SceneContext)
33
+ let node = createGroup()
34
+ add(ctx.parent, node)
35
+ syncNode(node, props)
36
+ let sizes = new Map<SceneNode, number>()
37
+ let declare = () => {
38
+ let levels = [...sizes.entries()].sort((a, b) => b[1] - a[1]).map(([n, size]) => ({ node: n, size }))
39
+ setLod(node, levels, { fade: untrack(() => props.fade) })
40
+ }
41
+ let registry: LodRegistry = {
42
+ group: node,
43
+ register(child, size) {
44
+ if (child.parent !== node) throw new Error("lodSize goes on a direct child of <Lod>")
45
+ sizes.set(child, size)
46
+ declare()
47
+ },
48
+ unregister(child) {
49
+ if (sizes.delete(child)) declare()
50
+ },
51
+ }
52
+ createEffect(
53
+ () => props.fade,
54
+ () => declare(),
55
+ { defer: true },
56
+ )
57
+ untrack(() => props.ref)?.(node)
58
+ onCleanup(() => destroy(node))
59
+ return <LodContext value={registry}>{provide(ctx, node, props)}</LodContext>
60
+ }
61
+
62
+ export type InstancedLodProps = PopulatedMeshProps & {
63
+ /** The levels nearest first, each what the population draws at it and
64
+ * the per-instance projected size below which it hands over (see
65
+ * createInstancedLod); fixed at creation. */
66
+ levels: InstancedLodLevel[]
67
+ /** Instance slots reserved up front (default 64; fixed at creation). */
68
+ capacity?: number
69
+ /** LOCAL bounds covering the population, fixed at creation (see
70
+ * InstancedMesh). */
71
+ bounds?: ArrayLike<number>
72
+ label?: string
73
+ /** Every level draws into the scene's shadow map; default false. */
74
+ castShadow?: boolean
75
+ ref?: (mesh: InstancedMeshNode) => void
76
+ }
77
+
78
+ /**
79
+ * An instanced LOD (createInstancedLod as a component): `<Instance>`
80
+ * children populate it exactly as under `<InstancedMesh>`, and each
81
+ * instance draws the level its own projected size picks.
82
+ */
83
+ export let InstancedLod: ParentComponent<InstancedLodProps> = props => {
84
+ let ctx = useContext(SceneContext)
85
+ let mesh = untrack(() => createInstancedLod(props.levels, { capacity: props.capacity, bounds: props.bounds, label: props.label }))
86
+ add(ctx.parent, mesh)
87
+ createEffect(
88
+ () => props.castShadow,
89
+ c => setCastShadow(mesh, c === true),
90
+ )
91
+ syncMesh(mesh, props)
92
+ untrack(() => props.ref)?.(mesh)
93
+ onCleanup(() => {
94
+ destroy(mesh)
95
+ disposeInstances(mesh)
96
+ })
97
+ return (
98
+ <SceneContext value={{ scene: ctx.scene, parent: mesh, viewport: ctx.viewport, pointer: ctx.pointer }}>
99
+ <InstancedMeshContext value={mesh}>{props.children}</InstancedMeshContext>
100
+ </SceneContext>
101
+ )
102
+ }
@@ -3,7 +3,8 @@ import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
5
5
  import type { TransformProps, PointerEventProps } from "./node-props.ts"
6
- import { add, remove } from "../node.ts"
6
+ import { add, destroy, setMorphWeights } from "../node.ts"
7
+ import type { MorphWeights } from "../node.ts"
7
8
  import { createMesh, setCastShadow, setCulling, setGeometry, setLayers, setMaterial, setMeshParams, setRenderOrder } from "../mesh.ts"
8
9
  import type { Mesh as MeshNode } from "../mesh.ts"
9
10
  import type { ShaderParams } from "@solidrt/core/gpu"
@@ -32,6 +33,13 @@ export type MeshProps = TransformProps & PointerEventProps & {
32
33
  frustumCulled?: boolean
33
34
  /** World units the culled box grows by (setCulling as a prop; default 0). */
34
35
  cullMargin?: number
36
+ /** Morph target weights (setMorphWeights as a prop) for geometry that
37
+ * carries `morphs` under a `morph: true` material: by name (keys merge,
38
+ * a key that disappears keeps its weight) or every weight in target
39
+ * order. A `weights` entry in `transition` animates each change. For
40
+ * weights changing every frame prefer `ref` + setMorphWeights from
41
+ * onFrame, the same split as setTransform. */
42
+ morphWeights?: MorphWeights
35
43
  ref?: (mesh: MeshNode) => void
36
44
  }
37
45
 
@@ -40,10 +48,13 @@ export type MeshProps = TransformProps & PointerEventProps & {
40
48
  // take them); the
41
49
  // ref and the cleanup are each component's own (a populated mesh frees
42
50
  // its buffers, a plain one is removed).
43
- export function syncMesh(mesh: MeshNode, props: Omit<MeshProps, "geometry" | "castShadow" | "ref">): void {
51
+ export function syncMesh(mesh: MeshNode, props: Omit<MeshProps, "geometry" | "material" | "castShadow" | "ref"> & { material?: Material }): void {
52
+ // Absent on an <InstancedLod>, whose levels each carry their own.
44
53
  createEffect(
45
54
  () => props.material,
46
- m => setMaterial(mesh, m),
55
+ m => {
56
+ if (m !== undefined) setMaterial(mesh, m)
57
+ },
47
58
  { defer: true },
48
59
  )
49
60
  createEffect(
@@ -81,12 +92,19 @@ export let Mesh: VoidComponent<MeshProps> = props => {
81
92
  () => props.castShadow,
82
93
  c => setCastShadow(mesh, c === true),
83
94
  )
95
+ createEffect(
96
+ () => props.morphWeights,
97
+ w => {
98
+ if (w !== undefined) setMorphWeights(mesh, w)
99
+ },
100
+ )
84
101
  syncMesh(mesh, props)
85
102
  untrack(() => props.ref)?.(mesh)
86
- onCleanup(() => remove(mesh))
103
+ onCleanup(() => destroy(mesh))
87
104
  return null
88
105
  }
89
106
 
90
107
  // The props both populated meshes share with Mesh: everything but the
91
- // geometry/material pair (documented per component) and the ref.
92
- export type PopulatedMeshProps = Omit<MeshProps, "geometry" | "material" | "ref">
108
+ // geometry/material pair (documented per component), the ref and the
109
+ // morph weights (a populated mesh does not morph yet).
110
+ export type PopulatedMeshProps = Omit<MeshProps, "geometry" | "material" | "ref" | "morphWeights">
@@ -1,4 +1,5 @@
1
- import { createEffect } from "@solidrt/core"
1
+ import { createEffect, onCleanup, useContext } from "@solidrt/core"
2
+ import { LodContext } from "./context.tsx"
2
3
  import { setTransform, setTransition, setVisible } from "../node.ts"
3
4
  import type { NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneNode, TransitionEndEvent } from "../node.ts"
4
5
  import type { NodeTransition } from "flux:spatial"
@@ -18,6 +19,10 @@ export type TransformProps = {
18
19
  transition?: NodeTransition | string | null
19
20
  /** A declared transition settled on one component. */
20
21
  onTransitionEnd?: (event: TransitionEndEvent) => void
22
+ /** Under a `<Lod>`: this node is a level of it, drawn while the group's
23
+ * projected size is at or above this and below the next larger level's
24
+ * (see createLod). Only on a direct child of `<Lod>`; throws elsewhere. */
25
+ lodSize?: number
21
26
  }
22
27
 
23
28
  /**
@@ -31,13 +36,20 @@ export type TransformProps = {
31
36
  * the built-in <Scene> leaf does (opt out with events={false}); an `output`
32
37
  * leaf spreads them itself.
33
38
  */
34
- export type PointerEventProps = {
39
+ export type PointerEventProps = BubblingPointerEventProps & {
40
+ /** Hover: enter/leave pair on the struck node alone, so only a hit
41
+ * target - a mesh or an instance - takes them; `<Group>` and `<Lod>`
42
+ * bubble and omit the pair. */
43
+ onPointerEnter?: (event: NodePointerEvent) => void
44
+ onPointerLeave?: (event: NodePointerEvent) => void
45
+ }
46
+
47
+ /** The pointer props every node takes: the events that bubble from the
48
+ * hit up through the Groups above it, without the hover pair. */
49
+ export type BubblingPointerEventProps = {
35
50
  onPointerDown?: (event: NodePointerEvent) => void
36
51
  onPointerMove?: (event: NodePointerEvent) => void
37
52
  onPointerUp?: (event: NodePointerEvent) => void
38
- /** Meshes and instances only: a Group never receives enter/leave. */
39
- onPointerEnter?: (event: NodePointerEvent) => void
40
- onPointerLeave?: (event: NodePointerEvent) => void
41
53
  /** The wheel over the node: `deltaX`/`deltaY`. */
42
54
  onWheel?: (event: NodeWheelEvent) => void
43
55
  /** A press released on the node within the slop, alone for its whole
@@ -87,4 +99,22 @@ export function syncNode(node: SceneNode, props: TransformProps & PointerEventPr
87
99
  () => props.transition,
88
100
  transition => setTransition(node, transition ?? null),
89
101
  )
102
+ // A level of the enclosing <Lod>, keyed by size; built only when the
103
+ // prop is present (the compiler emits the getter only then), so a plain
104
+ // node pays nothing.
105
+ if ("lodSize" in props) {
106
+ let lod = useContext(LodContext)
107
+ createEffect(
108
+ () => props.lodSize,
109
+ size => {
110
+ if (size === undefined) {
111
+ lod?.unregister(node)
112
+ return
113
+ }
114
+ if (lod === undefined) throw new Error("lodSize needs a <Lod> parent")
115
+ lod.register(node, size)
116
+ },
117
+ )
118
+ onCleanup(() => lod?.unregister(node))
119
+ }
90
120
  }
@@ -15,8 +15,9 @@ export type OrbitCameraProps = OrbitCameraOptions & {
15
15
  input?: InputMap<any>
16
16
  /** Action names per axis when the map's differ (null skips an axis). */
17
17
  actions?: Partial<Record<keyof OrbitAxes, string | null>>
18
- /** The control's handle (pose()/set()/eye()/orbiting()/active(), the
19
- * verbs, `axes` - also the debug command shape). */
18
+ /** The control's handle (pose()/set()/glideTo()/fit()/eye()/
19
+ * orbiting()/active(), the verbs, `axes` - also the debug command
20
+ * shape). */
20
21
  ref?: (orbit: OrbitCameraHandle) => void
21
22
  }
22
23
 
@@ -29,9 +30,10 @@ export type OrbitCameraProps = OrbitCameraOptions & {
29
30
  * (target, azimuth, elevation, distance) are initial values - change the
30
31
  * pose at runtime through `ref`'s set() or the verbs; every other prop is
31
32
  * live, forwarded to the control as a getter and read where it applies,
32
- * so a clamp, a rate or an anchor callback follows its prop, and a clamp
33
- * change re-clamps the pose at once. The frame loop runs only while
34
- * `active()` (auto-orbit on, or an axis rate driving); a camera moved
33
+ * so a clamp, a rate, `damping` or an anchor callback follows its prop,
34
+ * and a clamp change (`clampPose` included) re-clamps the pose at once.
35
+ * The frame loop runs only while `active()` (auto-orbit on, a glide or
36
+ * damped wheel notch in flight, or an axis rate driving); a camera moved
35
37
  * by drags alone leaves the app demand-driven idle.
36
38
  */
37
39
  export let OrbitCamera: VoidComponent<OrbitCameraProps> = props => {
@@ -59,14 +61,15 @@ export let OrbitCamera: VoidComponent<OrbitCameraProps> = props => {
59
61
  },
60
62
  )
61
63
  // A clamp prop change re-clamps the pose at once (set({}) applies the
62
- // clamps and pushes) - the update() call a Three OrbitControls app
63
- // makes after setting minDistance, done by the prop. Deferred: the
64
- // creation already clamped and pushed the initial pose. Untracked: the
65
- // control reads the clamps through the getters while it applies them,
66
- // and the apply wants the values of that moment (the compute above is
67
- // what tracks them).
64
+ // clamps and pushes, and writes no pose field, so a glide in flight
65
+ // keeps running) - the update() call a Three OrbitControls app makes
66
+ // after setting minDistance, done by the prop. Deferred: the creation
67
+ // already clamped and pushed the initial pose. Untracked: the control
68
+ // reads the clamps through the getters while it applies them, and the
69
+ // apply wants the values of that moment (the compute above is what
70
+ // tracks them).
68
71
  createEffect(
69
- () => [options.minDistance, options.maxDistance, options.minElevation, options.maxElevation],
72
+ () => [options.minDistance, options.maxDistance, options.minElevation, options.maxElevation, options.clampPose],
70
73
  () => untrack(() => orbit.set({})),
71
74
  { defer: true },
72
75
  )
@@ -3,7 +3,7 @@ import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
5
5
  import type { TransformProps } from "./node-props.ts"
6
- import { add, remove } from "../node.ts"
6
+ import { add, destroy } from "../node.ts"
7
7
  import { createPointLight, setLight } from "../light.ts"
8
8
  import type { PointLight as PointLightNode, SpotShadowOptions } from "../light.ts"
9
9
  import type { Vec3 } from "../math.ts"
@@ -47,6 +47,6 @@ export let PointLight: VoidComponent<PointLightProps> = props => {
47
47
  ([color, intensity, distance, decay, castShadow, shadow]) => setLight(light, { color, intensity, distance, decay, castShadow, shadow }),
48
48
  )
49
49
  untrack(() => props.ref)?.(light)
50
- onCleanup(() => remove(light))
50
+ onCleanup(() => destroy(light))
51
51
  return null
52
52
  }
@@ -3,7 +3,7 @@ import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncMesh } from "./mesh.tsx"
5
5
  import type { PopulatedMeshProps } from "./mesh.tsx"
6
- import { add } from "../node.ts"
6
+ import { add, destroy } from "../node.ts"
7
7
  import { createRecordMesh, disposeInstances, setCastShadow, setGeometry, setRecordCount, setRecords } from "../mesh.ts"
8
8
  import type { RecordMesh as RecordMeshNode } from "../mesh.ts"
9
9
  import type { Geometry } from "../geometry.ts"
@@ -11,12 +11,14 @@ import type { Material } from "../material.ts"
11
11
 
12
12
  export type RecordMeshProps = PopulatedMeshProps & {
13
13
  geometry: Geometry
14
- /** Must declare instanceAttributes (shaderMaterialClass), slot 0 only. */
14
+ /** Must declare instanceBuffers (shaderMaterialClass); the first is
15
+ * the records' layout. */
15
16
  material: Material
16
- /** Interleaved per-instance records (stride = the material's instance
17
- * attributes summed). Reactive; a later array larger than the buffer
18
- * grows it (capacity doubles into a replacement buffer). */
19
- records: Float32Array
17
+ /** The records, laid out in the material's first instance layout (a
18
+ * Float32Array over an all-float layout, bytes otherwise; see
19
+ * setRecords). Reactive; a later value larger than the buffer grows it
20
+ * (capacity doubles into a replacement buffer). */
21
+ records: ArrayBufferView
20
22
  /** How many records draw; default all of the latest `records`. */
21
23
  count?: number
22
24
  /** LOCAL bounds covering every instance ([minX..maxZ]), fixed at
@@ -63,6 +65,9 @@ export let RecordMesh: VoidComponent<RecordMeshProps> = props => {
63
65
  )
64
66
  syncMesh(mesh, props)
65
67
  untrack(() => props.ref)?.(mesh)
66
- onCleanup(() => disposeInstances(mesh))
68
+ onCleanup(() => {
69
+ destroy(mesh)
70
+ disposeInstances(mesh)
71
+ })
67
72
  return null
68
73
  }