@nakednous/tree 0.0.27 → 0.0.29

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
@@ -44,6 +44,59 @@ const DIAL = 3;
44
44
  const POINT = 0;
45
45
  const DIRECTION = 1;
46
46
 
47
+ // Pointer-hit shapes (pointerHit); bullsEyeLines' shape
48
+ const CIRCLE = 0;
49
+ const SQUARE = 1;
50
+
51
+ // Gizmo bits — one namespace per generator. The same value means different
52
+ // things to different generators, and no generator reads another's bits.
53
+ const NONE = 0;
54
+
55
+ // axesLines
56
+ const X = 1 << 0;
57
+ const _X = 1 << 1;
58
+ const Y = 1 << 2;
59
+ const _Y = 1 << 3;
60
+ const Z = 1 << 4;
61
+ const _Z = 1 << 5;
62
+ const LABELS = 1 << 6;
63
+
64
+ // frustumLines (LEFT … TOP also key a bounds object's planes)
65
+ const NEAR = 1 << 0;
66
+ const FAR = 1 << 1;
67
+ const LEFT = 1 << 2;
68
+ const RIGHT = 1 << 3;
69
+ const BOTTOM = 1 << 4;
70
+ const TOP = 1 << 5;
71
+ const BODY = 1 << 6;
72
+ const APEX = 1 << 7;
73
+
74
+ // pathLines
75
+ const PATH = 1 << 0;
76
+ const CENTER = 1 << 1;
77
+ const CONTROLS = 1 << 2;
78
+ const TANGENTS_IN = 1 << 3;
79
+ const TANGENTS_OUT = 1 << 4;
80
+ const TANGENTS = TANGENTS_IN | TANGENTS_OUT;
81
+ const HANDLES = 1 << 5;
82
+
83
+ // helmRigLines
84
+ const TRANSLATE = 1 << 0;
85
+ const ROTATE = 1 << 1;
86
+
87
+ // locusLines (HANDLE is the bridge's dot, not a line)
88
+ const HANDLE = 1 << 0;
89
+ const AIM = 1 << 1;
90
+ const LOCUS = 1 << 2;
91
+ const RING = 1 << 3;
92
+
93
+ // Semantic palette — normalised RGBA: red, lime, dodger blue; COLOR_DIM is
94
+ // the alpha of a dimmed stroke (the helm rig's baseline).
95
+ const COLOR_X = Object.freeze([1, 0, 0, 1]);
96
+ const COLOR_Y = Object.freeze([0, 1, 0, 1]);
97
+ const COLOR_Z = Object.freeze([30 / 255, 144 / 255, 1, 1]);
98
+ const COLOR_DIM = 110 / 255;
99
+
47
100
  /**
48
101
  * @file Quaternion algebra and mat4/mat3 conversions.
49
102
  * @module tree/quat
@@ -207,6 +260,10 @@ const qFromAxisAngle = (out, ax, ay, az, angle) => {
207
260
 
208
261
  /**
209
262
  * Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
263
+ * `dir` need not be unit. Right = dir × up, up re-orthogonalised as right × dir;
264
+ * when `dir` is parallel to `up` the up hint is re-seeded from the world axis
265
+ * least aligned with `dir` (the same seed qFromUnitVectors uses), so every
266
+ * direction yields a proper rotation.
210
267
  * @param {number[]} out
211
268
  * @param {number[]} dir Forward direction [x,y,z].
212
269
  * @param {number[]} [up] Up vector [x,y,z].
@@ -217,11 +274,18 @@ const qFromLookDir = (out, dir, up) => {
217
274
  const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
218
275
  fx/=fl; fy/=fl; fz/=fl;
219
276
  let ux=up?up[0]:0, uy=up?up[1]:1, uz=up?up[2]:0;
220
- let rx=uy*fz-uz*fy, ry=uz*fx-ux*fz, rz=ux*fy-uy*fx;
221
- const rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
277
+ let rx=fy*uz-fz*uy, ry=fz*ux-fx*uz, rz=fx*uy-fy*ux; // right = dir × up
278
+ let rl=Math.sqrt(rx*rx+ry*ry+rz*rz);
279
+ if (rl < 1e-8) { // dir ∥ up: re-seed up
280
+ const ax=Math.abs(fx), ay=Math.abs(fy), az=Math.abs(fz);
281
+ ux=0; uy=0; uz=0;
282
+ if (ax <= ay && ax <= az) ux=1; else if (ay <= az) uy=1; else uz=1;
283
+ rx=fy*uz-fz*uy; ry=fz*ux-fx*uz; rz=fx*uy-fy*ux;
284
+ rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
285
+ }
222
286
  rx/=rl; ry/=rl; rz/=rl;
223
- ux=fy*rz-fz*ry; uy=fz*rx-fx*rz; uz=fx*ry-fy*rx;
224
- return qFromRotMat3x3(out, rx,ry,rz, ux,uy,uz, -fx,-fy,-fz);
287
+ ux=ry*fz-rz*fy; uy=rz*fx-rx*fz; uz=rx*fy-ry*fx; // up = right × dir
288
+ return qFromRotMat3x3(out, rx,ux,-fx, ry,uy,-fy, rz,uz,-fz); // columns: right, up, back
225
289
  };
226
290
 
227
291
  /**
@@ -463,6 +527,34 @@ function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
463
527
  return out;
464
528
  }
465
529
 
530
+ // Lookat basis scratch — right (0–2), up (3–5), back (6–8): unit, world space.
531
+ const _lb = new Float64Array(9);
532
+
533
+ /**
534
+ * Orthonormal lookat basis into `_lb`: back = eye − center, right = up × back,
535
+ * up = back × right. When the view direction is parallel to the up hint the
536
+ * hint is re-seeded from the world axis least aligned with it — the seed
537
+ * qFromUnitVectors and qFromLookDir use — so the basis is always a rotation.
538
+ */
539
+ function _lookBasis(ex,ey,ez, cx,cy,cz, ux,uy,uz) {
540
+ let zx=ex-cx, zy=ey-cy, zz=ez-cz;
541
+ const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
542
+ zx/=zl; zy/=zl; zz/=zl;
543
+ let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
544
+ let xl=Math.sqrt(xx*xx+xy*xy+xz*xz);
545
+ if (xl < 1e-8) { // view ∥ up: re-seed up
546
+ const ax=Math.abs(zx), ay=Math.abs(zy), az=Math.abs(zz);
547
+ ux=0; uy=0; uz=0;
548
+ if (ax <= ay && ax <= az) ux=1; else if (ay <= az) uy=1; else uz=1;
549
+ xx=uy*zz-uz*zy; xy=uz*zx-ux*zz; xz=ux*zy-uy*zx;
550
+ xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
551
+ }
552
+ xx/=xl; xy/=xl; xz/=xl;
553
+ _lb[0]=xx; _lb[1]=xy; _lb[2]=xz;
554
+ _lb[3]=zy*xz-zz*xy; _lb[4]=zz*xx-zx*xz; _lb[5]=zx*xy-zy*xx;
555
+ _lb[6]=zx; _lb[7]=zy; _lb[8]=zz;
556
+ }
557
+
466
558
  /**
467
559
  * View matrix (world→eye) from lookat parameters.
468
560
  * Camera looks along −Z in eye space; right = normalize(up × (−Z)).
@@ -471,16 +563,12 @@ function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
471
563
  * @param {Float32Array|number[]} out 16-element destination.
472
564
  * @param {number} ex,ey,ez Eye (camera) position.
473
565
  * @param {number} cx,cy,cz Look-at target.
474
- * @param {number} ux,uy,uz World up hint (need not be unit).
566
+ * @param {number} ux,uy,uz World up hint (need not be unit; re-seeded when
567
+ * parallel to the view direction).
475
568
  */
476
569
  function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
477
- let zx=ex-cx, zy=ey-cy, zz=ez-cz;
478
- const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
479
- zx/=zl; zy/=zl; zz/=zl;
480
- let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
481
- const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
482
- xx/=xl; xy/=xl; xz/=xl;
483
- const yx=zy*xz-zz*xy, yy=zz*xx-zx*xz, yz=zx*xy-zy*xx;
570
+ _lookBasis(ex,ey,ez, cx,cy,cz, ux,uy,uz);
571
+ const xx=_lb[0],xy=_lb[1],xz=_lb[2], yx=_lb[3],yy=_lb[4],yz=_lb[5], zx=_lb[6],zy=_lb[7],zz=_lb[8];
484
572
  out[0]=xx; out[1]=yx; out[2]=zx; out[3]=0;
485
573
  out[4]=xy; out[5]=yy; out[6]=zy; out[7]=0;
486
574
  out[8]=xz; out[9]=yz; out[10]=zz; out[11]=0;
@@ -499,16 +587,12 @@ function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
499
587
  * @param {Float32Array|number[]} out 16-element destination.
500
588
  * @param {number} ex,ey,ez Eye position.
501
589
  * @param {number} cx,cy,cz Look-at target.
502
- * @param {number} ux,uy,uz World up hint.
590
+ * @param {number} ux,uy,uz World up hint (need not be unit; re-seeded when
591
+ * parallel to the view direction).
503
592
  */
504
593
  function mat4Eye(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
505
- let zx=ex-cx, zy=ey-cy, zz=ez-cz;
506
- const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
507
- zx/=zl; zy/=zl; zz/=zl;
508
- let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
509
- const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
510
- xx/=xl; xy/=xl; xz/=xl;
511
- const yx=zy*xz-zz*xy, yy=zz*xx-zx*xz, yz=zx*xy-zy*xx;
594
+ _lookBasis(ex,ey,ez, cx,cy,cz, ux,uy,uz);
595
+ const xx=_lb[0],xy=_lb[1],xz=_lb[2], yx=_lb[3],yy=_lb[4],yz=_lb[5], zx=_lb[6],zy=_lb[7],zz=_lb[8];
512
596
  out[0]=xx; out[1]=xy; out[2]=xz; out[3]=0;
513
597
  out[4]=yx; out[5]=yy; out[6]=yz; out[7]=0;
514
598
  out[8]=zx; out[9]=zy; out[10]=zz; out[11]=0;
@@ -845,8 +929,8 @@ function projRight (p, ndcZMin) { return p[15]===1 ? (1-p[12])/p[0] : projNear
845
929
  */
846
930
  function projTop(p, ndcZMin) {
847
931
  return p[15]===1
848
- ? ( Math.sign(p[5]) - p[13]) / p[5] // ortho
849
- : projNear(p,ndcZMin)*(1+p[9])/p[5]; // perspective (p[5]>0 in practice)
932
+ ? ( Math.sign(p[5]) - p[13]) / p[5] // ortho
933
+ : projNear(p,ndcZMin)*(Math.sign(p[5])+p[9])/p[5]; // perspective
850
934
  }
851
935
 
852
936
  /**
@@ -854,8 +938,8 @@ function projTop(p, ndcZMin) {
854
938
  */
855
939
  function projBottom(p, ndcZMin) {
856
940
  return p[15]===1
857
- ? (-Math.sign(p[5]) - p[13]) / p[5] // ortho
858
- : projNear(p,ndcZMin)*(p[9]-1)/p[5]; // perspective
941
+ ? (-Math.sign(p[5]) - p[13]) / p[5] // ortho
942
+ : projNear(p,ndcZMin)*(p[9]-Math.sign(p[5]))/p[5]; // perspective
859
943
  }
860
944
 
861
945
  /** Vertical field of view in radians (perspective only). */
@@ -883,9 +967,10 @@ function mat4MV(out, model, view) { return mat4Mul(out, view, model); }
883
967
  */
884
968
  function mat4Location(out, from, to) {
885
969
  // Same as: return mat4Invert(out, to) && mat4Mul(out, out, from);
886
- const a00=to[0],a01=to[1],a02=to[2],
887
- a10=to[4],a11=to[5],a12=to[6],
888
- a20=to[8],a21=to[9],a22=to[10];
970
+ // a_rc reads column-major: element (row r, column c) is to[c*4 + r].
971
+ const a00=to[0],a01=to[4],a02=to[8],
972
+ a10=to[1],a11=to[5],a12=to[9],
973
+ a20=to[2],a21=to[6],a22=to[10];
889
974
  const b01=a22*a11-a12*a21, b11=a12*a20-a22*a10, b21=a21*a10-a11*a20;
890
975
  let det=a00*b01+a01*b11+a02*b21;
891
976
  if (Math.abs(det) < 1e-12) return null;
@@ -905,14 +990,17 @@ function mat4Location(out, from, to) {
905
990
  }
906
991
 
907
992
  /**
908
- * Direction transform between frames: out = to₃ · inv(from₃).
909
- * Uses only the upper-left 3×3 blocks (rotation/scale, no translation).
910
- * @returns {ArrayLike<number>|null} out, or null if `from` is singular.
993
+ * Direction transform between frames: out = inv(to₃) · from₃ — a direction's
994
+ * coordinates in `from` become its coordinates in `to`, the same conversion
995
+ * mat4Location and mapDirection perform, on the upper-left 3×3 blocks only
996
+ * (rotation / scale, no translation).
997
+ * @returns {ArrayLike<number>|null} out, or null if `to` is singular.
911
998
  */
912
999
  function mat3Direction(out, from, to) {
913
- const a00=from[0],a01=from[1],a02=from[2],
914
- a10=from[4],a11=from[5],a12=from[6],
915
- a20=from[8],a21=from[9],a22=from[10];
1000
+ // a_rc reads column-major: element (row r, column c) is to[c*4 + r].
1001
+ const a00=to[0],a01=to[4],a02=to[8],
1002
+ a10=to[1],a11=to[5],a12=to[9],
1003
+ a20=to[2],a21=to[6],a22=to[10];
916
1004
  const b01=a22*a11-a12*a21, b11=a12*a20-a22*a10, b21=a21*a10-a11*a20;
917
1005
  let det=a00*b01+a01*b11+a02*b21;
918
1006
  if (Math.abs(det) < 1e-12) return null;
@@ -920,10 +1008,13 @@ function mat3Direction(out, from, to) {
920
1008
  const i00=b01*det, i01=(a02*a21-a22*a01)*det, i02=(a12*a01-a02*a11)*det;
921
1009
  const i10=b11*det, i11=(a22*a00-a02*a20)*det, i12=(a02*a10-a12*a00)*det;
922
1010
  const i20=b21*det, i21=(a01*a20-a21*a00)*det, i22=(a11*a00-a01*a10)*det;
923
- const t00=to[0],t01=to[1],t02=to[2], t10=to[4],t11=to[5],t12=to[6], t20=to[8],t21=to[9],t22=to[10];
924
- out[0]=t00*i00+t10*i01+t20*i02; out[1]=t01*i00+t11*i01+t21*i02; out[2]=t02*i00+t12*i01+t22*i02;
925
- out[3]=t00*i10+t10*i11+t20*i12; out[4]=t01*i10+t11*i11+t21*i12; out[5]=t02*i10+t12*i11+t22*i12;
926
- out[6]=t00*i20+t10*i21+t20*i22; out[7]=t01*i20+t11*i21+t21*i22; out[8]=t02*i20+t12*i21+t22*i22;
1011
+ // out = inv(to₃) · from₃, column by column of from.
1012
+ for (let c = 0; c < 3; c++) {
1013
+ const f0=from[c*4], f1=from[c*4+1], f2=from[c*4+2];
1014
+ out[c*3] = i00*f0 + i01*f1 + i02*f2;
1015
+ out[c*3+1] = i10*f0 + i11*f1 + i12*f2;
1016
+ out[c*3+2] = i20*f0 + i21*f1 + i22*f2;
1017
+ }
927
1018
  return out;
928
1019
  }
929
1020
 
@@ -1121,9 +1212,13 @@ function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
1121
1212
  }
1122
1213
 
1123
1214
  function _screenToWorldDir(out, dx, dy, dz, proj, eye, vpW, vpH, ndcZMin) {
1124
- // Inverse of _worldToScreenDir; signed vpW/vpH cancel the y-flip.
1125
- _applyDir(out, eye, dx/(vpW*0.5)/proj[0], dy/(vpH*0.5)/proj[5], dz/((1-ndcZMin)*0.5));
1126
- return out;
1215
+ // Inverse of _worldToScreenDir: undo the viewport scale, then the projection's
1216
+ // upper-triangular 3×3 block (p[8], p[9] carry an off-centre frustum), then
1217
+ // rotate eye→world. Signed vpW/vpH cancel the y-flip.
1218
+ const cz = dz/((1-ndcZMin)*0.5)/proj[10];
1219
+ const cy = (dy/(vpH*0.5) - proj[9]*cz)/proj[5];
1220
+ const cx = (dx/(vpW*0.5) - proj[8]*cz)/proj[0];
1221
+ return _applyDir(out, eye, cx, cy, cz);
1127
1222
  }
1128
1223
 
1129
1224
  function _screenToNDCDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
@@ -1231,6 +1326,63 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
1231
1326
  return out;
1232
1327
  }
1233
1328
 
1329
+ // ═══════════════════════════════════════════════════════════════════════════
1330
+ // Rays and pointer hits
1331
+ // ═══════════════════════════════════════════════════════════════════════════
1332
+
1333
+ /**
1334
+ * Screen point → world ray: origin on the near plane (screen depth 0), unit
1335
+ * direction toward the far plane (depth 1). Same bag and viewport contract as
1336
+ * mapLocation; `m.mat4PVInv` must be filled by the caller.
1337
+ *
1338
+ * @param {number[]} outO 3-element origin.
1339
+ * @param {number[]} outD 3-element unit direction.
1340
+ * @param {number} sx,sy Screen point.
1341
+ * @param {object} m Matrices bag — see module header.
1342
+ * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
1343
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
1344
+ * @returns {number[]|null} outD, or null when the bag carries no mat4PVInv
1345
+ * (singular P·V) or the ray has no length.
1346
+ */
1347
+ function unproject(outO, outD, sx, sy, m, vp, ndcZMin) {
1348
+ const ipv = m.mat4PVInv;
1349
+ if (!ipv) return null;
1350
+ _screenToWorld(outO, sx, sy, 0, ipv, vp, ndcZMin);
1351
+ _screenToWorld(outD, sx, sy, 1, ipv, vp, ndcZMin);
1352
+ const dx=outD[0]-outO[0], dy=outD[1]-outO[1], dz=outD[2]-outO[2];
1353
+ const l=Math.sqrt(dx*dx+dy*dy+dz*dz);
1354
+ if (!(l > 0)) return null;
1355
+ outD[0]=dx/l; outD[1]=dy/l; outD[2]=dz/l;
1356
+ return outD;
1357
+ }
1358
+
1359
+ const _hit = [0, 0, 0]; // pointerHit screen scratch
1360
+
1361
+ /**
1362
+ * Is the pointer within `radius` px of the projected world point (x, y, z)?
1363
+ * `shape` is CIRCLE (Euclidean, default) or SQUARE (Chebyshev); the boundary
1364
+ * hits. A point whose screen depth falls outside [0, 1] — behind the camera,
1365
+ * before the near plane or beyond the far plane — never hits.
1366
+ *
1367
+ * @param {number} px,py Pointer, screen px.
1368
+ * @param {number} x,y,z World point.
1369
+ * @param {number} radius Hit radius, px.
1370
+ * @param {object} m Matrices bag — see module header.
1371
+ * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
1372
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
1373
+ * @param {number} [shape=CIRCLE] CIRCLE or SQUARE.
1374
+ * @returns {boolean}
1375
+ */
1376
+ function pointerHit(px, py, x, y, z, radius, m, vp, ndcZMin, shape = CIRCLE) {
1377
+ _worldToScreen(_hit, x, y, z, _ensurePV(m), vp, ndcZMin);
1378
+ const d = _hit[2];
1379
+ if (!(d >= 0 && d <= 1)) return false;
1380
+ const dx = px - _hit[0], dy = py - _hit[1];
1381
+ return shape === SQUARE
1382
+ ? Math.abs(dx) <= radius && Math.abs(dy) <= radius
1383
+ : dx*dx + dy*dy <= radius*radius;
1384
+ }
1385
+
1234
1386
  // ═══════════════════════════════════════════════════════════════════════════
1235
1387
  // pixelRatio
1236
1388
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1252,6 +1404,32 @@ function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
1252
1404
  // Pick-matrix
1253
1405
  // ═══════════════════════════════════════════════════════════════════════════
1254
1406
 
1407
+ /**
1408
+ * The viewport matrix W: NDC into screen coordinates, the S · T that seats the
1409
+ * NDC cube in the viewport rectangle and carries NDC depth into [0, 1].
1410
+ *
1411
+ * ┌ w/2 0 0 x + w/2 ┐
1412
+ * │ 0 h/2 0 y + h/2 │ vp = [x, y, w, h], h < 0 for screen y-down
1413
+ * │ 0 0 1/(1−n) −n/(1−n) │ n = ndcZMin
1414
+ * └ 0 0 0 1 ┘
1415
+ *
1416
+ * W is affine, so world → screen is one composition, (W · P · V · p) / w, and
1417
+ * screen → world its inverse — the same points mapLocation's WORLD ↔ SCREEN reaches.
1418
+ *
1419
+ * @param {Float32Array|number[]} out 16-element destination.
1420
+ * @param {number[]} vp Viewport [x, y, w, h]; same signed convention as mapLocation.
1421
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
1422
+ * @returns {Float32Array|number[]} out
1423
+ */
1424
+ function mat4Viewport(out, vp, ndcZMin) {
1425
+ const hw = vp[2] / 2, hh = vp[3] / 2, dz = 1 / (1 - ndcZMin);
1426
+ out[0]=hw; out[1]=0; out[2]=0; out[3]=0;
1427
+ out[4]=0; out[5]=hh; out[6]=0; out[7]=0;
1428
+ out[8]=0; out[9]=0; out[10]=dz; out[11]=0;
1429
+ out[12]=vp[0] + hw; out[13]=vp[1] + hh; out[14]=-ndcZMin * dz; out[15]=1;
1430
+ return out;
1431
+ }
1432
+
1255
1433
  /**
1256
1434
  * Mutate a projection matrix in-place so that the pixel at (px, py) maps to
1257
1435
  * the full NDC square — making a 1×1 FBO render contain exactly that pixel.
@@ -1284,6 +1462,34 @@ function mat4Pick(proj, px, py, vp) {
1284
1462
  }
1285
1463
  }
1286
1464
 
1465
+ // ═══════════════════════════════════════════════════════════════════════════
1466
+ // Pick-id codec
1467
+ // ═══════════════════════════════════════════════════════════════════════════
1468
+
1469
+ /**
1470
+ * Pick id → colour: the 24-bit id packed into r, g, b as normalised floats,
1471
+ * R the low byte, alpha 1. Id 0 is the background; ids run 1 … 2²⁴ − 1.
1472
+ * @param {number[]} out 4-element destination.
1473
+ * @param {number} id Integer id.
1474
+ * @returns {number[]} out
1475
+ */
1476
+ function idToRgba(out, id) {
1477
+ out[0] = (id & 255) / 255;
1478
+ out[1] = ((id >> 8) & 255) / 255;
1479
+ out[2] = ((id >> 16) & 255) / 255;
1480
+ out[3] = 1;
1481
+ return out;
1482
+ }
1483
+
1484
+ /**
1485
+ * Colour bytes → pick id, the inverse of idToRgba on a readback.
1486
+ * @param {number} r,g,b Bytes 0 … 255 (fractions truncated).
1487
+ * @returns {number} id
1488
+ */
1489
+ function rgbaToId(r, g, b) {
1490
+ return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16);
1491
+ }
1492
+
1287
1493
  // =========================================================================
1288
1494
  // Decomposition
1289
1495
  // =========================================================================
@@ -1651,9 +1857,9 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
1651
1857
  function _parseQuat(v) {
1652
1858
  if (!v) return null;
1653
1859
 
1654
- // [x,y,z,w]
1655
- if (Array.isArray(v) && v.length === 4) return [v[0],v[1],v[2],v[3]];
1656
- if (ArrayBuffer.isView(v) && v.length >= 4) return [v[0],v[1],v[2],v[3]];
1860
+ // [x,y,z,w] — normalised like every other form
1861
+ if (Array.isArray(v) && v.length === 4) return qNormalize([v[0],v[1],v[2],v[3]]);
1862
+ if (ArrayBuffer.isView(v) && v.length >= 4) return qNormalize([v[0],v[1],v[2],v[3]]);
1657
1863
 
1658
1864
  if (typeof v !== 'object') return null;
1659
1865
 
@@ -2622,13 +2828,15 @@ class CameraTrack extends Track {
2622
2828
  * A constraint is any object exposing: `kind` (integer discriminant),
2623
2829
  * `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
2624
2830
  * optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])` — the
2625
- * basis re-aim seam the bridge's deferred `from` frame drives (§4.13).
2626
- * The p5.tree handle controller drives
2627
- * any conforming constraint (lifecycle, frame conversion, bind, hooks, pick);
2628
- * a new kind 6-DOF, or app-specific implements this contract here
2629
- * (portable, draw-free) plus a bridge-side locus/pick draw (`drawLocus` /
2630
- * `pickProxy` on createHandle), rather than forking the controller. The
2631
- * classes below are the reference implementation. See handle-design.md §9.
2831
+ * basis re-aim seam the bridge's deferred `from` frame drives — and
2832
+ * `proxy(ox,oy,oz, dx,dy,dz, radius)`, the analytic pick: the ray parameter
2833
+ * at which a ray in the working space meets the grab proxy of `radius`
2834
+ * working units, or Infinity (a kind without one gets a sphere at its
2835
+ * POINT). The handle controller drives any conforming constraint
2836
+ * (lifecycle, frame conversion, bind, hooks, pick); a new kind — 6-DOF, or
2837
+ * app-specific implements this contract here (portable, draw-free) plus a
2838
+ * bridge-side locus draw, rather than forking the controller. The classes
2839
+ * below are the reference implementation. See handle-design.md §9–§10.
2632
2840
  *
2633
2841
  * ── Conventions ────────────────────────────────────────────────────────────
2634
2842
  * Ray direction `d` is assumed unit (the bridge normalises). Plane / axis
@@ -2641,7 +2849,7 @@ class CameraTrack extends Track {
2641
2849
 
2642
2850
 
2643
2851
  const EPS = 1e-6;
2644
- const TWO_PI = Math.PI * 2;
2852
+ const TWO_PI$1 = Math.PI * 2;
2645
2853
 
2646
2854
  // Edge-on threshold for DIAL: below this |d·n| the plane hit is ill-conditioned
2647
2855
  // (dθ per pixel diverges) and the solve switches to the tangent-line fallback.
@@ -2657,10 +2865,10 @@ const _clamp = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
2657
2865
  const _num = (x, d) => _isNum(x) ? x : d;
2658
2866
 
2659
2867
  /** Wrap an angle to (−π, π]. */
2660
- const _wrapPi = (a) => a - TWO_PI * Math.round(a / TWO_PI);
2868
+ const _wrapPi = (a) => a - TWO_PI$1 * Math.round(a / TWO_PI$1);
2661
2869
 
2662
2870
  /** Parse a vec3 from array / typed array / {x,y,z}. Returns a fresh [x,y,z] or null. */
2663
- function _vec3(v) {
2871
+ function _vec3$1(v) {
2664
2872
  if (!v) return null;
2665
2873
  if (ArrayBuffer.isView(v) && v.length >= 3) return [v[0], v[1], v[2]];
2666
2874
  if (Array.isArray(v) && v.length >= 3) return [v[0], v[1], v[2]];
@@ -2669,7 +2877,7 @@ function _vec3(v) {
2669
2877
  }
2670
2878
 
2671
2879
  /** Normalise a vec3 in place; zero-length falls back to the given default axis. */
2672
- function _unit(v, dx, dy, dz) {
2880
+ function _unit$1(v, dx, dy, dz) {
2673
2881
  const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
2674
2882
  if (l < EPS) { v[0]=dx; v[1]=dy; v[2]=dz; return v; }
2675
2883
  v[0]/=l; v[1]/=l; v[2]/=l;
@@ -2682,12 +2890,12 @@ function _unit(v, dx, dy, dz) {
2682
2890
  * (Same derivation the p5 bridge uses for its plane quad — duplicated here
2683
2891
  * because the core cannot depend on the bridge.)
2684
2892
  */
2685
- function _basis(n, ub, vb) {
2893
+ function _basis$1(n, ub, vb) {
2686
2894
  const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
2687
2895
  let rx = 0, ry = 0, rz = 0;
2688
2896
  if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
2689
2897
  ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
2690
- _unit(ub, 1, 0, 0);
2898
+ _unit$1(ub, 1, 0, 0);
2691
2899
  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];
2692
2900
  }
2693
2901
 
@@ -2771,6 +2979,132 @@ function rayClosestPointOnAxis(out, ox,oy,oz, dx,dy,dz, px,py,pz, ux,uy,uz) {
2771
2979
  return s;
2772
2980
  }
2773
2981
 
2982
+ // =========================================================================
2983
+ // H2b Ray-primitive hit tests (pure, no out: the nearest t, or Infinity)
2984
+ // =========================================================================
2985
+ //
2986
+ // Beside the solve primitives above, which always write a point, these are
2987
+ // TESTS: they write nothing and return the ray parameter of the nearest hit
2988
+ // with t ≥ 0, or Infinity on a miss. A ray whose origin lies inside the
2989
+ // primitive hits at its exit, so a press from inside a proxy still grabs.
2990
+ // The ray direction is assumed unit.
2991
+
2992
+ /**
2993
+ * Ray–sphere hit test.
2994
+ * @param {number} ox,oy,oz Ray origin.
2995
+ * @param {number} dx,dy,dz Ray direction (unit).
2996
+ * @param {number} cx,cy,cz Sphere centre.
2997
+ * @param {number} r Sphere radius.
2998
+ * @returns {number} The nearest t ≥ 0, or Infinity.
2999
+ */
3000
+ function rayHitSphere(ox,oy,oz, dx,dy,dz, cx,cy,cz, r) {
3001
+ const lx=ox-cx, ly=oy-cy, lz=oz-cz;
3002
+ const b = lx*dx + ly*dy + lz*dz;
3003
+ const cc = lx*lx + ly*ly + lz*lz - r*r;
3004
+ const disc = b*b - cc;
3005
+ if (disc < 0) return Infinity;
3006
+ const s = Math.sqrt(disc);
3007
+ let t = -b - s;
3008
+ if (t < 0) t = -b + s; // origin inside: the exit
3009
+ return t < 0 ? Infinity : t; // both roots behind the origin
3010
+ }
3011
+
3012
+ // One end cap of a capsule: the sphere at (cx,cy,cz), a root accepted only
3013
+ // where the hit's axial coordinate h = wu + t·du lies beyond the segment on
3014
+ // that cap's side (h ≤ 0 at A, h ≥ L at B), so the point is on the capsule's
3015
+ // surface and not inside its cylinder.
3016
+ function _capHit(ox,oy,oz, dx,dy,dz, cx,cy,cz, r, wu, du, lim, endB) {
3017
+ const lx=ox-cx, ly=oy-cy, lz=oz-cz;
3018
+ const b = lx*dx + ly*dy + lz*dz;
3019
+ const cc = lx*lx + ly*ly + lz*lz - r*r;
3020
+ const disc = b*b - cc;
3021
+ if (disc < 0) return Infinity;
3022
+ const s = Math.sqrt(disc);
3023
+ let t = -b - s, h = wu + t*du;
3024
+ if (t < 0 || (endB ? h < lim : h > lim)) { t = -b + s; h = wu + t*du; }
3025
+ return (t >= 0 && (endB ? h >= lim : h <= lim)) ? t : Infinity;
3026
+ }
3027
+
3028
+ /**
3029
+ * Ray–capsule hit test: the segment A→B swept by radius r. The cylinder wall
3030
+ * counts within the segment's extent, each end sphere beyond it; the nearest
3031
+ * of the three wins. A zero-length segment is the sphere at A.
3032
+ * @param {number} ox,oy,oz Ray origin.
3033
+ * @param {number} dx,dy,dz Ray direction (unit).
3034
+ * @param {number} ax,ay,az Segment start.
3035
+ * @param {number} bx,by,bz Segment end.
3036
+ * @param {number} r Capsule radius.
3037
+ * @returns {number} The nearest t ≥ 0, or Infinity.
3038
+ */
3039
+ function rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r) {
3040
+ let ux=bx-ax, uy=by-ay, uz=bz-az;
3041
+ const L = Math.sqrt(ux*ux + uy*uy + uz*uz);
3042
+ if (L < EPS) return rayHitSphere(ox,oy,oz, dx,dy,dz, ax,ay,az, r);
3043
+ ux/=L; uy/=L; uz/=L;
3044
+ const wx=ox-ax, wy=oy-ay, wz=oz-az;
3045
+ const du = dx*ux + dy*uy + dz*uz; // d·u
3046
+ const wu = wx*ux + wy*uy + wz*uz; // w·u
3047
+ let best = Infinity;
3048
+ // Cylinder wall: the quadratic in the components perpendicular to u.
3049
+ const a = 1 - du*du;
3050
+ if (a > EPS) {
3051
+ const b = (dx*wx + dy*wy + dz*wz) - du*wu;
3052
+ const c = (wx*wx + wy*wy + wz*wz) - wu*wu - r*r;
3053
+ const disc = b*b - a*c;
3054
+ if (disc >= 0) {
3055
+ const s = Math.sqrt(disc);
3056
+ let t = (-b - s)/a, h = wu + t*du;
3057
+ if (t < 0 || h < 0 || h > L) { t = (-b + s)/a; h = wu + t*du; }
3058
+ if (t >= 0 && h >= 0 && h <= L) best = t;
3059
+ }
3060
+ }
3061
+ const ta = _capHit(ox,oy,oz, dx,dy,dz, ax,ay,az, r, wu, du, 0, false);
3062
+ if (ta < best) best = ta;
3063
+ const tb = _capHit(ox,oy,oz, dx,dy,dz, bx,by,bz, r, wu, du, L, true);
3064
+ if (tb < best) best = tb;
3065
+ return best;
3066
+ }
3067
+
3068
+ // Ring scratch — the plane normal and its in-plane basis. Function-local
3069
+ // working values, never state: rayHitRing runs to completion.
3070
+ const _rn = [0, 0, 1], _r0 = [1, 0, 0], _r1 = [0, 1, 0];
3071
+
3072
+ /**
3073
+ * Ray–ring hit test: the circle of radius R about c in the plane
3074
+ * perpendicular to u, swept by tube radius r — the analytic torus proxy as a
3075
+ * capsule chain, the circle polygonised into `detail` segments and each
3076
+ * tested with rayHitCapsule. The chain has volume in every direction, so it
3077
+ * never degenerates edge-on; the chordal error is R · (1 − cos(π / detail)).
3078
+ * @param {number} ox,oy,oz Ray origin.
3079
+ * @param {number} dx,dy,dz Ray direction (unit).
3080
+ * @param {number} cx,cy,cz Ring centre.
3081
+ * @param {number} ux,uy,uz Ring plane normal (normalised here).
3082
+ * @param {number} R Ring radius.
3083
+ * @param {number} r Tube radius.
3084
+ * @param {number} [detail=32] Chain segments (at least 3).
3085
+ * @returns {number} The nearest t ≥ 0, or Infinity.
3086
+ */
3087
+ function rayHitRing(ox,oy,oz, dx,dy,dz, cx,cy,cz, ux,uy,uz, R, r, detail = 32) {
3088
+ const n = _isNum(detail) && detail >= 3 ? Math.floor(detail) : 32;
3089
+ _rn[0]=ux; _rn[1]=uy; _rn[2]=uz;
3090
+ _unit$1(_rn, 0, 0, 1);
3091
+ _basis$1(_rn, _r0, _r1);
3092
+ const step = TWO_PI$1 / n;
3093
+ let best = Infinity;
3094
+ let ax = cx + R*_r0[0], ay = cy + R*_r0[1], az = cz + R*_r0[2]; // vertex at angle 0
3095
+ for (let i = 1; i <= n; i++) {
3096
+ const th = i === n ? 0 : i*step; // close the chain exactly
3097
+ const cs = Math.cos(th)*R, sn = Math.sin(th)*R;
3098
+ const bx = cx + cs*_r0[0] + sn*_r1[0];
3099
+ const by = cy + cs*_r0[1] + sn*_r1[1];
3100
+ const bz = cz + cs*_r0[2] + sn*_r1[2];
3101
+ const t = rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r);
3102
+ if (t < best) best = t;
3103
+ ax = bx; ay = by; az = bz;
3104
+ }
3105
+ return best;
3106
+ }
3107
+
2774
3108
  // =========================================================================
2775
3109
  // H3 Angular utilities (readout / authoring convenience)
2776
3110
  // =========================================================================
@@ -2830,17 +3164,17 @@ class Constraint {
2830
3164
  this.kind = kind;
2831
3165
 
2832
3166
  /** Constraint origin (sphere centre / plane point / axis anchor / dial centre). @type {number[]} */
2833
- this.anchor = _vec3(opts.anchor) || [0, 0, 0];
3167
+ this.anchor = _vec3$1(opts.anchor) || [0, 0, 0];
2834
3168
  /** Canonical unit direction — SPHERE. @type {number[]} */
2835
3169
  this.dir = [0, 0, 1];
2836
3170
  /** Constrained point — PLANE / AXIS / DIAL (and SPHERE scratch). @type {number[]} */
2837
3171
  this.pt = [this.anchor[0], this.anchor[1], this.anchor[2]];
2838
3172
  /** Plane normal (unit) — PLANE. @type {number[]} */
2839
- this.n = _unit(_vec3(opts.normal) || [0, 1, 0], 0, 1, 0);
3173
+ this.n = _unit$1(_vec3$1(opts.normal) || [0, 1, 0], 0, 1, 0);
2840
3174
  /** Axis / dial-plane normal (unit) — AXIS / DIAL. @type {number[]} */
2841
3175
  this.u = kind === DIAL
2842
- ? _unit(_vec3(opts.axis) || [0, 1, 0], 0, 1, 0)
2843
- : _unit(_vec3(opts.axis) || [1, 0, 0], 1, 0, 0);
3176
+ ? _unit$1(_vec3$1(opts.axis) || [0, 1, 0], 0, 1, 0)
3177
+ : _unit$1(_vec3$1(opts.axis) || [1, 0, 0], 1, 0, 0);
2844
3178
  /** Current scalar parameter — AXIS: t along the line; DIAL: accumulated θ. @type {number} */
2845
3179
  this.s = 0;
2846
3180
 
@@ -2851,7 +3185,7 @@ class Constraint {
2851
3185
  /** In-plane binormal u × r0 (unit) — DIAL. @type {number[]} */
2852
3186
  this.r1 = [0, 0, 1];
2853
3187
  if (kind === DIAL) {
2854
- const z = _vec3(opts.zero);
3188
+ const z = _vec3$1(opts.zero);
2855
3189
  this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
2856
3190
  }
2857
3191
 
@@ -2902,10 +3236,10 @@ class Constraint {
2902
3236
  this.r0[1] = zy - d*this.u[1];
2903
3237
  this.r0[2] = zz - d*this.u[2];
2904
3238
  const l = Math.sqrt(this.r0[0]**2 + this.r0[1]**2 + this.r0[2]**2);
2905
- if (l < EPS) _basis(this.u, this.r0, this.r1);
3239
+ if (l < EPS) _basis$1(this.u, this.r0, this.r1);
2906
3240
  else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
2907
3241
  } else {
2908
- _basis(this.u, this.r0, this.r1);
3242
+ _basis$1(this.u, this.r0, this.r1);
2909
3243
  }
2910
3244
  // r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
2911
3245
  this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
@@ -2929,7 +3263,7 @@ class Constraint {
2929
3263
  this.dir[0] = this.pt[0] - this.anchor[0];
2930
3264
  this.dir[1] = this.pt[1] - this.anchor[1];
2931
3265
  this.dir[2] = this.pt[2] - this.anchor[2];
2932
- _unit(this.dir, this.dir[0], this.dir[1], this.dir[2]);
3266
+ _unit$1(this.dir, this.dir[0], this.dir[1], this.dir[2]);
2933
3267
  } else if (this.kind === PLANE) {
2934
3268
  // Parallel ray returns Infinity and leaves pt unchanged (keep last).
2935
3269
  rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
@@ -3076,7 +3410,7 @@ class Constraint {
3076
3410
  if (px*px + py*py >= EPS*EPS) {
3077
3411
  const a = Math.atan2(py, px);
3078
3412
  // Nearest winding to the current θ preserves accumulated turns.
3079
- this.s = _clamp(a + TWO_PI * Math.round((this.s - a) / TWO_PI),
3413
+ this.s = _clamp(a + TWO_PI$1 * Math.round((this.s - a) / TWO_PI$1),
3080
3414
  this.min, this.max);
3081
3415
  }
3082
3416
  this._dialPoint();
@@ -3105,24 +3439,54 @@ class Constraint {
3105
3439
  if (this.kind === PLANE) {
3106
3440
  const px = this.n[0], py = this.n[1], pz = this.n[2];
3107
3441
  this.n[0] = ax; this.n[1] = ay; this.n[2] = az;
3108
- _unit(this.n, px, py, pz);
3442
+ _unit$1(this.n, px, py, pz);
3109
3443
  this.seed(this.pt[0], this.pt[1], this.pt[2]);
3110
3444
  } else if (this.kind === AXIS) {
3111
3445
  const px = this.u[0], py = this.u[1], pz = this.u[2];
3112
3446
  this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
3113
- _unit(this.u, px, py, pz);
3447
+ _unit$1(this.u, px, py, pz);
3114
3448
  this.pt[0] = this.anchor[0] + this.s*this.u[0];
3115
3449
  this.pt[1] = this.anchor[1] + this.s*this.u[1];
3116
3450
  this.pt[2] = this.anchor[2] + this.s*this.u[2];
3117
3451
  } else if (this.kind === DIAL) {
3118
3452
  const px = this.u[0], py = this.u[1], pz = this.u[2];
3119
3453
  this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
3120
- _unit(this.u, px, py, pz);
3454
+ _unit$1(this.u, px, py, pz);
3121
3455
  this._dialBasis(zx, zy, zz);
3122
3456
  this._dialPoint();
3123
3457
  }
3124
3458
  return this;
3125
3459
  }
3460
+
3461
+ /**
3462
+ * The analytic pick proxy: the ray parameter t at which a ray in the
3463
+ * working space meets the grab proxy, or Infinity. `radius` is the grab
3464
+ * size in working-space units — a constant pixel size the caller converted
3465
+ * through pixelRatio at the proxy's depth. SPHERE / PLANE / AXIS: a sphere
3466
+ * of `radius` at the reported POINT. DIAL: the ring at the anchor with
3467
+ * tube radius `radius` (rayHitRing), so a grab lands anywhere on the ring.
3468
+ *
3469
+ * @param {number} ox,oy,oz Ray origin.
3470
+ * @param {number} dx,dy,dz Ray direction (unit).
3471
+ * @param {number} radius Grab radius, working units.
3472
+ * @returns {number} The nearest t ≥ 0, or Infinity.
3473
+ */
3474
+ proxy(ox, oy, oz, dx, dy, dz, radius) {
3475
+ const a = this.anchor;
3476
+ if (this.kind === DIAL) {
3477
+ return rayHitRing(ox,oy,oz, dx,dy,dz, a[0], a[1], a[2],
3478
+ this.u[0], this.u[1], this.u[2], this._radius, radius);
3479
+ }
3480
+ let px, py, pz;
3481
+ if (this.kind === SPHERE) {
3482
+ px = a[0] + this.dir[0]*this._radius;
3483
+ py = a[1] + this.dir[1]*this._radius;
3484
+ pz = a[2] + this.dir[2]*this._radius;
3485
+ } else {
3486
+ px = this.pt[0]; py = this.pt[1]; pz = this.pt[2];
3487
+ }
3488
+ return rayHitSphere(ox,oy,oz, dx,dy,dz, px,py,pz, radius);
3489
+ }
3126
3490
  }
3127
3491
 
3128
3492
  /**
@@ -3480,9 +3844,12 @@ class PoseHelm {
3480
3844
  * naive difference takes the long way round — the angular rate spikes at the
3481
3845
  * crossing. Flipping cur into prev's hemisphere first keeps r the shortest arc.
3482
3846
  *
3483
- * Fed through a helm with an identity profile in WORLD, the integrated pose
3484
- * retraces the source (the round-trip e9 asserts). A 1:1, non-integrated
3485
- * consumer skips the helm and applyPoses the absolute pose directly.
3847
+ * Fed through a helm in WORLD whose profile has unit `sens`, in-order lanes and
3848
+ * `Tz` / `Rr` signs of −1 — the helm's lanes are eye-frame and the null basis
3849
+ * is the identity eye matrix (forward −Z), so the two Z channels flip against
3850
+ * world axes — the integrated pose retraces the source (the round-trip e9
3851
+ * asserts). A 1:1, non-integrated consumer skips the helm and applyPoses the
3852
+ * absolute pose directly.
3486
3853
  *
3487
3854
  * @param {{ lin:number[], ang:number[] }} [out] Destination; omit for a fresh one.
3488
3855
  * @param {{ pos:ArrayLike<number>, rot:ArrayLike<number> }} prev Previous pose.
@@ -3681,5 +4048,1081 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
3681
4048
  return allIn ? VISIBLE : SEMIVISIBLE;
3682
4049
  }
3683
4050
 
3684
- export { AXIS, CameraTrack, Constraint, DIAL, DIRECTION, EYE, HELM_CHANNELS, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, POINT, PoseHelm, PoseTrack, SCREEN, SELF, SEMIVISIBLE, SPHERE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, azElFromDir, boxVisibility, createConstraint, dirFromAzEl, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4Eye, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Persp, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4View, oneEuro, pixelRatio, pointVisibility, poseDelta, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qConjugate, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qFromUnitVectors, qMul, qNegate, qNlerp, qNormalize, qRotateVec3, qSet, qSlerp, qToAxisAngle, qToMat4, rayClosestPointOnAxis, rayPlane, raySphere, sphereVisibility, transformToMat4 };
4051
+ /**
4052
+ * @file Camera state ↔ matrices: builders, decomposers, and in-place edits.
4053
+ * @module tree/camera
4054
+ * @license AGPL-3.0-only
4055
+ *
4056
+ * The camera is plain data — the CameraTrack keyframe shape:
4057
+ *
4058
+ * cam = {
4059
+ * eye: [x, y, z], // position
4060
+ * center: [x, y, z], // lookat target; |center − eye| is the gaze distance
4061
+ * up: [x, y, z], // up hint, need not be unit
4062
+ * fov: number | null, // vertical field of view, radians — perspective
4063
+ * halfHeight: number | null, // world-unit half-height at the near plane — orthographic
4064
+ * near: number, // > 0
4065
+ * far: number, // > near
4066
+ * }
4067
+ *
4068
+ * Exactly one of fov / halfHeight is meaningful; fov wins when both are set.
4069
+ * With both null the lens is "unchanged": cameraProj and cameraPlanes return
4070
+ * null and leave their output untouched, so a track that evaluates null into
4071
+ * the state keeps the last projection installed. A track writes the state
4072
+ * directly — `track.eval(cam)` — and a pose drives it through cameraFromPose.
4073
+ *
4074
+ * The state carries no aspect ratio — the viewport owns it — so one state is
4075
+ * portable between targets of different sizes; every builder that needs it
4076
+ * takes it as an argument.
4077
+ *
4078
+ * Vectors are plain number[] (f64, authoring state); matrices are written into
4079
+ * whatever 16-element buffer the caller passes. createCamera is the one
4080
+ * allocating call; everything else is out-first (or in-place) and zero-alloc.
4081
+ *
4082
+ * Every function that needs the camera frame derives it through mat4Eye, so
4083
+ * planes and edits agree with cameraEye / cameraView exactly — including the
4084
+ * up re-seed when the view direction is parallel to the up hint.
4085
+ */
4086
+
4087
+
4088
+ const _E$1 = new Float64Array(16); // eye→world frame scratch: right 0–2, up 4–6, back 8–10
4089
+ const _d = [0, 0, 0]; // view-direction scratch
4090
+
4091
+ /** Eye→world frame of `cam` into `_E`. */
4092
+ function _frame(cam) {
4093
+ const e = cam.eye, c = cam.center, u = cam.up;
4094
+ mat4Eye(_E$1, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
4095
+ }
4096
+
4097
+ /** Gaze distance |center − eye|, or 1 when the state is degenerate. */
4098
+ function _gaze(cam) {
4099
+ const dx = cam.center[0]-cam.eye[0], dy = cam.center[1]-cam.eye[1], dz = cam.center[2]-cam.eye[2];
4100
+ const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
4101
+ return d > 0 ? d : 1;
4102
+ }
4103
+
4104
+ function _vec3(v, x, y, z) { return v != null ? [v[0], v[1], v[2]] : [x, y, z]; }
4105
+
4106
+ // =========================================================================
4107
+ // State
4108
+ // =========================================================================
4109
+
4110
+ /**
4111
+ * Allocate a camera state — the one allocating call, setup-time.
4112
+ * Defaults: eye [0, 0, 500], center [0, 0, 0], up [0, 1, 0], fov π/3,
4113
+ * halfHeight null, near 0.1, far 1000. Passing halfHeight without fov yields
4114
+ * an orthographic state (fov null).
4115
+ *
4116
+ * @param {{ eye?:number[], center?:number[], up?:number[], fov?:number|null,
4117
+ * halfHeight?:number|null, near?:number, far?:number }} [opts]
4118
+ * @returns {{ eye:number[], center:number[], up:number[], fov:number|null,
4119
+ * halfHeight:number|null, near:number, far:number }}
4120
+ */
4121
+ function createCamera(opts) {
4122
+ const o = opts || {};
4123
+ const ortho = o.halfHeight != null && o.fov === undefined;
4124
+ return {
4125
+ eye: _vec3(o.eye, 0, 0, 500),
4126
+ center: _vec3(o.center, 0, 0, 0),
4127
+ up: _vec3(o.up, 0, 1, 0),
4128
+ fov: o.fov !== undefined ? o.fov : (ortho ? null : Math.PI / 3),
4129
+ halfHeight: o.halfHeight !== undefined ? o.halfHeight : null,
4130
+ near: typeof o.near === 'number' ? o.near : 0.1,
4131
+ far: typeof o.far === 'number' ? o.far : 1000,
4132
+ };
4133
+ }
4134
+
4135
+ /**
4136
+ * Copy one camera state into another (an orbit's home, a track's capture).
4137
+ * @param {object} out Destination state.
4138
+ * @param {object} cam Source state.
4139
+ * @returns {object} out
4140
+ */
4141
+ function cameraCopy(out, cam) {
4142
+ out.eye[0]=cam.eye[0]; out.eye[1]=cam.eye[1]; out.eye[2]=cam.eye[2];
4143
+ out.center[0]=cam.center[0]; out.center[1]=cam.center[1]; out.center[2]=cam.center[2];
4144
+ out.up[0]=cam.up[0]; out.up[1]=cam.up[1]; out.up[2]=cam.up[2];
4145
+ out.fov = cam.fov; out.halfHeight = cam.halfHeight;
4146
+ out.near = cam.near; out.far = cam.far;
4147
+ return out;
4148
+ }
4149
+
4150
+ // =========================================================================
4151
+ // Builders — state → matrices
4152
+ // =========================================================================
4153
+
4154
+ /**
4155
+ * View matrix (world→eye) from the state's lookat.
4156
+ * @param {Float32Array|number[]} out 16-element destination.
4157
+ * @param {object} cam
4158
+ * @returns {Float32Array|number[]} out
4159
+ */
4160
+ function cameraView(out, cam) {
4161
+ const e = cam.eye, c = cam.center, u = cam.up;
4162
+ return mat4View(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
4163
+ }
4164
+
4165
+ /**
4166
+ * Eye matrix (eye→world) from the state's lookat.
4167
+ * @param {Float32Array|number[]} out 16-element destination.
4168
+ * @param {object} cam
4169
+ * @returns {Float32Array|number[]} out
4170
+ */
4171
+ function cameraEye(out, cam) {
4172
+ const e = cam.eye, c = cam.center, u = cam.up;
4173
+ return mat4Eye(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
4174
+ }
4175
+
4176
+ /**
4177
+ * Projection matrix from the state's lens: mat4Persp from fov, or mat4Ortho
4178
+ * from halfHeight, with symmetric extents (top = near · tan(fov / 2) or
4179
+ * halfHeight; right = top · aspect).
4180
+ *
4181
+ * @param {Float32Array|number[]} out 16-element destination.
4182
+ * @param {object} cam
4183
+ * @param {number} aspect Viewport width / height.
4184
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
4185
+ * @param {number} [ndcYSign=1] +1 NDC y-up; −1 NDC y-down.
4186
+ * @returns {Float32Array|number[]|null} out, or null (out untouched) when
4187
+ * both fov and halfHeight are null.
4188
+ */
4189
+ function cameraProj(out, cam, aspect, ndcZMin, ndcYSign = 1) {
4190
+ const near = cam.near, far = cam.far;
4191
+ if (cam.fov != null) {
4192
+ const top = near * Math.tan(cam.fov / 2), right = top * aspect;
4193
+ return mat4Persp(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
4194
+ }
4195
+ if (cam.halfHeight != null) {
4196
+ const top = cam.halfHeight, right = top * aspect;
4197
+ return mat4Ortho(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
4198
+ }
4199
+ return null;
4200
+ }
4201
+
4202
+ /**
4203
+ * The six frustum planes of the state, world space — frustumPlanes over the
4204
+ * lookat basis and the symmetric extents cameraProj uses, so visibility tests
4205
+ * run against a camera state without any matrix.
4206
+ *
4207
+ * @param {Float64Array} planes 24-element destination.
4208
+ * @param {object} cam
4209
+ * @param {number} aspect Viewport width / height.
4210
+ * @returns {Float64Array|null} planes, or null (planes untouched) when both
4211
+ * fov and halfHeight are null.
4212
+ */
4213
+ function cameraPlanes(planes, cam, aspect) {
4214
+ const ortho = cam.fov == null;
4215
+ if (ortho && cam.halfHeight == null) return null;
4216
+ const top = ortho ? cam.halfHeight : cam.near * Math.tan(cam.fov / 2), right = top * aspect;
4217
+ _frame(cam);
4218
+ const e = cam.eye;
4219
+ return frustumPlanes(planes,
4220
+ e[0], e[1], e[2],
4221
+ -_E$1[8], -_E$1[9], -_E$1[10],
4222
+ _E$1[4], _E$1[5], _E$1[6],
4223
+ _E$1[0], _E$1[1], _E$1[2],
4224
+ ortho, cam.near, cam.far, -right, right, top, -top);
4225
+ }
4226
+
4227
+ // =========================================================================
4228
+ // Decomposers — matrices and poses → state
4229
+ // =========================================================================
4230
+
4231
+ /**
4232
+ * Read a state back from an eye matrix and a projection: eye ← column 3,
4233
+ * up ← column 1, forward ← −column 2 of E; center ← eye + forward · d with d
4234
+ * the state's current gaze distance (1 when degenerate), so the distance
4235
+ * survives a round trip. The lens comes from the projection queries: fov or
4236
+ * halfHeight by projIsOrtho, near and far under `ndcZMin`.
4237
+ *
4238
+ * @param {object} cam State written in place.
4239
+ * @param {ArrayLike<number>} E Eye matrix (eye→world), 16 elements.
4240
+ * @param {ArrayLike<number>} P Projection matrix, 16 elements.
4241
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
4242
+ * @returns {object} cam
4243
+ */
4244
+ function cameraFromMat4(cam, E, P, ndcZMin) {
4245
+ const d = _gaze(cam);
4246
+ let fx = -E[8], fy = -E[9], fz = -E[10];
4247
+ const fl = Math.sqrt(fx*fx+fy*fy+fz*fz) || 1;
4248
+ fx /= fl; fy /= fl; fz /= fl;
4249
+ cam.eye[0]=E[12]; cam.eye[1]=E[13]; cam.eye[2]=E[14];
4250
+ cam.up[0]=E[4]; cam.up[1]=E[5]; cam.up[2]=E[6];
4251
+ cam.center[0]=cam.eye[0]+fx*d; cam.center[1]=cam.eye[1]+fy*d; cam.center[2]=cam.eye[2]+fz*d;
4252
+ if (projIsOrtho(P)) { cam.fov = null; cam.halfHeight = projTop(P, ndcZMin); }
4253
+ else { cam.fov = projFov(P); cam.halfHeight = null; }
4254
+ cam.near = projNear(P, ndcZMin);
4255
+ cam.far = projFar(P);
4256
+ return cam;
4257
+ }
4258
+
4259
+ /**
4260
+ * Drive the lookat from a TRS pose: eye ← pos, up and forward from the
4261
+ * rotation's columns 1 and −2, center ← eye + forward · d with d the current
4262
+ * gaze distance (1 when degenerate). The lens is untouched; `scl` is ignored.
4263
+ *
4264
+ * @param {object} cam State written in place.
4265
+ * @param {{ pos:number[], rot:number[] }} pose rot is a unit quaternion [x,y,z,w].
4266
+ * @returns {object} cam
4267
+ */
4268
+ function cameraFromPose(cam, pose) {
4269
+ const d = _gaze(cam);
4270
+ const q = pose.rot, x=q[0], y=q[1], z=q[2], w=q[3];
4271
+ const x2=x+x, y2=y+y, z2=z+z;
4272
+ const xx=x*x2, xy=x*y2, xz=x*z2, yy=y*y2, yz=y*z2, zz=z*z2, wx=w*x2, wy=w*y2, wz=w*z2;
4273
+ const ux=xy-wz, uy=1-(xx+zz), uz=yz+wx; // column 1 of qToMat4(rot): up
4274
+ const bx=xz+wy, by=yz-wx, bz=1-(xx+yy); // column 2: back
4275
+ cam.eye[0]=pose.pos[0]; cam.eye[1]=pose.pos[1]; cam.eye[2]=pose.pos[2];
4276
+ cam.up[0]=ux; cam.up[1]=uy; cam.up[2]=uz;
4277
+ cam.center[0]=cam.eye[0]-bx*d; cam.center[1]=cam.eye[1]-by*d; cam.center[2]=cam.eye[2]-bz*d;
4278
+ return cam;
4279
+ }
4280
+
4281
+ /**
4282
+ * The lookat as a TRS pose: pos ← eye, rot ← qFromLookDir(center − eye, up) —
4283
+ * the rotation of cameraEye, so a helm seeded from it continues the frame.
4284
+ *
4285
+ * @param {{ pos:number[], rot:number[] }} pose Written in place.
4286
+ * @param {object} cam
4287
+ * @returns {{ pos:number[], rot:number[] }} pose
4288
+ */
4289
+ function cameraToPose(pose, cam) {
4290
+ pose.pos[0]=cam.eye[0]; pose.pos[1]=cam.eye[1]; pose.pos[2]=cam.eye[2];
4291
+ _d[0]=cam.center[0]-cam.eye[0]; _d[1]=cam.center[1]-cam.eye[1]; _d[2]=cam.center[2]-cam.eye[2];
4292
+ qFromLookDir(pose.rot, _d, cam.up);
4293
+ return pose;
4294
+ }
4295
+
4296
+ // =========================================================================
4297
+ // Edits — in place, chainable, zero-alloc
4298
+ // =========================================================================
4299
+
4300
+ /**
4301
+ * Orbit the eye about the center: `dAz` rotates it about the up hint
4302
+ * (right-handed); `dEl` raises its elevation above the plane through the
4303
+ * center perpendicular to the hint, clamped to ±opts.maxEl so the view
4304
+ * direction never reaches the hint (the pole guard). `up` is left as the hint
4305
+ * it was, so an orbit never rolls. A state already at the pole is pulled
4306
+ * inside the guard by its first non-zero edit; (0, 0) is a no-op.
4307
+ *
4308
+ * @param {object} cam
4309
+ * @param {number} dAz Azimuth delta, radians.
4310
+ * @param {number} dEl Elevation delta, radians.
4311
+ * @param {{ maxEl?:number }} [opts] Elevation limit; default π/2 − 1e-3.
4312
+ * @returns {object} cam
4313
+ */
4314
+ function cameraOrbit(cam, dAz, dEl, opts) {
4315
+ if (dAz === 0 && dEl === 0) return cam;
4316
+ const maxEl = opts && typeof opts.maxEl === 'number' ? opts.maxEl : Math.PI / 2 - 1e-3;
4317
+ const e = cam.eye, c = cam.center;
4318
+ let ux=cam.up[0], uy=cam.up[1], uz=cam.up[2];
4319
+ const ul = Math.sqrt(ux*ux+uy*uy+uz*uz) || 1;
4320
+ ux/=ul; uy/=ul; uz/=ul;
4321
+ const dx=e[0]-c[0], dy=e[1]-c[1], dz=e[2]-c[2];
4322
+ const r = Math.sqrt(dx*dx+dy*dy+dz*dz);
4323
+ _frame(cam);
4324
+ const px=_E$1[4], py=_E$1[5], pz=_E$1[6]; // the eye's up
4325
+ const bx=_E$1[8], by=_E$1[9], bz=_E$1[10]; // back: center → eye, unit
4326
+ // elevation: raise by θ within the (back, up) plane, clamped
4327
+ const el = Math.asin(Math.max(-1, Math.min(1, bx*ux+by*uy+bz*uz)));
4328
+ const th = Math.max(-maxEl, Math.min(maxEl, el + dEl)) - el;
4329
+ const ct = Math.cos(th), st = Math.sin(th);
4330
+ const ox = r*(bx*ct+px*st), oy = r*(by*ct+py*st), oz = r*(bz*ct+pz*st);
4331
+ // azimuth: rotate about the hint (Rodrigues)
4332
+ const ca = Math.cos(dAz), sa = Math.sin(dAz), k = (ux*ox+uy*oy+uz*oz)*(1-ca);
4333
+ e[0] = c[0] + ox*ca + (uy*oz-uz*oy)*sa + ux*k;
4334
+ e[1] = c[1] + oy*ca + (uz*ox-ux*oz)*sa + uy*k;
4335
+ e[2] = c[2] + oz*ca + (ux*oy-uy*ox)*sa + uz*k;
4336
+ return cam;
4337
+ }
4338
+
4339
+ /**
4340
+ * Dolly: scale the gaze distance by `factor` — the eye moves along the view
4341
+ * direction under perspective; under orthographic (fov null, halfHeight set)
4342
+ * halfHeight is scaled instead, since moving the eye changes nothing on
4343
+ * screen. opts.min / opts.max clamp the scaled quantity. An edit that would
4344
+ * make it non-positive is a no-op.
4345
+ *
4346
+ * @param {object} cam
4347
+ * @param {number} factor
4348
+ * @param {{ min?:number, max?:number }} [opts]
4349
+ * @returns {object} cam
4350
+ */
4351
+ function cameraDolly(cam, factor, opts) {
4352
+ const min = opts && typeof opts.min === 'number' ? opts.min : 0;
4353
+ const max = opts && typeof opts.max === 'number' ? opts.max : Infinity;
4354
+ if (cam.fov == null && cam.halfHeight != null) {
4355
+ const h = Math.max(min, Math.min(max, cam.halfHeight * factor));
4356
+ if (h > 0) cam.halfHeight = h;
4357
+ return cam;
4358
+ }
4359
+ const e = cam.eye, c = cam.center;
4360
+ const dx=e[0]-c[0], dy=e[1]-c[1], dz=e[2]-c[2];
4361
+ const r = Math.sqrt(dx*dx+dy*dy+dz*dz);
4362
+ if (r === 0) return cam;
4363
+ const r1 = Math.max(min, Math.min(max, r * factor));
4364
+ if (!(r1 > 0)) return cam;
4365
+ const s = r1 / r;
4366
+ e[0]=c[0]+dx*s; e[1]=c[1]+dy*s; e[2]=c[2]+dz*s;
4367
+ return cam;
4368
+ }
4369
+
4370
+ /**
4371
+ * Pan: translate eye and center by `dx` along the eye's right and `dy` along
4372
+ * its up, world units. The caller converts pixels through pixelRatio at the
4373
+ * center's depth so a pan tracks the pointer.
4374
+ *
4375
+ * @param {object} cam
4376
+ * @param {number} dx
4377
+ * @param {number} dy
4378
+ * @returns {object} cam
4379
+ */
4380
+ function cameraPan(cam, dx, dy) {
4381
+ _frame(cam);
4382
+ const tx=_E$1[0]*dx+_E$1[4]*dy, ty=_E$1[1]*dx+_E$1[5]*dy, tz=_E$1[2]*dx+_E$1[6]*dy;
4383
+ const e = cam.eye, c = cam.center;
4384
+ e[0]+=tx; e[1]+=ty; e[2]+=tz;
4385
+ c[0]+=tx; c[1]+=ty; c[2]+=tz;
4386
+ return cam;
4387
+ }
4388
+
4389
+ /**
4390
+ * @file Gizmo line generators in the arrays shape.
4391
+ * @module tree/gizmo
4392
+ * @license AGPL-3.0-only
4393
+ *
4394
+ * A gizmo is geometry that explains: axes, a grid, a frustum, a path, a rig,
4395
+ * a handle's locus. This module generates its vertices — renderer-free, into
4396
+ * caller-owned arrays shaped the way twgl's createBufferInfoFromArrays
4397
+ * consumes them and a WebGPU vertex buffer is filled from — and nothing
4398
+ * else. Drawing, colour state, HUD mode, textures, the dot at a handle's
4399
+ * point, text: all the bridge's or the host's.
4400
+ *
4401
+ * ── The arrays shape ───────────────────────────────────────────────────────
4402
+ *
4403
+ * out = {
4404
+ * position: { numComponents: 3, data: Float32Array(3 · capacity) },
4405
+ * color: { numComponents: 4, data: Float32Array(4 · capacity) }, // optional
4406
+ * texcoord: { numComponents: 2, data: Float32Array(2 · capacity) }, // optional — panes
4407
+ * count: 0, // vertices written
4408
+ * labels: [], // optional — { x, y, z, text }
4409
+ * }
4410
+ *
4411
+ * Line generators write line lists — vertex pairs, no indices; paneTris
4412
+ * writes two triangles. capacity is position.data.length / 3.
4413
+ *
4414
+ * ── The contract — snprintf-style ──────────────────────────────────────────
4415
+ * Every generator gen(out, …) → n returns the vertex count it needs, writes
4416
+ * min(n, capacity) vertices, and sets out.count to what it wrote. A caller
4417
+ * sizes once and grows never in steady state:
4418
+ *
4419
+ * let n = axesLines(out, opts)
4420
+ * if (n > capacityOf(out)) { growArrays(out, n); axesLines(out, opts) }
4421
+ *
4422
+ * Each generator states its count formula so a caller can pre-size exactly.
4423
+ *
4424
+ * ── Colour ─────────────────────────────────────────────────────────────────
4425
+ * If out.color exists, every vertex written gets a colour: a generator with
4426
+ * semantic colouring (axes, the helm rig) writes its palette, every other
4427
+ * generator writes opts.color (default white). If out.color is absent
4428
+ * nothing is written and the bridge draws with a uniform colour.
4429
+ *
4430
+ * ── Frames ─────────────────────────────────────────────────────────────────
4431
+ * A generator writes in the frame the caller means — model space for scene
4432
+ * gizmos (the bridge's M places them), screen pixels for HUD gizmos.
4433
+ * Nothing here consults a camera except locusLines and frustumLines, which
4434
+ * take what they need explicitly. Signatures: out first, the subject second
4435
+ * where there is one, options last.
4436
+ */
4437
+
4438
+
4439
+ const TWO_PI = Math.PI * 2;
4440
+ const _AXIS_COLORS = [COLOR_X, COLOR_Y, COLOR_Z];
4441
+ const _U = [1, 0, 0], _V = [0, 1, 0]; // the HUD plane's basis
4442
+ const _E = new Float64Array(16); // a camera state's eye matrix
4443
+ const _p3 = [0, 0, 0]; // a transformed corner / a sampled point
4444
+ const _q3 = [0, 0, 0]; // the previous sampled point
4445
+ const _c24 = new Float64Array(24); // frustum corners scratch
4446
+ const _tIn = [0, 0, 0], _tOut = [0, 0, 0]; // a keyframe's tangents
4447
+ const _act = [0, 0, 0, 0, 0, 0]; // a helm's activity
4448
+ const _tip = [0, 0, 0], _ha = [0, 0, 0]; // an arrow's tip and head base
4449
+ const _AXES = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
4450
+ const _b0 = [0, 0, 0], _b1 = [0, 0, 0], _b2 = [0, 0, 0]; // a locus basis
4451
+ const _UV_DEFAULT = [0, 1, 1, 1, 1, 0, 0, 0]; // paneTris: p0 top-left → (0, 1), clockwise
4452
+
4453
+ // =========================================================================
4454
+ // G1 Arrays — the one allocating call, growth, capacity
4455
+ // =========================================================================
4456
+
4457
+ /**
4458
+ * Allocate an arrays object of `capacity` vertices — the one allocating
4459
+ * call, setup-time. Flags add the optional attributes and the label list.
4460
+ *
4461
+ * @param {number} capacity Vertices.
4462
+ * @param {{ color?:boolean, texcoord?:boolean, labels?:boolean }} [opts]
4463
+ * @returns {{ position:{numComponents:number,data:Float32Array},
4464
+ * color?:{numComponents:number,data:Float32Array},
4465
+ * texcoord?:{numComponents:number,data:Float32Array},
4466
+ * count:number, labels?:object[] }}
4467
+ */
4468
+ function createArrays(capacity, opts) {
4469
+ const o = opts || {};
4470
+ const n = Math.max(0, capacity | 0);
4471
+ const out = { position: { numComponents: 3, data: new Float32Array(3 * n) } };
4472
+ if (o.color) out.color = { numComponents: 4, data: new Float32Array(4 * n) };
4473
+ if (o.texcoord) out.texcoord = { numComponents: 2, data: new Float32Array(2 * n) };
4474
+ out.count = 0;
4475
+ if (o.labels) out.labels = [];
4476
+ return out;
4477
+ }
4478
+
4479
+ /**
4480
+ * Reallocate an arrays object to a new capacity, keeping its attribute set;
4481
+ * the data is fresh (a generator refills it) and count is 0.
4482
+ *
4483
+ * @param {object} out An arrays object from createArrays.
4484
+ * @param {number} capacity Vertices.
4485
+ * @returns {object} out
4486
+ */
4487
+ function growArrays(out, capacity) {
4488
+ const n = Math.max(0, capacity | 0);
4489
+ out.position.data = new Float32Array(3 * n);
4490
+ if (out.color) out.color.data = new Float32Array(4 * n);
4491
+ if (out.texcoord) out.texcoord.data = new Float32Array(2 * n);
4492
+ out.count = 0;
4493
+ if (out.labels) out.labels.length = 0;
4494
+ return out;
4495
+ }
4496
+
4497
+ /**
4498
+ * The vertex capacity of an arrays object: position.data.length / 3.
4499
+ * @param {object} out
4500
+ * @returns {number}
4501
+ */
4502
+ function capacityOf(out) {
4503
+ return (out.position.data.length / 3) | 0;
4504
+ }
4505
+
4506
+ // =========================================================================
4507
+ // G2 The writer — a cursor over out; counts every vertex, writes those
4508
+ // within capacity. Module-level scratch: a generator runs to completion.
4509
+ // =========================================================================
4510
+
4511
+ const _w = { pos: null, col: null, tex: null, cap: 0, n: 0, r: 1, g: 1, b: 1, a: 1 };
4512
+ const _WHITE = [1, 1, 1, 1];
4513
+
4514
+ function _begin(out) {
4515
+ _w.pos = out.position.data;
4516
+ _w.col = out.color ? out.color.data : null;
4517
+ _w.tex = out.texcoord ? out.texcoord.data : null;
4518
+ _w.cap = (_w.pos.length / 3) | 0;
4519
+ _w.n = 0;
4520
+ if (out.labels) out.labels.length = 0;
4521
+ }
4522
+
4523
+ /** Set the current colour from an RGB(A) array, with an alpha override. */
4524
+ function _color(c, alpha) {
4525
+ const v = c || _WHITE;
4526
+ _w.r = v[0]; _w.g = v[1]; _w.b = v[2];
4527
+ _w.a = alpha != null ? alpha : (v.length > 3 ? v[3] : 1);
4528
+ }
4529
+
4530
+ function _vertex(x, y, z) {
4531
+ const n = _w.n++;
4532
+ if (n >= _w.cap) return;
4533
+ const p = _w.pos, i = 3 * n;
4534
+ p[i] = x; p[i + 1] = y; p[i + 2] = z;
4535
+ const c = _w.col;
4536
+ if (c) { const j = 4 * n; c[j] = _w.r; c[j + 1] = _w.g; c[j + 2] = _w.b; c[j + 3] = _w.a; }
4537
+ }
4538
+
4539
+ function _line(x0, y0, z0, x1, y1, z1) {
4540
+ _vertex(x0, y0, z0);
4541
+ _vertex(x1, y1, z1);
4542
+ }
4543
+
4544
+ /** A vertex with a texture coordinate (written when out.texcoord exists). */
4545
+ function _vertexUV(x, y, z, u, v) {
4546
+ const n = _w.n;
4547
+ _vertex(x, y, z);
4548
+ if (_w.tex && n < _w.cap) { _w.tex[2*n] = u; _w.tex[2*n + 1] = v; }
4549
+ }
4550
+
4551
+ /** Normalise v in place; a zero vector becomes (dx, dy, dz). */
4552
+ function _unit(v, dx, dy, dz) {
4553
+ const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
4554
+ if (l < 1e-9) { v[0] = dx; v[1] = dy; v[2] = dz; return v; }
4555
+ v[0] /= l; v[1] /= l; v[2] /= l;
4556
+ return v;
4557
+ }
4558
+
4559
+ /** Orthonormal in-plane basis (ub, vb) for a unit normal n, seeded from the least-aligned axis. */
4560
+ function _basis(n, ub, vb) {
4561
+ const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
4562
+ let rx = 0, ry = 0, rz = 0;
4563
+ if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
4564
+ ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
4565
+ _unit(ub, 1, 0, 0);
4566
+ 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];
4567
+ }
4568
+
4569
+ /** The four edges of a square of half-extent h at c, spanned by u, v. */
4570
+ function _square(c, u, v, h) {
4571
+ const x0 = c[0] - h*u[0] - h*v[0], y0 = c[1] - h*u[1] - h*v[1], z0 = c[2] - h*u[2] - h*v[2];
4572
+ const x1 = c[0] + h*u[0] - h*v[0], y1 = c[1] + h*u[1] - h*v[1], z1 = c[2] + h*u[2] - h*v[2];
4573
+ const x2 = c[0] + h*u[0] + h*v[0], y2 = c[1] + h*u[1] + h*v[1], z2 = c[2] + h*u[2] + h*v[2];
4574
+ const x3 = c[0] - h*u[0] + h*v[0], y3 = c[1] - h*u[1] + h*v[1], z3 = c[2] - h*u[2] + h*v[2];
4575
+ _line(x0, y0, z0, x1, y1, z1);
4576
+ _line(x1, y1, z1, x2, y2, z2);
4577
+ _line(x2, y2, z2, x3, y3, z3);
4578
+ _line(x3, y3, z3, x0, y0, z0);
4579
+ }
4580
+
4581
+ /** Close the write: count ← what fits; return what was needed. */
4582
+ function _end(out) {
4583
+ out.count = _w.n < _w.cap ? _w.n : _w.cap;
4584
+ return _w.n;
4585
+ }
4586
+
4587
+ /** A sampled circle (or arc of `sweep`) of radius r at c, spanned by u, v: n segments. */
4588
+ function _ring(cx, cy, cz, r, u, v, n, sweep) {
4589
+ let px = 0, py = 0, pz = 0;
4590
+ for (let i = 0; i <= n; i++) {
4591
+ const t = (i / n) * sweep;
4592
+ const ct = Math.cos(t) * r, st = Math.sin(t) * r;
4593
+ const x = cx + ct*u[0] + st*v[0];
4594
+ const y = cy + ct*u[1] + st*v[1];
4595
+ const z = cz + ct*u[2] + st*v[2];
4596
+ if (i > 0) _line(px, py, pz, x, y, z);
4597
+ px = x; py = y; pz = z;
4598
+ }
4599
+ }
4600
+
4601
+ // =========================================================================
4602
+ // G3 Axes, grid, cross, bulls-eye, ring
4603
+ // =========================================================================
4604
+
4605
+ /**
4606
+ * A coordinate frame at the origin: six half-axes by bit and, with LABELS,
4607
+ * the X (2 lines) · Y (4) · Z (3) glyphs at 1.04 · size, sized size / 40 ×
4608
+ * size / 30. Semantic colour per axis and its glyph (COLOR_X / Y / Z), or
4609
+ * opts.color when `semantic` is false.
4610
+ *
4611
+ * Count: 2 · axes + 18 · (LABELS ? 1 : 0), at most 30.
4612
+ *
4613
+ * @param {object} out Arrays object.
4614
+ * @param {{ size?:number, bits?:number, semantic?:boolean, color?:number[] }} [opts]
4615
+ * @returns {number} Vertices needed.
4616
+ */
4617
+ function axesLines(out, opts) {
4618
+ const o = opts || {};
4619
+ const size = o.size ?? 100;
4620
+ const bits = o.bits ?? (LABELS | X | Y | Z);
4621
+ const semantic = o.semantic !== false;
4622
+ const axis = (i) => _color(semantic ? _AXIS_COLORS[i] : o.color);
4623
+ _begin(out);
4624
+ if (bits & LABELS) {
4625
+ const cw = size/40, ch = size/30, cs = 1.04*size;
4626
+ axis(0);
4627
+ _line(cs, cw, -ch, cs, -cw, ch);
4628
+ _line(cs, -cw, -ch, cs, cw, ch);
4629
+ axis(1);
4630
+ _line( cw, cs, ch, 0, cs, 0);
4631
+ _line( 0, cs, 0, -cw, cs, ch);
4632
+ _line(-cw, cs, ch, 0, cs, 0);
4633
+ _line( 0, cs, 0, 0, cs, -ch);
4634
+ axis(2);
4635
+ _line(-cw, -ch, cs, cw, -ch, cs);
4636
+ _line( cw, -ch, cs, -cw, ch, cs);
4637
+ _line(-cw, ch, cs, cw, ch, cs);
4638
+ }
4639
+ axis(0);
4640
+ if (bits & X) _line(0, 0, 0, size, 0, 0);
4641
+ if (bits & _X) _line(0, 0, 0, -size, 0, 0);
4642
+ axis(1);
4643
+ if (bits & Y) _line(0, 0, 0, 0, size, 0);
4644
+ if (bits & _Y) _line(0, 0, 0, 0, -size, 0);
4645
+ axis(2);
4646
+ if (bits & Z) _line(0, 0, 0, 0, 0, size);
4647
+ if (bits & _Z) _line(0, 0, 0, 0, 0, -size);
4648
+ return _end(out);
4649
+ }
4650
+
4651
+ /**
4652
+ * A grid in the XY plane: subdivisions + 1 lines each way spanning ±size.
4653
+ * Orientation is the caller's M (a ground plane is a rotation about X).
4654
+ *
4655
+ * Count: 4 · (subdivisions + 1).
4656
+ *
4657
+ * @param {object} out Arrays object.
4658
+ * @param {{ size?:number, subdivisions?:number, color?:number[] }} [opts]
4659
+ * @returns {number} Vertices needed.
4660
+ */
4661
+ function gridLines(out, opts) {
4662
+ const o = opts || {};
4663
+ const size = o.size ?? 100;
4664
+ const sub = Math.max(1, (o.subdivisions ?? 10) | 0);
4665
+ _begin(out);
4666
+ _color(o.color);
4667
+ for (let i = 0; i <= sub; i++) {
4668
+ const pos = size * (2*i/sub - 1);
4669
+ _line(pos, -size, 0, pos, size, 0);
4670
+ _line(-size, pos, 0, size, pos, 0);
4671
+ }
4672
+ return _end(out);
4673
+ }
4674
+
4675
+ /**
4676
+ * A crosshair in HUD space (z = 0, target pixels): two lines of `size`
4677
+ * through (x, y). The bridge projects a model origin and converts a world
4678
+ * size to pixels before this call.
4679
+ *
4680
+ * Count: 4.
4681
+ *
4682
+ * @param {object} out Arrays object.
4683
+ * @param {{ x?:number, y?:number, size?:number, color?:number[] }} [opts]
4684
+ * @returns {number} Vertices needed.
4685
+ */
4686
+ function crossLines(out, opts) {
4687
+ const o = opts || {};
4688
+ const x = o.x ?? 0, y = o.y ?? 0, half = (o.size ?? 50) / 2;
4689
+ _begin(out);
4690
+ _color(o.color);
4691
+ _line(x - half, y, 0, x + half, y, 0);
4692
+ _line(x, y - half, 0, x, y + half, 0);
4693
+ return _end(out);
4694
+ }
4695
+
4696
+ /**
4697
+ * A bulls-eye in HUD space (z = 0, target pixels): a sampled circle of
4698
+ * radius size / 2 (`detail` segments) or the cornered square (8 lines),
4699
+ * plus the central cross at 0.6 · half.
4700
+ *
4701
+ * Count: 2 · detail + 4 (CIRCLE) or 20 (SQUARE).
4702
+ *
4703
+ * @param {object} out Arrays object.
4704
+ * @param {{ x?:number, y?:number, size?:number, shape?:number, detail?:number, color?:number[] }} [opts]
4705
+ * @returns {number} Vertices needed.
4706
+ */
4707
+ function bullsEyeLines(out, opts) {
4708
+ const o = opts || {};
4709
+ const x = o.x ?? 0, y = o.y ?? 0, half = (o.size ?? 50) / 2;
4710
+ const shape = o.shape ?? CIRCLE;
4711
+ const detail = Math.max(3, (o.detail ?? 50) | 0);
4712
+ _begin(out);
4713
+ _color(o.color);
4714
+ if (shape === CIRCLE) {
4715
+ _ring(x, y, 0, half, _U, _V, detail, TWO_PI);
4716
+ } else {
4717
+ const c = 0.6 * half;
4718
+ _line(x-half, y-half+c, 0, x-half, y-half, 0);
4719
+ _line(x-half, y-half, 0, x-half+c, y-half, 0);
4720
+ _line(x+half-c, y-half, 0, x+half, y-half, 0);
4721
+ _line(x+half, y-half, 0, x+half, y-half+c, 0);
4722
+ _line(x+half, y+half-c, 0, x+half, y+half, 0);
4723
+ _line(x+half, y+half, 0, x+half-c, y+half, 0);
4724
+ _line(x-half+c, y+half, 0, x-half, y+half, 0);
4725
+ _line(x-half, y+half, 0, x-half, y+half-c, 0);
4726
+ }
4727
+ const ch = 0.6 * half;
4728
+ _line(x - ch, y, 0, x + ch, y, 0);
4729
+ _line(x, y - ch, 0, x, y + ch, 0);
4730
+ return _end(out);
4731
+ }
4732
+
4733
+ /**
4734
+ * A sampled circle of radius r at (cx, cy, cz) spanned by the orthonormal
4735
+ * u, v — the shared primitive; a partial `sweep` gives an arc from u.
4736
+ *
4737
+ * Count: 2 · detail.
4738
+ *
4739
+ * @param {object} out Arrays object.
4740
+ * @param {number} cx,cy,cz Centre.
4741
+ * @param {number} r Radius.
4742
+ * @param {number[]} u,v Orthonormal in-plane basis.
4743
+ * @param {{ detail?:number, sweep?:number, color?:number[] }} [opts]
4744
+ * @returns {number} Vertices needed.
4745
+ */
4746
+ function ringLines(out, cx, cy, cz, r, u, v, opts) {
4747
+ const o = opts || {};
4748
+ const detail = Math.max(1, (o.detail ?? 48) | 0);
4749
+ const sweep = o.sweep ?? TWO_PI;
4750
+ _begin(out);
4751
+ _color(o.color);
4752
+ _ring(cx, cy, cz, r, u, v, detail, sweep);
4753
+ return _end(out);
4754
+ }
4755
+
4756
+ // =========================================================================
4757
+ // G4 Frustum and Hermite
4758
+ // =========================================================================
4759
+
4760
+ const _isMat = (cam) => cam != null && cam.mat4Eye != null && cam.mat4Proj != null;
4761
+
4762
+ /** Corner i of out24 ← E · (x, y, z). */
4763
+ function _corner(out24, i, E, x, y, z) {
4764
+ mat4MulPoint(_p3, E, x, y, z);
4765
+ out24[3*i] = _p3[0]; out24[3*i + 1] = _p3[1]; out24[3*i + 2] = _p3[2];
4766
+ }
4767
+
4768
+ /**
4769
+ * The eight world-space corners of a camera's frustum: the near face 0–3
4770
+ * counter-clockwise from bottom-left (BL, BR, TR, TL), then the far face
4771
+ * 4–7 in the same order — so corners 3, 2, 1, 0 are a pane's TL, TR, BR,
4772
+ * BL. `cam` is a camera state (its symmetric extents from fov or
4773
+ * halfHeight and `aspect`), or { mat4Eye, mat4Proj, ndcZMin? } for a
4774
+ * matrix-captured camera (the extents read off the projection). The far
4775
+ * extents follow by similar triangles, or equal the near ones under
4776
+ * orthographic.
4777
+ *
4778
+ * @param {Float64Array|number[]} out24 24-element destination.
4779
+ * @param {object} cam Camera state, or { mat4Eye, mat4Proj, ndcZMin? }.
4780
+ * @param {number} [aspect=1] Viewport width / height (state form).
4781
+ * @param {number} [ndcZMin=WEBGL] NDC-z convention when the matrix form carries none.
4782
+ * @returns {Float64Array|number[]|null} out24, or null when the state's lens is unset.
4783
+ */
4784
+ function frustumCorners(out24, cam, aspect, ndcZMin) {
4785
+ let E, n, f, l, r, t, b, ortho;
4786
+ if (_isMat(cam)) {
4787
+ E = cam.mat4Eye;
4788
+ const P = cam.mat4Proj, z = cam.ndcZMin ?? ndcZMin ?? WEBGL;
4789
+ ortho = projIsOrtho(P);
4790
+ n = projNear(P, z); f = projFar(P);
4791
+ l = projLeft(P, z); r = projRight(P, z); t = projTop(P, z); b = projBottom(P, z);
4792
+ } else {
4793
+ ortho = cam.fov == null;
4794
+ if (ortho && cam.halfHeight == null) return null;
4795
+ n = cam.near; f = cam.far;
4796
+ t = ortho ? cam.halfHeight : n * Math.tan(cam.fov / 2);
4797
+ r = t * (aspect ?? 1);
4798
+ b = -t; l = -r;
4799
+ E = cameraEye(_E, cam);
4800
+ }
4801
+ const k = ortho ? 1 : f / n;
4802
+ _corner(out24, 0, E, l, b, -n);
4803
+ _corner(out24, 1, E, r, b, -n);
4804
+ _corner(out24, 2, E, r, t, -n);
4805
+ _corner(out24, 3, E, l, t, -n);
4806
+ _corner(out24, 4, E, k*l, k*b, -f);
4807
+ _corner(out24, 5, E, k*r, k*b, -f);
4808
+ _corner(out24, 6, E, k*r, k*t, -f);
4809
+ _corner(out24, 7, E, k*l, k*t, -f);
4810
+ return out24;
4811
+ }
4812
+
4813
+ /** Line between corners i and j of the scratch corners. */
4814
+ function _edge(i, j) {
4815
+ _line(_c24[3*i], _c24[3*i + 1], _c24[3*i + 2], _c24[3*j], _c24[3*j + 1], _c24[3*j + 2]);
4816
+ }
4817
+
4818
+ /**
4819
+ * A camera's frustum as edges by bit: NEAR and FAR the two rectangles,
4820
+ * BODY the four edges joining them, APEX (perspective only) the eye to
4821
+ * the near corners. `cam` as frustumCorners takes it.
4822
+ *
4823
+ * Count: 8 · (NEAR + FAR + BODY + APEX), at most 32.
4824
+ *
4825
+ * @param {object} out Arrays object.
4826
+ * @param {object} cam Camera state, or { mat4Eye, mat4Proj, ndcZMin? }.
4827
+ * @param {{ aspect?:number, ndcZMin?:number, bits?:number, color?:number[] }} [opts]
4828
+ * @returns {number} Vertices needed (0 when the state's lens is unset).
4829
+ */
4830
+ function frustumLines(out, cam, opts) {
4831
+ const o = opts || {};
4832
+ const bits = o.bits ?? (NEAR | FAR | BODY | APEX);
4833
+ _begin(out);
4834
+ _color(o.color);
4835
+ if (frustumCorners(_c24, cam, o.aspect ?? 1, o.ndcZMin ?? WEBGL) === null) return _end(out);
4836
+ if (bits & NEAR) { _edge(0, 1); _edge(1, 2); _edge(2, 3); _edge(3, 0); }
4837
+ if (bits & FAR) { _edge(4, 5); _edge(5, 6); _edge(6, 7); _edge(7, 4); }
4838
+ if (bits & BODY) { _edge(0, 4); _edge(1, 5); _edge(2, 6); _edge(3, 7); }
4839
+ const persp = _isMat(cam) ? !projIsOrtho(cam.mat4Proj) : cam.fov != null;
4840
+ if ((bits & APEX) && persp) {
4841
+ const E = _isMat(cam) ? cam.mat4Eye : null;
4842
+ const ex = E ? E[12] : cam.eye[0], ey = E ? E[13] : cam.eye[1], ez = E ? E[14] : cam.eye[2];
4843
+ for (let i = 0; i < 4; i++) _line(ex, ey, ez, _c24[3*i], _c24[3*i + 1], _c24[3*i + 2]);
4844
+ }
4845
+ return _end(out);
4846
+ }
4847
+
4848
+ /**
4849
+ * One cubic Hermite segment through hermiteVec3, as a polyline of
4850
+ * `samples` steps.
4851
+ *
4852
+ * Count: 2 · samples.
4853
+ *
4854
+ * @param {object} out Arrays object.
4855
+ * @param {number[]} p0,t0 Start point and its outgoing tangent.
4856
+ * @param {number[]} p1,t1 End point and its incoming tangent.
4857
+ * @param {{ samples?:number, color?:number[] }} [opts]
4858
+ * @returns {number} Vertices needed.
4859
+ */
4860
+ function hermiteLines(out, p0, t0, p1, t1, opts) {
4861
+ const o = opts || {};
4862
+ const N = Math.max(1, (o.samples ?? 32) | 0);
4863
+ _begin(out);
4864
+ _color(o.color);
4865
+ for (let i = 0; i <= N; i++) {
4866
+ hermiteVec3(_p3, p0, t0, p1, t1, i / N);
4867
+ if (i > 0) _line(_q3[0], _q3[1], _q3[2], _p3[0], _p3[1], _p3[2]);
4868
+ _q3[0] = _p3[0]; _q3[1] = _p3[1]; _q3[2] = _p3[2];
4869
+ }
4870
+ return _end(out);
4871
+ }
4872
+
4873
+ // =========================================================================
4874
+ // G5 Path, helm rig, locus, pane
4875
+ // =========================================================================
4876
+
4877
+ /**
4878
+ * A PoseTrack or CameraTrack's path by bit, over the track's own samplers
4879
+ * (samplePos / sampleEye / sampleCenter and the tangent readers), so the
4880
+ * interpolation modes are honoured: PATH the sampled polyline, `samples`
4881
+ * per segment; CONTROLS the straight control polygon; TANGENTS_IN /
4882
+ * TANGENTS_OUT the tangent at each keyframe scaled by `tangentScale`;
4883
+ * CENTER (camera tracks) the gaze line eye → center per keyframe and a
4884
+ * three-axis star of half-size `centerSize` at the center. `target`
4885
+ * ('eye' or 'center') picks a camera track's path for the first three
4886
+ * bits. Markers and handles are the bridge's composition.
4887
+ *
4888
+ * Count: 2 · samples · segments (PATH) + 2 · segments (CONTROLS) +
4889
+ * 2 · keyframes per tangent bit + 8 · keyframes (CENTER).
4890
+ *
4891
+ * @param {object} out Arrays object.
4892
+ * @param {object} track PoseTrack or CameraTrack.
4893
+ * @param {{ bits?:number, samples?:number, tangentScale?:number, target?:string,
4894
+ * centerSize?:number, color?:number[] }} [opts]
4895
+ * @returns {number} Vertices needed.
4896
+ */
4897
+ function pathLines(out, track, opts) {
4898
+ const o = opts || {};
4899
+ const bits = o.bits ?? (PATH | CONTROLS | TANGENTS_IN | TANGENTS_OUT);
4900
+ const N = Math.max(1, (o.samples ?? 32) | 0);
4901
+ const ts = o.tangentScale ?? 0.25;
4902
+ const cs = o.centerSize ?? 4;
4903
+ const kfs = track.keyframes, n = kfs.length;
4904
+ const isCamera = typeof track.sampleEye === 'function';
4905
+ const useCenter = isCamera && o.target === 'center';
4906
+ const field = isCamera ? (useCenter ? 'center' : 'eye') : 'pos';
4907
+ const sampler = isCamera ? (useCenter ? 'sampleCenter' : 'sampleEye') : 'samplePos';
4908
+ const tangents = isCamera ? (useCenter ? 'centerTangents' : 'eyeTangents') : 'tangents';
4909
+ _begin(out);
4910
+ _color(o.color);
4911
+ if ((bits & PATH) && n > 1) {
4912
+ for (let seg = 0; seg < n - 1; seg++) {
4913
+ for (let i = 0; i <= N; i++) {
4914
+ track[sampler](_p3, seg, i / N);
4915
+ if (i > 0) _line(_q3[0], _q3[1], _q3[2], _p3[0], _p3[1], _p3[2]);
4916
+ _q3[0] = _p3[0]; _q3[1] = _p3[1]; _q3[2] = _p3[2];
4917
+ }
4918
+ }
4919
+ }
4920
+ if (bits & CONTROLS) {
4921
+ for (let i = 0; i < n - 1; i++) {
4922
+ const a = kfs[i][field], b = kfs[i + 1][field];
4923
+ _line(a[0], a[1], a[2], b[0], b[1], b[2]);
4924
+ }
4925
+ }
4926
+ if (bits & (TANGENTS_IN | TANGENTS_OUT)) {
4927
+ for (let i = 0; i < n; i++) {
4928
+ track[tangents](_tIn, _tOut, i);
4929
+ const k = kfs[i][field];
4930
+ if (bits & TANGENTS_IN) _line(k[0] - ts*_tIn[0], k[1] - ts*_tIn[1], k[2] - ts*_tIn[2], k[0], k[1], k[2]);
4931
+ if (bits & TANGENTS_OUT) _line(k[0], k[1], k[2], k[0] + ts*_tOut[0], k[1] + ts*_tOut[1], k[2] + ts*_tOut[2]);
4932
+ }
4933
+ }
4934
+ if ((bits & CENTER) && isCamera) {
4935
+ for (let i = 0; i < n; i++) {
4936
+ const e = kfs[i].eye, c = kfs[i].center;
4937
+ _line(e[0], e[1], e[2], c[0], c[1], c[2]);
4938
+ _line(c[0] - cs, c[1], c[2], c[0] + cs, c[1], c[2]);
4939
+ _line(c[0], c[1] - cs, c[2], c[0], c[1] + cs, c[2]);
4940
+ _line(c[0], c[1], c[2] - cs, c[0], c[1], c[2] + cs);
4941
+ }
4942
+ }
4943
+ return _end(out);
4944
+ }
4945
+
4946
+ /** An arrow along principal axis `axis` (0 X, 1 Y, 2 Z): signed length L, head size h — 5 lines. */
4947
+ function _arrow(axis, L, h) {
4948
+ const a = (axis + 1) % 3, b = (axis + 2) % 3;
4949
+ _tip[0] = _tip[1] = _tip[2] = 0; _tip[axis] = L;
4950
+ _line(0, 0, 0, _tip[0], _tip[1], _tip[2]);
4951
+ const s = Math.sign(L) || 1;
4952
+ _ha[0] = _ha[1] = _ha[2] = 0; _ha[axis] = L - s*h;
4953
+ _ha[a] = h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4954
+ _ha[a] = -h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4955
+ _ha[a] = 0;
4956
+ _ha[b] = h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4957
+ _ha[b] = -h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4958
+ }
4959
+
4960
+ /**
4961
+ * A helm's rig, colour always semantic: per translation channel a dim
4962
+ * baseline arrow of signed length sign · size · sens / 0.30 and, while the
4963
+ * channel's activity is non-zero, a bright arrow of length ∝ |activity| /
4964
+ * (sens · fullScale); per rotation channel a dim ring of radius size / 2 ·
4965
+ * sens / 0.0025 and a bright arc sweeping π · f in the live direction.
4966
+ * Dim and bright are the alpha of the written colour (COLOR_DIM, 1). With
4967
+ * `identify`, one anchor per channel goes to out.labels as { x, y, z,
4968
+ * text: 'L' + lane }. Orientation is the caller's M.
4969
+ *
4970
+ * Count: 10 · 3 · 2 (arrows) + 96 · 3 (rings) + 48 · 3 (arcs), at most 492;
4971
+ * the bright half only while a channel is active.
4972
+ *
4973
+ * @param {object} out Arrays object.
4974
+ * @param {object} helm A PoseHelm (profile, fullScale, activity).
4975
+ * @param {{ size?:number, bits?:number, identify?:boolean }} [opts]
4976
+ * @returns {number} Vertices needed.
4977
+ */
4978
+ function helmRigLines(out, helm, opts) {
4979
+ const o = opts || {};
4980
+ const size = o.size ?? 100;
4981
+ const bits = o.bits ?? (TRANSLATE | ROTATE);
4982
+ const identify = o.identify === true;
4983
+ const prof = helm.profile;
4984
+ const head = size * 0.08, ringR0 = size * 0.5;
4985
+ const TREF = 0.30, RREF = 0.0025, FULL = helm.fullScale, ARC_FULL = Math.PI;
4986
+ helm.activity(_act);
4987
+ _begin(out);
4988
+ const labels = identify && out.labels ? out.labels : null;
4989
+ if (bits & TRANSLATE) {
4990
+ const T = [prof.Tx, prof.Ty, prof.Tz];
4991
+ for (let ax = 0; ax < 3; ax++) {
4992
+ const ch = T[ax];
4993
+ const L = ch.sign * size * (ch.sens / TREF);
4994
+ _color(_AXIS_COLORS[ax], COLOR_DIM);
4995
+ _arrow(ax, L, head);
4996
+ const a = _act[ax];
4997
+ if (a !== 0) {
4998
+ const f = Math.min(Math.abs(a) / (ch.sens * FULL), 1);
4999
+ _color(_AXIS_COLORS[ax], 1);
5000
+ _arrow(ax, Math.sign(a) * f * Math.abs(L), head);
5001
+ }
5002
+ if (labels) {
5003
+ const lp = [0, 0, 0]; lp[ax] = L + ch.sign * head * 1.5;
5004
+ labels.push({ x: lp[0], y: lp[1], z: lp[2], text: 'L' + ch.lane });
5005
+ }
5006
+ }
5007
+ }
5008
+ if (bits & ROTATE) {
5009
+ const R = [prof.Rp, prof.Ry, prof.Rr]; // pitch ⊥ X, yaw ⊥ Y, roll ⊥ Z
5010
+ for (let ax = 0; ax < 3; ax++) {
5011
+ const ch = R[ax];
5012
+ const r = ringR0 * (ch.sens / RREF);
5013
+ const u = _AXES[(ax + 1) % 3], v = _AXES[(ax + 2) % 3];
5014
+ _color(_AXIS_COLORS[ax], COLOR_DIM);
5015
+ _ring(0, 0, 0, r, u, v, 48, TWO_PI);
5016
+ const a = _act[3 + ax];
5017
+ if (a !== 0) {
5018
+ const f = Math.min(Math.abs(a) / (ch.sens * FULL), 1);
5019
+ _color(_AXIS_COLORS[ax], 1);
5020
+ _ring(0, 0, 0, r, u, v, 24, Math.sign(a) * f * ARC_FULL);
5021
+ }
5022
+ if (labels) {
5023
+ const lp = [0, 0, 0]; lp[(ax + 1) % 3] = r;
5024
+ labels.push({ x: lp[0], y: lp[1], z: lp[2], text: 'L' + ch.lane });
5025
+ }
5026
+ }
5027
+ }
5028
+ return _end(out);
5029
+ }
5030
+
5031
+ /**
5032
+ * A handle's stroked parts by bit. AIM: anchor → `point` (the handle's
5033
+ * current point, read by the caller with value). LOCUS by constraint.kind:
5034
+ * SPHERE three great circles about the anchor; PLANE a square of
5035
+ * half-extent 100 in the plane's basis; AXIS the segment anchor + [min,
5036
+ * max] · u; DIAL the ring in the dial plane; a constraint flagged `view`
5037
+ * (the host's VIEW) a screen-aligned square of half-extent 100 at `point`,
5038
+ * its basis the camera's right and up read off `mat4View`. RING: SPHERE
5039
+ * the view-facing limb, a ring perpendicular to anchor − eye (the eye from
5040
+ * `mat4View`); PLANE the border of the locus square (written once when
5041
+ * both bits ask for it). A constraint supplying locus(out, opts) is
5042
+ * dispatched to it. The HANDLE dot is not a line and not generated here.
5043
+ *
5044
+ * Count: AIM 2; LOCUS SPHERE 288 · PLANE 8 · AXIS 2 · DIAL 96 · view 8;
5045
+ * RING SPHERE 96 · PLANE 8 (shared with LOCUS); at most 386.
5046
+ *
5047
+ * @param {object} out Arrays object.
5048
+ * @param {object} constraint A contract-conforming constraint.
5049
+ * @param {{ bits?:number, mat4View?:ArrayLike<number>, point?:number[], color?:number[] }} [opts]
5050
+ * @returns {number} Vertices needed.
5051
+ */
5052
+ function locusLines(out, constraint, opts) {
5053
+ const o = opts || {};
5054
+ if (typeof constraint.locus === 'function') return constraint.locus(out, o);
5055
+ const bits = o.bits ?? (AIM | LOCUS);
5056
+ const c = constraint, a = c.anchor, pt = o.point, V = o.mat4View;
5057
+ _begin(out);
5058
+ _color(o.color);
5059
+ if ((bits & AIM) && a && pt) _line(a[0], a[1], a[2], pt[0], pt[1], pt[2]);
5060
+ if (c.view === true) {
5061
+ if ((bits & LOCUS) && pt && V) {
5062
+ _b0[0] = V[0]; _b0[1] = V[4]; _b0[2] = V[8]; // the camera's right
5063
+ _b1[0] = V[1]; _b1[1] = V[5]; _b1[2] = V[9]; // the camera's up
5064
+ _square(pt, _b0, _b1, 100);
5065
+ }
5066
+ } else if (c.kind === SPHERE && a) {
5067
+ if (bits & LOCUS) {
5068
+ _ring(a[0], a[1], a[2], c.radius, _AXES[0], _AXES[1], 48, TWO_PI);
5069
+ _ring(a[0], a[1], a[2], c.radius, _AXES[1], _AXES[2], 48, TWO_PI);
5070
+ _ring(a[0], a[1], a[2], c.radius, _AXES[2], _AXES[0], 48, TWO_PI);
5071
+ }
5072
+ if ((bits & RING) && V) {
5073
+ // eye = −Rᵀ t of the view matrix; the limb is ⊥ anchor − eye
5074
+ const tx = V[12], ty = V[13], tz = V[14];
5075
+ _b2[0] = a[0] + (V[0]*tx + V[1]*ty + V[2]*tz);
5076
+ _b2[1] = a[1] + (V[4]*tx + V[5]*ty + V[6]*tz);
5077
+ _b2[2] = a[2] + (V[8]*tx + V[9]*ty + V[10]*tz);
5078
+ _unit(_b2, 0, 0, 1);
5079
+ _basis(_b2, _b0, _b1);
5080
+ _ring(a[0], a[1], a[2], c.radius, _b0, _b1, 48, TWO_PI);
5081
+ }
5082
+ } else if (c.kind === PLANE && a) {
5083
+ if (bits & (LOCUS | RING)) {
5084
+ _basis(c.n, _b0, _b1);
5085
+ _square(a, _b0, _b1, 100);
5086
+ }
5087
+ } else if (c.kind === AXIS && a) {
5088
+ if (bits & LOCUS) {
5089
+ const u = c.u;
5090
+ _line(a[0] + c.min*u[0], a[1] + c.min*u[1], a[2] + c.min*u[2],
5091
+ a[0] + c.max*u[0], a[1] + c.max*u[1], a[2] + c.max*u[2]);
5092
+ }
5093
+ } else if (c.kind === DIAL && a) {
5094
+ if (bits & LOCUS) _ring(a[0], a[1], a[2], c.radius, c.r0, c.r1, 48, TWO_PI);
5095
+ }
5096
+ return _end(out);
5097
+ }
5098
+
5099
+ /**
5100
+ * A textured quad as two triangles (p0, p1, p2) (p0, p2, p3) — the winding
5101
+ * of the corner order — with texcoord written when the array exists.
5102
+ * Default uvs: p0 (top-left) → (0, 1), p1 → (1, 1), p2 → (1, 0), p3 →
5103
+ * (0, 0), so a texture in GL's bottom-up space reads upright with no
5104
+ * flip; `opts.uvs` overrides, four pairs flat in corner order.
5105
+ *
5106
+ * Count: 6.
5107
+ *
5108
+ * @param {object} out Arrays object.
5109
+ * @param {number[]} p0,p1,p2,p3 Corners, top-left clockwise.
5110
+ * @param {{ uvs?:number[], color?:number[] }} [opts]
5111
+ * @returns {number} Vertices needed.
5112
+ */
5113
+ function paneTris(out, p0, p1, p2, p3, opts) {
5114
+ const o = opts || {};
5115
+ const uv = o.uvs || _UV_DEFAULT;
5116
+ _begin(out);
5117
+ _color(o.color);
5118
+ _vertexUV(p0[0], p0[1], p0[2], uv[0], uv[1]);
5119
+ _vertexUV(p1[0], p1[1], p1[2], uv[2], uv[3]);
5120
+ _vertexUV(p2[0], p2[1], p2[2], uv[4], uv[5]);
5121
+ _vertexUV(p0[0], p0[1], p0[2], uv[0], uv[1]);
5122
+ _vertexUV(p2[0], p2[1], p2[2], uv[4], uv[5]);
5123
+ _vertexUV(p3[0], p3[1], p3[2], uv[6], uv[7]);
5124
+ return _end(out);
5125
+ }
5126
+
5127
+ export { AIM, APEX, AXIS, BODY, BOTTOM, CENTER, CIRCLE, COLOR_DIM, COLOR_X, COLOR_Y, COLOR_Z, CONTROLS, CameraTrack, Constraint, DIAL, DIRECTION, EYE, FAR, HANDLE, HANDLES, HELM_CHANNELS, INVISIBLE, LABELS, LEFT, LOCUS, MATRIX, MODEL, NDC, NEAR, NONE, ORIGIN, PATH, PLANE, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, POINT, PoseHelm, PoseTrack, RIGHT, RING, ROTATE, SCREEN, SELF, SEMIVISIBLE, SPHERE, SQUARE, TANGENTS, TANGENTS_IN, TANGENTS_OUT, TOP, TRANSLATE, VISIBLE, WEBGL, WEBGPU, WORLD, X, Y, Z, _X, _Y, _Z, _i, _j, _k, axesLines, azElFromDir, boxVisibility, bullsEyeLines, cameraCopy, cameraDolly, cameraEye, cameraFromMat4, cameraFromPose, cameraOrbit, cameraPan, cameraPlanes, cameraProj, cameraToPose, cameraView, capacityOf, createArrays, createCamera, createConstraint, crossLines, dirFromAzEl, distanceToPlane, frustumCorners, frustumLines, frustumPlanes, gridLines, growArrays, helmRigLines, hermiteLines, hermiteVec3, i, idToRgba, j, k, lerpVec3, locusLines, 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, mat4Viewport, oneEuro, paneTris, pathLines, pixelRatio, pointVisibility, pointerHit, poseDelta, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qConjugate, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qFromUnitVectors, qMul, qNegate, qNlerp, qNormalize, qRotateVec3, qSet, qSlerp, qToAxisAngle, qToMat4, rayClosestPointOnAxis, rayHitCapsule, rayHitRing, rayHitSphere, rayPlane, raySphere, rgbaToId, ringLines, sphereVisibility, transformToMat4, unproject };
3685
5128
  //# sourceMappingURL=index.js.map