@nakednous/tree 0.0.23 → 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/dist/index.js CHANGED
@@ -30,6 +30,16 @@ const _i = Object.freeze([-1, 0, 0]);
30
30
  const _j = Object.freeze([0, -1, 0]);
31
31
  const _k = Object.freeze([0, 0, -1]);
32
32
 
33
+ // Handle constraint kinds
34
+ const SPHERE = 0;
35
+ const PLANE = 1;
36
+ const AXIS = 2;
37
+ const DIAL = 3;
38
+
39
+ // Handle report modes
40
+ const POINT = 0;
41
+ const DIRECTION = 1;
42
+
33
43
  /**
34
44
  * @file Quaternion algebra and mat4/mat3 conversions.
35
45
  * @module tree/quat
@@ -72,7 +82,7 @@ const qNegate = (out, a) => {
72
82
  out[0]=-a[0]; out[1]=-a[1]; out[2]=-a[2]; out[3]=-a[3]; return out;
73
83
  };
74
84
 
75
- /** Hamilton product out = a * b. @returns {number[]} out */
85
+ /** Hamilton product out = a * b. Alias-safe: out may be a or b. @returns {number[]} out */
76
86
  const qMul = (out, a, b) => {
77
87
  const ax=a[0],ay=a[1],az=a[2],aw=a[3], bx=b[0],by=b[1],bz=b[2],bw=b[3];
78
88
  out[0]=aw*bx+ax*bw+ay*bz-az*by;
@@ -82,6 +92,34 @@ const qMul = (out, a, b) => {
82
92
  return out;
83
93
  };
84
94
 
95
+ /**
96
+ * Conjugate out = [−x, −y, −z, w] — the inverse of a UNIT quaternion.
97
+ * (Not qNegate: −q is the SAME rotation on the other hypersphere hemisphere;
98
+ * the conjugate is the opposite rotation.)
99
+ * @returns {number[]} out
100
+ */
101
+ const qConjugate = (out, a) => {
102
+ out[0]=-a[0]; out[1]=-a[1]; out[2]=-a[2]; out[3]=a[3]; return out;
103
+ };
104
+
105
+ /**
106
+ * Rotate a vec3 by a unit quaternion: out = q · v · q⁻¹, expanded to the
107
+ * allocation-free t = 2(qv × v); out = v + w·t + qv × t form.
108
+ * Alias-safe: out may be v.
109
+ * @param {number[]} out 3-element destination.
110
+ * @param {number[]} q Unit quaternion [x,y,z,w].
111
+ * @param {number[]} v Vector [x,y,z].
112
+ * @returns {number[]} out
113
+ */
114
+ const qRotateVec3 = (out, q, v) => {
115
+ const qx=q[0],qy=q[1],qz=q[2],qw=q[3], vx=v[0],vy=v[1],vz=v[2];
116
+ const tx=2*(qy*vz-qz*vy), ty=2*(qz*vx-qx*vz), tz=2*(qx*vy-qy*vx);
117
+ out[0]=vx+qw*tx+qy*tz-qz*ty;
118
+ out[1]=vy+qw*ty+qz*tx-qx*tz;
119
+ out[2]=vz+qw*tz+qx*ty-qy*tx;
120
+ return out;
121
+ };
122
+
85
123
  // =========================================================================
86
124
  // Interpolation
87
125
  // =========================================================================
@@ -121,6 +159,32 @@ const qNlerp = (out, a, b, t) => {
121
159
  // Construction
122
160
  // =========================================================================
123
161
 
162
+ /**
163
+ * Shortest-arc rotation taking unit vector a to unit vector b (three.js's
164
+ * setFromUnitVectors; the arcball delta). Antiparallel inputs rotate 180°
165
+ * about an axis ⊥ a, seeded from the world axis least aligned with a.
166
+ * @param {number[]} out
167
+ * @param {number[]} a Unit vector [x,y,z].
168
+ * @param {number[]} b Unit vector [x,y,z].
169
+ * @returns {number[]} out (normalised)
170
+ */
171
+ const qFromUnitVectors = (out, a, b) => {
172
+ const d = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
173
+ if (d < -0.999999) {
174
+ const ax = Math.abs(a[0]), ay = Math.abs(a[1]), az = Math.abs(a[2]);
175
+ let rx = 0, ry = 0, rz = 0;
176
+ if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
177
+ out[0]=ry*a[2]-rz*a[1]; out[1]=rz*a[0]-rx*a[2]; out[2]=rx*a[1]-ry*a[0];
178
+ out[3]=0; // 180°: w = cos(π/2) = 0
179
+ } else {
180
+ out[0]=a[1]*b[2]-a[2]*b[1];
181
+ out[1]=a[2]*b[0]-a[0]*b[2];
182
+ out[2]=a[0]*b[1]-a[1]*b[0];
183
+ out[3]=1+d;
184
+ }
185
+ return qNormalize(out);
186
+ };
187
+
124
188
  /**
125
189
  * Build a quaternion from axis-angle.
126
190
  * @param {number[]} out
@@ -1162,9 +1226,9 @@ function mat4ToRotation(out4, m) {
1162
1226
  *
1163
1227
  * ── Exports ──────────────────────────────────────────────────────────────
1164
1228
  * Quaternion helpers (re-exported from quat.js)
1165
- * qSet qCopy qDot qNormalize qNegate qMul qSlerp qNlerp
1166
- * qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
1167
- * qToAxisAngle
1229
+ * qSet qCopy qDot qNormalize qNegate qMul qConjugate qRotateVec3
1230
+ * qSlerp qNlerp qFromUnitVectors qFromAxisAngle qFromLookDir
1231
+ * qFromRotMat3x3 qFromMat4 qToMat4 qToAxisAngle
1168
1232
  * Spline / vector helpers
1169
1233
  * hermiteVec3 lerpVec3
1170
1234
  * Transform / mat4 helpers
@@ -1439,7 +1503,7 @@ const mat4ToTransform = (out, m) => {
1439
1503
  // S4a Spec parser — PoseTrack
1440
1504
  // =========================================================================
1441
1505
 
1442
- const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
1506
+ const _isNum$1 = (x) => typeof x === 'number' && Number.isFinite(x);
1443
1507
  const _clamp01 = (x) => x < 0 ? 0 : (x > 1 ? 1 : x);
1444
1508
  const _clampS = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
1445
1509
 
@@ -1700,7 +1764,7 @@ class Track {
1700
1764
 
1701
1765
  /** Playback rate. Signed: negative reverses, 0 freezes. Assigning never starts or stops playback. @type {number} */
1702
1766
  get rate() { return this._rate; }
1703
- set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
1767
+ set rate(v) { this._rate = (_isNum$1(v)) ? v : 1; }
1704
1768
 
1705
1769
  /** Number of interpolatable segments (keyframes.length − 1, min 0). @type {number} */
1706
1770
  get segments() { return Math.max(0, this.keyframes.length - 1); }
@@ -1739,13 +1803,13 @@ class Track {
1739
1803
  this._rate = rateOrOpts;
1740
1804
  } else if (rateOrOpts && typeof rateOrOpts === 'object') {
1741
1805
  const o = rateOrOpts;
1742
- if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
1806
+ if (_isNum$1(o.duration)) this.duration = Math.max(1, o.duration | 0);
1743
1807
  if ('loop' in o) this.loop = !!o.loop;
1744
1808
  if ('bounce' in o) this.bounce = !!o.bounce;
1745
1809
  if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
1746
1810
  if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
1747
1811
  if (typeof o.onStop === 'function') this.onStop = o.onStop;
1748
- if (_isNum(o.rate)) this._rate = o.rate;
1812
+ if (_isNum$1(o.rate)) this._rate = o.rate;
1749
1813
  }
1750
1814
 
1751
1815
  const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
@@ -1811,7 +1875,7 @@ class Track {
1811
1875
  * @returns {boolean} true if removed; false if index was invalid.
1812
1876
  */
1813
1877
  remove(index) {
1814
- if (!_isNum(index)) return false;
1878
+ if (!_isNum$1(index)) return false;
1815
1879
  const i = index | 0;
1816
1880
  if (i < 0 || i >= this.keyframes.length) return false;
1817
1881
  this.keyframes.splice(i, 1);
@@ -1837,7 +1901,7 @@ class Track {
1837
1901
  const nSeg = this.segments;
1838
1902
  if (nSeg === 0) { this.seg = 0; this.f = 0; return this; }
1839
1903
  const dur = Math.max(1, this.duration | 0);
1840
- if (_isNum(segIndex)) {
1904
+ if (_isNum$1(segIndex)) {
1841
1905
  this.seg = _clampS(segIndex | 0, 0, nSeg - 1);
1842
1906
  this.f = _clamp01(t) * dur;
1843
1907
  } else {
@@ -2030,7 +2094,7 @@ class PoseTrack extends Track {
2030
2094
  * @returns {boolean} true on success; false for invalid index or spec.
2031
2095
  */
2032
2096
  set(index, spec) {
2033
- if (!_isNum(index)) return false;
2097
+ if (!_isNum$1(index)) return false;
2034
2098
  const i = index | 0, kf = _parseSpec(spec);
2035
2099
  if (!kf || i < 0 || i > this.keyframes.length) return false;
2036
2100
  if (i === this.keyframes.length) this.keyframes.push(kf);
@@ -2216,7 +2280,7 @@ class CameraTrack extends Track {
2216
2280
  * @returns {boolean}
2217
2281
  */
2218
2282
  set(index, spec) {
2219
- if (!_isNum(index)) return false;
2283
+ if (!_isNum$1(index)) return false;
2220
2284
  const i = index | 0, kf = _parseCameraSpec(spec);
2221
2285
  if (!kf || i < 0 || i > this.keyframes.length) return false;
2222
2286
  if (i === this.keyframes.length) this.keyframes.push(kf);
@@ -2375,6 +2439,588 @@ class CameraTrack extends Track {
2375
2439
  }
2376
2440
  }
2377
2441
 
2442
+ /**
2443
+ * @file Constraint solver, canonical handle state, and ray-primitive
2444
+ * intersections for interactive manipulators.
2445
+ * @module tree/handle
2446
+ * @license AGPL-3.0-only
2447
+ *
2448
+ * The numeric brain of the manipulator handle. Renderer- and frame-agnostic:
2449
+ * inputs are plain numbers in ONE working space chosen by the caller (the
2450
+ * bridge), and the solver never learns world vs eye — it solves in whatever
2451
+ * space the ray and geometry are expressed in. The bridge converts the pointer
2452
+ * ray into the working frame before calling solve(), and converts the value
2453
+ * back out via mapLocation / mapDirection.
2454
+ *
2455
+ * Zero dependencies on p5, DOM, WebGL, or WebGPU. Out-first throughout; no
2456
+ * allocation in solve() / value(). Vectors are passed as flat scalars (matching
2457
+ * form.js), state is held as plain number[] (matching track.js keyframes).
2458
+ *
2459
+ * ── Storage convention ─────────────────────────────────────────────────────
2460
+ * The core distinguishes two value shapes deliberately:
2461
+ * mat4 — 16-element ArrayLike, typically Float32Array, because matrices
2462
+ * cross the GL boundary (contiguous f32 is what the renderer wants).
2463
+ * vec3 / quat — plain number[] (f64), because they are authoring/state values
2464
+ * and because the frozen basis-vector constants (i, j, k, ORIGIN)
2465
+ * cannot be expressed as frozen typed arrays. Handle state follows
2466
+ * the vec3 rule.
2467
+ *
2468
+ * ── Constraint kinds ───────────────────────────────────────────────────────
2469
+ * SPHERE 2-DOF heading on a sphere of radius r about an anchor.
2470
+ * Canonical state is a UNIT DIRECTION (renormalised every solve) —
2471
+ * no stored Euler angles, so no gimbal degeneracy at the poles.
2472
+ * az/el are derived on request via azEl().
2473
+ * PLANE 2-DOF point on a fixed plane (anchor + unit normal).
2474
+ * AXIS 1-DOF point on a line (anchor + unit dir), scalar t clamped to extent.
2475
+ * DIAL 1-DOF angle on a circle of radius r in the plane (anchor, axis).
2476
+ * Canonical state is an ACCUMULATED angle θ (multi-turn winding is
2477
+ * preserved: each solve applies the smaller signed arc, so θ counts
2478
+ * full turns — essential for bind targets like "rotate this 720°").
2479
+ * θ=0 sits at the reference direction `zero` (derived from the axis
2480
+ * when not supplied); θ grows right-handed about the axis. Near
2481
+ * edge-on view (the ray almost parallel to the dial plane, where a
2482
+ * plane hit races to infinity — the classic rotate-gizmo failure)
2483
+ * the solve falls back to the TANGENT LINE of the circle at the
2484
+ * current angle, converting linear drag to dθ: bounded, monotone,
2485
+ * and still pure ray geometry.
2486
+ *
2487
+ * VIEW (the camera-facing free-translate constraint) is NOT a core kind: the
2488
+ * bridge implements it with PLANE, feeding a fresh camera-derived normal and
2489
+ * anchor each frame. The core stays oblivious to the camera.
2490
+ *
2491
+ * ── Report modes ───────────────────────────────────────────────────────────
2492
+ * DIRECTION value() writes the unit direction: SPHERE → the heading;
2493
+ * DIAL → the radial unit at θ.
2494
+ * POINT value() writes a position: SPHERE → anchor + dir·radius;
2495
+ * DIAL → the point on the circle; PLANE / AXIS → the constrained
2496
+ * point.
2497
+ *
2498
+ * ── State ownership & portability ──────────────────────────────────────────
2499
+ * Each Constraint owns its own state; there is no module-level scratch. solve()
2500
+ * writes into instance fields, never shared globals. This keeps the type a
2501
+ * plain-data state machine: it ports 1:1 to a Rust enum + impl that is
2502
+ * `Send + Sync` for free, with `&mut self` on solve() making concurrent
2503
+ * mutation a compile error. The binding closure deliberately lives in the
2504
+ * bridge, not here — a stored callback would forfeit that guarantee.
2505
+ *
2506
+ * ── Extension contract ─────────────────────────────────────────────────────
2507
+ * A constraint is any object exposing: `kind` (integer discriminant),
2508
+ * `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
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
2512
+ * any conforming constraint (lifecycle, frame conversion, bind, hooks, pick);
2513
+ * a new kind — 6-DOF, or app-specific — implements this contract here
2514
+ * (portable, draw-free) plus a bridge-side locus/pick draw (`drawLocus` /
2515
+ * `pickProxy` on createHandle), rather than forking the controller. The
2516
+ * classes below are the reference implementation. See handle-design.md §9.
2517
+ *
2518
+ * ── Conventions ────────────────────────────────────────────────────────────
2519
+ * Ray direction `d` is assumed unit (the bridge normalises). Plane / axis
2520
+ * normals and directions are normalised at construction. The angular utilities
2521
+ * use a right-handed convention: az about +Y, el measured from the XZ plane.
2522
+ * solve() does NOT depend on this convention — it stores the hit direction
2523
+ * directly; az/el are a readout/authoring convenience only. DIAL's θ is
2524
+ * right-handed about its own axis, measured from `zero`.
2525
+ */
2526
+
2527
+
2528
+ const EPS = 1e-6;
2529
+ const TWO_PI = Math.PI * 2;
2530
+
2531
+ // Edge-on threshold for DIAL: below this |d·n| the plane hit is ill-conditioned
2532
+ // (dθ per pixel diverges) and the solve switches to the tangent-line fallback.
2533
+ // ≈ 4.6° of incidence. Tune empirically (handle-experiments/e2).
2534
+ const DIAL_EDGE = 0.08;
2535
+
2536
+ // =========================================================================
2537
+ // H1 Small private helpers
2538
+ // =========================================================================
2539
+
2540
+ const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
2541
+ const _clamp = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
2542
+ const _num = (x, d) => _isNum(x) ? x : d;
2543
+
2544
+ /** Wrap an angle to (−π, π]. */
2545
+ const _wrapPi = (a) => a - TWO_PI * Math.round(a / TWO_PI);
2546
+
2547
+ /** Parse a vec3 from array / typed array / {x,y,z}. Returns a fresh [x,y,z] or null. */
2548
+ function _vec3(v) {
2549
+ if (!v) return null;
2550
+ if (ArrayBuffer.isView(v) && v.length >= 3) return [v[0], v[1], v[2]];
2551
+ if (Array.isArray(v) && v.length >= 3) return [v[0], v[1], v[2]];
2552
+ if (typeof v === 'object' && 'x' in v) return [v.x || 0, v.y || 0, v.z || 0];
2553
+ return null;
2554
+ }
2555
+
2556
+ /** Normalise a vec3 in place; zero-length falls back to the given default axis. */
2557
+ function _unit(v, dx, dy, dz) {
2558
+ const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
2559
+ if (l < EPS) { v[0]=dx; v[1]=dy; v[2]=dz; return v; }
2560
+ v[0]/=l; v[1]/=l; v[2]/=l;
2561
+ return v;
2562
+ }
2563
+
2564
+ /**
2565
+ * Orthonormal in-plane basis for a unit normal n, written into ub, vb. Seeds
2566
+ * from the world axis least aligned with n so the first cross can't degenerate.
2567
+ * (Same derivation the p5 bridge uses for its plane quad — duplicated here
2568
+ * because the core cannot depend on the bridge.)
2569
+ */
2570
+ function _basis(n, ub, vb) {
2571
+ const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
2572
+ let rx = 0, ry = 0, rz = 0;
2573
+ if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
2574
+ ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
2575
+ _unit(ub, 1, 0, 0);
2576
+ vb[0] = n[1]*ub[2] - n[2]*ub[1]; vb[1] = n[2]*ub[0] - n[0]*ub[2]; vb[2] = n[0]*ub[1] - n[1]*ub[0];
2577
+ }
2578
+
2579
+ // =========================================================================
2580
+ // H2 Ray-primitive intersections (pure, out-first, scalar params)
2581
+ // =========================================================================
2582
+
2583
+ /**
2584
+ * Nearest ray–sphere intersection. The ray direction is assumed unit, so the
2585
+ * quadratic's leading coefficient is 1. On a miss, writes the closest-approach
2586
+ * point projected onto the sphere (so the handle tracks the limb gracefully
2587
+ * rather than snapping away).
2588
+ *
2589
+ * @param {number[]} out 3-element destination (hit point).
2590
+ * @param {number} ox,oy,oz Ray origin.
2591
+ * @param {number} dx,dy,dz Ray direction (unit).
2592
+ * @param {number} cx,cy,cz Sphere centre.
2593
+ * @param {number} r Sphere radius.
2594
+ * @returns {number} Ray parameter t at the written hit.
2595
+ */
2596
+ function raySphere(out, ox,oy,oz, dx,dy,dz, cx,cy,cz, r) {
2597
+ const lx=ox-cx, ly=oy-cy, lz=oz-cz;
2598
+ const b = lx*dx + ly*dy + lz*dz;
2599
+ const cc = lx*lx + ly*ly + lz*lz - r*r;
2600
+ const disc = b*b - cc;
2601
+ let t;
2602
+ if (disc >= 0) {
2603
+ const s = Math.sqrt(disc);
2604
+ t = -b - s;
2605
+ if (t < 0) t = -b + s; // origin inside the sphere: take far root
2606
+ out[0]=ox+t*dx; out[1]=oy+t*dy; out[2]=oz+t*dz;
2607
+ } else {
2608
+ t = -b; // closest approach along the ray
2609
+ let hx=ox+t*dx-cx, hy=oy+t*dy-cy, hz=oz+t*dz-cz;
2610
+ const hl = Math.sqrt(hx*hx + hy*hy + hz*hz) || 1;
2611
+ const k = r/hl;
2612
+ out[0]=cx+hx*k; out[1]=cy+hy*k; out[2]=cz+hz*k;
2613
+ }
2614
+ return t;
2615
+ }
2616
+
2617
+ /**
2618
+ * Ray–plane intersection. A near-parallel ray (|d·n| < EPS) returns Infinity
2619
+ * and leaves `out` untouched — the caller keeps the previous value.
2620
+ *
2621
+ * @param {number[]} out 3-element destination (hit point).
2622
+ * @param {number} ox,oy,oz Ray origin.
2623
+ * @param {number} dx,dy,dz Ray direction (unit).
2624
+ * @param {number} px,py,pz A point on the plane.
2625
+ * @param {number} nx,ny,nz Plane normal (unit).
2626
+ * @returns {number} Ray parameter t, or Infinity if parallel.
2627
+ */
2628
+ function rayPlane(out, ox,oy,oz, dx,dy,dz, px,py,pz, nx,ny,nz) {
2629
+ const den = dx*nx + dy*ny + dz*nz;
2630
+ if (den < EPS && den > -EPS) return Infinity;
2631
+ const t = ((px-ox)*nx + (py-oy)*ny + (pz-oz)*nz) / den;
2632
+ out[0]=ox+t*dx; out[1]=oy+t*dy; out[2]=oz+t*dz;
2633
+ return t;
2634
+ }
2635
+
2636
+ /**
2637
+ * Closest point on the infinite line (p, u) to the ray (o, d). Both `d` and
2638
+ * `u` are assumed unit. For a ray parallel to the line, projects the ray
2639
+ * origin onto the line.
2640
+ *
2641
+ * @param {number[]} out 3-element destination (closest point on the line).
2642
+ * @param {number} ox,oy,oz Ray origin.
2643
+ * @param {number} dx,dy,dz Ray direction (unit).
2644
+ * @param {number} px,py,pz A point on the line.
2645
+ * @param {number} ux,uy,uz Line direction (unit).
2646
+ * @returns {number} Signed parameter s along u at the written point (unclamped).
2647
+ */
2648
+ function rayClosestPointOnAxis(out, ox,oy,oz, dx,dy,dz, px,py,pz, ux,uy,uz) {
2649
+ const wx=ox-px, wy=oy-py, wz=oz-pz;
2650
+ const b = dx*ux + dy*uy + dz*uz; // d·u
2651
+ const dd = dx*wx + dy*wy + dz*wz; // d·w
2652
+ const e = ux*wx + uy*wy + uz*wz; // u·w
2653
+ const den = 1 - b*b; // (d·d)(u·u) − (d·u)² with d,u unit
2654
+ const s = (den > EPS) ? (e - b*dd) / den : e;
2655
+ out[0]=px+s*ux; out[1]=py+s*uy; out[2]=pz+s*uz;
2656
+ return s;
2657
+ }
2658
+
2659
+ // =========================================================================
2660
+ // H3 Angular utilities (readout / authoring convenience)
2661
+ // =========================================================================
2662
+
2663
+ /**
2664
+ * Unit direction from azimuth/elevation (right-handed: az about +Y, el from XZ).
2665
+ * @param {number[]} out 3-element destination.
2666
+ * @param {number} az Azimuth (radians).
2667
+ * @param {number} el Elevation (radians).
2668
+ * @returns {number[]} out
2669
+ */
2670
+ function dirFromAzEl(out, az, el) {
2671
+ const ce = Math.cos(el);
2672
+ out[0]=ce*Math.cos(az); out[1]=Math.sin(el); out[2]=ce*Math.sin(az);
2673
+ return out;
2674
+ }
2675
+
2676
+ /**
2677
+ * Azimuth/elevation from a unit direction. Inverse of dirFromAzEl.
2678
+ * @param {number[]} out2 2-element destination [az, el] (radians).
2679
+ * @param {number} dx,dy,dz Unit direction.
2680
+ * @returns {number[]} out2
2681
+ */
2682
+ function azElFromDir(out2, dx, dy, dz) {
2683
+ out2[0] = Math.atan2(dz, dx); // az
2684
+ out2[1] = Math.asin(_clamp(dy, -1, 1)); // el
2685
+ return out2;
2686
+ }
2687
+
2688
+ // =========================================================================
2689
+ // H4 Constraint — tagged state machine (one class, kind discriminant)
2690
+ // =========================================================================
2691
+
2692
+ /**
2693
+ * A draggable constraint: maps a ray to a constrained value and holds the
2694
+ * canonical state between drags. One class with a `kind` discriminant (never
2695
+ * per-constraint subclasses) so it maps cleanly to a Rust enum.
2696
+ *
2697
+ * Construction options (all optional unless noted):
2698
+ * SPHERE — { radius = 1, report = DIRECTION, anchor = [0,0,0] }
2699
+ * PLANE — { anchor = [0,0,0], normal = [0,1,0] }
2700
+ * AXIS — { anchor = [0,0,0], axis = [1,0,0], extent = [-1, 1] }
2701
+ * DIAL — { anchor = [0,0,0], axis = [0,1,0], radius = 1, zero?,
2702
+ * extent = [-∞, ∞] }
2703
+ * `axis` is the dial-plane normal (θ is right-handed about it);
2704
+ * `zero` is the in-plane reference direction for θ=0 (projected
2705
+ * onto the plane and normalised; derived from the axis when
2706
+ * absent); `extent` clamps θ in radians — unbounded by default,
2707
+ * so the dial winds multiple turns.
2708
+ *
2709
+ * @param {number} kind SPHERE | PLANE | AXIS | DIAL.
2710
+ * @param {Object} [opts]
2711
+ */
2712
+ class Constraint {
2713
+ constructor(kind, opts = {}) {
2714
+ /** Constraint kind discriminant. @type {number} */
2715
+ this.kind = kind;
2716
+
2717
+ /** Constraint origin (sphere centre / plane point / axis anchor / dial centre). @type {number[]} */
2718
+ this.anchor = _vec3(opts.anchor) || [0, 0, 0];
2719
+ /** Canonical unit direction — SPHERE. @type {number[]} */
2720
+ this.dir = [0, 0, 1];
2721
+ /** Constrained point — PLANE / AXIS / DIAL (and SPHERE scratch). @type {number[]} */
2722
+ this.pt = [this.anchor[0], this.anchor[1], this.anchor[2]];
2723
+ /** Plane normal (unit) — PLANE. @type {number[]} */
2724
+ this.n = _unit(_vec3(opts.normal) || [0, 1, 0], 0, 1, 0);
2725
+ /** Axis / dial-plane normal (unit) — AXIS / DIAL. @type {number[]} */
2726
+ this.u = kind === DIAL
2727
+ ? _unit(_vec3(opts.axis) || [0, 1, 0], 0, 1, 0)
2728
+ : _unit(_vec3(opts.axis) || [1, 0, 0], 1, 0, 0);
2729
+ /** Current scalar parameter — AXIS: t along the line; DIAL: accumulated θ. @type {number} */
2730
+ this.s = 0;
2731
+
2732
+ // DIAL in-plane basis: r0 (the θ=0 reference) and r1 = u × r0, so the
2733
+ // point at θ is anchor + radius·(cosθ·r0 + sinθ·r1) — right-handed about u.
2734
+ /** θ=0 reference direction (unit, in-plane) — DIAL. @type {number[]} */
2735
+ this.r0 = [1, 0, 0];
2736
+ /** In-plane binormal u × r0 (unit) — DIAL. @type {number[]} */
2737
+ this.r1 = [0, 0, 1];
2738
+ if (kind === DIAL) {
2739
+ const z = _vec3(opts.zero);
2740
+ this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
2741
+ }
2742
+
2743
+ // Extent: AXIS clamps t (default [-1, 1]); DIAL clamps θ in radians
2744
+ // (default unbounded, so the dial winds freely).
2745
+ const ext = Array.isArray(opts.extent) ? opts.extent : null;
2746
+ const dial = kind === DIAL;
2747
+ /** Minimum scalar parameter. @type {number} */
2748
+ this.min = ext && _isNum(ext[0]) ? ext[0] : (dial ? -Infinity : -1);
2749
+ /** Maximum scalar parameter. @type {number} */
2750
+ this.max = ext && _isNum(ext[1]) ? ext[1] : (dial ? Infinity : 1);
2751
+
2752
+ // Sphere / dial radius (private backing — see radius getter/setter).
2753
+ this._radius = _num(opts.radius, 1);
2754
+
2755
+ /**
2756
+ * Default report mode. SPHERE defaults to DIRECTION; PLANE / AXIS / DIAL
2757
+ * report a POINT.
2758
+ * @type {number}
2759
+ */
2760
+ this.report = (opts.report === POINT || opts.report === DIRECTION)
2761
+ ? opts.report
2762
+ : (kind === SPHERE ? DIRECTION : POINT);
2763
+
2764
+ if (kind === DIAL) this._dialPoint();
2765
+ }
2766
+
2767
+ /** Sphere / dial radius. @type {number} */
2768
+ get radius() { return this._radius; }
2769
+ set radius(r) { this._radius = _isNum(r) ? r : this._radius; if (this.kind === DIAL) this._dialPoint(); }
2770
+
2771
+ // Recompute the DIAL point from θ (anchor + ρ(cosθ·r0 + sinθ·r1)).
2772
+ _dialPoint() {
2773
+ const c = Math.cos(this.s) * this._radius, sn = Math.sin(this.s) * this._radius;
2774
+ this.pt[0] = this.anchor[0] + c*this.r0[0] + sn*this.r1[0];
2775
+ this.pt[1] = this.anchor[1] + c*this.r0[1] + sn*this.r1[1];
2776
+ this.pt[2] = this.anchor[2] + c*this.r0[2] + sn*this.r1[2];
2777
+ }
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
+
2801
+ /**
2802
+ * Update the canonical state from a ray in the working space. The ray
2803
+ * direction is assumed unit. Chainable.
2804
+ *
2805
+ * @param {number} ox,oy,oz Ray origin.
2806
+ * @param {number} dx,dy,dz Ray direction (unit).
2807
+ * @returns {Constraint} this
2808
+ */
2809
+ solve(ox, oy, oz, dx, dy, dz) {
2810
+ if (this.kind === SPHERE) {
2811
+ // Hit the sphere into pt, then derive the unit heading from the anchor.
2812
+ raySphere(this.pt, ox,oy,oz, dx,dy,dz,
2813
+ this.anchor[0], this.anchor[1], this.anchor[2], this._radius);
2814
+ this.dir[0] = this.pt[0] - this.anchor[0];
2815
+ this.dir[1] = this.pt[1] - this.anchor[1];
2816
+ this.dir[2] = this.pt[2] - this.anchor[2];
2817
+ _unit(this.dir, this.dir[0], this.dir[1], this.dir[2]);
2818
+ } else if (this.kind === PLANE) {
2819
+ // Parallel ray returns Infinity and leaves pt unchanged (keep last).
2820
+ rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
2821
+ this.anchor[0], this.anchor[1], this.anchor[2],
2822
+ this.n[0], this.n[1], this.n[2]);
2823
+ } else if (this.kind === AXIS) {
2824
+ let s = rayClosestPointOnAxis(this.pt, ox,oy,oz, dx,dy,dz,
2825
+ this.anchor[0], this.anchor[1], this.anchor[2],
2826
+ this.u[0], this.u[1], this.u[2]);
2827
+ s = _clamp(s, this.min, this.max);
2828
+ this.s = s;
2829
+ this.pt[0] = this.anchor[0] + s*this.u[0]; // clamped point
2830
+ this.pt[1] = this.anchor[1] + s*this.u[1];
2831
+ this.pt[2] = this.anchor[2] + s*this.u[2];
2832
+ } else if (this.kind === DIAL) {
2833
+ this._solveDial(ox, oy, oz, dx, dy, dz);
2834
+ }
2835
+ return this;
2836
+ }
2837
+
2838
+ // DIAL solve. Face-on: hit the dial plane, derive the raw angle of the hit
2839
+ // about the anchor, and accumulate the smaller signed arc into θ (winding
2840
+ // preserved). Edge-on (|d·u| < DIAL_EDGE): the plane hit diverges, so solve
2841
+ // the ray against the TANGENT LINE of the circle at the current θ instead —
2842
+ // the signed tangent parameter over the radius is dθ, bounded and monotone.
2843
+ _solveDial(ox, oy, oz, dx, dy, dz) {
2844
+ const u = this.u, r0 = this.r0, r1 = this.r1, a = this.anchor;
2845
+ const den = dx*u[0] + dy*u[1] + dz*u[2];
2846
+ let dth;
2847
+ if (den >= DIAL_EDGE || den <= -DIAL_EDGE) {
2848
+ // Well-conditioned: plane hit → raw angle → smaller signed arc.
2849
+ const t = rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
2850
+ a[0], a[1], a[2], u[0], u[1], u[2]);
2851
+ if (t === Infinity) return; // keep last (paranoia; den guards)
2852
+ const vx = this.pt[0]-a[0], vy = this.pt[1]-a[1], vz = this.pt[2]-a[2];
2853
+ const raw = Math.atan2(vx*r1[0] + vy*r1[1] + vz*r1[2],
2854
+ vx*r0[0] + vy*r0[1] + vz*r0[2]);
2855
+ dth = _wrapPi(raw - _wrapPi(this.s));
2856
+ } else {
2857
+ // Edge-on fallback: tangent at the current point, w = −sinθ·r0 + cosθ·r1.
2858
+ const cs = Math.cos(this.s), sn = Math.sin(this.s);
2859
+ const wx = -sn*r0[0] + cs*r1[0];
2860
+ const wy = -sn*r0[1] + cs*r1[1];
2861
+ const wz = -sn*r0[2] + cs*r1[2];
2862
+ this._dialPoint(); // tangent anchored at the current point
2863
+ const sl = rayClosestPointOnAxis(this.pt, ox,oy,oz, dx,dy,dz,
2864
+ this.pt[0], this.pt[1], this.pt[2],
2865
+ wx, wy, wz);
2866
+ dth = sl / (this._radius || 1);
2867
+ }
2868
+ this.s = _clamp(this.s + dth, this.min, this.max);
2869
+ this._dialPoint();
2870
+ }
2871
+
2872
+ /**
2873
+ * Write the current value into `out`. `report` overrides the default for
2874
+ * this call (e.g. read a SPHERE's point even when its default is DIRECTION).
2875
+ * SPHERE+POINT and DIAL+POINT are derived from the angle/heading and the
2876
+ * live radius, so they stay correct after a radius change without
2877
+ * re-solving. DIAL+DIRECTION writes the radial unit at θ.
2878
+ *
2879
+ * @param {number[]} out 3-element destination.
2880
+ * @param {number} [report] POINT | DIRECTION override.
2881
+ * @returns {number[]} out
2882
+ */
2883
+ value(out, report) {
2884
+ const r = (report === POINT || report === DIRECTION) ? report : this.report;
2885
+ if (this.kind === SPHERE) {
2886
+ if (r === DIRECTION) {
2887
+ out[0]=this.dir[0]; out[1]=this.dir[1]; out[2]=this.dir[2];
2888
+ } else {
2889
+ out[0]=this.anchor[0]+this.dir[0]*this._radius;
2890
+ out[1]=this.anchor[1]+this.dir[1]*this._radius;
2891
+ out[2]=this.anchor[2]+this.dir[2]*this._radius;
2892
+ }
2893
+ } else if (this.kind === DIAL) {
2894
+ const cs = Math.cos(this.s), sn = Math.sin(this.s);
2895
+ if (r === DIRECTION) {
2896
+ out[0]=cs*this.r0[0]+sn*this.r1[0];
2897
+ out[1]=cs*this.r0[1]+sn*this.r1[1];
2898
+ out[2]=cs*this.r0[2]+sn*this.r1[2];
2899
+ } else {
2900
+ out[0]=this.anchor[0]+(cs*this.r0[0]+sn*this.r1[0])*this._radius;
2901
+ out[1]=this.anchor[1]+(cs*this.r0[1]+sn*this.r1[1])*this._radius;
2902
+ out[2]=this.anchor[2]+(cs*this.r0[2]+sn*this.r1[2])*this._radius;
2903
+ }
2904
+ } else {
2905
+ out[0]=this.pt[0]; out[1]=this.pt[1]; out[2]=this.pt[2];
2906
+ }
2907
+ return out;
2908
+ }
2909
+
2910
+ /**
2911
+ * Current scalar parameter: AXIS → signed t along the line; DIAL →
2912
+ * accumulated angle θ in radians (multi-turn).
2913
+ * @returns {number} The parameter, or NaN for non-scalar constraints.
2914
+ */
2915
+ scalar() {
2916
+ return (this.kind === AXIS || this.kind === DIAL) ? this.s : NaN;
2917
+ }
2918
+
2919
+ /**
2920
+ * Derive [az, el] from the current SPHERE direction (SPHERE only).
2921
+ * @param {number[]} out2 2-element destination [az, el].
2922
+ * @returns {number[]} out2
2923
+ */
2924
+ azEl(out2) {
2925
+ return azElFromDir(out2, this.dir[0], this.dir[1], this.dir[2]);
2926
+ }
2927
+
2928
+ /**
2929
+ * Seed the canonical state from a value (used by the bridge on bind() so the
2930
+ * handle starts at the bound target's value). Chainable.
2931
+ * SPHERE — sets the unit direction (a point value recovers its heading).
2932
+ * PLANE — projects the value onto the plane.
2933
+ * AXIS — projects the value onto the line, clamped to extent.
2934
+ * DIAL — projects the value onto the dial plane, derives the wrapped
2935
+ * angle, and picks the WINDING NEAREST the current θ (so an
2936
+ * external sync doesn't unwind accumulated turns), clamped.
2937
+ *
2938
+ * @param {number} x,y,z Seed value.
2939
+ * @returns {Constraint} this
2940
+ */
2941
+ seed(x, y, z) {
2942
+ if (this.kind === SPHERE) {
2943
+ const vx=x-this.anchor[0], vy=y-this.anchor[1], vz=z-this.anchor[2];
2944
+ const l = Math.sqrt(vx*vx + vy*vy + vz*vz);
2945
+ if (l >= EPS) { this.dir[0]=vx/l; this.dir[1]=vy/l; this.dir[2]=vz/l; }
2946
+ } else if (this.kind === PLANE) {
2947
+ const wx=x-this.anchor[0], wy=y-this.anchor[1], wz=z-this.anchor[2];
2948
+ const d = wx*this.n[0] + wy*this.n[1] + wz*this.n[2];
2949
+ this.pt[0]=x-d*this.n[0]; this.pt[1]=y-d*this.n[1]; this.pt[2]=z-d*this.n[2];
2950
+ } else if (this.kind === AXIS) {
2951
+ const wx=x-this.anchor[0], wy=y-this.anchor[1], wz=z-this.anchor[2];
2952
+ const s = _clamp(wx*this.u[0] + wy*this.u[1] + wz*this.u[2], this.min, this.max);
2953
+ this.s = s;
2954
+ this.pt[0]=this.anchor[0]+s*this.u[0];
2955
+ this.pt[1]=this.anchor[1]+s*this.u[1];
2956
+ this.pt[2]=this.anchor[2]+s*this.u[2];
2957
+ } else if (this.kind === DIAL) {
2958
+ const wx=x-this.anchor[0], wy=y-this.anchor[1], wz=z-this.anchor[2];
2959
+ const px = wx*this.r0[0] + wy*this.r0[1] + wz*this.r0[2];
2960
+ const py = wx*this.r1[0] + wy*this.r1[1] + wz*this.r1[2];
2961
+ if (px*px + py*py >= EPS*EPS) {
2962
+ const a = Math.atan2(py, px);
2963
+ // Nearest winding to the current θ preserves accumulated turns.
2964
+ this.s = _clamp(a + TWO_PI * Math.round((this.s - a) / TWO_PI),
2965
+ this.min, this.max);
2966
+ }
2967
+ this._dialPoint();
2968
+ }
2969
+ return this;
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
+ }
3011
+ }
3012
+
3013
+ /**
3014
+ * Convenience factory mirroring the constructor. Handy for headless tests and
3015
+ * for the bridge, which otherwise calls `new Constraint(...)` directly.
3016
+ * @param {number} kind SPHERE | PLANE | AXIS | DIAL.
3017
+ * @param {Object} [opts]
3018
+ * @returns {Constraint}
3019
+ */
3020
+ function createConstraint(kind, opts) {
3021
+ return new Constraint(kind, opts);
3022
+ }
3023
+
2378
3024
  /**
2379
3025
  * @file Frustum planes and visibility tests — zero allocations.
2380
3026
  * @module tree/visibility
@@ -2534,5 +3180,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
2534
3180
  return allIn ? VISIBLE : SEMIVISIBLE;
2535
3181
  }
2536
3182
 
2537
- export { CameraTrack, EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, boxVisibility, 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, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNlerp, qNormalize, qSet, qSlerp, qToAxisAngle, qToMat4, sphereVisibility, transformToMat4 };
3183
+ 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 };
2538
3184
  //# sourceMappingURL=index.js.map