@nakednous/tree 0.0.26 → 0.0.27
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/README.md +43 -1
- package/dist/index.js +232 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ query.js — you have a matrix, you want information
|
|
|
41
41
|
quat.js — quaternion algebra and mat4/mat3 conversions
|
|
42
42
|
track.js — spline math and keyframe animation state machines
|
|
43
43
|
helm.js — 6-DOF rate-stream integrator — the Track family's live-input sibling
|
|
44
|
+
filter.js — input conditioning: the 1€ filter + absolute→rate differencing
|
|
44
45
|
handle.js — constraint solver + ray primitives for interactive manipulators
|
|
45
46
|
```
|
|
46
47
|
|
|
@@ -351,14 +352,55 @@ SELF the helm's OWN evolving pose — body-relative
|
|
|
351
352
|
|
|
352
353
|
```js
|
|
353
354
|
helm.deadzone = 8 // rest-drift floor — |rate| ≤ deadzone reads as 0
|
|
355
|
+
helm.filter = oneEuro({ minCutoff: 1, beta: 0.5 }) // optional input conditioner (filter → deadzone)
|
|
356
|
+
helm.fullScale = 500 // raw full-deflection magnitude a read-out divides by
|
|
354
357
|
helm.activity(out6) // six effective rates (post deadzone·sign·sens), channel order
|
|
355
|
-
helm.home([pose]) // re-home pos + rot (NOT reset — no keyframes); clears pending rate
|
|
358
|
+
helm.home([pose]) // re-home pos + rot (NOT reset — no keyframes); clears pending rate; resets filter
|
|
356
359
|
```
|
|
357
360
|
|
|
361
|
+
`filter` is an optional input conditioner (default `null`): when set, `step` runs it over the fed rate before the deadzone — filter then deadzone, the two orthogonal (the 1€ removes zero-mean jitter; the deadzone's exact zero is the only no-creep guarantee, since a low-pass passes DC). `fullScale` (default `500`) is the raw full-deflection magnitude a read-out divides by, so a transport on a different input scale declares its own and its meters read honestly. `activity()` reports the raw fed rate (pre-filter) by design.
|
|
362
|
+
|
|
358
363
|
The `p5.tree` bridge wraps this into `createCameraHelm` / `createPoseHelm` (transport, camera basis, draw-loop player) plus the `helmRig` gizmo and the `createPanel(helm)` profile editor.
|
|
359
364
|
|
|
360
365
|
---
|
|
361
366
|
|
|
367
|
+
### Input conditioning — `oneEuro` · `poseDelta`
|
|
368
|
+
|
|
369
|
+
Two helpers for the rate stream a helm feeds on — flat, out-first, zero-alloc (`filter.js`).
|
|
370
|
+
|
|
371
|
+
**`oneEuro({ minCutoff, beta, dCutoff })`** is the [1€ filter](https://gery.casiez.net/1euro/) (Casiez et al., CHI'12): a first-order low-pass whose cutoff rises with signal speed — heavy smoothing at rest, low lag under motion. It returns a stateful carrying function, dispatched on its first argument:
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
import { oneEuro } from '@nakednous/tree'
|
|
375
|
+
|
|
376
|
+
const f = oneEuro({ minCutoff: 1, beta: 0.5 }) // params live-mutable: f.minCutoff, f.beta
|
|
377
|
+
|
|
378
|
+
// scalar form — returns the filtered number
|
|
379
|
+
const y = f(rawScalar, dt)
|
|
380
|
+
|
|
381
|
+
// vec form — out-first, zero-alloc after warm-up
|
|
382
|
+
const out = [0, 0, 0]
|
|
383
|
+
f(out, rawVec3, dt)
|
|
384
|
+
|
|
385
|
+
f.reset() // drop state — next call re-seeds
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
It removes zero-mean **jitter**, not a DC **bias**: a low-pass passes a constant offset, so a resting bias survives it and still integrates to drift — pair it with a deadzone (the helm applies filter → deadzone in that order).
|
|
389
|
+
|
|
390
|
+
**`poseDelta(out, prev, cur, dt)`** differences two absolute poses into the `{ lin, ang }` rate a helm feeds on — the bridge from an absolute transport (a tracked hand, a marker, a played keyframe) to the rate stream.
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
import { poseDelta } from '@nakednous/tree'
|
|
394
|
+
|
|
395
|
+
const rate = { lin: [0, 0, 0], ang: [0, 0, 0] }
|
|
396
|
+
poseDelta(rate, prevPose, curPose, dt) // prev / cur: { pos:[x,y,z], rot:[x,y,z,w] }
|
|
397
|
+
helm.feed(rate.lin, rate.ang)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The angular half carries a **double-cover guard**: a quaternion and its negation are the same orientation, so a source that returns a canonicalised quaternion makes the stored value jump hemispheres as the true orientation sweeps through `w = 0`. When `dot(prev, cur) < 0`, `poseDelta` flips `cur` into `prev`'s hemisphere before differencing, so the relative rotation always takes the short arc — without it the angular rate spikes toward `2π/dt` at every crossing.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
362
404
|
### Coordinate-space mapping
|
|
363
405
|
|
|
364
406
|
`mapLocation` and `mapDirection` convert points and vectors between any pair of named spaces. All work is done in flat scalar arithmetic — no objects created per call.
|
package/dist/index.js
CHANGED
|
@@ -293,6 +293,117 @@ const qToAxisAngle = (q, out) => {
|
|
|
293
293
|
return out;
|
|
294
294
|
};
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* @file 1€ input filter — speed-adaptive first-order low-pass for noisy input.
|
|
298
|
+
* @module tree/filter
|
|
299
|
+
* @license AGPL-3.0-only
|
|
300
|
+
*
|
|
301
|
+
* A jitter conditioner for noisy / absolute input streams (the helm's optional
|
|
302
|
+
* `filter` slot; sketch-side handle conditioning). It is NOT a deadzone
|
|
303
|
+
* replacement: a low-pass passes DC, so a constant rest bias survives it and
|
|
304
|
+
* still integrates to creep — only the deadzone's exact-zero clamp removes that.
|
|
305
|
+
* The 1€ removes zero-mean jitter; the two are orthogonal and coexist, applied
|
|
306
|
+
* filter → deadzone (condition the continuous signal first, then gate to zero).
|
|
307
|
+
*
|
|
308
|
+
* First-order low-pass whose cutoff rises with the signal's speed: slow (rest)
|
|
309
|
+
* → low cutoff → heavy smoothing; fast (motion) → high cutoff → low lag. Tuned
|
|
310
|
+
* by `minCutoff` (Hz, the rest cutoff) and `beta` (how fast the cutoff opens
|
|
311
|
+
* with speed). Reimplemented from the paper's equations — `tree` is zero-dep, so
|
|
312
|
+
* the ~one-page primitive is vendored, not depended on (the same call made for
|
|
313
|
+
* quaternions over gl-matrix).
|
|
314
|
+
*
|
|
315
|
+
* Reference: Casiez, G., Roussel, N., & Vogel, D. (2012). 1€ Filter: A Simple
|
|
316
|
+
* Speed-based Low-pass Filter for Noisy Input in Interactive Systems. CHI '12,
|
|
317
|
+
* 2527–2530. DOI 10.1145/2207676.2208639 · https://gery.casiez.net/1euro/
|
|
318
|
+
*/
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Build a stateful 1€ filter as an out-first carrying function.
|
|
323
|
+
*
|
|
324
|
+
* The returned `f` carries the previous filtered value and derivative across
|
|
325
|
+
* calls (one state slot per vector component), and is used in one of two forms:
|
|
326
|
+
*
|
|
327
|
+
* - vec form `f(out, raw, dt)` — `out`/`raw` are equal-length ArrayLikes;
|
|
328
|
+
* writes the filtered vector into `out` and returns it.
|
|
329
|
+
* Zero-allocation after the first call (state sized to `raw`).
|
|
330
|
+
* - scalar form `f(raw, dt)` — `raw` is a number; returns the filtered number.
|
|
331
|
+
*
|
|
332
|
+
* The form is chosen by the first argument's type (number ⇒ scalar). `minCutoff`,
|
|
333
|
+
* `beta`, and `dCutoff` are live-mutable on the returned function (`f.minCutoff`,
|
|
334
|
+
* `f.beta`, `f.dCutoff`) and read every call, so a panel can tune them against
|
|
335
|
+
* live noise. `f.reset()` drops the carried state at a discontinuity (a
|
|
336
|
+
* re-acquired source, a helm `home()`); smear across one and the output lurches.
|
|
337
|
+
*
|
|
338
|
+
* @param {Object} [opts]
|
|
339
|
+
* @param {number} [opts.minCutoff=1] Minimum (rest) cutoff frequency, in Hz.
|
|
340
|
+
* @param {number} [opts.beta=0] Speed coefficient (cutoff = minCutoff +
|
|
341
|
+
* beta·|filtered derivative|); unitless.
|
|
342
|
+
* @param {number} [opts.dCutoff=1] Derivative cutoff frequency, in Hz.
|
|
343
|
+
* @returns {Function} A carrying filter `f(out, raw, dt)` / `f(raw, dt)` with a
|
|
344
|
+
* `reset()` method and live `minCutoff` / `beta` / `dCutoff`.
|
|
345
|
+
*/
|
|
346
|
+
function oneEuro(opts) {
|
|
347
|
+
opts = opts || {};
|
|
348
|
+
|
|
349
|
+
// Carried state: previous filtered value + previous filtered derivative.
|
|
350
|
+
// number (scalar form) | number[] (vec form). primed = state is seeded.
|
|
351
|
+
let xPrev = null;
|
|
352
|
+
let dxPrev = null;
|
|
353
|
+
let primed = false;
|
|
354
|
+
|
|
355
|
+
// Low-pass smoothing factor for a sample period `dt` and a cutoff frequency
|
|
356
|
+
// (the paper's α = 1 / (1 + τ/Te), τ = 1 / (2π·cutoff)).
|
|
357
|
+
const alpha = (dt, cutoff) => {
|
|
358
|
+
const tau = 1 / (2 * Math.PI * cutoff);
|
|
359
|
+
return 1 / (1 + tau / dt);
|
|
360
|
+
};
|
|
361
|
+
const smooth = (a, x, xp) => a * x + (1 - a) * xp;
|
|
362
|
+
|
|
363
|
+
const scalar = (x, dt) => {
|
|
364
|
+
if (!primed) { xPrev = x; dxPrev = 0; primed = true; return x; }
|
|
365
|
+
const dx = (x - xPrev) / dt;
|
|
366
|
+
const edx = smooth(alpha(dt, f.dCutoff), dx, dxPrev);
|
|
367
|
+
const a = alpha(dt, f.minCutoff + f.beta * Math.abs(edx));
|
|
368
|
+
const xh = smooth(a, x, xPrev);
|
|
369
|
+
xPrev = xh; dxPrev = edx;
|
|
370
|
+
return xh;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const vec = (out, raw, dt) => {
|
|
374
|
+
const n = raw.length;
|
|
375
|
+
if (!primed) {
|
|
376
|
+
if (!Array.isArray(xPrev) || xPrev.length !== n) {
|
|
377
|
+
xPrev = new Array(n); dxPrev = new Array(n);
|
|
378
|
+
}
|
|
379
|
+
for (let i = 0; i < n; i++) { xPrev[i] = raw[i]; dxPrev[i] = 0; out[i] = raw[i]; }
|
|
380
|
+
primed = true;
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
for (let i = 0; i < n; i++) {
|
|
384
|
+
const dx = (raw[i] - xPrev[i]) / dt;
|
|
385
|
+
const edx = smooth(alpha(dt, f.dCutoff), dx, dxPrev[i]);
|
|
386
|
+
const a = alpha(dt, f.minCutoff + f.beta * Math.abs(edx));
|
|
387
|
+
const xh = smooth(a, raw[i], xPrev[i]);
|
|
388
|
+
xPrev[i] = xh; dxPrev[i] = edx;
|
|
389
|
+
out[i] = xh;
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const f = (out, raw, dt) =>
|
|
395
|
+
(typeof out === 'number') ? scalar(out, raw) : vec(out, raw, dt);
|
|
396
|
+
|
|
397
|
+
f.minCutoff = (opts.minCutoff != null) ? opts.minCutoff : 1;
|
|
398
|
+
f.beta = (opts.beta != null) ? opts.beta : 0;
|
|
399
|
+
f.dCutoff = (opts.dCutoff != null) ? opts.dCutoff : 1;
|
|
400
|
+
|
|
401
|
+
/** Drop the carried state — call at a discontinuity (re-acquire / home). */
|
|
402
|
+
f.reset = () => { primed = false; return f; };
|
|
403
|
+
|
|
404
|
+
return f;
|
|
405
|
+
}
|
|
406
|
+
|
|
296
407
|
/**
|
|
297
408
|
* @file Matrix construction from geometric specs and partial decomposition.
|
|
298
409
|
* @module tree/form
|
|
@@ -3044,6 +3155,12 @@ function createConstraint(kind, opts) {
|
|
|
3044
3155
|
* eval(out) read the current { pos, rot } (zero-alloc, as Track.eval)
|
|
3045
3156
|
* home(pose?) re-home the integrated pose (NOT reset — no keyframes to clear)
|
|
3046
3157
|
*
|
|
3158
|
+
* Value layer (opt-in, §10): a per-helm `fullScale` keeps the read-outs honest
|
|
3159
|
+
* across input scales; an opt-in `filter` (oneEuro) conditions the fed rates
|
|
3160
|
+
* before the deadzone inside step; `poseDelta` differences two absolute poses
|
|
3161
|
+
* into a feedable rate. With `filter` null and the default `fullScale`, the
|
|
3162
|
+
* clean rate-native path is unchanged.
|
|
3163
|
+
*
|
|
3047
3164
|
* Quaternion algebra is provided by quat.js. Out-first throughout; no allocation
|
|
3048
3165
|
* in feed/step/eval. Storage convention (matches the rest of the core): vec3 and
|
|
3049
3166
|
* quat state are plain number[] (f64) — the same shape as track.js keyframes and
|
|
@@ -3076,6 +3193,23 @@ function createConstraint(kind, opts) {
|
|
|
3076
3193
|
|
|
3077
3194
|
const _dq = [0, 0, 0, 1];
|
|
3078
3195
|
|
|
3196
|
+
// Scratch for the optional input filter (helm.filter). The fed rates are packed
|
|
3197
|
+
// into one 6-vector, conditioned, and unpacked to lin/ang triples before the
|
|
3198
|
+
// deadzone — so a single filter carries one state per helm. Shared across
|
|
3199
|
+
// instances (step is synchronous, at most once per helm per frame).
|
|
3200
|
+
const _f6raw = [0, 0, 0, 0, 0, 0];
|
|
3201
|
+
const _f6out = [0, 0, 0, 0, 0, 0];
|
|
3202
|
+
const _fLin = [0, 0, 0];
|
|
3203
|
+
const _fAng = [0, 0, 0];
|
|
3204
|
+
|
|
3205
|
+
/**
|
|
3206
|
+
* Default full-deflection input magnitude — the fed rate a read-out shows as
|
|
3207
|
+
* full (the SpaceNavigator's saturated lane). Private to core: a transport on a
|
|
3208
|
+
* different raw scale sets helm.fullScale and never reads this, so there is no
|
|
3209
|
+
* public HELM_FULL export. Display-only; integration uses profile.sens directly.
|
|
3210
|
+
*/
|
|
3211
|
+
const HELM_FULL_SCALE = 500;
|
|
3212
|
+
|
|
3079
3213
|
// Deadzone gate — |v| > dz keeps v, else 0. Module-level so step allocates no
|
|
3080
3214
|
// closure. Strictly-greater matches the e7 reference (rest reads exact 0).
|
|
3081
3215
|
const _dz = (v, dz) => (v > dz || v < -dz) ? v : 0;
|
|
@@ -3128,6 +3262,24 @@ class PoseHelm {
|
|
|
3128
3262
|
*/
|
|
3129
3263
|
this.deadzone = 8;
|
|
3130
3264
|
|
|
3265
|
+
/**
|
|
3266
|
+
* Full-deflection input magnitude for the read-outs (gizmo overlay + panel
|
|
3267
|
+
* meters): a fed rate of |fullScale| reads as a full bar / arrow. Set by a
|
|
3268
|
+
* transport whose saturated rate differs from the default (a gamepad stick
|
|
3269
|
+
* that saturates at 1 sets fullScale = 1). Display-only — integration uses
|
|
3270
|
+
* profile.sens directly, so this never affects flight. @type {number}
|
|
3271
|
+
*/
|
|
3272
|
+
this.fullScale = HELM_FULL_SCALE;
|
|
3273
|
+
|
|
3274
|
+
/**
|
|
3275
|
+
* Optional input conditioner applied to the fed rates BEFORE the deadzone,
|
|
3276
|
+
* inside step — a oneEuro filter, or any f(out, raw, dt) carrying function
|
|
3277
|
+
* with a reset(). null (the default) is the clean rate-native path: the
|
|
3278
|
+
* filter branch is skipped, so a clean device pays nothing. Set for noisy /
|
|
3279
|
+
* absolute sources; home() resets it. @type {?Function}
|
|
3280
|
+
*/
|
|
3281
|
+
this.filter = null;
|
|
3282
|
+
|
|
3131
3283
|
/**
|
|
3132
3284
|
* The space fed rates are interpreted in: a space constant (WORLD | EYE) or
|
|
3133
3285
|
* a mat4 frame. Declarative only — the bridge reads this to resolve the
|
|
@@ -3189,7 +3341,22 @@ class PoseHelm {
|
|
|
3189
3341
|
*/
|
|
3190
3342
|
step(out, dt, basis) {
|
|
3191
3343
|
const p = this.profile, dz = this.deadzone;
|
|
3192
|
-
|
|
3344
|
+
let lin = this._lin, ang = this._ang;
|
|
3345
|
+
|
|
3346
|
+
// Value layer — condition the fed rates BEFORE the deadzone (the filter
|
|
3347
|
+
// narrows the jitter band; the deadzone then gates the residual to exact
|
|
3348
|
+
// zero). The filter carries state, so it is ticked exactly once per step;
|
|
3349
|
+
// activity() reads the raw fed rate and is left unfiltered (the read-out
|
|
3350
|
+
// shows device input, normalized by fullScale). Skipped when filter is null.
|
|
3351
|
+
const filter = this.filter;
|
|
3352
|
+
if (filter) {
|
|
3353
|
+
_f6raw[0] = lin[0]; _f6raw[1] = lin[1]; _f6raw[2] = lin[2];
|
|
3354
|
+
_f6raw[3] = ang[0]; _f6raw[4] = ang[1]; _f6raw[5] = ang[2];
|
|
3355
|
+
filter(_f6out, _f6raw, dt);
|
|
3356
|
+
_fLin[0] = _f6out[0]; _fLin[1] = _f6out[1]; _fLin[2] = _f6out[2];
|
|
3357
|
+
_fAng[0] = _f6out[3]; _fAng[1] = _f6out[4]; _fAng[2] = _f6out[5];
|
|
3358
|
+
lin = _fLin; ang = _fAng;
|
|
3359
|
+
}
|
|
3193
3360
|
|
|
3194
3361
|
// Basis axes (right / up / forward). No basis ⇒ the identity eye matrix:
|
|
3195
3362
|
// right +X, up +Y, forward −Z. Forward is −col2 (an eye→world matrix stores
|
|
@@ -3288,10 +3455,73 @@ class PoseHelm {
|
|
|
3288
3455
|
}
|
|
3289
3456
|
this._lin[0] = this._lin[1] = this._lin[2] = 0;
|
|
3290
3457
|
this._ang[0] = this._ang[1] = this._ang[2] = 0;
|
|
3458
|
+
if (this.filter) this.filter.reset(); // drop filter state at the discontinuity
|
|
3291
3459
|
return this;
|
|
3292
3460
|
}
|
|
3293
3461
|
}
|
|
3294
3462
|
|
|
3463
|
+
// =========================================================================
|
|
3464
|
+
// poseDelta — absolute pose → 6-DOF rate
|
|
3465
|
+
// =========================================================================
|
|
3466
|
+
|
|
3467
|
+
/**
|
|
3468
|
+
* Difference two consecutive absolute poses into a 6-DOF rate the helm can be
|
|
3469
|
+
* fed — the bridge for absolute-pose transports (gesture / landmark / marker /
|
|
3470
|
+
* IMU) into feed(lin, ang). Out-first, zero-allocation.
|
|
3471
|
+
*
|
|
3472
|
+
* Linear rate is (cur.pos − prev.pos) / dt. Angular rate is the world-frame
|
|
3473
|
+
* angular velocity ω carrying prev.rot onto cur.rot over dt: the relative
|
|
3474
|
+
* rotation r = cur · conj(prev) (so cur = r · prev, the world-frame compose the
|
|
3475
|
+
* step integrates), read as axis · angle / dt.
|
|
3476
|
+
*
|
|
3477
|
+
* The double-cover guard is the whole reason to ship this rather than inline it:
|
|
3478
|
+
* a quaternion and its negation are the same orientation, so when
|
|
3479
|
+
* dot(prev.rot, cur.rot) < 0 the two samples sit on opposite hemispheres and a
|
|
3480
|
+
* naive difference takes the long way round — the angular rate spikes at the
|
|
3481
|
+
* crossing. Flipping cur into prev's hemisphere first keeps r the shortest arc.
|
|
3482
|
+
*
|
|
3483
|
+
* Fed through a helm with an identity profile in WORLD, the integrated pose
|
|
3484
|
+
* retraces the source (the round-trip e9 asserts). A 1:1, non-integrated
|
|
3485
|
+
* consumer skips the helm and applyPoses the absolute pose directly.
|
|
3486
|
+
*
|
|
3487
|
+
* @param {{ lin:number[], ang:number[] }} [out] Destination; omit for a fresh one.
|
|
3488
|
+
* @param {{ pos:ArrayLike<number>, rot:ArrayLike<number> }} prev Previous pose.
|
|
3489
|
+
* @param {{ pos:ArrayLike<number>, rot:ArrayLike<number> }} cur Current pose.
|
|
3490
|
+
* @param {number} dt Elapsed seconds between the two samples.
|
|
3491
|
+
* @returns {{ lin:number[], ang:number[] }} out
|
|
3492
|
+
*/
|
|
3493
|
+
function poseDelta(out, prev, cur, dt) {
|
|
3494
|
+
out = out || { lin: [0, 0, 0], ang: [0, 0, 0] };
|
|
3495
|
+
const inv = 1 / dt;
|
|
3496
|
+
|
|
3497
|
+
out.lin[0] = (cur.pos[0] - prev.pos[0]) * inv;
|
|
3498
|
+
out.lin[1] = (cur.pos[1] - prev.pos[1]) * inv;
|
|
3499
|
+
out.lin[2] = (cur.pos[2] - prev.pos[2]) * inv;
|
|
3500
|
+
|
|
3501
|
+
const px = prev.rot[0], py = prev.rot[1], pz = prev.rot[2], pw = prev.rot[3];
|
|
3502
|
+
let cx = cur.rot[0], cy = cur.rot[1], cz = cur.rot[2], cw = cur.rot[3];
|
|
3503
|
+
|
|
3504
|
+
// Double-cover guard: bring cur into prev's hemisphere so r is the short arc.
|
|
3505
|
+
if (px * cx + py * cy + pz * cz + pw * cw < 0) { cx = -cx; cy = -cy; cz = -cz; cw = -cw; }
|
|
3506
|
+
|
|
3507
|
+
// Relative rotation r = cur · conj(prev), conj(prev) = (−px, −py, −pz, pw).
|
|
3508
|
+
const rx = -cw * px + cx * pw - cy * pz + cz * py;
|
|
3509
|
+
const ry = -cw * py + cx * pz + cy * pw - cz * px;
|
|
3510
|
+
const rz = -cw * pz - cx * py + cy * px + cz * pw;
|
|
3511
|
+
const rw = cw * pw + cx * px + cy * py + cz * pz; // = dot(prev, cur) ≥ 0 → angle ≤ π
|
|
3512
|
+
|
|
3513
|
+
// Axis · angle of r → ω = axis · angle / dt. Below the threshold the rotation
|
|
3514
|
+
// is negligible (no well-defined axis) and the angular rate is exact zero.
|
|
3515
|
+
const sinHalf = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
3516
|
+
if (sinHalf < 1e-8) {
|
|
3517
|
+
out.ang[0] = 0; out.ang[1] = 0; out.ang[2] = 0;
|
|
3518
|
+
} else {
|
|
3519
|
+
const k = (2 * Math.atan2(sinHalf, rw)) * inv / sinHalf;
|
|
3520
|
+
out.ang[0] = rx * k; out.ang[1] = ry * k; out.ang[2] = rz * k;
|
|
3521
|
+
}
|
|
3522
|
+
return out;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3295
3525
|
/**
|
|
3296
3526
|
* @file Frustum planes and visibility tests — zero allocations.
|
|
3297
3527
|
* @module tree/visibility
|
|
@@ -3451,5 +3681,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
3451
3681
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
3452
3682
|
}
|
|
3453
3683
|
|
|
3454
|
-
export { AXIS, CameraTrack, Constraint, DIAL, DIRECTION, EYE, HELM_CHANNELS, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, POINT, PoseHelm, PoseTrack, SCREEN, SELF, SEMIVISIBLE, SPHERE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, azElFromDir, boxVisibility, createConstraint, dirFromAzEl, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4Eye, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Persp, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4View, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qConjugate, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qFromUnitVectors, qMul, qNegate, qNlerp, qNormalize, qRotateVec3, qSet, qSlerp, qToAxisAngle, qToMat4, rayClosestPointOnAxis, rayPlane, raySphere, sphereVisibility, transformToMat4 };
|
|
3684
|
+
export { AXIS, CameraTrack, Constraint, DIAL, DIRECTION, EYE, HELM_CHANNELS, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, POINT, PoseHelm, PoseTrack, SCREEN, SELF, SEMIVISIBLE, SPHERE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, azElFromDir, boxVisibility, createConstraint, dirFromAzEl, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4Eye, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Persp, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4View, oneEuro, pixelRatio, pointVisibility, poseDelta, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qConjugate, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qFromUnitVectors, qMul, qNegate, qNlerp, qNormalize, qRotateVec3, qSet, qSlerp, qToAxisAngle, qToMat4, rayClosestPointOnAxis, rayPlane, raySphere, sphereVisibility, transformToMat4 };
|
|
3455
3685
|
//# sourceMappingURL=index.js.map
|