@solidrt/3d 0.0.47 → 0.0.48

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 CHANGED
@@ -11,7 +11,8 @@ blendMode and pointer events like any element. Design rationale:
11
11
 
12
12
  - Two layers. The imperative core is Solid-free: `createScene`,
13
13
  `createMesh(geometry, material)`, `add`/`remove`, `setTransform`,
14
- `setVisible` - plain objects with dirty flags, batched to a microtask,
14
+ `lookAt`, `getRotation`, `setVisible` - plain objects with dirty flags, batched to a
15
+ microtask,
15
16
  one `setDrawParams` (uModel, plus uNormal for materials declaring it)
16
17
  per changed mesh and ONE `setTargetParams` (the shared uViewProj +
17
18
  uCamPos) per camera change, however many meshes. The component
@@ -49,8 +50,8 @@ blendMode and pointer events like any element. Design rationale:
49
50
  | Component | Props |
50
51
  | --- | --- |
51
52
  | `Scene` | `width`, `height` (target pixels), `clearColor?`, `label?`, `ref?(scene)`, `output?(texture)` |
52
- | `Group` | `position?`, `rotation?` (Euler radians, x-y-z order), `scale?` (number = uniform), `visible?`, `ref?(node)` |
53
- | `Mesh` | `geometry`, `material`, transforms as Group, `ref?(mesh)` |
53
+ | `Group` | `position?`, `rotation?` (Euler radians, XYZ order), `quaternion?` (either, not both), `scale?` (number = uniform), `visible?`, `ref?(node)` |
54
+ | `Mesh` | `geometry`, `material`, transforms as Group, `params?` (per-mesh uniforms, merge semantics - no unset), `ref?(mesh)` |
54
55
  | `PerspectiveCamera` | `fov?` (vertical DEGREES, default 60), `near?`, `far?`, `position?`, `lookAt?`, `up?` |
55
56
 
56
57
  Output composition: without `output`, `Scene` emits a minimal
@@ -129,10 +130,21 @@ Winding is normalized, so either authoring direction works.
129
130
  with a quarter-round bevel at both rims; `lathe(profile, segs?, angle?,
130
131
  start?)` revolves a CLOSED (x = radius, y = height) profile about the y
131
132
  axis - watertight by construction, flat caps on partial sweeps;
132
- `shape(profile)` fills one flat (facing +z, like circle);
133
- `triangulate(points)` is the ear-clipping core (fan fallback, never drops
134
- a cap), exported for custom flat work. These pick uint16/uint32 indices
135
- by vertex count automatically.
133
+ `sweep(profile, path)` runs the profile along an open 3D polyline with
134
+ MITRED joints (each cross-section sits on its bend's bisector plane, so
135
+ bends never gape or overlap) and flat caps at both ends. The path
136
+ mirrors the profile convention: bare `[x, y, z]` points crease (a strap
137
+ folding over an edge), `{ p, smooth }` points shade continuous (tag a
138
+ sampled curve's points); the profile's y starts as close to world up as
139
+ the first segment allows, then parallel-transports without spinning.
140
+ Closed loops are NOT supported yet - overlap the ends by a segment to
141
+ fake one. `tube(path, radius?, radialSegs?)` is the round-profile
142
+ shorthand (wire, rope, pipe), and `pathFrames(path)` exports the
143
+ per-segment frames (tangents, cross-section axes, arc lengths) for
144
+ custom work along a path. `shape(profile)` fills one flat (facing +z,
145
+ like circle); `triangulate(points)` is the ear-clipping core (fan
146
+ fallback, never drops a cap), exported for custom flat work. These pick
147
+ uint16/uint32 indices by vertex count automatically.
136
148
 
137
149
  Materials:
138
150
 
@@ -158,7 +170,10 @@ Materials:
158
170
  pipeline preamble. App-driven uniforms beyond the standard set: seed
159
171
  via `params`, then write per mesh with
160
172
  `setMeshParams(mesh, { name: value })` (validated names; values persist
161
- across entry rebuilds; frame-rate-safe like setTransform).
173
+ across entry rebuilds; frame-rate-safe like setTransform) or declaratively
174
+ with the `Mesh` `params` prop (same merge semantics - a key that
175
+ disappears from the object keeps its old value; for per-frame values
176
+ prefer `ref` + setMeshParams from onFrame, the setTransform split).
162
177
 
163
178
  Lighting GLSL (`@solidrt/3d/glsl`): exported string constants composed
164
179
  into shaderMaterial sources with plain template literals - `LIT_VERTEX`
@@ -186,10 +201,77 @@ system.
186
201
  - Alpha does not blend in v1: pipelines are opaque (`blend: "none"`), a
187
202
  translucent color overwrites. Transparency waits on blend factors +
188
203
  sorting (research note, staging step 4).
189
- - Rotation is Euler radians applied x, then y, then z. No quaternions in
190
- v1.
191
- - Transforms have ONE write path: `setTransform`/`setVisible` (or the
192
- props that call them). Mutating `node.position` directly does not sync.
204
+ - Rotation is stored as a QUATERNION (`node.quaternion`, `[x, y, z, w]`,
205
+ always unit). There is exactly one rotation field: no `node.rotation`
206
+ shadowing it, because a second field is a second thing to go stale (an
207
+ aimed node whose Euler triple still reads as the old pose is the bug
208
+ this model deletes). Euler triples are a boundary format only -
209
+ `setTransform({ rotation })` and the `rotation` prop convert in,
210
+ `getRotation(node, out?)` converts out.
211
+ - Euler triples are XYZ order (x applied first: `R = Rx * Ry * Rz`),
212
+ Three's `Euler` default, so a triple copied from a Three scene means the
213
+ same thing here. This CHANGED 2026-08-11: the old `compose()` built
214
+ `Rz * Ry * Rx` (Three's `'ZYX'`) while its comment claimed XYZ. Every
215
+ rotation triple then in the repo, examples, demos and projects was
216
+ single-axis, which is order-independent, so the fix moved no pixels -
217
+ verified, not assumed. There is ONE order and no order argument: a
218
+ per-call order is how one triple ends up meaning two things.
219
+ - `getRotation` cannot recover the triple that was written, only a triple
220
+ meaning the same rotation (and at the poles it pins z to 0 and folds the
221
+ roll into x). It is for reading and debugging; anything composing or
222
+ interpolating rotations works with the quaternion.
223
+ - `eulerFromQuat` extracts y with `atan2(m02, cos(y))`, NOT Three's
224
+ `asin(m02)`: asin's derivative blows up at the poles, turning 1e-16 of
225
+ matrix error into 1e-8 of angle. Same reason its pole branch starts at
226
+ `cos(y) < 1e-7` rather than Three's `|m02| > 0.9999999` (which is
227
+ `cos(y) ~ 4.5e-4` - three orders early, and inside that band Three
228
+ silently discards real roll). Do not "restore parity" here.
229
+ - Aim with `lookAt(node, target, up?)`, never by extracting angles by
230
+ hand. Three's `Object3D.lookAt` semantics deliberately: `target` and
231
+ `up` are WORLD space (ancestor transforms are undone, and the ancestor
232
+ chain is refreshed on the spot rather than waiting for the sync), and
233
+ local +z ends up pointing at the target. To aim along a DIRECTION, add
234
+ it to `worldPosition(node)` - the same conversion Three asks for.
235
+ +z is the library's own sweep axis, so `extrude`/`sweep`/`tube` output
236
+ needs no correction. For a y-axis solid (`cylinder`, `cone`) use
237
+ `quatFromTo(q, [0, 1, 0], dir)` instead of correcting lookAt's +z.
238
+ Divergences from Three, both deliberate: `up` is an argument, NOT a
239
+ per-node field (Three's `object.up` is hidden state that costs a vector
240
+ on every node), and degenerate frames pick a stable perpendicular
241
+ instead of Three's epsilon nudge of the eye.
242
+ There is no `setTransform(node, { matrix })`, and lookAt is a MUTATOR,
243
+ not a rotation-returning function.
244
+ - `quatFromTo` is Three's `setFromUnitVectors`, renamed after Unity's
245
+ `FromToRotation` / glam's `from_rotation_arc`: the Three name states a
246
+ precondition instead of the operation, and ours has no such
247
+ precondition (it normalizes). Check Unity/glam/Godot too before copying
248
+ a Three name that reads as an artifact of its class layout.
249
+ - The composition set: `quatFromAxisAngle` (radians; normalizes the axis -
250
+ Three/Unity/glam all require a unit axis and silently corrupt
251
+ otherwise), `quatMultiply` (same order contract as the mat4 `multiply`:
252
+ `a * b`, b applies first; does NOT renormalize - the unit product only
253
+ drifts under long accumulation, and setTransform renormalizes on
254
+ write), `quatSlerp` (shortest path across the double cover, constant
255
+ angular velocity, unit output; the damped follow is
256
+ `quatSlerp(q, q, target, 1 - Math.exp(-k * dt))`). All aim/verb usage
257
+ live in `examples/aim.tsx`.
258
+ - `setTransform` NORMALIZES an incoming quaternion, and passing `rotation`
259
+ and `quaternion` in one call throws. A non-unit quaternion scales
260
+ geometry by `|q|^2` through `compose()` - Three leaves that trap open
261
+ and documents it; we close it at the one write path instead of paying
262
+ for a check in every compose.
263
+ - `lookAt` is exact for rotation and uniform scale up the chain. A
264
+ non-uniformly scaled ancestor shears the frame, so the aim is
265
+ approximate - Three has the identical limitation (both read the parent's
266
+ upper 3x3 as if it were a rotation), and the fix is not to special-case
267
+ it here but to not shear parents of things you aim.
268
+ - The package root's `lookAt` is the scene verb; `@solidrt/3d/math` keeps
269
+ its own `lookAt` (the camera view matrix) on the SUBPATH ONLY, the same
270
+ collision rule the Vec3 helpers follow - and the same Object3D/Matrix4
271
+ split Three makes under one name. Do not re-export math's from the root.
272
+ - Transforms have ONE write path: `setTransform`/`lookAt`/`setVisible` (or
273
+ the props that call them). Mutating `node.position` directly does not
274
+ sync. Components have no `lookAt` prop - aim through a `ref`.
193
275
  - A camera change is ONE `setTargetParams` write (uViewProj + uCamPos are
194
276
  target state), independent of mesh count - never reintroduce per-mesh
195
277
  camera writes (uEye-style per-mesh params are exactly the O(scene) cost
package/README.md CHANGED
@@ -37,10 +37,24 @@ props, chain a post-effect shader target, or return null and composite
37
37
 
38
38
  There is also an imperative layer underneath (`createScene`, `createMesh`,
39
39
  `setTransform`, ...) usable without components, plus a small math module
40
- (`@solidrt/3d/math`: column-major mat4, perspective, lookAt). For HUD
40
+ (`@solidrt/3d/math`: column-major mat4, perspective, lookAt). To aim a
41
+ node, `lookAt(node, target, up?)` points its local +z at a world point,
42
+ Three's `Object3D.lookAt`; `worldPosition(node)` is the companion for
43
+ aiming along a direction, and `quatFromTo` aims any other axis. For HUD
41
44
  overlays, `scene.project(point)` maps a world point to scene pixels.
45
+
46
+ Rotation is stored as a quaternion (`quaternion` prop, `node.quaternion`),
47
+ so aiming and interpolation are gimbal-free and there is no second
48
+ rotation field to fall out of step. Euler triples stay the easy way to
49
+ author one - the `rotation` prop takes radians in XYZ order, matching
50
+ Three's `Euler` default, and `getRotation(node)` reads one back. The
51
+ verbs: `quatFromAxisAngle`, `quatMultiply`, and `quatSlerp` (smooth
52
+ tracking, damped follows) round out `quatFromTo`; `examples/aim.tsx`
53
+ shows each aiming style live.
42
54
  Custom materials get a standard uniform set - per-mesh `uModel`/`uNormal`,
43
- shared `uViewProj`/`uCamPos`, each written once per change - and
55
+ shared `uViewProj`/`uCamPos`, each written once per change - plus your own
56
+ uniforms per mesh, declaratively via the `params` prop on `<Mesh>` or
57
+ imperatively via `setMeshParams` - and
44
58
  `@solidrt/3d/glsl` exports the lighting pieces (hemisphere, lambert,
45
59
  blinn, fresnel, a standard vertex stage) to compose your own lit looks
46
60
  from plain template literals.
@@ -48,8 +62,9 @@ from plain template literals.
48
62
  v1 scope: unlit color/textured materials plus `shaderMaterial` (your own
49
63
  GLSL as a first-class material), geometry generators (box, plane, circle,
50
64
  ring, sphere, cylinder, cone, torus, torus knot), a profile kit for custom
51
- solids (`extrude` with bevels, `lathe`, flat `shape`, with `fillet`/
52
- `roundRect`/`triangulate` helpers), a per-vertex data channel
65
+ solids (`extrude` with bevels, `lathe`, polyline `sweep`/`tube` with
66
+ mitred joints, flat `shape`, with `fillet`/`roundRect`/`triangulate`
67
+ helpers), a per-vertex data channel
53
68
  (`withColors` adds an `aColor` vec4 - tint, baked AO, any four scalars -
54
69
  to any geometry, for materials that read it), one perspective camera
55
70
  with an orbit control (`createOrbitCamera`: drag, pinch/wheel zoom, auto-orbit).
@@ -7,3 +7,11 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
7
7
  texture leaf, `<PerspectiveCamera>`, a ground plane, a spinning
8
8
  `<Group>` of unlit meshes with real depth-buffer occlusion, geometry
9
9
  and pipeline sharing, and the one-signal onFrame drive.
10
+ - `sweep-paths.tsx` - swept solids along polylines: a flat strap folding
11
+ over a crate (bare path points crease on the mitred bends) and a coiled
12
+ tube (smooth-tagged helix, one continuous mesh), lit via the exported
13
+ GLSL so the creased-vs-smooth normals actually show.
14
+ - `aim.tsx` - the rotation verbs, one pointer each tracking an orbiting
15
+ target: `lookAt` for a +z solid, `quatFromTo` for aiming a y-axis cone,
16
+ and a `quatSlerp` damped follow that visibly lags; all driven from
17
+ onFrame through refs, no per-frame signals.
@@ -0,0 +1,119 @@
1
+ // Aiming and rotation: a target orbits, and three fixed pointers track it,
2
+ // each through a different rotation verb.
3
+ // - lookAt(node, target) the z-axis rod: one call, world semantics
4
+ // - quatFromTo(y, direction) the cone: aiming an axis other than +z
5
+ // - quatSlerp damped follow the lazy cone: visibly lags, then catches up
6
+ // All three drive their nodes from onFrame through refs - the frame-rate
7
+ // escape hatch - so no per-frame signals exist; the one signal-free scene
8
+ // re-renders because setTransform marks the moved nodes dirty.
9
+ import { onFrame, pct, render } from "@solidrt/core"
10
+ import {
11
+ cone,
12
+ lookAt,
13
+ Mesh,
14
+ PerspectiveCamera,
15
+ plane,
16
+ quat,
17
+ quatFromTo,
18
+ quatSlerp,
19
+ Scene,
20
+ setTransform,
21
+ sphere,
22
+ tube,
23
+ unlit,
24
+ worldPosition,
25
+ } from "@solidrt/3d"
26
+ import type { MeshNode, Vec3 } from "@solidrt/3d"
27
+
28
+ const SIZE = 720
29
+ const Y_AXIS: Vec3 = [0, 1, 0]
30
+
31
+ function App() {
32
+ let target!: MeshNode
33
+ let rod!: MeshNode
34
+ let cannon!: MeshNode
35
+ let lazy!: MeshNode
36
+
37
+ // Allocated once; every per-frame write reuses them.
38
+ let targetPos: Vec3 = [0, 0, 0]
39
+ let dir: Vec3 = [0, 0, 0]
40
+ let aimQ = quat()
41
+
42
+ // A world-space direction from a node to the target: the documented
43
+ // recipe, worldPosition + subtract (exact here - the pointers have no
44
+ // transformed ancestors - and correct even if they get some).
45
+ let aimFrom = (node: MeshNode) => {
46
+ let p = worldPosition(node, dir)
47
+ dir[0] = targetPos[0] - p[0]
48
+ dir[1] = targetPos[1] - p[1]
49
+ dir[2] = targetPos[2] - p[2]
50
+ return dir
51
+ }
52
+
53
+ onFrame(tick => {
54
+ let t = tick / 1500
55
+ targetPos[0] = Math.cos(t) * 1.9
56
+ targetPos[1] = 1.1 + Math.sin(t * 0.7) * 0.7
57
+ targetPos[2] = Math.sin(t) * 1.9
58
+ setTransform(target, { position: targetPos })
59
+
60
+ // The rod is a +z solid (tube paths run along z): lookAt is the whole
61
+ // aiming story, a world-space point in, done.
62
+ lookAt(rod, targetPos)
63
+
64
+ // The cone points along +y, not +z, so lookAt would aim its side.
65
+ // quatFromTo rotates the axis you name onto the direction you want.
66
+ quatFromTo(aimQ, Y_AXIS, aimFrom(cannon))
67
+ setTransform(cannon, { quaternion: aimQ })
68
+
69
+ // Damped follow: slerp the CURRENT rotation a fixed fraction of the
70
+ // way toward the aimed one each frame. The 0.04 makes the lag obvious;
71
+ // a real app uses 1 - Math.exp(-k * dt) to stay frame-rate independent.
72
+ quatFromTo(aimQ, Y_AXIS, aimFrom(lazy))
73
+ quatSlerp(aimQ, lazy.quaternion, aimQ, 0.04)
74
+ setTransform(lazy, { quaternion: aimQ })
75
+ })
76
+
77
+ let ball = sphere(0.22)
78
+ let pointer = cone(0.3, 0.9)
79
+
80
+ return (
81
+ <window>
82
+ <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
83
+ <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="aim">
84
+ <PerspectiveCamera fov={55} position={[0, 2.6, 4.6]} lookAt={[0, 0.7, 0]} />
85
+ <Mesh
86
+ geometry={plane(7, 7, "floor")}
87
+ material={unlit({ color: [0.16, 0.17, 0.22] })}
88
+ rotation={[-Math.PI / 2, 0, 0]}
89
+ />
90
+ <Mesh
91
+ geometry={ball}
92
+ material={unlit({ color: [0.95, 0.85, 0.4] })}
93
+ ref={n => (target = n)}
94
+ />
95
+ <Mesh
96
+ geometry={tube([[0, 0, 0], [0, 0, 1.1]], 0.09, 10, "rod")}
97
+ material={unlit({ color: [0.85, 0.3, 0.3] })}
98
+ position={[-1.4, 0.5, 0]}
99
+ ref={n => (rod = n)}
100
+ />
101
+ <Mesh
102
+ geometry={pointer}
103
+ material={unlit({ color: [0.35, 0.65, 0.9] })}
104
+ position={[1.4, 0.5, 0]}
105
+ ref={n => (cannon = n)}
106
+ />
107
+ <Mesh
108
+ geometry={pointer}
109
+ material={unlit({ color: [0.45, 0.8, 0.5] })}
110
+ position={[0, 0.5, -1.4]}
111
+ ref={n => (lazy = n)}
112
+ />
113
+ </Scene>
114
+ </view>
115
+ </window>
116
+ )
117
+ }
118
+
119
+ render(() => <App />)
@@ -0,0 +1,79 @@
1
+ // Swept solids along 3D polylines. sweep() runs a 2D profile along a
2
+ // path with mitred joints: bare path points crease - the strap folds
3
+ // over the crate's edges like real webbing - while smooth-tagged points
4
+ // share averaged normals, so the helix sweeps into ONE continuous coil
5
+ // (per-segment boxes with unmitred gaps are exactly what this replaces).
6
+ // tube() is the round-profile shorthand. Lit materials on purpose:
7
+ // creased vs smooth joints differ only in normals, which unlit color
8
+ // would hide.
9
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
10
+ import { box, Group, Mesh, PerspectiveCamera, plane, roundRect, Scene, shaderMaterial, sweep, tube, unlit } from "@solidrt/3d"
11
+ import type { SweepPath } from "@solidrt/3d"
12
+ import { HEMISPHERE, LAMBERT, LIT_VERTEX } from "@solidrt/3d/glsl"
13
+
14
+ const SIZE = 720
15
+
16
+ function App() {
17
+ let [spin, setSpin] = createSignal(0)
18
+ onFrame(tick => setSpin(tick / 4000))
19
+
20
+ // One hemisphere + lambert look per color (a material instance is the
21
+ // pipeline handle, so each look is created once and shared).
22
+ let lit = (r: number, g: number, b: number) =>
23
+ shaderMaterial({
24
+ vertex: LIT_VERTEX,
25
+ fragment: `
26
+ in vec3 vNormal;
27
+ ${HEMISPHERE}
28
+ ${LAMBERT}
29
+ void main() {
30
+ vec3 n = normalize(vNormal);
31
+ vec3 l = normalize(vec3(0.5, 0.8, 0.4));
32
+ vec3 base = vec3(${r}, ${g}, ${b});
33
+ fragColor = vec4(base * (hemisphere(n, vec3(0.45), vec3(0.22)) + 0.8 * lambert(n, l)), 1.0);
34
+ }`,
35
+ })
36
+
37
+ // The strap hugs the crate at half its thickness; every bend is a bare
38
+ // (sharp) point, so each fold creases exactly on a crate edge.
39
+ let o = 0.013
40
+ let strapPath: SweepPath = [
41
+ [-1.75, o, 0],
42
+ [-1.3 - o, o, 0],
43
+ [-1.3 - o, 0.6 + o, 0],
44
+ [-0.3 + o, 0.6 + o, 0],
45
+ [-0.3 + o, o, 0],
46
+ [0.15, o, 0],
47
+ ]
48
+ let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, "strap")
49
+
50
+ // A smooth-tagged helix: one continuous tube, not a stack of segments.
51
+ let coilPath: SweepPath = []
52
+ for (let i = 0; i <= 60; i++) {
53
+ let a = (i / 60) * Math.PI * 5
54
+ coilPath.push({ p: [0.85 + Math.cos(a) * 0.35, 0.055 + i * 0.0095, Math.sin(a) * 0.35], smooth: true })
55
+ }
56
+ let coil = tube(coilPath, 0.05, 12, "coil")
57
+
58
+ return (
59
+ <window>
60
+ <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
61
+ <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
62
+ <PerspectiveCamera fov={55} position={[0, 1.9, 3.9]} lookAt={[0, 0.35, 0]} />
63
+ <Mesh
64
+ geometry={plane(6, 6, "floor")}
65
+ material={unlit({ color: [0.16, 0.17, 0.22] })}
66
+ rotation={[-Math.PI / 2, 0, 0]}
67
+ />
68
+ <Group rotation={[0, spin(), 0]}>
69
+ <Mesh geometry={box(1, 0.6, 0.8)} material={lit(0.55, 0.42, 0.28)} position={[-0.8, 0.3, 0]} />
70
+ <Mesh geometry={strap} material={lit(0.9, 0.55, 0.2)} />
71
+ <Mesh geometry={coil} material={lit(0.45, 0.6, 0.8)} />
72
+ </Group>
73
+ </Scene>
74
+ </view>
75
+ </window>
76
+ )
77
+ }
78
+
79
+ render(() => <App />)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -17,6 +17,6 @@
17
17
  ],
18
18
  "peerDependencies": {
19
19
  "@solidjs/signals": "2.0.0-beta.31",
20
- "@solidrt/core": "0.0.47"
20
+ "@solidrt/core": "0.0.48"
21
21
  }
22
22
  }
@@ -16,13 +16,15 @@ import {
16
16
  remove,
17
17
  setGeometry,
18
18
  setMaterial,
19
+ setMeshParams,
19
20
  setTransform,
20
21
  setVisible,
21
22
  } from "./scene.ts"
23
+ import type { ShaderParams } from "@solidrt/core/gpu"
22
24
  import type { Mesh as MeshNode, Scene as SceneHandle, SceneNode } from "./scene.ts"
23
25
  import type { Geometry } from "./geometry.ts"
24
26
  import type { Material } from "./material.ts"
25
- import type { Vec3 } from "./math.ts"
27
+ import type { Quat, Vec3 } from "./math.ts"
26
28
 
27
29
  type SceneCtx = { scene: SceneHandle; parent: SceneNode }
28
30
  let SceneContext = createContext<SceneCtx>()
@@ -37,17 +39,20 @@ export function useScene(): SceneCtx {
37
39
 
38
40
  export type TransformProps = {
39
41
  position?: Vec3
40
- /** Euler radians, applied x then y then z. */
42
+ /** Euler radians in XYZ order (x first), Three's `Euler` default. */
41
43
  rotation?: Vec3
44
+ /** The rotation as a quaternion - what the node stores. Pass this or
45
+ * `rotation`, not both. */
46
+ quaternion?: Quat
42
47
  scale?: Vec3 | number
43
48
  visible?: boolean
44
49
  }
45
50
 
46
51
  function syncNode(node: SceneNode, props: TransformProps): void {
47
52
  createEffect(
48
- () => [props.position, props.rotation, props.scale, props.visible] as const,
49
- ([position, rotation, scale, visible]) => {
50
- setTransform(node, { position, rotation, scale })
53
+ () => [props.position, props.rotation, props.quaternion, props.scale, props.visible] as const,
54
+ ([position, rotation, quaternion, scale, visible]) => {
55
+ setTransform(node, { position, rotation, quaternion, scale })
51
56
  setVisible(node, visible !== false)
52
57
  },
53
58
  )
@@ -114,6 +119,12 @@ export let Group: ParentComponent<TransformProps & { ref?: (node: SceneNode) =>
114
119
  export type MeshProps = TransformProps & {
115
120
  geometry: Geometry
116
121
  material: Material
122
+ /** Per-mesh uniforms for a custom material (setMeshParams as a prop).
123
+ * Keys merge - a key that disappears keeps its old value; there is no
124
+ * unset. Names must be declared by the material's shaders. For values
125
+ * changing every frame prefer `ref` + setMeshParams from onFrame, the
126
+ * same split as setTransform. */
127
+ params?: ShaderParams
117
128
  ref?: (mesh: MeshNode) => void
118
129
  }
119
130
 
@@ -132,6 +143,12 @@ export let Mesh: VoidComponent<MeshProps> = props => {
132
143
  m => setMaterial(mesh, m),
133
144
  { defer: true },
134
145
  )
146
+ createEffect(
147
+ () => props.params,
148
+ p => {
149
+ if (p !== undefined) setMeshParams(mesh, p)
150
+ },
151
+ )
135
152
  syncNode(mesh, props)
136
153
  untrack(() => props.ref)?.(mesh)
137
154
  onCleanup(() => remove(mesh))
package/src/geometry.ts CHANGED
@@ -43,6 +43,12 @@ export const VERTEX_LAYOUTS: Record<VertexLayout, VertexAttribute[]> = {
43
43
  export const FLOATS_PER_VERTEX = 8
44
44
  const COLORED_FLOATS = 12
45
45
 
46
+ /** Uint16 indices when they fit, Uint32Array past 64k vertices - the draw
47
+ * entry follows the array type. The tail of every unbounded generator. */
48
+ export function packIndices(indices: number[], vertexCount: number): Uint16Array | Uint32Array {
49
+ return vertexCount > 65535 ? new Uint32Array(indices) : new Uint16Array(indices)
50
+ }
51
+
46
52
  export type Geometry = {
47
53
  /** Interleaved [pos.xyz, normal.xyz, uv.xy] per vertex, plus color.rgba
48
54
  * in the "colored" layout. */
package/src/index.ts CHANGED
@@ -5,17 +5,21 @@
5
5
  // without Solid components) and the component face (Scene/Mesh/Group/
6
6
  // PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
7
7
 
8
- export { add, createGroup, createMesh, createScene, remove, setGeometry, setMaterial, setMeshParams, setTransform, setVisible } from "./scene.ts"
8
+ export { add, createGroup, createMesh, createScene, getRotation, lookAt, remove, setGeometry, setMaterial, setMeshParams, setTransform, setVisible, worldPosition } from "./scene.ts"
9
9
  export type { CameraUpdate, Mesh as MeshNode, Scene as SceneHandle, SceneNode, SceneOptions, TransformUpdate } from "./scene.ts"
10
10
  export { box, circle, cone, cylinder, disposeGeometry, fillColors, plane, ring, sphere, torus, torusKnot, withColors, FLOATS_PER_VERTEX, VERTEX_LAYOUTS } from "./geometry.ts"
11
11
  export type { ColorFill, Geometry, VertexLayout } from "./geometry.ts"
12
- export { extrude, fillet, lathe, roundRect, shape, triangulate } from "./profile.ts"
12
+ export { fillet, roundRect, shape, triangulate } from "./profile.ts"
13
13
  export type { Profile, ProfilePoint } from "./profile.ts"
14
+ export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
15
+ export type { PathFrames, PathPoint, SweepPath } from "./sweep.ts"
14
16
  export { shaderMaterial, unlit } from "./material.ts"
15
17
  export type { Material, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
16
18
  export { Group, Mesh, PerspectiveCamera, Scene, useScene } from "./components.tsx"
17
19
  export type { MeshProps, PerspectiveCameraProps, SceneProps, TransformProps } from "./components.tsx"
18
20
  export { createOrbitCamera } from "./orbit.ts"
19
21
  export type { OrbitCamera, OrbitCameraOptions, OrbitPose } from "./orbit.ts"
20
- export { compose, copy, identity, lookAt, mat4, multiply, normalMatrix, perspective } from "./math.ts"
21
- export type { Mat4, Vec2, Vec3 } from "./math.ts"
22
+ // math's lookAt (the camera view matrix) stays on the /math subpath: the
23
+ // root's lookAt is the scene verb, the same split as `add`.
24
+ export { compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
25
+ export type { Mat4, Quat, Vec2, Vec3 } from "./math.ts"