@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/LICENSE +661 -0
- package/README.md +31 -5
- package/dist/index.js +659 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
1166
|
-
*
|
|
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
|