@nakednous/tree 0.0.24 → 0.0.26
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 +73 -7
- package/dist/index.js +338 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@nakednous/tree`
|
|
2
2
|
|
|
3
|
-
Pure numeric core for animation, coordinate-space mapping, and visibility — **zero dependencies**, runs anywhere.
|
|
3
|
+
Pure numeric core for animation, rate-driven control, coordinate-space mapping, and visibility — **zero dependencies**, runs anywhere.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -33,13 +33,14 @@ import * as tree from '@nakednous/tree'
|
|
|
33
33
|
|
|
34
34
|
The dependency direction is strict: `@nakednous/tree` never imports from the bridge or the DOM layer. This is what lets the same `PoseTrack` that drives a camera path also animate any object — headless, server-side, or in a future renderer.
|
|
35
35
|
|
|
36
|
-
Source is organised into
|
|
36
|
+
Source is organised into six focused modules:
|
|
37
37
|
|
|
38
38
|
```
|
|
39
39
|
form.js — you have specs, you want a matrix
|
|
40
40
|
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
|
+
helm.js — 6-DOF rate-stream integrator — the Track family's live-input sibling
|
|
43
44
|
handle.js — constraint solver + ray primitives for interactive manipulators
|
|
44
45
|
```
|
|
45
46
|
|
|
@@ -296,6 +297,68 @@ One-keyframe behaviour: `play()` with exactly one keyframe snaps `eval()` to tha
|
|
|
296
297
|
|
|
297
298
|
---
|
|
298
299
|
|
|
300
|
+
### PoseHelm — 6-DOF rate-driven pose
|
|
301
|
+
|
|
302
|
+
The rate-stream sibling of the Track family. Where a track produces a pose from keyframes over time, a `PoseHelm` produces one from a live 6-DOF delta stream — a SpaceNavigator, a tracked hand, an agent policy. It holds a profile plus the integrated pose; there is no timeline (no keyframes, no `play` / `seek` / `loop`), and it never learns about a camera — the host hands it a resolved `basis` each step.
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
import { PoseHelm } from '@nakednous/tree'
|
|
306
|
+
|
|
307
|
+
const helm = new PoseHelm()
|
|
308
|
+
const out = { pos: [0,0,0], rot: [0,0,0,1] }
|
|
309
|
+
|
|
310
|
+
// a transport feeds raw lane rates — either half may be omitted:
|
|
311
|
+
helm.feed([tx, ty, tz], [rx, ry, rz])
|
|
312
|
+
|
|
313
|
+
// per-frame — host-driven, zero allocation:
|
|
314
|
+
helm.step(out, dt, basis) // integrate dt seconds, write the new { pos, rot }
|
|
315
|
+
helm.eval(out) // read the current pose without integrating
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`feed` is the input (as `add` is a track's); `step` + `eval` parallel `tick` + `eval`. `step` is host-driven — the bridge calls it each frame, exactly as a sketch never calls `track.tick()`.
|
|
319
|
+
|
|
320
|
+
**Profile — sign · sens · lane.** The whole sign / sensitivity / axis-map question is one flat declarative object. Six channels — three translation (Tx Ty Tz), three rotation (Rp pitch, Ry yaw, Rr roll) — each `{ sign, sens, lane }`:
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
helm.profile = {
|
|
324
|
+
Tx: { sign: +1, sens: 0.30, lane: 0 }, // lane = which fed channel drives +X
|
|
325
|
+
Ty: { sign: +1, sens: 0.30, lane: 2 },
|
|
326
|
+
Tz: { sign: -1, sens: 0.30, lane: 1 },
|
|
327
|
+
Rp: { sign: -1, sens: 0.0025, lane: 0 },
|
|
328
|
+
Ry: { sign: -1, sens: 0.0025, lane: 2 },
|
|
329
|
+
Rr: { sign: +1, sens: 0.0018, lane: 1 },
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- **sign** — per-app direction (camera-fly vs object-grab invert).
|
|
334
|
+
- **sens** — per-axis sensitivity (tame roll without touching the rest).
|
|
335
|
+
- **lane** — input-channel permutation: which fed channel drives this DOF. `T*` lanes index the translation triple, `R*` the rotation triple.
|
|
336
|
+
|
|
337
|
+
`sens` does all the scaling, so the same raw `feed()` suits any transport — only the profile changes. The default is SpaceNavigator-tuned and meant to be replaced wholesale for a different device. `HELM_CHANNELS` is the frozen order `['Tx','Ty','Tz','Rp','Ry','Rr']`.
|
|
338
|
+
|
|
339
|
+
**Frame — `from`.** `helm.from` names the space fed rates are interpreted in — a declaration the host reads to resolve the per-step `basis` (the core stays camera-agnostic):
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
WORLD world axes — the identity basis (step's basis is null)
|
|
343
|
+
EYE a viewing camera's frame — screen-relative (default)
|
|
344
|
+
SELF the helm's OWN evolving pose — body-relative
|
|
345
|
+
<mat4> an explicit fixed frame
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`step` rotates both linear and angular rates through `basis`, then composes the quaternion world-frame — one code path covering body-fly and screen-relative manipulation. `SELF` is body-relative (a per-frame-rebuilt pose matrix); it is a helm `from` value only, not a general mapping space.
|
|
349
|
+
|
|
350
|
+
**Rest of the surface.**
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
helm.deadzone = 8 // rest-drift floor — |rate| ≤ deadzone reads as 0
|
|
354
|
+
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
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
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
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
299
362
|
### Coordinate-space mapping
|
|
300
363
|
|
|
301
364
|
`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.
|
|
@@ -376,19 +439,19 @@ Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INV
|
|
|
376
439
|
`handle.js` is the renderer-agnostic core of an interactive manipulator: ray-primitive intersections, az/el utilities, and a `Constraint` state machine. The `p5.tree` bridge wraps these into a draggable handle; this package supplies the math and the **contract** that makes the handle extensible.
|
|
377
440
|
|
|
378
441
|
```js
|
|
379
|
-
import { createConstraint, SPHERE, PLANE, AXIS, POINT, DIRECTION,
|
|
442
|
+
import { createConstraint, SPHERE, PLANE, AXIS, DIAL, POINT, DIRECTION,
|
|
380
443
|
raySphere, rayPlane, rayClosestPointOnAxis,
|
|
381
444
|
dirFromAzEl, azElFromDir } from '@nakednous/tree'
|
|
382
445
|
|
|
383
|
-
const c = createConstraint(SPHERE, { radius: 1 }) // or PLANE / AXIS
|
|
446
|
+
const c = createConstraint(SPHERE, { radius: 1 }) // or PLANE / AXIS / DIAL
|
|
384
447
|
const out = [0, 0, 0]
|
|
385
448
|
c.solve(ox,oy,oz, dx,dy,dz) // ray (working space) → canonical state; chainable
|
|
386
449
|
c.value(out, DIRECTION) // write the reported value into out(3)
|
|
387
450
|
```
|
|
388
451
|
|
|
389
|
-
`SPHERE` stores a unit direction (gimbal-free); `PLANE` / `AXIS` store a constrained point. `value` reports a `DIRECTION` (unit) or a `POINT` per kind. Ray primitives are out-first and assume a unit ray direction; `rayPlane` returns `Infinity` when the ray is parallel.
|
|
452
|
+
`SPHERE` stores a unit direction (gimbal-free); `PLANE` / `AXIS` store a constrained point; `DIAL` stores an accumulated angle θ (multi-turn winding preserved). `value` reports a `DIRECTION` (unit) or a `POINT` per kind. `aim(ax,ay,az[, zx,zy,zz])` re-aims the constraint basis in the working space — `PLANE` takes a new normal (point re-projected), `AXIS` a new direction (`t` preserved), `DIAL` a new plane normal plus optional θ=0 reference (θ preserved) — the seam the `p5.tree` bridge's deferred `from` frame drives. Ray primitives are out-first and assume a unit ray direction; `rayPlane` returns `Infinity` when the ray is parallel.
|
|
390
453
|
|
|
391
|
-
**Constraint contract (extension seam).** A constraint is any object exposing `kind`, `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and optionally `scalar()` / `azEl(out2)`. The handle controller drives any conforming constraint, so a new kind — rotation, 6-DOF, or app-specific — implements this contract (portable, draw-free) plus a bridge-side locus draw, rather than forking the controller. The built-in `Constraint` is the reference implementation. Full design: [`handle-design.md`](./handle-design.md).
|
|
454
|
+
**Constraint contract (extension seam).** A constraint is any object exposing `kind`, `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])`. The handle controller drives any conforming constraint, so a new kind — rotation, 6-DOF, or app-specific — implements this contract (portable, draw-free) plus a bridge-side locus draw, rather than forking the controller. The built-in `Constraint` is the reference implementation. Full design: [`handle-design.md`](./handle-design.md).
|
|
392
455
|
|
|
393
456
|
---
|
|
394
457
|
|
|
@@ -455,6 +518,9 @@ projLeft projRight projTop projBottom
|
|
|
455
518
|
// Coordinate spaces
|
|
456
519
|
WORLD, EYE, NDC, SCREEN, MODEL, MATRIX
|
|
457
520
|
|
|
521
|
+
// Helm integrator frame (helm `from` only — body-relative, not a mapping space)
|
|
522
|
+
SELF
|
|
523
|
+
|
|
458
524
|
// NDC Z convention
|
|
459
525
|
WEBGL // −1 (z ∈ [−1, 1])
|
|
460
526
|
WEBGPU // 0 (z ∈ [0, 1])
|
|
@@ -463,7 +529,7 @@ WEBGPU // 0 (z ∈ [0, 1])
|
|
|
463
529
|
INVISIBLE, VISIBLE, SEMIVISIBLE
|
|
464
530
|
|
|
465
531
|
// Manipulator constraint kinds & report modes
|
|
466
|
-
SPHERE, PLANE, AXIS
|
|
532
|
+
SPHERE, PLANE, AXIS, DIAL
|
|
467
533
|
POINT, DIRECTION
|
|
468
534
|
|
|
469
535
|
// Basis vectors (frozen)
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,10 @@ const SCREEN = 'SCREEN';
|
|
|
12
12
|
const MODEL = 'MODEL';
|
|
13
13
|
const MATRIX = 'MATRIX';
|
|
14
14
|
|
|
15
|
+
// Integrator frame source (helm `from` only): the construct's own current pose
|
|
16
|
+
// — body-relative; not a general mapDirection space.
|
|
17
|
+
const SELF = 'SELF';
|
|
18
|
+
|
|
15
19
|
// NDC Z convention (only difference between backends)
|
|
16
20
|
const WEBGL = -1; // z ∈ [−1, 1]
|
|
17
21
|
const WEBGPU = 0; // z ∈ [0, 1]
|
|
@@ -2506,7 +2510,9 @@ class CameraTrack extends Track {
|
|
|
2506
2510
|
* ── Extension contract ─────────────────────────────────────────────────────
|
|
2507
2511
|
* A constraint is any object exposing: `kind` (integer discriminant),
|
|
2508
2512
|
* `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
|
|
2509
|
-
* optionally `scalar()` / `azEl(out2)
|
|
2513
|
+
* optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])` — the
|
|
2514
|
+
* basis re-aim seam the bridge's deferred `from` frame drives (§4.13).
|
|
2515
|
+
* The p5.tree handle controller drives
|
|
2510
2516
|
* any conforming constraint (lifecycle, frame conversion, bind, hooks, pick);
|
|
2511
2517
|
* a new kind — 6-DOF, or app-specific — implements this contract here
|
|
2512
2518
|
* (portable, draw-free) plus a bridge-side locus/pick draw (`drawLocus` /
|
|
@@ -2735,22 +2741,7 @@ class Constraint {
|
|
|
2735
2741
|
this.r1 = [0, 0, 1];
|
|
2736
2742
|
if (kind === DIAL) {
|
|
2737
2743
|
const z = _vec3(opts.zero);
|
|
2738
|
-
|
|
2739
|
-
// Project the supplied reference onto the dial plane.
|
|
2740
|
-
const d = z[0]*this.u[0] + z[1]*this.u[1] + z[2]*this.u[2];
|
|
2741
|
-
this.r0[0] = z[0] - d*this.u[0];
|
|
2742
|
-
this.r0[1] = z[1] - d*this.u[1];
|
|
2743
|
-
this.r0[2] = z[2] - d*this.u[2];
|
|
2744
|
-
const l = Math.sqrt(this.r0[0]**2 + this.r0[1]**2 + this.r0[2]**2);
|
|
2745
|
-
if (l < EPS) _basis(this.u, this.r0, this.r1);
|
|
2746
|
-
else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
|
|
2747
|
-
} else {
|
|
2748
|
-
_basis(this.u, this.r0, this.r1);
|
|
2749
|
-
}
|
|
2750
|
-
// r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
|
|
2751
|
-
this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
|
|
2752
|
-
this.r1[1] = this.u[2]*this.r0[0] - this.u[0]*this.r0[2];
|
|
2753
|
-
this.r1[2] = this.u[0]*this.r0[1] - this.u[1]*this.r0[0];
|
|
2744
|
+
this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
|
|
2754
2745
|
}
|
|
2755
2746
|
|
|
2756
2747
|
// Extent: AXIS clamps t (default [-1, 1]); DIAL clamps θ in radians
|
|
@@ -2789,6 +2780,28 @@ class Constraint {
|
|
|
2789
2780
|
this.pt[2] = this.anchor[2] + c*this.r0[2] + sn*this.r1[2];
|
|
2790
2781
|
}
|
|
2791
2782
|
|
|
2783
|
+
// Build the DIAL in-plane basis (r0, r1) from the current axis u and an
|
|
2784
|
+
// optional θ=0 reference (zx,zy,zz): the reference is projected onto the
|
|
2785
|
+
// dial plane and normalised; absent (NaN) or degenerate, r0 derives from u
|
|
2786
|
+
// via the least-aligned-axis seed. r1 = u × r0, right-handed about u.
|
|
2787
|
+
_dialBasis(zx, zy, zz) {
|
|
2788
|
+
if (_isNum(zx) && _isNum(zy) && _isNum(zz)) {
|
|
2789
|
+
const d = zx*this.u[0] + zy*this.u[1] + zz*this.u[2];
|
|
2790
|
+
this.r0[0] = zx - d*this.u[0];
|
|
2791
|
+
this.r0[1] = zy - d*this.u[1];
|
|
2792
|
+
this.r0[2] = zz - d*this.u[2];
|
|
2793
|
+
const l = Math.sqrt(this.r0[0]**2 + this.r0[1]**2 + this.r0[2]**2);
|
|
2794
|
+
if (l < EPS) _basis(this.u, this.r0, this.r1);
|
|
2795
|
+
else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
|
|
2796
|
+
} else {
|
|
2797
|
+
_basis(this.u, this.r0, this.r1);
|
|
2798
|
+
}
|
|
2799
|
+
// r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
|
|
2800
|
+
this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
|
|
2801
|
+
this.r1[1] = this.u[2]*this.r0[0] - this.u[0]*this.r0[2];
|
|
2802
|
+
this.r1[2] = this.u[0]*this.r0[1] - this.u[1]*this.r0[0];
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2792
2805
|
/**
|
|
2793
2806
|
* Update the canonical state from a ray in the working space. The ray
|
|
2794
2807
|
* direction is assumed unit. Chainable.
|
|
@@ -2959,6 +2972,46 @@ class Constraint {
|
|
|
2959
2972
|
}
|
|
2960
2973
|
return this;
|
|
2961
2974
|
}
|
|
2975
|
+
|
|
2976
|
+
/**
|
|
2977
|
+
* Re-aim the constraint basis in the working space — the deferred-frame
|
|
2978
|
+
* seam (the p5.tree bridge's `from` opt resolves its symbolic basis through
|
|
2979
|
+
* mapDirection and calls this). Per kind:
|
|
2980
|
+
* PLANE — new normal; the point is re-projected onto the new plane.
|
|
2981
|
+
* AXIS — new direction; the scalar t is preserved, the point recomputed.
|
|
2982
|
+
* DIAL — new plane normal + optional θ=0 reference; θ is preserved, the
|
|
2983
|
+
* in-plane basis rebuilt (reference re-derived when omitted),
|
|
2984
|
+
* the point recomputed.
|
|
2985
|
+
* SPHERE — no basis; no-op.
|
|
2986
|
+
* Inputs are normalised; a zero-length axis keeps the previous one.
|
|
2987
|
+
* Chainable.
|
|
2988
|
+
*
|
|
2989
|
+
* @param {number} ax,ay,az New normal (PLANE) / direction (AXIS) / dial-plane normal (DIAL).
|
|
2990
|
+
* @param {number} [zx,zy,zz] DIAL only — θ=0 reference (re-derived when omitted).
|
|
2991
|
+
* @returns {Constraint} this
|
|
2992
|
+
*/
|
|
2993
|
+
aim(ax, ay, az, zx, zy, zz) {
|
|
2994
|
+
if (this.kind === PLANE) {
|
|
2995
|
+
const px = this.n[0], py = this.n[1], pz = this.n[2];
|
|
2996
|
+
this.n[0] = ax; this.n[1] = ay; this.n[2] = az;
|
|
2997
|
+
_unit(this.n, px, py, pz);
|
|
2998
|
+
this.seed(this.pt[0], this.pt[1], this.pt[2]);
|
|
2999
|
+
} else if (this.kind === AXIS) {
|
|
3000
|
+
const px = this.u[0], py = this.u[1], pz = this.u[2];
|
|
3001
|
+
this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
|
|
3002
|
+
_unit(this.u, px, py, pz);
|
|
3003
|
+
this.pt[0] = this.anchor[0] + this.s*this.u[0];
|
|
3004
|
+
this.pt[1] = this.anchor[1] + this.s*this.u[1];
|
|
3005
|
+
this.pt[2] = this.anchor[2] + this.s*this.u[2];
|
|
3006
|
+
} else if (this.kind === DIAL) {
|
|
3007
|
+
const px = this.u[0], py = this.u[1], pz = this.u[2];
|
|
3008
|
+
this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
|
|
3009
|
+
_unit(this.u, px, py, pz);
|
|
3010
|
+
this._dialBasis(zx, zy, zz);
|
|
3011
|
+
this._dialPoint();
|
|
3012
|
+
}
|
|
3013
|
+
return this;
|
|
3014
|
+
}
|
|
2962
3015
|
}
|
|
2963
3016
|
|
|
2964
3017
|
/**
|
|
@@ -2972,6 +3025,273 @@ function createConstraint(kind, opts) {
|
|
|
2972
3025
|
return new Constraint(kind, opts);
|
|
2973
3026
|
}
|
|
2974
3027
|
|
|
3028
|
+
/**
|
|
3029
|
+
* @file Source-agnostic 6-DOF pose helm — integrates a live rate stream into a
|
|
3030
|
+
* { pos, rot } pose. Renderer- and transport-agnostic. Zero dependencies.
|
|
3031
|
+
* @module tree/helm
|
|
3032
|
+
* @license AGPL-3.0-only
|
|
3033
|
+
*
|
|
3034
|
+
* A helm is the rate-stream sibling of the Track family: where a Track produces
|
|
3035
|
+
* a pose from keyframes over time, a helm produces a pose from a live 6-DOF
|
|
3036
|
+
* delta stream (a SpaceNavigator, a tracked hand, an agent policy). It is NOT a
|
|
3037
|
+
* handle — a handle reports a `vec3` solved from a pointer ray; a helm reports a
|
|
3038
|
+
* pose (position + orientation).
|
|
3039
|
+
*
|
|
3040
|
+
* Family placement (peer of PoseTrack, NOT a Track subclass — no timeline):
|
|
3041
|
+
* feed(translation, rotation) push the latest raw device rate (input, as Track.add)
|
|
3042
|
+
* step(out, dt, basis) integrate the rate by dt into the pose (BRIDGE-DRIVEN,
|
|
3043
|
+
* as Track.tick) — writes the new pose into out
|
|
3044
|
+
* eval(out) read the current { pos, rot } (zero-alloc, as Track.eval)
|
|
3045
|
+
* home(pose?) re-home the integrated pose (NOT reset — no keyframes to clear)
|
|
3046
|
+
*
|
|
3047
|
+
* Quaternion algebra is provided by quat.js. Out-first throughout; no allocation
|
|
3048
|
+
* in feed/step/eval. Storage convention (matches the rest of the core): vec3 and
|
|
3049
|
+
* quat state are plain number[] (f64) — the same shape as track.js keyframes and
|
|
3050
|
+
* the frozen basis-vector constants.
|
|
3051
|
+
*
|
|
3052
|
+
* ── Frame discipline (`from` / `basis`) ──────────────────────────────────────
|
|
3053
|
+
* The space the rates are interpreted in is named by `from` (WORLD | EYE | mat4),
|
|
3054
|
+
* a declaration the BRIDGE reads — the core never learns about a camera. The
|
|
3055
|
+
* resolved orientation arrives per-step as `basis`: an eye→world mat4 (or null
|
|
3056
|
+
* for WORLD ≡ the identity basis). Both the linear and angular rates are rotated
|
|
3057
|
+
* through that basis, then the quaternion is composed world-frame. This is the
|
|
3058
|
+
* single code path that covers both manipulation conventions:
|
|
3059
|
+
*
|
|
3060
|
+
* - camera body-fly — basis is the driven camera's own eye matrix (which this
|
|
3061
|
+
* helm produced last frame ⇒ equals the integrated `q`,
|
|
3062
|
+
* zero staleness); rotating a body delta through `q` then
|
|
3063
|
+
* world-composing is algebraically a body-frame compose.
|
|
3064
|
+
* - screen-relative — basis is the *viewing* camera's eye matrix (an external
|
|
3065
|
+
* frame); a push moves the target relative to the screen.
|
|
3066
|
+
*
|
|
3067
|
+
* Per-channel `from` ({ translation, rotation }) is a deferred, non-breaking
|
|
3068
|
+
* extension (one basis suffices today — pose-helm-design.md §8.3).
|
|
3069
|
+
*/
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
// =========================================================================
|
|
3073
|
+
// Module-level scratch — shared across helm instances (non-reentrant; step
|
|
3074
|
+
// is synchronous and called at most once per helm per frame).
|
|
3075
|
+
// =========================================================================
|
|
3076
|
+
|
|
3077
|
+
const _dq = [0, 0, 0, 1];
|
|
3078
|
+
|
|
3079
|
+
// Deadzone gate — |v| > dz keeps v, else 0. Module-level so step allocates no
|
|
3080
|
+
// closure. Strictly-greater matches the e7 reference (rest reads exact 0).
|
|
3081
|
+
const _dz = (v, dz) => (v > dz || v < -dz) ? v : 0;
|
|
3082
|
+
|
|
3083
|
+
/**
|
|
3084
|
+
* Build the canonical default profile. SpaceNavigator-tuned lane permutation
|
|
3085
|
+
* and the eye-frame manipulation feel found in the e7 experiments; it is the
|
|
3086
|
+
* per-app / per-device mapping layer and is meant to be overwritten wholesale
|
|
3087
|
+
* (`helm.profile = { … }`) for a different transport.
|
|
3088
|
+
*
|
|
3089
|
+
* sign — per-app direction (camera-fly vs object-grab invert; flip live).
|
|
3090
|
+
* sens — per-axis sensitivity (tame roll without touching the rest).
|
|
3091
|
+
* lane — input-channel permutation: which fed channel drives this DOF.
|
|
3092
|
+
* T* lanes index the translation triple; R* lanes the rotation triple.
|
|
3093
|
+
*/
|
|
3094
|
+
function _defaultProfile() {
|
|
3095
|
+
return {
|
|
3096
|
+
Tx: { sign: 1, sens: 0.30, lane: 0 },
|
|
3097
|
+
Ty: { sign: 1, sens: 0.30, lane: 2 },
|
|
3098
|
+
Tz: { sign: -1, sens: 0.30, lane: 1 },
|
|
3099
|
+
Rp: { sign: -1, sens: 0.0025, lane: 0 },
|
|
3100
|
+
Ry: { sign: -1, sens: 0.0025, lane: 2 },
|
|
3101
|
+
Rr: { sign: 1, sens: 0.0018, lane: 1 },
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
/** Channel order — readout / iteration convenience (Tx Ty Tz Rp Ry Rr). */
|
|
3106
|
+
const HELM_CHANNELS = Object.freeze(['Tx', 'Ty', 'Tz', 'Rp', 'Ry', 'Rr']);
|
|
3107
|
+
|
|
3108
|
+
// =========================================================================
|
|
3109
|
+
// PoseHelm
|
|
3110
|
+
// =========================================================================
|
|
3111
|
+
|
|
3112
|
+
/**
|
|
3113
|
+
* Source-agnostic 6-DOF pose producer. Holds a profile + the integrated pose;
|
|
3114
|
+
* no timeline (no keyframes, no play/seek/loop).
|
|
3115
|
+
*/
|
|
3116
|
+
class PoseHelm {
|
|
3117
|
+
constructor() {
|
|
3118
|
+
/**
|
|
3119
|
+
* DOF mapping table. Six channels, each `{ sign, sens, lane }`.
|
|
3120
|
+
* Public and mutable — set wholesale to re-map a device or app.
|
|
3121
|
+
* @type {{ Tx:Object, Ty:Object, Tz:Object, Rp:Object, Ry:Object, Rr:Object }}
|
|
3122
|
+
*/
|
|
3123
|
+
this.profile = _defaultProfile();
|
|
3124
|
+
|
|
3125
|
+
/**
|
|
3126
|
+
* Rest-drift floor. A fed rate with |value| ≤ deadzone reads as 0. Devices
|
|
3127
|
+
* that rest at exact 0 need only a small floor. @type {number}
|
|
3128
|
+
*/
|
|
3129
|
+
this.deadzone = 8;
|
|
3130
|
+
|
|
3131
|
+
/**
|
|
3132
|
+
* The space fed rates are interpreted in: a space constant (WORLD | EYE) or
|
|
3133
|
+
* a mat4 frame. Declarative only — the bridge reads this to resolve the
|
|
3134
|
+
* per-step `basis`; the core stays camera-agnostic. @type {string|ArrayLike<number>}
|
|
3135
|
+
*/
|
|
3136
|
+
this.from = 'EYE';
|
|
3137
|
+
|
|
3138
|
+
// Latest fed raw rates (refreshed by feed; persist until the next feed —
|
|
3139
|
+
// the transport is responsible for feeding 0 when motion should stop).
|
|
3140
|
+
this._lin = [0, 0, 0];
|
|
3141
|
+
this._ang = [0, 0, 0];
|
|
3142
|
+
|
|
3143
|
+
// Integrated pose — the source of truth.
|
|
3144
|
+
this._pos = [0, 0, 0];
|
|
3145
|
+
this._q = [0, 0, 0, 1];
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
/**
|
|
3149
|
+
* Push the latest raw 6-DOF device rate. Either argument may be omitted to
|
|
3150
|
+
* leave that half unchanged (devices report the two halves on separate
|
|
3151
|
+
* frames). The value persists until the next feed. Raw device units — the
|
|
3152
|
+
* profile's `sens` does the scaling, so the same feed() suits any transport.
|
|
3153
|
+
*
|
|
3154
|
+
* @param {ArrayLike<number>} [translation] [tx, ty, tz] raw lane rates.
|
|
3155
|
+
* @param {ArrayLike<number>} [rotation] [rx, ry, rz] raw lane rates.
|
|
3156
|
+
* @returns {PoseHelm} this
|
|
3157
|
+
*/
|
|
3158
|
+
feed(translation, rotation) {
|
|
3159
|
+
if (translation) {
|
|
3160
|
+
this._lin[0] = translation[0] || 0;
|
|
3161
|
+
this._lin[1] = translation[1] || 0;
|
|
3162
|
+
this._lin[2] = translation[2] || 0;
|
|
3163
|
+
}
|
|
3164
|
+
if (rotation) {
|
|
3165
|
+
this._ang[0] = rotation[0] || 0;
|
|
3166
|
+
this._ang[1] = rotation[1] || 0;
|
|
3167
|
+
this._ang[2] = rotation[2] || 0;
|
|
3168
|
+
}
|
|
3169
|
+
return this;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
/**
|
|
3173
|
+
* Integrate the latest fed rate by `dt` seconds into the pose, then write the
|
|
3174
|
+
* pose into `out`. Bridge-driven (the player calls it each frame; a sketch
|
|
3175
|
+
* never does, as it never calls track.tick()).
|
|
3176
|
+
*
|
|
3177
|
+
* `basis` is the resolved `from` frame: an eye→world column-major mat4
|
|
3178
|
+
* (16-element ArrayLike), or null/omitted for WORLD (the identity basis). Its
|
|
3179
|
+
* columns supply the right/up/back axes; forward is −col2. Both the linear and
|
|
3180
|
+
* angular rates are rotated through it, then `q` is composed world-frame.
|
|
3181
|
+
*
|
|
3182
|
+
* Zero-allocation. `out` is `{ pos:number[3], rot:number[4] }`; omit it for a
|
|
3183
|
+
* fresh object.
|
|
3184
|
+
*
|
|
3185
|
+
* @param {{ pos:number[], rot:number[] }} [out]
|
|
3186
|
+
* @param {number} dt Elapsed time in seconds.
|
|
3187
|
+
* @param {ArrayLike<number>} [basis] Eye→world mat4, or null for WORLD.
|
|
3188
|
+
* @returns {{ pos:number[], rot:number[] }} out
|
|
3189
|
+
*/
|
|
3190
|
+
step(out, dt, basis) {
|
|
3191
|
+
const p = this.profile, dz = this.deadzone;
|
|
3192
|
+
const lin = this._lin, ang = this._ang;
|
|
3193
|
+
|
|
3194
|
+
// Basis axes (right / up / forward). No basis ⇒ the identity eye matrix:
|
|
3195
|
+
// right +X, up +Y, forward −Z. Forward is −col2 (an eye→world matrix stores
|
|
3196
|
+
// the camera's BACK in col2), and the identity's col2 is +Z, so forward is
|
|
3197
|
+
// −Z — making step(out, dt, null) identical to step(out, dt, IDENTITY_MAT4),
|
|
3198
|
+
// as the `from` contract ("null ≡ the identity basis") promises.
|
|
3199
|
+
let rX = 1, rY = 0, rZ = 0;
|
|
3200
|
+
let uX = 0, uY = 1, uZ = 0;
|
|
3201
|
+
let fX = 0, fY = 0, fZ = -1;
|
|
3202
|
+
if (basis) {
|
|
3203
|
+
rX = basis[0]; rY = basis[1]; rZ = basis[2];
|
|
3204
|
+
uX = basis[4]; uY = basis[5]; uZ = basis[6];
|
|
3205
|
+
fX = -basis[8]; fY = -basis[9]; fZ = -basis[10];
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// Angular: three rates → one delta quat, rotated into world, world-composed.
|
|
3209
|
+
const wx = _dz(ang[p.Rp.lane], dz) * p.Rp.sign * p.Rp.sens;
|
|
3210
|
+
const wy = _dz(ang[p.Ry.lane], dz) * p.Ry.sign * p.Ry.sens;
|
|
3211
|
+
const wz = _dz(ang[p.Rr.lane], dz) * p.Rr.sign * p.Rr.sens;
|
|
3212
|
+
const w = Math.sqrt(wx * wx + wy * wy + wz * wz);
|
|
3213
|
+
if (w > 0) {
|
|
3214
|
+
// Axis in world = basis · (wx,wy,wz); its length is w (basis orthonormal),
|
|
3215
|
+
// so the rotation angle is w·dt. qFromAxisAngle renormalizes the axis.
|
|
3216
|
+
const ax = wx * rX + wy * uX + wz * fX;
|
|
3217
|
+
const ay = wx * rY + wy * uY + wz * fY;
|
|
3218
|
+
const az = wx * rZ + wy * uZ + wz * fZ;
|
|
3219
|
+
qFromAxisAngle(_dq, ax, ay, az, w * dt);
|
|
3220
|
+
qMul(this._q, _dq, this._q); // delta already in world → world-frame compose
|
|
3221
|
+
qNormalize(this._q);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// Linear: three rates rotated into world, integrated by dt.
|
|
3225
|
+
const tx = _dz(lin[p.Tx.lane], dz) * p.Tx.sign * p.Tx.sens;
|
|
3226
|
+
const ty = _dz(lin[p.Ty.lane], dz) * p.Ty.sign * p.Ty.sens;
|
|
3227
|
+
const tz = _dz(lin[p.Tz.lane], dz) * p.Tz.sign * p.Tz.sens;
|
|
3228
|
+
this._pos[0] += (tx * rX + ty * uX + tz * fX) * dt;
|
|
3229
|
+
this._pos[1] += (tx * rY + ty * uY + tz * fY) * dt;
|
|
3230
|
+
this._pos[2] += (tx * rZ + ty * uZ + tz * fZ) * dt;
|
|
3231
|
+
|
|
3232
|
+
return this.eval(out);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
/**
|
|
3236
|
+
* Read the current pose into `out` (zero-alloc). The shared output contract
|
|
3237
|
+
* that feeds applyPose — mirrors Track.eval.
|
|
3238
|
+
*
|
|
3239
|
+
* @param {{ pos:number[], rot:number[] }} [out]
|
|
3240
|
+
* @returns {{ pos:number[], rot:number[] }} out
|
|
3241
|
+
*/
|
|
3242
|
+
eval(out) {
|
|
3243
|
+
out = out || { pos: [0, 0, 0], rot: [0, 0, 0, 1] };
|
|
3244
|
+
out.pos[0] = this._pos[0]; out.pos[1] = this._pos[1]; out.pos[2] = this._pos[2];
|
|
3245
|
+
out.rot[0] = this._q[0]; out.rot[1] = this._q[1];
|
|
3246
|
+
out.rot[2] = this._q[2]; out.rot[3] = this._q[3];
|
|
3247
|
+
return out;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
/**
|
|
3251
|
+
* Write the six effective channel rates (post deadzone·sign·sens) into `out6`,
|
|
3252
|
+
* in channel order [Tx, Ty, Tz, Rp, Ry, Rr]. The gizmo reads this to light the
|
|
3253
|
+
* DOFs being driven; readouts can show signed magnitudes. Zero-alloc.
|
|
3254
|
+
*
|
|
3255
|
+
* @param {number[]} out6 6-element destination.
|
|
3256
|
+
* @returns {number[]} out6
|
|
3257
|
+
*/
|
|
3258
|
+
activity(out6) {
|
|
3259
|
+
const p = this.profile, dz = this.deadzone, lin = this._lin, ang = this._ang;
|
|
3260
|
+
out6[0] = _dz(lin[p.Tx.lane], dz) * p.Tx.sign * p.Tx.sens;
|
|
3261
|
+
out6[1] = _dz(lin[p.Ty.lane], dz) * p.Ty.sign * p.Ty.sens;
|
|
3262
|
+
out6[2] = _dz(lin[p.Tz.lane], dz) * p.Tz.sign * p.Tz.sens;
|
|
3263
|
+
out6[3] = _dz(ang[p.Rp.lane], dz) * p.Rp.sign * p.Rp.sens;
|
|
3264
|
+
out6[4] = _dz(ang[p.Ry.lane], dz) * p.Ry.sign * p.Ry.sens;
|
|
3265
|
+
out6[5] = _dz(ang[p.Rr.lane], dz) * p.Rr.sign * p.Rr.sens;
|
|
3266
|
+
return out6;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
/**
|
|
3270
|
+
* Re-home the integrated pose. Sets position + orientation to `pose` (or the
|
|
3271
|
+
* identity pose when omitted) and clears any pending rate so the helm rests at
|
|
3272
|
+
* a clean, known state. NOT a reset — there are no keyframes to clear.
|
|
3273
|
+
*
|
|
3274
|
+
* @param {{ pos?:ArrayLike<number>, rot?:ArrayLike<number> }} [pose]
|
|
3275
|
+
* @returns {PoseHelm} this
|
|
3276
|
+
*/
|
|
3277
|
+
home(pose) {
|
|
3278
|
+
if (pose && pose.pos) {
|
|
3279
|
+
this._pos[0] = pose.pos[0]; this._pos[1] = pose.pos[1]; this._pos[2] = pose.pos[2];
|
|
3280
|
+
} else {
|
|
3281
|
+
this._pos[0] = 0; this._pos[1] = 0; this._pos[2] = 0;
|
|
3282
|
+
}
|
|
3283
|
+
if (pose && pose.rot) {
|
|
3284
|
+
this._q[0] = pose.rot[0]; this._q[1] = pose.rot[1];
|
|
3285
|
+
this._q[2] = pose.rot[2]; this._q[3] = pose.rot[3];
|
|
3286
|
+
} else {
|
|
3287
|
+
this._q[0] = 0; this._q[1] = 0; this._q[2] = 0; this._q[3] = 1;
|
|
3288
|
+
}
|
|
3289
|
+
this._lin[0] = this._lin[1] = this._lin[2] = 0;
|
|
3290
|
+
this._ang[0] = this._ang[1] = this._ang[2] = 0;
|
|
3291
|
+
return this;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
2975
3295
|
/**
|
|
2976
3296
|
* @file Frustum planes and visibility tests — zero allocations.
|
|
2977
3297
|
* @module tree/visibility
|
|
@@ -3131,5 +3451,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
3131
3451
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
3132
3452
|
}
|
|
3133
3453
|
|
|
3134
|
-
export { AXIS, CameraTrack, Constraint, DIAL, DIRECTION, EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, POINT, PoseTrack, SCREEN, 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 };
|
|
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 };
|
|
3135
3455
|
//# sourceMappingURL=index.js.map
|