@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/src/math.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
export type Vec2 = [number, number]
|
|
13
13
|
export type Vec3 = [number, number, number]
|
|
14
14
|
export type Vec4 = [number, number, number, number]
|
|
15
|
+
/** A rotation as [x, y, z, w] - glTF's and Three's component order. */
|
|
16
|
+
export type Quat = [number, number, number, number]
|
|
15
17
|
// prettier-ignore
|
|
16
18
|
export type Mat4 = [
|
|
17
19
|
number, number, number, number,
|
|
@@ -85,26 +87,23 @@ export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/**
|
|
88
|
-
* Compose translation + rotation + scale into a local matrix
|
|
89
|
-
*
|
|
90
|
-
*
|
|
90
|
+
* Compose translation + rotation + scale into a local matrix - Three's
|
|
91
|
+
* `Matrix4.compose` signature, rotation as a quaternion.
|
|
92
|
+
*
|
|
93
|
+
* `rotation` must be a UNIT quaternion: a non-unit one scales the geometry
|
|
94
|
+
* by |q|^2, silently. The scene closes that trap by normalizing on write
|
|
95
|
+
* (setTransform) rather than paying for a check on every compose.
|
|
91
96
|
*/
|
|
92
|
-
export function compose(out: Mat4, position: Vec3, rotation:
|
|
93
|
-
let
|
|
94
|
-
let
|
|
95
|
-
let
|
|
96
|
-
let
|
|
97
|
-
let
|
|
98
|
-
let
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let r20 = -sy
|
|
103
|
-
let r21 = cy * sx
|
|
104
|
-
let r22 = cy * cx
|
|
105
|
-
out[0] = r00 * scale[0]; out[1] = r10 * scale[0]; out[2] = r20 * scale[0]; out[3] = 0
|
|
106
|
-
out[4] = r01 * scale[1]; out[5] = r11 * scale[1]; out[6] = r21 * scale[1]; out[7] = 0
|
|
107
|
-
out[8] = r02 * scale[2]; out[9] = r12 * scale[2]; out[10] = r22 * scale[2]; out[11] = 0
|
|
97
|
+
export function compose(out: Mat4, position: Vec3, rotation: Quat, scale: Vec3): Mat4 {
|
|
98
|
+
let x = rotation[0], y = rotation[1], z = rotation[2], w = rotation[3]
|
|
99
|
+
let x2 = x + x, y2 = y + y, z2 = z + z
|
|
100
|
+
let xx = x * x2, xy = x * y2, xz = x * z2
|
|
101
|
+
let yy = y * y2, yz = y * z2, zz = z * z2
|
|
102
|
+
let wx = w * x2, wy = w * y2, wz = w * z2
|
|
103
|
+
let sx = scale[0], sy = scale[1], sz = scale[2]
|
|
104
|
+
out[0] = (1 - (yy + zz)) * sx; out[1] = (xy + wz) * sx; out[2] = (xz - wy) * sx; out[3] = 0
|
|
105
|
+
out[4] = (xy - wz) * sy; out[5] = (1 - (xx + zz)) * sy; out[6] = (yz + wx) * sy; out[7] = 0
|
|
106
|
+
out[8] = (xz + wy) * sz; out[9] = (yz - wx) * sz; out[10] = (1 - (xx + yy)) * sz; out[11] = 0
|
|
108
107
|
out[12] = position[0]; out[13] = position[1]; out[14] = position[2]; out[15] = 1
|
|
109
108
|
return out
|
|
110
109
|
}
|
|
@@ -150,6 +149,278 @@ export function perspective(out: Mat4, fovy: number, aspect: number, near: numbe
|
|
|
150
149
|
return out
|
|
151
150
|
}
|
|
152
151
|
|
|
152
|
+
// Quaternions, the rotation the scene actually stores. Euler triples are a
|
|
153
|
+
// boundary format only - authoring (setTransform's `rotation`, the
|
|
154
|
+
// components' `rotation` prop) and reading back (getRotation) - so the order
|
|
155
|
+
// convention and gimbal lock live at that boundary and nowhere else.
|
|
156
|
+
|
|
157
|
+
/** A fresh identity rotation. */
|
|
158
|
+
export function quat(): Quat {
|
|
159
|
+
return [0, 0, 0, 1]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Unit quaternion; a zero-length input comes back as the identity. `out`
|
|
163
|
+
* may alias `q`. */
|
|
164
|
+
export function quatNormalize(out: Quat, q: Quat): Quat {
|
|
165
|
+
let len = Math.hypot(q[0], q[1], q[2], q[3])
|
|
166
|
+
if (len === 0) {
|
|
167
|
+
out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
out[0] = q[0] / len; out[1] = q[1] / len; out[2] = q[2] / len; out[3] = q[3] / len
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Euler radians to a quaternion, in XYZ order: x applied first, then y,
|
|
176
|
+
* then z (R = Rx * Ry * Rz on column vectors), Three's `Euler` default - a
|
|
177
|
+
* triple copied from a Three scene means the same thing here.
|
|
178
|
+
*
|
|
179
|
+
* ONE order exists, deliberately: a per-call order argument is how the same
|
|
180
|
+
* triple ends up meaning two different things in two places, and the
|
|
181
|
+
* quaternion is right there for anything an order was going to express.
|
|
182
|
+
*/
|
|
183
|
+
export function quatFromEuler(out: Quat, euler: Vec3): Quat {
|
|
184
|
+
let c1 = Math.cos(euler[0] / 2), s1 = Math.sin(euler[0] / 2)
|
|
185
|
+
let c2 = Math.cos(euler[1] / 2), s2 = Math.sin(euler[1] / 2)
|
|
186
|
+
let c3 = Math.cos(euler[2] / 2), s3 = Math.sin(euler[2] / 2)
|
|
187
|
+
out[0] = s1 * c2 * c3 + c1 * s2 * s3
|
|
188
|
+
out[1] = c1 * s2 * c3 - s1 * c2 * s3
|
|
189
|
+
out[2] = c1 * c2 * s3 + s1 * s2 * c3
|
|
190
|
+
out[3] = c1 * c2 * c3 - s1 * s2 * s3
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A quaternion back to Euler radians in the same XYZ order - the inverse of
|
|
196
|
+
* quatFromEuler, a convenience for reading and debugging rather than a peer
|
|
197
|
+
* of the quaternion: the mapping is many-to-one (a triple and that triple
|
|
198
|
+
* plus a full turn agree), and at the poles (local +z straight up or down)
|
|
199
|
+
* only the sum of x and z is determined, so this pins z to 0 and folds the
|
|
200
|
+
* roll into x. Round-tripping the result reproduces the rotation exactly;
|
|
201
|
+
* it need not reproduce the triple you started from.
|
|
202
|
+
*/
|
|
203
|
+
export function eulerFromQuat(out: Vec3, q: Quat): Vec3 {
|
|
204
|
+
let x = q[0], y = q[1], z = q[2], w = q[3]
|
|
205
|
+
let x2 = x + x, y2 = y + y, z2 = z + z
|
|
206
|
+
let xx = x * x2, xy = x * y2, xz = x * z2
|
|
207
|
+
let yy = y * y2, yz = y * z2, zz = z * z2
|
|
208
|
+
let wx = w * x2, wy = w * y2, wz = w * z2
|
|
209
|
+
// The same matrix entries compose() writes: m02 = sin(y) alone, and the
|
|
210
|
+
// x/z pair reads off the rest unless cos(y) is 0 (the pole).
|
|
211
|
+
let m00 = 1 - (yy + zz)
|
|
212
|
+
let m01 = xy - wz
|
|
213
|
+
let m02 = xz + wy
|
|
214
|
+
// cos(y), which both remaining pairs scale with. atan2 against it beats
|
|
215
|
+
// asin(m02) - Three's form - near the poles, where asin's derivative
|
|
216
|
+
// blows up and a 1e-16 error in m02 becomes 1e-8 in the angle. It also
|
|
217
|
+
// lets the pole branch start three orders of magnitude later: the pairs
|
|
218
|
+
// stay well-conditioned until cos(y) approaches the noise floor.
|
|
219
|
+
let cy = Math.hypot(m00, m01)
|
|
220
|
+
out[1] = Math.atan2(m02, cy)
|
|
221
|
+
if (cy > 1e-7) {
|
|
222
|
+
out[0] = Math.atan2(wx - yz, 1 - (xx + yy))
|
|
223
|
+
out[2] = Math.atan2(-m01, m00)
|
|
224
|
+
} else {
|
|
225
|
+
// Only x + z (at +y) or x - z (at -y) is determined; pin z and fold
|
|
226
|
+
// the whole roll into x.
|
|
227
|
+
out[0] = Math.atan2(yz + wx, 1 - (xx + zz))
|
|
228
|
+
out[2] = 0
|
|
229
|
+
}
|
|
230
|
+
return out
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The rotation of `angle` RADIANS about `axis` - Three's `setFromAxisAngle`,
|
|
235
|
+
* Unity's `AngleAxis` (which takes degrees; this takes radians like
|
|
236
|
+
* everything else here). The axis need not be normalized - the named
|
|
237
|
+
* engines all require a unit axis and silently corrupt the rotation
|
|
238
|
+
* otherwise, the same precondition trap `quatFromTo` closes. A zero axis
|
|
239
|
+
* yields the identity.
|
|
240
|
+
*/
|
|
241
|
+
export function quatFromAxisAngle(out: Quat, axis: Vec3, angle: number): Quat {
|
|
242
|
+
let x = axis[0], y = axis[1], z = axis[2]
|
|
243
|
+
let len = Math.hypot(x, y, z)
|
|
244
|
+
if (len === 0) {
|
|
245
|
+
out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
let s = Math.sin(angle / 2) / len
|
|
249
|
+
out[0] = x * s
|
|
250
|
+
out[1] = y * s
|
|
251
|
+
out[2] = z * s
|
|
252
|
+
out[3] = Math.cos(angle / 2)
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* out = a * b - the same order contract as the mat4 `multiply` above: on
|
|
258
|
+
* column vectors b applies first, so `quatMultiply(q, spin, q)` composes a
|
|
259
|
+
* further world-frame spin onto q while `quatMultiply(q, q, spin)` spins
|
|
260
|
+
* about q's own local frame. `out` may alias `a` or `b`.
|
|
261
|
+
*
|
|
262
|
+
* The product of unit quaternions is unit up to float drift, so this does
|
|
263
|
+
* not renormalize; an accumulator composed every frame drifts slowly, and
|
|
264
|
+
* the scene's setTransform renormalizes on write anyway.
|
|
265
|
+
*/
|
|
266
|
+
export function quatMultiply(out: Quat, a: Quat, b: Quat): Quat {
|
|
267
|
+
let ax = a[0], ay = a[1], az = a[2], aw = a[3]
|
|
268
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3]
|
|
269
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by
|
|
270
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx
|
|
271
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw
|
|
272
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz
|
|
273
|
+
return out
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Spherical interpolation from `a` to `b`: constant angular velocity along
|
|
278
|
+
* the shortest path (the sign of `b` is flipped when the pair straddles the
|
|
279
|
+
* quaternion double cover, so it never takes the long way round). t = 0 is
|
|
280
|
+
* `a`, t = 1 is `b`'s rotation; inputs must be unit and the result is unit.
|
|
281
|
+
* `out` may alias `a` or `b`.
|
|
282
|
+
*
|
|
283
|
+
* The canonical damped follow is
|
|
284
|
+
* `quatSlerp(q, q, target, 1 - Math.exp(-k * dt))` - frame-rate
|
|
285
|
+
* independent, k is the tracking speed.
|
|
286
|
+
*/
|
|
287
|
+
export function quatSlerp(out: Quat, a: Quat, b: Quat, t: number): Quat {
|
|
288
|
+
let ax = a[0], ay = a[1], az = a[2], aw = a[3]
|
|
289
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3]
|
|
290
|
+
let cos = ax * bx + ay * by + az * bz + aw * bw
|
|
291
|
+
if (cos < 0) {
|
|
292
|
+
cos = -cos
|
|
293
|
+
bx = -bx; by = -by; bz = -bz; bw = -bw
|
|
294
|
+
}
|
|
295
|
+
let wa: number
|
|
296
|
+
let wb: number
|
|
297
|
+
if (cos < 0.9995) {
|
|
298
|
+
let theta = Math.acos(cos > 1 ? 1 : cos)
|
|
299
|
+
let sin = Math.sin(theta)
|
|
300
|
+
wa = Math.sin((1 - t) * theta) / sin
|
|
301
|
+
wb = Math.sin(t * theta) / sin
|
|
302
|
+
} else {
|
|
303
|
+
// Nearly identical: sin(theta) is noise, and a straight lerp is within
|
|
304
|
+
// float precision of the arc - normalized below like any other result.
|
|
305
|
+
wa = 1 - t
|
|
306
|
+
wb = t
|
|
307
|
+
}
|
|
308
|
+
out[0] = wa * ax + wb * bx
|
|
309
|
+
out[1] = wa * ay + wb * by
|
|
310
|
+
out[2] = wa * az + wb * bz
|
|
311
|
+
out[3] = wa * aw + wb * bw
|
|
312
|
+
return quatNormalize(out, out)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The shortest-arc rotation taking `from` to `to`: Unity's
|
|
317
|
+
* `Quaternion.FromToRotation`, glam's `Quat::from_rotation_arc`. Three
|
|
318
|
+
* calls this `setFromUnitVectors`; renamed because that name states a
|
|
319
|
+
* precondition instead of the operation, and this one has no such
|
|
320
|
+
* precondition - neither input need be normalized.
|
|
321
|
+
*
|
|
322
|
+
* This is how a y-axis solid gets aimed - `quatFromTo(q, [0, 1, 0], dir)`
|
|
323
|
+
* for a cylinder or cone - where lookAt's +z convention would need a
|
|
324
|
+
* correction. Opposite vectors have no shortest arc (every half turn is
|
|
325
|
+
* equally short); a stable perpendicular axis is picked. A zero-length
|
|
326
|
+
* input yields the identity.
|
|
327
|
+
*/
|
|
328
|
+
export function quatFromTo(out: Quat, from: Vec3, to: Vec3): Quat {
|
|
329
|
+
let ax = from[0], ay = from[1], az = from[2]
|
|
330
|
+
let bx = to[0], by = to[1], bz = to[2]
|
|
331
|
+
let la = Math.hypot(ax, ay, az)
|
|
332
|
+
let lb = Math.hypot(bx, by, bz)
|
|
333
|
+
if (la === 0 || lb === 0) {
|
|
334
|
+
out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
|
|
335
|
+
return out
|
|
336
|
+
}
|
|
337
|
+
ax /= la; ay /= la; az /= la
|
|
338
|
+
bx /= lb; by /= lb; bz /= lb
|
|
339
|
+
let r = ax * bx + ay * by + az * bz + 1
|
|
340
|
+
if (r < 1e-6) {
|
|
341
|
+
// Antiparallel: the cross product vanishes, so take any perpendicular
|
|
342
|
+
// axis - crossing with the smaller of from's x/z components cannot
|
|
343
|
+
// vanish too, and picking off from alone keeps the choice stable.
|
|
344
|
+
r = 0
|
|
345
|
+
if (Math.abs(ax) > Math.abs(az)) {
|
|
346
|
+
out[0] = -ay; out[1] = ax; out[2] = 0
|
|
347
|
+
} else {
|
|
348
|
+
out[0] = 0; out[1] = -az; out[2] = ay
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
out[0] = ay * bz - az * by
|
|
352
|
+
out[1] = az * bx - ax * bz
|
|
353
|
+
out[2] = ax * by - ay * bx
|
|
354
|
+
}
|
|
355
|
+
out[3] = r
|
|
356
|
+
return quatNormalize(out, out)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The rotation that points the local +z axis along `forward`, with `up`
|
|
361
|
+
* choosing the roll about it - the object-aiming counterpart of lookAt(),
|
|
362
|
+
* which builds the camera's inverse frame. Neither input need be
|
|
363
|
+
* normalized. Degenerate inputs (zero forward, up parallel to forward) fall
|
|
364
|
+
* back to a stable perpendicular instead of producing NaNs.
|
|
365
|
+
*/
|
|
366
|
+
export function quatFromFrame(out: Quat, forward: Vec3, up: Vec3): Quat {
|
|
367
|
+
let zx = forward[0], zy = forward[1], zz = forward[2]
|
|
368
|
+
let len = Math.hypot(zx, zy, zz)
|
|
369
|
+
if (len === 0) {
|
|
370
|
+
zx = 0; zy = 0; zz = 1
|
|
371
|
+
} else {
|
|
372
|
+
zx /= len; zy /= len; zz /= len
|
|
373
|
+
}
|
|
374
|
+
let xx = up[1] * zz - up[2] * zy
|
|
375
|
+
let xy = up[2] * zx - up[0] * zz
|
|
376
|
+
let xz = up[0] * zy - up[1] * zx
|
|
377
|
+
len = Math.hypot(xx, xy, xz)
|
|
378
|
+
if (len === 0) {
|
|
379
|
+
// up is parallel to forward: cross with a world axis that cannot be,
|
|
380
|
+
// picked off z's own components so the choice is stable per direction.
|
|
381
|
+
let ax = Math.abs(zx) < 0.9 ? 1 : 0
|
|
382
|
+
let ay = ax === 1 ? 0 : 1
|
|
383
|
+
xx = ay * zz
|
|
384
|
+
xy = -ax * zz
|
|
385
|
+
xz = ax * zy - ay * zx
|
|
386
|
+
len = Math.hypot(xx, xy, xz)
|
|
387
|
+
}
|
|
388
|
+
xx /= len; xy /= len; xz /= len
|
|
389
|
+
let yx = zy * xz - zz * xy
|
|
390
|
+
let yy = zz * xx - zx * xz
|
|
391
|
+
let yz = zx * xy - zy * xx
|
|
392
|
+
// X | Y | Z are the rotation's columns, so its diagonal is xx, yy, zz.
|
|
393
|
+
// Branching on the largest diagonal entry keeps the divisor away from
|
|
394
|
+
// zero; the basis is orthonormal, so the result is already unit.
|
|
395
|
+
let trace = xx + yy + zz
|
|
396
|
+
if (trace > 0) {
|
|
397
|
+
let s = 0.5 / Math.sqrt(trace + 1)
|
|
398
|
+
out[0] = (yz - zy) * s
|
|
399
|
+
out[1] = (zx - xz) * s
|
|
400
|
+
out[2] = (xy - yx) * s
|
|
401
|
+
out[3] = 0.25 / s
|
|
402
|
+
} else if (xx > yy && xx > zz) {
|
|
403
|
+
let s = 2 * Math.sqrt(1 + xx - yy - zz)
|
|
404
|
+
out[0] = 0.25 * s
|
|
405
|
+
out[1] = (yx + xy) / s
|
|
406
|
+
out[2] = (zx + xz) / s
|
|
407
|
+
out[3] = (yz - zy) / s
|
|
408
|
+
} else if (yy > zz) {
|
|
409
|
+
let s = 2 * Math.sqrt(1 + yy - xx - zz)
|
|
410
|
+
out[0] = (yx + xy) / s
|
|
411
|
+
out[1] = 0.25 * s
|
|
412
|
+
out[2] = (zy + yz) / s
|
|
413
|
+
out[3] = (zx - xz) / s
|
|
414
|
+
} else {
|
|
415
|
+
let s = 2 * Math.sqrt(1 + zz - xx - yy)
|
|
416
|
+
out[0] = (zx + xz) / s
|
|
417
|
+
out[1] = (zy + yz) / s
|
|
418
|
+
out[2] = 0.25 * s
|
|
419
|
+
out[3] = (xy - yx) / s
|
|
420
|
+
}
|
|
421
|
+
return out
|
|
422
|
+
}
|
|
423
|
+
|
|
153
424
|
// Vec3 helpers for geometry construction. These allocate (unlike the matrix
|
|
154
425
|
// functions above): they serve generation-time code - curve frames, normals -
|
|
155
426
|
// not the per-frame path. Exposed on the /math subpath only, so `add` does
|
|
@@ -185,6 +456,10 @@ export function normalize(v: Vec3): Vec3 {
|
|
|
185
456
|
* View matrix (world -> camera) for a camera at `eye` looking at `target`
|
|
186
457
|
* with the given `up`. Degenerate inputs (eye == target, up parallel to the
|
|
187
458
|
* view direction) fall back to axis defaults instead of producing NaNs.
|
|
459
|
+
*
|
|
460
|
+
* On the /math subpath ONLY - the package root's `lookAt` is the scene verb
|
|
461
|
+
* that aims a node (the Matrix4/Object3D split Three makes under the same
|
|
462
|
+
* name), the same collision rule the Vec3 helpers follow.
|
|
188
463
|
*/
|
|
189
464
|
export function lookAt(out: Mat4, eye: Vec3, target: Vec3, up: Vec3): Mat4 {
|
|
190
465
|
let zx = eye[0] - target[0]
|