@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 +94 -12
- package/README.md +19 -4
- package/examples/README.md +8 -0
- package/examples/aim.tsx +119 -0
- package/examples/sweep-paths.tsx +79 -0
- package/package.json +2 -2
- package/src/components.tsx +22 -5
- package/src/geometry.ts +6 -0
- package/src/index.ts +8 -4
- package/src/math.ts +294 -19
- package/src/profile.ts +43 -248
- package/src/scene.ts +136 -12
- package/src/sweep.ts +460 -0
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
|
|
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,
|
|
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
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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).
|
|
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 -
|
|
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`,
|
|
52
|
-
|
|
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).
|
package/examples/README.md
CHANGED
|
@@ -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.
|
package/examples/aim.tsx
ADDED
|
@@ -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.
|
|
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.
|
|
20
|
+
"@solidrt/core": "0.0.48"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/src/components.tsx
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
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"
|