@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 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
- const lin = this._lin, ang = this._ang;
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