@nakednous/tree 0.0.24 → 0.0.25

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
@@ -376,19 +376,19 @@ Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INV
376
376
  `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
377
 
378
378
  ```js
379
- import { createConstraint, SPHERE, PLANE, AXIS, POINT, DIRECTION,
379
+ import { createConstraint, SPHERE, PLANE, AXIS, DIAL, POINT, DIRECTION,
380
380
  raySphere, rayPlane, rayClosestPointOnAxis,
381
381
  dirFromAzEl, azElFromDir } from '@nakednous/tree'
382
382
 
383
- const c = createConstraint(SPHERE, { radius: 1 }) // or PLANE / AXIS
383
+ const c = createConstraint(SPHERE, { radius: 1 }) // or PLANE / AXIS / DIAL
384
384
  const out = [0, 0, 0]
385
385
  c.solve(ox,oy,oz, dx,dy,dz) // ray (working space) → canonical state; chainable
386
386
  c.value(out, DIRECTION) // write the reported value into out(3)
387
387
  ```
388
388
 
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.
389
+ `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
390
 
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).
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)` / `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
392
 
393
393
  ---
394
394
 
@@ -463,7 +463,7 @@ WEBGPU // 0 (z ∈ [0, 1])
463
463
  INVISIBLE, VISIBLE, SEMIVISIBLE
464
464
 
465
465
  // Manipulator constraint kinds & report modes
466
- SPHERE, PLANE, AXIS
466
+ SPHERE, PLANE, AXIS, DIAL
467
467
  POINT, DIRECTION
468
468
 
469
469
  // Basis vectors (frozen)
package/dist/index.js CHANGED
@@ -2506,7 +2506,9 @@ class CameraTrack extends Track {
2506
2506
  * ── Extension contract ─────────────────────────────────────────────────────
2507
2507
  * A constraint is any object exposing: `kind` (integer discriminant),
2508
2508
  * `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
2509
- * optionally `scalar()` / `azEl(out2)`. The p5.tree handle controller drives
2509
+ * optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])` the
2510
+ * basis re-aim seam the bridge's deferred `from` frame drives (§4.13).
2511
+ * The p5.tree handle controller drives
2510
2512
  * any conforming constraint (lifecycle, frame conversion, bind, hooks, pick);
2511
2513
  * a new kind — 6-DOF, or app-specific — implements this contract here
2512
2514
  * (portable, draw-free) plus a bridge-side locus/pick draw (`drawLocus` /
@@ -2735,22 +2737,7 @@ class Constraint {
2735
2737
  this.r1 = [0, 0, 1];
2736
2738
  if (kind === DIAL) {
2737
2739
  const z = _vec3(opts.zero);
2738
- if (z) {
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];
2740
+ this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
2754
2741
  }
2755
2742
 
2756
2743
  // Extent: AXIS clamps t (default [-1, 1]); DIAL clamps θ in radians
@@ -2789,6 +2776,28 @@ class Constraint {
2789
2776
  this.pt[2] = this.anchor[2] + c*this.r0[2] + sn*this.r1[2];
2790
2777
  }
2791
2778
 
2779
+ // Build the DIAL in-plane basis (r0, r1) from the current axis u and an
2780
+ // optional θ=0 reference (zx,zy,zz): the reference is projected onto the
2781
+ // dial plane and normalised; absent (NaN) or degenerate, r0 derives from u
2782
+ // via the least-aligned-axis seed. r1 = u × r0, right-handed about u.
2783
+ _dialBasis(zx, zy, zz) {
2784
+ if (_isNum(zx) && _isNum(zy) && _isNum(zz)) {
2785
+ const d = zx*this.u[0] + zy*this.u[1] + zz*this.u[2];
2786
+ this.r0[0] = zx - d*this.u[0];
2787
+ this.r0[1] = zy - d*this.u[1];
2788
+ this.r0[2] = zz - d*this.u[2];
2789
+ const l = Math.sqrt(this.r0[0]**2 + this.r0[1]**2 + this.r0[2]**2);
2790
+ if (l < EPS) _basis(this.u, this.r0, this.r1);
2791
+ else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
2792
+ } else {
2793
+ _basis(this.u, this.r0, this.r1);
2794
+ }
2795
+ // r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
2796
+ this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
2797
+ this.r1[1] = this.u[2]*this.r0[0] - this.u[0]*this.r0[2];
2798
+ this.r1[2] = this.u[0]*this.r0[1] - this.u[1]*this.r0[0];
2799
+ }
2800
+
2792
2801
  /**
2793
2802
  * Update the canonical state from a ray in the working space. The ray
2794
2803
  * direction is assumed unit. Chainable.
@@ -2959,6 +2968,46 @@ class Constraint {
2959
2968
  }
2960
2969
  return this;
2961
2970
  }
2971
+
2972
+ /**
2973
+ * Re-aim the constraint basis in the working space — the deferred-frame
2974
+ * seam (the p5.tree bridge's `from` opt resolves its symbolic basis through
2975
+ * mapDirection and calls this). Per kind:
2976
+ * PLANE — new normal; the point is re-projected onto the new plane.
2977
+ * AXIS — new direction; the scalar t is preserved, the point recomputed.
2978
+ * DIAL — new plane normal + optional θ=0 reference; θ is preserved, the
2979
+ * in-plane basis rebuilt (reference re-derived when omitted),
2980
+ * the point recomputed.
2981
+ * SPHERE — no basis; no-op.
2982
+ * Inputs are normalised; a zero-length axis keeps the previous one.
2983
+ * Chainable.
2984
+ *
2985
+ * @param {number} ax,ay,az New normal (PLANE) / direction (AXIS) / dial-plane normal (DIAL).
2986
+ * @param {number} [zx,zy,zz] DIAL only — θ=0 reference (re-derived when omitted).
2987
+ * @returns {Constraint} this
2988
+ */
2989
+ aim(ax, ay, az, zx, zy, zz) {
2990
+ if (this.kind === PLANE) {
2991
+ const px = this.n[0], py = this.n[1], pz = this.n[2];
2992
+ this.n[0] = ax; this.n[1] = ay; this.n[2] = az;
2993
+ _unit(this.n, px, py, pz);
2994
+ this.seed(this.pt[0], this.pt[1], this.pt[2]);
2995
+ } else if (this.kind === AXIS) {
2996
+ const px = this.u[0], py = this.u[1], pz = this.u[2];
2997
+ this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
2998
+ _unit(this.u, px, py, pz);
2999
+ this.pt[0] = this.anchor[0] + this.s*this.u[0];
3000
+ this.pt[1] = this.anchor[1] + this.s*this.u[1];
3001
+ this.pt[2] = this.anchor[2] + this.s*this.u[2];
3002
+ } else if (this.kind === DIAL) {
3003
+ const px = this.u[0], py = this.u[1], pz = this.u[2];
3004
+ this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
3005
+ _unit(this.u, px, py, pz);
3006
+ this._dialBasis(zx, zy, zz);
3007
+ this._dialPoint();
3008
+ }
3009
+ return this;
3010
+ }
2962
3011
  }
2963
3012
 
2964
3013
  /**