@nakednous/tree 0.0.27 → 0.0.28

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
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1284,6 +1436,34 @@ function mat4Pick(proj, px, py, vp) {
1284
1436
  }
1285
1437
  }
1286
1438
 
1439
+ // ═══════════════════════════════════════════════════════════════════════════
1440
+ // Pick-id codec
1441
+ // ═══════════════════════════════════════════════════════════════════════════
1442
+
1443
+ /**
1444
+ * Pick id → colour: the 24-bit id packed into r, g, b as normalised floats,
1445
+ * R the low byte, alpha 1. Id 0 is the background; ids run 1 … 2²⁴ − 1.
1446
+ * @param {number[]} out 4-element destination.
1447
+ * @param {number} id Integer id.
1448
+ * @returns {number[]} out
1449
+ */
1450
+ function idToRgba(out, id) {
1451
+ out[0] = (id & 255) / 255;
1452
+ out[1] = ((id >> 8) & 255) / 255;
1453
+ out[2] = ((id >> 16) & 255) / 255;
1454
+ out[3] = 1;
1455
+ return out;
1456
+ }
1457
+
1458
+ /**
1459
+ * Colour bytes → pick id, the inverse of idToRgba on a readback.
1460
+ * @param {number} r,g,b Bytes 0 … 255 (fractions truncated).
1461
+ * @returns {number} id
1462
+ */
1463
+ function rgbaToId(r, g, b) {
1464
+ return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16);
1465
+ }
1466
+
1287
1467
  // =========================================================================
1288
1468
  // Decomposition
1289
1469
  // =========================================================================
@@ -1651,9 +1831,9 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
1651
1831
  function _parseQuat(v) {
1652
1832
  if (!v) return null;
1653
1833
 
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]];
1834
+ // [x,y,z,w] — normalised like every other form
1835
+ if (Array.isArray(v) && v.length === 4) return qNormalize([v[0],v[1],v[2],v[3]]);
1836
+ if (ArrayBuffer.isView(v) && v.length >= 4) return qNormalize([v[0],v[1],v[2],v[3]]);
1657
1837
 
1658
1838
  if (typeof v !== 'object') return null;
1659
1839
 
@@ -2622,13 +2802,15 @@ class CameraTrack extends Track {
2622
2802
  * A constraint is any object exposing: `kind` (integer discriminant),
2623
2803
  * `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
2624
2804
  * 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.
2805
+ * basis re-aim seam the bridge's deferred `from` frame drives — and
2806
+ * `proxy(ox,oy,oz, dx,dy,dz, radius)`, the analytic pick: the ray parameter
2807
+ * at which a ray in the working space meets the grab proxy of `radius`
2808
+ * working units, or Infinity (a kind without one gets a sphere at its
2809
+ * POINT). The handle controller drives any conforming constraint
2810
+ * (lifecycle, frame conversion, bind, hooks, pick); a new kind — 6-DOF, or
2811
+ * app-specific implements this contract here (portable, draw-free) plus a
2812
+ * bridge-side locus draw, rather than forking the controller. The classes
2813
+ * below are the reference implementation. See handle-design.md §9–§10.
2632
2814
  *
2633
2815
  * ── Conventions ────────────────────────────────────────────────────────────
2634
2816
  * Ray direction `d` is assumed unit (the bridge normalises). Plane / axis
@@ -2641,7 +2823,7 @@ class CameraTrack extends Track {
2641
2823
 
2642
2824
 
2643
2825
  const EPS = 1e-6;
2644
- const TWO_PI = Math.PI * 2;
2826
+ const TWO_PI$1 = Math.PI * 2;
2645
2827
 
2646
2828
  // Edge-on threshold for DIAL: below this |d·n| the plane hit is ill-conditioned
2647
2829
  // (dθ per pixel diverges) and the solve switches to the tangent-line fallback.
@@ -2657,10 +2839,10 @@ const _clamp = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
2657
2839
  const _num = (x, d) => _isNum(x) ? x : d;
2658
2840
 
2659
2841
  /** Wrap an angle to (−π, π]. */
2660
- const _wrapPi = (a) => a - TWO_PI * Math.round(a / TWO_PI);
2842
+ const _wrapPi = (a) => a - TWO_PI$1 * Math.round(a / TWO_PI$1);
2661
2843
 
2662
2844
  /** Parse a vec3 from array / typed array / {x,y,z}. Returns a fresh [x,y,z] or null. */
2663
- function _vec3(v) {
2845
+ function _vec3$1(v) {
2664
2846
  if (!v) return null;
2665
2847
  if (ArrayBuffer.isView(v) && v.length >= 3) return [v[0], v[1], v[2]];
2666
2848
  if (Array.isArray(v) && v.length >= 3) return [v[0], v[1], v[2]];
@@ -2669,7 +2851,7 @@ function _vec3(v) {
2669
2851
  }
2670
2852
 
2671
2853
  /** Normalise a vec3 in place; zero-length falls back to the given default axis. */
2672
- function _unit(v, dx, dy, dz) {
2854
+ function _unit$1(v, dx, dy, dz) {
2673
2855
  const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
2674
2856
  if (l < EPS) { v[0]=dx; v[1]=dy; v[2]=dz; return v; }
2675
2857
  v[0]/=l; v[1]/=l; v[2]/=l;
@@ -2682,12 +2864,12 @@ function _unit(v, dx, dy, dz) {
2682
2864
  * (Same derivation the p5 bridge uses for its plane quad — duplicated here
2683
2865
  * because the core cannot depend on the bridge.)
2684
2866
  */
2685
- function _basis(n, ub, vb) {
2867
+ function _basis$1(n, ub, vb) {
2686
2868
  const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
2687
2869
  let rx = 0, ry = 0, rz = 0;
2688
2870
  if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
2689
2871
  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);
2872
+ _unit$1(ub, 1, 0, 0);
2691
2873
  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
2874
  }
2693
2875
 
@@ -2771,6 +2953,132 @@ function rayClosestPointOnAxis(out, ox,oy,oz, dx,dy,dz, px,py,pz, ux,uy,uz) {
2771
2953
  return s;
2772
2954
  }
2773
2955
 
2956
+ // =========================================================================
2957
+ // H2b Ray-primitive hit tests (pure, no out: the nearest t, or Infinity)
2958
+ // =========================================================================
2959
+ //
2960
+ // Beside the solve primitives above, which always write a point, these are
2961
+ // TESTS: they write nothing and return the ray parameter of the nearest hit
2962
+ // with t ≥ 0, or Infinity on a miss. A ray whose origin lies inside the
2963
+ // primitive hits at its exit, so a press from inside a proxy still grabs.
2964
+ // The ray direction is assumed unit.
2965
+
2966
+ /**
2967
+ * Ray–sphere hit test.
2968
+ * @param {number} ox,oy,oz Ray origin.
2969
+ * @param {number} dx,dy,dz Ray direction (unit).
2970
+ * @param {number} cx,cy,cz Sphere centre.
2971
+ * @param {number} r Sphere radius.
2972
+ * @returns {number} The nearest t ≥ 0, or Infinity.
2973
+ */
2974
+ function rayHitSphere(ox,oy,oz, dx,dy,dz, cx,cy,cz, r) {
2975
+ const lx=ox-cx, ly=oy-cy, lz=oz-cz;
2976
+ const b = lx*dx + ly*dy + lz*dz;
2977
+ const cc = lx*lx + ly*ly + lz*lz - r*r;
2978
+ const disc = b*b - cc;
2979
+ if (disc < 0) return Infinity;
2980
+ const s = Math.sqrt(disc);
2981
+ let t = -b - s;
2982
+ if (t < 0) t = -b + s; // origin inside: the exit
2983
+ return t < 0 ? Infinity : t; // both roots behind the origin
2984
+ }
2985
+
2986
+ // One end cap of a capsule: the sphere at (cx,cy,cz), a root accepted only
2987
+ // where the hit's axial coordinate h = wu + t·du lies beyond the segment on
2988
+ // that cap's side (h ≤ 0 at A, h ≥ L at B), so the point is on the capsule's
2989
+ // surface and not inside its cylinder.
2990
+ function _capHit(ox,oy,oz, dx,dy,dz, cx,cy,cz, r, wu, du, lim, endB) {
2991
+ const lx=ox-cx, ly=oy-cy, lz=oz-cz;
2992
+ const b = lx*dx + ly*dy + lz*dz;
2993
+ const cc = lx*lx + ly*ly + lz*lz - r*r;
2994
+ const disc = b*b - cc;
2995
+ if (disc < 0) return Infinity;
2996
+ const s = Math.sqrt(disc);
2997
+ let t = -b - s, h = wu + t*du;
2998
+ if (t < 0 || (endB ? h < lim : h > lim)) { t = -b + s; h = wu + t*du; }
2999
+ return (t >= 0 && (endB ? h >= lim : h <= lim)) ? t : Infinity;
3000
+ }
3001
+
3002
+ /**
3003
+ * Ray–capsule hit test: the segment A→B swept by radius r. The cylinder wall
3004
+ * counts within the segment's extent, each end sphere beyond it; the nearest
3005
+ * of the three wins. A zero-length segment is the sphere at A.
3006
+ * @param {number} ox,oy,oz Ray origin.
3007
+ * @param {number} dx,dy,dz Ray direction (unit).
3008
+ * @param {number} ax,ay,az Segment start.
3009
+ * @param {number} bx,by,bz Segment end.
3010
+ * @param {number} r Capsule radius.
3011
+ * @returns {number} The nearest t ≥ 0, or Infinity.
3012
+ */
3013
+ function rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r) {
3014
+ let ux=bx-ax, uy=by-ay, uz=bz-az;
3015
+ const L = Math.sqrt(ux*ux + uy*uy + uz*uz);
3016
+ if (L < EPS) return rayHitSphere(ox,oy,oz, dx,dy,dz, ax,ay,az, r);
3017
+ ux/=L; uy/=L; uz/=L;
3018
+ const wx=ox-ax, wy=oy-ay, wz=oz-az;
3019
+ const du = dx*ux + dy*uy + dz*uz; // d·u
3020
+ const wu = wx*ux + wy*uy + wz*uz; // w·u
3021
+ let best = Infinity;
3022
+ // Cylinder wall: the quadratic in the components perpendicular to u.
3023
+ const a = 1 - du*du;
3024
+ if (a > EPS) {
3025
+ const b = (dx*wx + dy*wy + dz*wz) - du*wu;
3026
+ const c = (wx*wx + wy*wy + wz*wz) - wu*wu - r*r;
3027
+ const disc = b*b - a*c;
3028
+ if (disc >= 0) {
3029
+ const s = Math.sqrt(disc);
3030
+ let t = (-b - s)/a, h = wu + t*du;
3031
+ if (t < 0 || h < 0 || h > L) { t = (-b + s)/a; h = wu + t*du; }
3032
+ if (t >= 0 && h >= 0 && h <= L) best = t;
3033
+ }
3034
+ }
3035
+ const ta = _capHit(ox,oy,oz, dx,dy,dz, ax,ay,az, r, wu, du, 0, false);
3036
+ if (ta < best) best = ta;
3037
+ const tb = _capHit(ox,oy,oz, dx,dy,dz, bx,by,bz, r, wu, du, L, true);
3038
+ if (tb < best) best = tb;
3039
+ return best;
3040
+ }
3041
+
3042
+ // Ring scratch — the plane normal and its in-plane basis. Function-local
3043
+ // working values, never state: rayHitRing runs to completion.
3044
+ const _rn = [0, 0, 1], _r0 = [1, 0, 0], _r1 = [0, 1, 0];
3045
+
3046
+ /**
3047
+ * Ray–ring hit test: the circle of radius R about c in the plane
3048
+ * perpendicular to u, swept by tube radius r — the analytic torus proxy as a
3049
+ * capsule chain, the circle polygonised into `detail` segments and each
3050
+ * tested with rayHitCapsule. The chain has volume in every direction, so it
3051
+ * never degenerates edge-on; the chordal error is R · (1 − cos(π / detail)).
3052
+ * @param {number} ox,oy,oz Ray origin.
3053
+ * @param {number} dx,dy,dz Ray direction (unit).
3054
+ * @param {number} cx,cy,cz Ring centre.
3055
+ * @param {number} ux,uy,uz Ring plane normal (normalised here).
3056
+ * @param {number} R Ring radius.
3057
+ * @param {number} r Tube radius.
3058
+ * @param {number} [detail=32] Chain segments (at least 3).
3059
+ * @returns {number} The nearest t ≥ 0, or Infinity.
3060
+ */
3061
+ function rayHitRing(ox,oy,oz, dx,dy,dz, cx,cy,cz, ux,uy,uz, R, r, detail = 32) {
3062
+ const n = _isNum(detail) && detail >= 3 ? Math.floor(detail) : 32;
3063
+ _rn[0]=ux; _rn[1]=uy; _rn[2]=uz;
3064
+ _unit$1(_rn, 0, 0, 1);
3065
+ _basis$1(_rn, _r0, _r1);
3066
+ const step = TWO_PI$1 / n;
3067
+ let best = Infinity;
3068
+ let ax = cx + R*_r0[0], ay = cy + R*_r0[1], az = cz + R*_r0[2]; // vertex at angle 0
3069
+ for (let i = 1; i <= n; i++) {
3070
+ const th = i === n ? 0 : i*step; // close the chain exactly
3071
+ const cs = Math.cos(th)*R, sn = Math.sin(th)*R;
3072
+ const bx = cx + cs*_r0[0] + sn*_r1[0];
3073
+ const by = cy + cs*_r0[1] + sn*_r1[1];
3074
+ const bz = cz + cs*_r0[2] + sn*_r1[2];
3075
+ const t = rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r);
3076
+ if (t < best) best = t;
3077
+ ax = bx; ay = by; az = bz;
3078
+ }
3079
+ return best;
3080
+ }
3081
+
2774
3082
  // =========================================================================
2775
3083
  // H3 Angular utilities (readout / authoring convenience)
2776
3084
  // =========================================================================
@@ -2830,17 +3138,17 @@ class Constraint {
2830
3138
  this.kind = kind;
2831
3139
 
2832
3140
  /** Constraint origin (sphere centre / plane point / axis anchor / dial centre). @type {number[]} */
2833
- this.anchor = _vec3(opts.anchor) || [0, 0, 0];
3141
+ this.anchor = _vec3$1(opts.anchor) || [0, 0, 0];
2834
3142
  /** Canonical unit direction — SPHERE. @type {number[]} */
2835
3143
  this.dir = [0, 0, 1];
2836
3144
  /** Constrained point — PLANE / AXIS / DIAL (and SPHERE scratch). @type {number[]} */
2837
3145
  this.pt = [this.anchor[0], this.anchor[1], this.anchor[2]];
2838
3146
  /** Plane normal (unit) — PLANE. @type {number[]} */
2839
- this.n = _unit(_vec3(opts.normal) || [0, 1, 0], 0, 1, 0);
3147
+ this.n = _unit$1(_vec3$1(opts.normal) || [0, 1, 0], 0, 1, 0);
2840
3148
  /** Axis / dial-plane normal (unit) — AXIS / DIAL. @type {number[]} */
2841
3149
  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);
3150
+ ? _unit$1(_vec3$1(opts.axis) || [0, 1, 0], 0, 1, 0)
3151
+ : _unit$1(_vec3$1(opts.axis) || [1, 0, 0], 1, 0, 0);
2844
3152
  /** Current scalar parameter — AXIS: t along the line; DIAL: accumulated θ. @type {number} */
2845
3153
  this.s = 0;
2846
3154
 
@@ -2851,7 +3159,7 @@ class Constraint {
2851
3159
  /** In-plane binormal u × r0 (unit) — DIAL. @type {number[]} */
2852
3160
  this.r1 = [0, 0, 1];
2853
3161
  if (kind === DIAL) {
2854
- const z = _vec3(opts.zero);
3162
+ const z = _vec3$1(opts.zero);
2855
3163
  this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
2856
3164
  }
2857
3165
 
@@ -2902,10 +3210,10 @@ class Constraint {
2902
3210
  this.r0[1] = zy - d*this.u[1];
2903
3211
  this.r0[2] = zz - d*this.u[2];
2904
3212
  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);
3213
+ if (l < EPS) _basis$1(this.u, this.r0, this.r1);
2906
3214
  else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
2907
3215
  } else {
2908
- _basis(this.u, this.r0, this.r1);
3216
+ _basis$1(this.u, this.r0, this.r1);
2909
3217
  }
2910
3218
  // r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
2911
3219
  this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
@@ -2929,7 +3237,7 @@ class Constraint {
2929
3237
  this.dir[0] = this.pt[0] - this.anchor[0];
2930
3238
  this.dir[1] = this.pt[1] - this.anchor[1];
2931
3239
  this.dir[2] = this.pt[2] - this.anchor[2];
2932
- _unit(this.dir, this.dir[0], this.dir[1], this.dir[2]);
3240
+ _unit$1(this.dir, this.dir[0], this.dir[1], this.dir[2]);
2933
3241
  } else if (this.kind === PLANE) {
2934
3242
  // Parallel ray returns Infinity and leaves pt unchanged (keep last).
2935
3243
  rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
@@ -3076,7 +3384,7 @@ class Constraint {
3076
3384
  if (px*px + py*py >= EPS*EPS) {
3077
3385
  const a = Math.atan2(py, px);
3078
3386
  // Nearest winding to the current θ preserves accumulated turns.
3079
- this.s = _clamp(a + TWO_PI * Math.round((this.s - a) / TWO_PI),
3387
+ this.s = _clamp(a + TWO_PI$1 * Math.round((this.s - a) / TWO_PI$1),
3080
3388
  this.min, this.max);
3081
3389
  }
3082
3390
  this._dialPoint();
@@ -3105,24 +3413,54 @@ class Constraint {
3105
3413
  if (this.kind === PLANE) {
3106
3414
  const px = this.n[0], py = this.n[1], pz = this.n[2];
3107
3415
  this.n[0] = ax; this.n[1] = ay; this.n[2] = az;
3108
- _unit(this.n, px, py, pz);
3416
+ _unit$1(this.n, px, py, pz);
3109
3417
  this.seed(this.pt[0], this.pt[1], this.pt[2]);
3110
3418
  } else if (this.kind === AXIS) {
3111
3419
  const px = this.u[0], py = this.u[1], pz = this.u[2];
3112
3420
  this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
3113
- _unit(this.u, px, py, pz);
3421
+ _unit$1(this.u, px, py, pz);
3114
3422
  this.pt[0] = this.anchor[0] + this.s*this.u[0];
3115
3423
  this.pt[1] = this.anchor[1] + this.s*this.u[1];
3116
3424
  this.pt[2] = this.anchor[2] + this.s*this.u[2];
3117
3425
  } else if (this.kind === DIAL) {
3118
3426
  const px = this.u[0], py = this.u[1], pz = this.u[2];
3119
3427
  this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
3120
- _unit(this.u, px, py, pz);
3428
+ _unit$1(this.u, px, py, pz);
3121
3429
  this._dialBasis(zx, zy, zz);
3122
3430
  this._dialPoint();
3123
3431
  }
3124
3432
  return this;
3125
3433
  }
3434
+
3435
+ /**
3436
+ * The analytic pick proxy: the ray parameter t at which a ray in the
3437
+ * working space meets the grab proxy, or Infinity. `radius` is the grab
3438
+ * size in working-space units — a constant pixel size the caller converted
3439
+ * through pixelRatio at the proxy's depth. SPHERE / PLANE / AXIS: a sphere
3440
+ * of `radius` at the reported POINT. DIAL: the ring at the anchor with
3441
+ * tube radius `radius` (rayHitRing), so a grab lands anywhere on the ring.
3442
+ *
3443
+ * @param {number} ox,oy,oz Ray origin.
3444
+ * @param {number} dx,dy,dz Ray direction (unit).
3445
+ * @param {number} radius Grab radius, working units.
3446
+ * @returns {number} The nearest t ≥ 0, or Infinity.
3447
+ */
3448
+ proxy(ox, oy, oz, dx, dy, dz, radius) {
3449
+ const a = this.anchor;
3450
+ if (this.kind === DIAL) {
3451
+ return rayHitRing(ox,oy,oz, dx,dy,dz, a[0], a[1], a[2],
3452
+ this.u[0], this.u[1], this.u[2], this._radius, radius);
3453
+ }
3454
+ let px, py, pz;
3455
+ if (this.kind === SPHERE) {
3456
+ px = a[0] + this.dir[0]*this._radius;
3457
+ py = a[1] + this.dir[1]*this._radius;
3458
+ pz = a[2] + this.dir[2]*this._radius;
3459
+ } else {
3460
+ px = this.pt[0]; py = this.pt[1]; pz = this.pt[2];
3461
+ }
3462
+ return rayHitSphere(ox,oy,oz, dx,dy,dz, px,py,pz, radius);
3463
+ }
3126
3464
  }
3127
3465
 
3128
3466
  /**
@@ -3480,9 +3818,12 @@ class PoseHelm {
3480
3818
  * naive difference takes the long way round — the angular rate spikes at the
3481
3819
  * crossing. Flipping cur into prev's hemisphere first keeps r the shortest arc.
3482
3820
  *
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.
3821
+ * Fed through a helm in WORLD whose profile has unit `sens`, in-order lanes and
3822
+ * `Tz` / `Rr` signs of −1 — the helm's lanes are eye-frame and the null basis
3823
+ * is the identity eye matrix (forward −Z), so the two Z channels flip against
3824
+ * world axes — the integrated pose retraces the source (the round-trip e9
3825
+ * asserts). A 1:1, non-integrated consumer skips the helm and applyPoses the
3826
+ * absolute pose directly.
3486
3827
  *
3487
3828
  * @param {{ lin:number[], ang:number[] }} [out] Destination; omit for a fresh one.
3488
3829
  * @param {{ pos:ArrayLike<number>, rot:ArrayLike<number> }} prev Previous pose.
@@ -3681,5 +4022,1081 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
3681
4022
  return allIn ? VISIBLE : SEMIVISIBLE;
3682
4023
  }
3683
4024
 
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 };
4025
+ /**
4026
+ * @file Camera state ↔ matrices: builders, decomposers, and in-place edits.
4027
+ * @module tree/camera
4028
+ * @license AGPL-3.0-only
4029
+ *
4030
+ * The camera is plain data — the CameraTrack keyframe shape:
4031
+ *
4032
+ * cam = {
4033
+ * eye: [x, y, z], // position
4034
+ * center: [x, y, z], // lookat target; |center − eye| is the gaze distance
4035
+ * up: [x, y, z], // up hint, need not be unit
4036
+ * fov: number | null, // vertical field of view, radians — perspective
4037
+ * halfHeight: number | null, // world-unit half-height at the near plane — orthographic
4038
+ * near: number, // > 0
4039
+ * far: number, // > near
4040
+ * }
4041
+ *
4042
+ * Exactly one of fov / halfHeight is meaningful; fov wins when both are set.
4043
+ * With both null the lens is "unchanged": cameraProj and cameraPlanes return
4044
+ * null and leave their output untouched, so a track that evaluates null into
4045
+ * the state keeps the last projection installed. A track writes the state
4046
+ * directly — `track.eval(cam)` — and a pose drives it through cameraFromPose.
4047
+ *
4048
+ * The state carries no aspect ratio — the viewport owns it — so one state is
4049
+ * portable between targets of different sizes; every builder that needs it
4050
+ * takes it as an argument.
4051
+ *
4052
+ * Vectors are plain number[] (f64, authoring state); matrices are written into
4053
+ * whatever 16-element buffer the caller passes. createCamera is the one
4054
+ * allocating call; everything else is out-first (or in-place) and zero-alloc.
4055
+ *
4056
+ * Every function that needs the camera frame derives it through mat4Eye, so
4057
+ * planes and edits agree with cameraEye / cameraView exactly — including the
4058
+ * up re-seed when the view direction is parallel to the up hint.
4059
+ */
4060
+
4061
+
4062
+ const _E$1 = new Float64Array(16); // eye→world frame scratch: right 0–2, up 4–6, back 8–10
4063
+ const _d = [0, 0, 0]; // view-direction scratch
4064
+
4065
+ /** Eye→world frame of `cam` into `_E`. */
4066
+ function _frame(cam) {
4067
+ const e = cam.eye, c = cam.center, u = cam.up;
4068
+ mat4Eye(_E$1, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
4069
+ }
4070
+
4071
+ /** Gaze distance |center − eye|, or 1 when the state is degenerate. */
4072
+ function _gaze(cam) {
4073
+ const dx = cam.center[0]-cam.eye[0], dy = cam.center[1]-cam.eye[1], dz = cam.center[2]-cam.eye[2];
4074
+ const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
4075
+ return d > 0 ? d : 1;
4076
+ }
4077
+
4078
+ function _vec3(v, x, y, z) { return v != null ? [v[0], v[1], v[2]] : [x, y, z]; }
4079
+
4080
+ // =========================================================================
4081
+ // State
4082
+ // =========================================================================
4083
+
4084
+ /**
4085
+ * Allocate a camera state — the one allocating call, setup-time.
4086
+ * Defaults: eye [0, 0, 500], center [0, 0, 0], up [0, 1, 0], fov π/3,
4087
+ * halfHeight null, near 0.1, far 1000. Passing halfHeight without fov yields
4088
+ * an orthographic state (fov null).
4089
+ *
4090
+ * @param {{ eye?:number[], center?:number[], up?:number[], fov?:number|null,
4091
+ * halfHeight?:number|null, near?:number, far?:number }} [opts]
4092
+ * @returns {{ eye:number[], center:number[], up:number[], fov:number|null,
4093
+ * halfHeight:number|null, near:number, far:number }}
4094
+ */
4095
+ function createCamera(opts) {
4096
+ const o = opts || {};
4097
+ const ortho = o.halfHeight != null && o.fov === undefined;
4098
+ return {
4099
+ eye: _vec3(o.eye, 0, 0, 500),
4100
+ center: _vec3(o.center, 0, 0, 0),
4101
+ up: _vec3(o.up, 0, 1, 0),
4102
+ fov: o.fov !== undefined ? o.fov : (ortho ? null : Math.PI / 3),
4103
+ halfHeight: o.halfHeight !== undefined ? o.halfHeight : null,
4104
+ near: typeof o.near === 'number' ? o.near : 0.1,
4105
+ far: typeof o.far === 'number' ? o.far : 1000,
4106
+ };
4107
+ }
4108
+
4109
+ /**
4110
+ * Copy one camera state into another (an orbit's home, a track's capture).
4111
+ * @param {object} out Destination state.
4112
+ * @param {object} cam Source state.
4113
+ * @returns {object} out
4114
+ */
4115
+ function cameraCopy(out, cam) {
4116
+ out.eye[0]=cam.eye[0]; out.eye[1]=cam.eye[1]; out.eye[2]=cam.eye[2];
4117
+ out.center[0]=cam.center[0]; out.center[1]=cam.center[1]; out.center[2]=cam.center[2];
4118
+ out.up[0]=cam.up[0]; out.up[1]=cam.up[1]; out.up[2]=cam.up[2];
4119
+ out.fov = cam.fov; out.halfHeight = cam.halfHeight;
4120
+ out.near = cam.near; out.far = cam.far;
4121
+ return out;
4122
+ }
4123
+
4124
+ // =========================================================================
4125
+ // Builders — state → matrices
4126
+ // =========================================================================
4127
+
4128
+ /**
4129
+ * View matrix (world→eye) from the state's lookat.
4130
+ * @param {Float32Array|number[]} out 16-element destination.
4131
+ * @param {object} cam
4132
+ * @returns {Float32Array|number[]} out
4133
+ */
4134
+ function cameraView(out, cam) {
4135
+ const e = cam.eye, c = cam.center, u = cam.up;
4136
+ return mat4View(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
4137
+ }
4138
+
4139
+ /**
4140
+ * Eye matrix (eye→world) from the state's lookat.
4141
+ * @param {Float32Array|number[]} out 16-element destination.
4142
+ * @param {object} cam
4143
+ * @returns {Float32Array|number[]} out
4144
+ */
4145
+ function cameraEye(out, cam) {
4146
+ const e = cam.eye, c = cam.center, u = cam.up;
4147
+ return mat4Eye(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
4148
+ }
4149
+
4150
+ /**
4151
+ * Projection matrix from the state's lens: mat4Persp from fov, or mat4Ortho
4152
+ * from halfHeight, with symmetric extents (top = near · tan(fov / 2) or
4153
+ * halfHeight; right = top · aspect).
4154
+ *
4155
+ * @param {Float32Array|number[]} out 16-element destination.
4156
+ * @param {object} cam
4157
+ * @param {number} aspect Viewport width / height.
4158
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
4159
+ * @param {number} [ndcYSign=1] +1 NDC y-up; −1 NDC y-down.
4160
+ * @returns {Float32Array|number[]|null} out, or null (out untouched) when
4161
+ * both fov and halfHeight are null.
4162
+ */
4163
+ function cameraProj(out, cam, aspect, ndcZMin, ndcYSign = 1) {
4164
+ const near = cam.near, far = cam.far;
4165
+ if (cam.fov != null) {
4166
+ const top = near * Math.tan(cam.fov / 2), right = top * aspect;
4167
+ return mat4Persp(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
4168
+ }
4169
+ if (cam.halfHeight != null) {
4170
+ const top = cam.halfHeight, right = top * aspect;
4171
+ return mat4Ortho(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
4172
+ }
4173
+ return null;
4174
+ }
4175
+
4176
+ /**
4177
+ * The six frustum planes of the state, world space — frustumPlanes over the
4178
+ * lookat basis and the symmetric extents cameraProj uses, so visibility tests
4179
+ * run against a camera state without any matrix.
4180
+ *
4181
+ * @param {Float64Array} planes 24-element destination.
4182
+ * @param {object} cam
4183
+ * @param {number} aspect Viewport width / height.
4184
+ * @returns {Float64Array|null} planes, or null (planes untouched) when both
4185
+ * fov and halfHeight are null.
4186
+ */
4187
+ function cameraPlanes(planes, cam, aspect) {
4188
+ const ortho = cam.fov == null;
4189
+ if (ortho && cam.halfHeight == null) return null;
4190
+ const top = ortho ? cam.halfHeight : cam.near * Math.tan(cam.fov / 2), right = top * aspect;
4191
+ _frame(cam);
4192
+ const e = cam.eye;
4193
+ return frustumPlanes(planes,
4194
+ e[0], e[1], e[2],
4195
+ -_E$1[8], -_E$1[9], -_E$1[10],
4196
+ _E$1[4], _E$1[5], _E$1[6],
4197
+ _E$1[0], _E$1[1], _E$1[2],
4198
+ ortho, cam.near, cam.far, -right, right, top, -top);
4199
+ }
4200
+
4201
+ // =========================================================================
4202
+ // Decomposers — matrices and poses → state
4203
+ // =========================================================================
4204
+
4205
+ /**
4206
+ * Read a state back from an eye matrix and a projection: eye ← column 3,
4207
+ * up ← column 1, forward ← −column 2 of E; center ← eye + forward · d with d
4208
+ * the state's current gaze distance (1 when degenerate), so the distance
4209
+ * survives a round trip. The lens comes from the projection queries: fov or
4210
+ * halfHeight by projIsOrtho, near and far under `ndcZMin`.
4211
+ *
4212
+ * @param {object} cam State written in place.
4213
+ * @param {ArrayLike<number>} E Eye matrix (eye→world), 16 elements.
4214
+ * @param {ArrayLike<number>} P Projection matrix, 16 elements.
4215
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
4216
+ * @returns {object} cam
4217
+ */
4218
+ function cameraFromMat4(cam, E, P, ndcZMin) {
4219
+ const d = _gaze(cam);
4220
+ let fx = -E[8], fy = -E[9], fz = -E[10];
4221
+ const fl = Math.sqrt(fx*fx+fy*fy+fz*fz) || 1;
4222
+ fx /= fl; fy /= fl; fz /= fl;
4223
+ cam.eye[0]=E[12]; cam.eye[1]=E[13]; cam.eye[2]=E[14];
4224
+ cam.up[0]=E[4]; cam.up[1]=E[5]; cam.up[2]=E[6];
4225
+ cam.center[0]=cam.eye[0]+fx*d; cam.center[1]=cam.eye[1]+fy*d; cam.center[2]=cam.eye[2]+fz*d;
4226
+ if (projIsOrtho(P)) { cam.fov = null; cam.halfHeight = projTop(P, ndcZMin); }
4227
+ else { cam.fov = projFov(P); cam.halfHeight = null; }
4228
+ cam.near = projNear(P, ndcZMin);
4229
+ cam.far = projFar(P);
4230
+ return cam;
4231
+ }
4232
+
4233
+ /**
4234
+ * Drive the lookat from a TRS pose: eye ← pos, up and forward from the
4235
+ * rotation's columns 1 and −2, center ← eye + forward · d with d the current
4236
+ * gaze distance (1 when degenerate). The lens is untouched; `scl` is ignored.
4237
+ *
4238
+ * @param {object} cam State written in place.
4239
+ * @param {{ pos:number[], rot:number[] }} pose rot is a unit quaternion [x,y,z,w].
4240
+ * @returns {object} cam
4241
+ */
4242
+ function cameraFromPose(cam, pose) {
4243
+ const d = _gaze(cam);
4244
+ const q = pose.rot, x=q[0], y=q[1], z=q[2], w=q[3];
4245
+ const x2=x+x, y2=y+y, z2=z+z;
4246
+ 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;
4247
+ const ux=xy-wz, uy=1-(xx+zz), uz=yz+wx; // column 1 of qToMat4(rot): up
4248
+ const bx=xz+wy, by=yz-wx, bz=1-(xx+yy); // column 2: back
4249
+ cam.eye[0]=pose.pos[0]; cam.eye[1]=pose.pos[1]; cam.eye[2]=pose.pos[2];
4250
+ cam.up[0]=ux; cam.up[1]=uy; cam.up[2]=uz;
4251
+ cam.center[0]=cam.eye[0]-bx*d; cam.center[1]=cam.eye[1]-by*d; cam.center[2]=cam.eye[2]-bz*d;
4252
+ return cam;
4253
+ }
4254
+
4255
+ /**
4256
+ * The lookat as a TRS pose: pos ← eye, rot ← qFromLookDir(center − eye, up) —
4257
+ * the rotation of cameraEye, so a helm seeded from it continues the frame.
4258
+ *
4259
+ * @param {{ pos:number[], rot:number[] }} pose Written in place.
4260
+ * @param {object} cam
4261
+ * @returns {{ pos:number[], rot:number[] }} pose
4262
+ */
4263
+ function cameraToPose(pose, cam) {
4264
+ pose.pos[0]=cam.eye[0]; pose.pos[1]=cam.eye[1]; pose.pos[2]=cam.eye[2];
4265
+ _d[0]=cam.center[0]-cam.eye[0]; _d[1]=cam.center[1]-cam.eye[1]; _d[2]=cam.center[2]-cam.eye[2];
4266
+ qFromLookDir(pose.rot, _d, cam.up);
4267
+ return pose;
4268
+ }
4269
+
4270
+ // =========================================================================
4271
+ // Edits — in place, chainable, zero-alloc
4272
+ // =========================================================================
4273
+
4274
+ /**
4275
+ * Orbit the eye about the center: `dAz` rotates it about the up hint
4276
+ * (right-handed); `dEl` raises its elevation above the plane through the
4277
+ * center perpendicular to the hint, clamped to ±opts.maxEl so the view
4278
+ * direction never reaches the hint (the pole guard). `up` is left as the hint
4279
+ * it was, so an orbit never rolls. A state already at the pole is pulled
4280
+ * inside the guard by its first non-zero edit; (0, 0) is a no-op.
4281
+ *
4282
+ * @param {object} cam
4283
+ * @param {number} dAz Azimuth delta, radians.
4284
+ * @param {number} dEl Elevation delta, radians.
4285
+ * @param {{ maxEl?:number }} [opts] Elevation limit; default π/2 − 1e-3.
4286
+ * @returns {object} cam
4287
+ */
4288
+ function cameraOrbit(cam, dAz, dEl, opts) {
4289
+ if (dAz === 0 && dEl === 0) return cam;
4290
+ const maxEl = opts && typeof opts.maxEl === 'number' ? opts.maxEl : Math.PI / 2 - 1e-3;
4291
+ const e = cam.eye, c = cam.center;
4292
+ let ux=cam.up[0], uy=cam.up[1], uz=cam.up[2];
4293
+ const ul = Math.sqrt(ux*ux+uy*uy+uz*uz) || 1;
4294
+ ux/=ul; uy/=ul; uz/=ul;
4295
+ const dx=e[0]-c[0], dy=e[1]-c[1], dz=e[2]-c[2];
4296
+ const r = Math.sqrt(dx*dx+dy*dy+dz*dz);
4297
+ _frame(cam);
4298
+ const px=_E$1[4], py=_E$1[5], pz=_E$1[6]; // the eye's up
4299
+ const bx=_E$1[8], by=_E$1[9], bz=_E$1[10]; // back: center → eye, unit
4300
+ // elevation: raise by θ within the (back, up) plane, clamped
4301
+ const el = Math.asin(Math.max(-1, Math.min(1, bx*ux+by*uy+bz*uz)));
4302
+ const th = Math.max(-maxEl, Math.min(maxEl, el + dEl)) - el;
4303
+ const ct = Math.cos(th), st = Math.sin(th);
4304
+ const ox = r*(bx*ct+px*st), oy = r*(by*ct+py*st), oz = r*(bz*ct+pz*st);
4305
+ // azimuth: rotate about the hint (Rodrigues)
4306
+ const ca = Math.cos(dAz), sa = Math.sin(dAz), k = (ux*ox+uy*oy+uz*oz)*(1-ca);
4307
+ e[0] = c[0] + ox*ca + (uy*oz-uz*oy)*sa + ux*k;
4308
+ e[1] = c[1] + oy*ca + (uz*ox-ux*oz)*sa + uy*k;
4309
+ e[2] = c[2] + oz*ca + (ux*oy-uy*ox)*sa + uz*k;
4310
+ return cam;
4311
+ }
4312
+
4313
+ /**
4314
+ * Dolly: scale the gaze distance by `factor` — the eye moves along the view
4315
+ * direction under perspective; under orthographic (fov null, halfHeight set)
4316
+ * halfHeight is scaled instead, since moving the eye changes nothing on
4317
+ * screen. opts.min / opts.max clamp the scaled quantity. An edit that would
4318
+ * make it non-positive is a no-op.
4319
+ *
4320
+ * @param {object} cam
4321
+ * @param {number} factor
4322
+ * @param {{ min?:number, max?:number }} [opts]
4323
+ * @returns {object} cam
4324
+ */
4325
+ function cameraDolly(cam, factor, opts) {
4326
+ const min = opts && typeof opts.min === 'number' ? opts.min : 0;
4327
+ const max = opts && typeof opts.max === 'number' ? opts.max : Infinity;
4328
+ if (cam.fov == null && cam.halfHeight != null) {
4329
+ const h = Math.max(min, Math.min(max, cam.halfHeight * factor));
4330
+ if (h > 0) cam.halfHeight = h;
4331
+ return cam;
4332
+ }
4333
+ const e = cam.eye, c = cam.center;
4334
+ const dx=e[0]-c[0], dy=e[1]-c[1], dz=e[2]-c[2];
4335
+ const r = Math.sqrt(dx*dx+dy*dy+dz*dz);
4336
+ if (r === 0) return cam;
4337
+ const r1 = Math.max(min, Math.min(max, r * factor));
4338
+ if (!(r1 > 0)) return cam;
4339
+ const s = r1 / r;
4340
+ e[0]=c[0]+dx*s; e[1]=c[1]+dy*s; e[2]=c[2]+dz*s;
4341
+ return cam;
4342
+ }
4343
+
4344
+ /**
4345
+ * Pan: translate eye and center by `dx` along the eye's right and `dy` along
4346
+ * its up, world units. The caller converts pixels through pixelRatio at the
4347
+ * center's depth so a pan tracks the pointer.
4348
+ *
4349
+ * @param {object} cam
4350
+ * @param {number} dx
4351
+ * @param {number} dy
4352
+ * @returns {object} cam
4353
+ */
4354
+ function cameraPan(cam, dx, dy) {
4355
+ _frame(cam);
4356
+ 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;
4357
+ const e = cam.eye, c = cam.center;
4358
+ e[0]+=tx; e[1]+=ty; e[2]+=tz;
4359
+ c[0]+=tx; c[1]+=ty; c[2]+=tz;
4360
+ return cam;
4361
+ }
4362
+
4363
+ /**
4364
+ * @file Gizmo line generators in the arrays shape.
4365
+ * @module tree/gizmo
4366
+ * @license AGPL-3.0-only
4367
+ *
4368
+ * A gizmo is geometry that explains: axes, a grid, a frustum, a path, a rig,
4369
+ * a handle's locus. This module generates its vertices — renderer-free, into
4370
+ * caller-owned arrays shaped the way twgl's createBufferInfoFromArrays
4371
+ * consumes them and a WebGPU vertex buffer is filled from — and nothing
4372
+ * else. Drawing, colour state, HUD mode, textures, the dot at a handle's
4373
+ * point, text: all the bridge's or the host's.
4374
+ *
4375
+ * ── The arrays shape ───────────────────────────────────────────────────────
4376
+ *
4377
+ * out = {
4378
+ * position: { numComponents: 3, data: Float32Array(3 · capacity) },
4379
+ * color: { numComponents: 4, data: Float32Array(4 · capacity) }, // optional
4380
+ * texcoord: { numComponents: 2, data: Float32Array(2 · capacity) }, // optional — panes
4381
+ * count: 0, // vertices written
4382
+ * labels: [], // optional — { x, y, z, text }
4383
+ * }
4384
+ *
4385
+ * Line generators write line lists — vertex pairs, no indices; paneTris
4386
+ * writes two triangles. capacity is position.data.length / 3.
4387
+ *
4388
+ * ── The contract — snprintf-style ──────────────────────────────────────────
4389
+ * Every generator gen(out, …) → n returns the vertex count it needs, writes
4390
+ * min(n, capacity) vertices, and sets out.count to what it wrote. A caller
4391
+ * sizes once and grows never in steady state:
4392
+ *
4393
+ * let n = axesLines(out, opts)
4394
+ * if (n > capacityOf(out)) { growArrays(out, n); axesLines(out, opts) }
4395
+ *
4396
+ * Each generator states its count formula so a caller can pre-size exactly.
4397
+ *
4398
+ * ── Colour ─────────────────────────────────────────────────────────────────
4399
+ * If out.color exists, every vertex written gets a colour: a generator with
4400
+ * semantic colouring (axes, the helm rig) writes its palette, every other
4401
+ * generator writes opts.color (default white). If out.color is absent
4402
+ * nothing is written and the bridge draws with a uniform colour.
4403
+ *
4404
+ * ── Frames ─────────────────────────────────────────────────────────────────
4405
+ * A generator writes in the frame the caller means — model space for scene
4406
+ * gizmos (the bridge's M places them), screen pixels for HUD gizmos.
4407
+ * Nothing here consults a camera except locusLines and frustumLines, which
4408
+ * take what they need explicitly. Signatures: out first, the subject second
4409
+ * where there is one, options last.
4410
+ */
4411
+
4412
+
4413
+ const TWO_PI = Math.PI * 2;
4414
+ const _AXIS_COLORS = [COLOR_X, COLOR_Y, COLOR_Z];
4415
+ const _U = [1, 0, 0], _V = [0, 1, 0]; // the HUD plane's basis
4416
+ const _E = new Float64Array(16); // a camera state's eye matrix
4417
+ const _p3 = [0, 0, 0]; // a transformed corner / a sampled point
4418
+ const _q3 = [0, 0, 0]; // the previous sampled point
4419
+ const _c24 = new Float64Array(24); // frustum corners scratch
4420
+ const _tIn = [0, 0, 0], _tOut = [0, 0, 0]; // a keyframe's tangents
4421
+ const _act = [0, 0, 0, 0, 0, 0]; // a helm's activity
4422
+ const _tip = [0, 0, 0], _ha = [0, 0, 0]; // an arrow's tip and head base
4423
+ const _AXES = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
4424
+ const _b0 = [0, 0, 0], _b1 = [0, 0, 0], _b2 = [0, 0, 0]; // a locus basis
4425
+ const _UV_DEFAULT = [0, 1, 1, 1, 1, 0, 0, 0]; // paneTris: p0 top-left → (0, 1), clockwise
4426
+
4427
+ // =========================================================================
4428
+ // G1 Arrays — the one allocating call, growth, capacity
4429
+ // =========================================================================
4430
+
4431
+ /**
4432
+ * Allocate an arrays object of `capacity` vertices — the one allocating
4433
+ * call, setup-time. Flags add the optional attributes and the label list.
4434
+ *
4435
+ * @param {number} capacity Vertices.
4436
+ * @param {{ color?:boolean, texcoord?:boolean, labels?:boolean }} [opts]
4437
+ * @returns {{ position:{numComponents:number,data:Float32Array},
4438
+ * color?:{numComponents:number,data:Float32Array},
4439
+ * texcoord?:{numComponents:number,data:Float32Array},
4440
+ * count:number, labels?:object[] }}
4441
+ */
4442
+ function createArrays(capacity, opts) {
4443
+ const o = opts || {};
4444
+ const n = Math.max(0, capacity | 0);
4445
+ const out = { position: { numComponents: 3, data: new Float32Array(3 * n) } };
4446
+ if (o.color) out.color = { numComponents: 4, data: new Float32Array(4 * n) };
4447
+ if (o.texcoord) out.texcoord = { numComponents: 2, data: new Float32Array(2 * n) };
4448
+ out.count = 0;
4449
+ if (o.labels) out.labels = [];
4450
+ return out;
4451
+ }
4452
+
4453
+ /**
4454
+ * Reallocate an arrays object to a new capacity, keeping its attribute set;
4455
+ * the data is fresh (a generator refills it) and count is 0.
4456
+ *
4457
+ * @param {object} out An arrays object from createArrays.
4458
+ * @param {number} capacity Vertices.
4459
+ * @returns {object} out
4460
+ */
4461
+ function growArrays(out, capacity) {
4462
+ const n = Math.max(0, capacity | 0);
4463
+ out.position.data = new Float32Array(3 * n);
4464
+ if (out.color) out.color.data = new Float32Array(4 * n);
4465
+ if (out.texcoord) out.texcoord.data = new Float32Array(2 * n);
4466
+ out.count = 0;
4467
+ if (out.labels) out.labels.length = 0;
4468
+ return out;
4469
+ }
4470
+
4471
+ /**
4472
+ * The vertex capacity of an arrays object: position.data.length / 3.
4473
+ * @param {object} out
4474
+ * @returns {number}
4475
+ */
4476
+ function capacityOf(out) {
4477
+ return (out.position.data.length / 3) | 0;
4478
+ }
4479
+
4480
+ // =========================================================================
4481
+ // G2 The writer — a cursor over out; counts every vertex, writes those
4482
+ // within capacity. Module-level scratch: a generator runs to completion.
4483
+ // =========================================================================
4484
+
4485
+ const _w = { pos: null, col: null, tex: null, cap: 0, n: 0, r: 1, g: 1, b: 1, a: 1 };
4486
+ const _WHITE = [1, 1, 1, 1];
4487
+
4488
+ function _begin(out) {
4489
+ _w.pos = out.position.data;
4490
+ _w.col = out.color ? out.color.data : null;
4491
+ _w.tex = out.texcoord ? out.texcoord.data : null;
4492
+ _w.cap = (_w.pos.length / 3) | 0;
4493
+ _w.n = 0;
4494
+ if (out.labels) out.labels.length = 0;
4495
+ }
4496
+
4497
+ /** Set the current colour from an RGB(A) array, with an alpha override. */
4498
+ function _color(c, alpha) {
4499
+ const v = c || _WHITE;
4500
+ _w.r = v[0]; _w.g = v[1]; _w.b = v[2];
4501
+ _w.a = alpha != null ? alpha : (v.length > 3 ? v[3] : 1);
4502
+ }
4503
+
4504
+ function _vertex(x, y, z) {
4505
+ const n = _w.n++;
4506
+ if (n >= _w.cap) return;
4507
+ const p = _w.pos, i = 3 * n;
4508
+ p[i] = x; p[i + 1] = y; p[i + 2] = z;
4509
+ const c = _w.col;
4510
+ 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; }
4511
+ }
4512
+
4513
+ function _line(x0, y0, z0, x1, y1, z1) {
4514
+ _vertex(x0, y0, z0);
4515
+ _vertex(x1, y1, z1);
4516
+ }
4517
+
4518
+ /** A vertex with a texture coordinate (written when out.texcoord exists). */
4519
+ function _vertexUV(x, y, z, u, v) {
4520
+ const n = _w.n;
4521
+ _vertex(x, y, z);
4522
+ if (_w.tex && n < _w.cap) { _w.tex[2*n] = u; _w.tex[2*n + 1] = v; }
4523
+ }
4524
+
4525
+ /** Normalise v in place; a zero vector becomes (dx, dy, dz). */
4526
+ function _unit(v, dx, dy, dz) {
4527
+ const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
4528
+ if (l < 1e-9) { v[0] = dx; v[1] = dy; v[2] = dz; return v; }
4529
+ v[0] /= l; v[1] /= l; v[2] /= l;
4530
+ return v;
4531
+ }
4532
+
4533
+ /** Orthonormal in-plane basis (ub, vb) for a unit normal n, seeded from the least-aligned axis. */
4534
+ function _basis(n, ub, vb) {
4535
+ const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
4536
+ let rx = 0, ry = 0, rz = 0;
4537
+ if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
4538
+ ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
4539
+ _unit(ub, 1, 0, 0);
4540
+ 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];
4541
+ }
4542
+
4543
+ /** The four edges of a square of half-extent h at c, spanned by u, v. */
4544
+ function _square(c, u, v, h) {
4545
+ 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];
4546
+ 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];
4547
+ 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];
4548
+ 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];
4549
+ _line(x0, y0, z0, x1, y1, z1);
4550
+ _line(x1, y1, z1, x2, y2, z2);
4551
+ _line(x2, y2, z2, x3, y3, z3);
4552
+ _line(x3, y3, z3, x0, y0, z0);
4553
+ }
4554
+
4555
+ /** Close the write: count ← what fits; return what was needed. */
4556
+ function _end(out) {
4557
+ out.count = _w.n < _w.cap ? _w.n : _w.cap;
4558
+ return _w.n;
4559
+ }
4560
+
4561
+ /** A sampled circle (or arc of `sweep`) of radius r at c, spanned by u, v: n segments. */
4562
+ function _ring(cx, cy, cz, r, u, v, n, sweep) {
4563
+ let px = 0, py = 0, pz = 0;
4564
+ for (let i = 0; i <= n; i++) {
4565
+ const t = (i / n) * sweep;
4566
+ const ct = Math.cos(t) * r, st = Math.sin(t) * r;
4567
+ const x = cx + ct*u[0] + st*v[0];
4568
+ const y = cy + ct*u[1] + st*v[1];
4569
+ const z = cz + ct*u[2] + st*v[2];
4570
+ if (i > 0) _line(px, py, pz, x, y, z);
4571
+ px = x; py = y; pz = z;
4572
+ }
4573
+ }
4574
+
4575
+ // =========================================================================
4576
+ // G3 Axes, grid, cross, bulls-eye, ring
4577
+ // =========================================================================
4578
+
4579
+ /**
4580
+ * A coordinate frame at the origin: six half-axes by bit and, with LABELS,
4581
+ * the X (2 lines) · Y (4) · Z (3) glyphs at 1.04 · size, sized size / 40 ×
4582
+ * size / 30. Semantic colour per axis and its glyph (COLOR_X / Y / Z), or
4583
+ * opts.color when `semantic` is false.
4584
+ *
4585
+ * Count: 2 · axes + 18 · (LABELS ? 1 : 0), at most 30.
4586
+ *
4587
+ * @param {object} out Arrays object.
4588
+ * @param {{ size?:number, bits?:number, semantic?:boolean, color?:number[] }} [opts]
4589
+ * @returns {number} Vertices needed.
4590
+ */
4591
+ function axesLines(out, opts) {
4592
+ const o = opts || {};
4593
+ const size = o.size ?? 100;
4594
+ const bits = o.bits ?? (LABELS | X | Y | Z);
4595
+ const semantic = o.semantic !== false;
4596
+ const axis = (i) => _color(semantic ? _AXIS_COLORS[i] : o.color);
4597
+ _begin(out);
4598
+ if (bits & LABELS) {
4599
+ const cw = size/40, ch = size/30, cs = 1.04*size;
4600
+ axis(0);
4601
+ _line(cs, cw, -ch, cs, -cw, ch);
4602
+ _line(cs, -cw, -ch, cs, cw, ch);
4603
+ axis(1);
4604
+ _line( cw, cs, ch, 0, cs, 0);
4605
+ _line( 0, cs, 0, -cw, cs, ch);
4606
+ _line(-cw, cs, ch, 0, cs, 0);
4607
+ _line( 0, cs, 0, 0, cs, -ch);
4608
+ axis(2);
4609
+ _line(-cw, -ch, cs, cw, -ch, cs);
4610
+ _line( cw, -ch, cs, -cw, ch, cs);
4611
+ _line(-cw, ch, cs, cw, ch, cs);
4612
+ }
4613
+ axis(0);
4614
+ if (bits & X) _line(0, 0, 0, size, 0, 0);
4615
+ if (bits & _X) _line(0, 0, 0, -size, 0, 0);
4616
+ axis(1);
4617
+ if (bits & Y) _line(0, 0, 0, 0, size, 0);
4618
+ if (bits & _Y) _line(0, 0, 0, 0, -size, 0);
4619
+ axis(2);
4620
+ if (bits & Z) _line(0, 0, 0, 0, 0, size);
4621
+ if (bits & _Z) _line(0, 0, 0, 0, 0, -size);
4622
+ return _end(out);
4623
+ }
4624
+
4625
+ /**
4626
+ * A grid in the XY plane: subdivisions + 1 lines each way spanning ±size.
4627
+ * Orientation is the caller's M (a ground plane is a rotation about X).
4628
+ *
4629
+ * Count: 4 · (subdivisions + 1).
4630
+ *
4631
+ * @param {object} out Arrays object.
4632
+ * @param {{ size?:number, subdivisions?:number, color?:number[] }} [opts]
4633
+ * @returns {number} Vertices needed.
4634
+ */
4635
+ function gridLines(out, opts) {
4636
+ const o = opts || {};
4637
+ const size = o.size ?? 100;
4638
+ const sub = Math.max(1, (o.subdivisions ?? 10) | 0);
4639
+ _begin(out);
4640
+ _color(o.color);
4641
+ for (let i = 0; i <= sub; i++) {
4642
+ const pos = size * (2*i/sub - 1);
4643
+ _line(pos, -size, 0, pos, size, 0);
4644
+ _line(-size, pos, 0, size, pos, 0);
4645
+ }
4646
+ return _end(out);
4647
+ }
4648
+
4649
+ /**
4650
+ * A crosshair in HUD space (z = 0, target pixels): two lines of `size`
4651
+ * through (x, y). The bridge projects a model origin and converts a world
4652
+ * size to pixels before this call.
4653
+ *
4654
+ * Count: 4.
4655
+ *
4656
+ * @param {object} out Arrays object.
4657
+ * @param {{ x?:number, y?:number, size?:number, color?:number[] }} [opts]
4658
+ * @returns {number} Vertices needed.
4659
+ */
4660
+ function crossLines(out, opts) {
4661
+ const o = opts || {};
4662
+ const x = o.x ?? 0, y = o.y ?? 0, half = (o.size ?? 50) / 2;
4663
+ _begin(out);
4664
+ _color(o.color);
4665
+ _line(x - half, y, 0, x + half, y, 0);
4666
+ _line(x, y - half, 0, x, y + half, 0);
4667
+ return _end(out);
4668
+ }
4669
+
4670
+ /**
4671
+ * A bulls-eye in HUD space (z = 0, target pixels): a sampled circle of
4672
+ * radius size / 2 (`detail` segments) or the cornered square (8 lines),
4673
+ * plus the central cross at 0.6 · half.
4674
+ *
4675
+ * Count: 2 · detail + 4 (CIRCLE) or 20 (SQUARE).
4676
+ *
4677
+ * @param {object} out Arrays object.
4678
+ * @param {{ x?:number, y?:number, size?:number, shape?:number, detail?:number, color?:number[] }} [opts]
4679
+ * @returns {number} Vertices needed.
4680
+ */
4681
+ function bullsEyeLines(out, opts) {
4682
+ const o = opts || {};
4683
+ const x = o.x ?? 0, y = o.y ?? 0, half = (o.size ?? 50) / 2;
4684
+ const shape = o.shape ?? CIRCLE;
4685
+ const detail = Math.max(3, (o.detail ?? 50) | 0);
4686
+ _begin(out);
4687
+ _color(o.color);
4688
+ if (shape === CIRCLE) {
4689
+ _ring(x, y, 0, half, _U, _V, detail, TWO_PI);
4690
+ } else {
4691
+ const c = 0.6 * half;
4692
+ _line(x-half, y-half+c, 0, x-half, y-half, 0);
4693
+ _line(x-half, y-half, 0, x-half+c, y-half, 0);
4694
+ _line(x+half-c, y-half, 0, x+half, y-half, 0);
4695
+ _line(x+half, y-half, 0, x+half, y-half+c, 0);
4696
+ _line(x+half, y+half-c, 0, x+half, y+half, 0);
4697
+ _line(x+half, y+half, 0, x+half-c, y+half, 0);
4698
+ _line(x-half+c, y+half, 0, x-half, y+half, 0);
4699
+ _line(x-half, y+half, 0, x-half, y+half-c, 0);
4700
+ }
4701
+ const ch = 0.6 * half;
4702
+ _line(x - ch, y, 0, x + ch, y, 0);
4703
+ _line(x, y - ch, 0, x, y + ch, 0);
4704
+ return _end(out);
4705
+ }
4706
+
4707
+ /**
4708
+ * A sampled circle of radius r at (cx, cy, cz) spanned by the orthonormal
4709
+ * u, v — the shared primitive; a partial `sweep` gives an arc from u.
4710
+ *
4711
+ * Count: 2 · detail.
4712
+ *
4713
+ * @param {object} out Arrays object.
4714
+ * @param {number} cx,cy,cz Centre.
4715
+ * @param {number} r Radius.
4716
+ * @param {number[]} u,v Orthonormal in-plane basis.
4717
+ * @param {{ detail?:number, sweep?:number, color?:number[] }} [opts]
4718
+ * @returns {number} Vertices needed.
4719
+ */
4720
+ function ringLines(out, cx, cy, cz, r, u, v, opts) {
4721
+ const o = opts || {};
4722
+ const detail = Math.max(1, (o.detail ?? 48) | 0);
4723
+ const sweep = o.sweep ?? TWO_PI;
4724
+ _begin(out);
4725
+ _color(o.color);
4726
+ _ring(cx, cy, cz, r, u, v, detail, sweep);
4727
+ return _end(out);
4728
+ }
4729
+
4730
+ // =========================================================================
4731
+ // G4 Frustum and Hermite
4732
+ // =========================================================================
4733
+
4734
+ const _isMat = (cam) => cam != null && cam.mat4Eye != null && cam.mat4Proj != null;
4735
+
4736
+ /** Corner i of out24 ← E · (x, y, z). */
4737
+ function _corner(out24, i, E, x, y, z) {
4738
+ mat4MulPoint(_p3, E, x, y, z);
4739
+ out24[3*i] = _p3[0]; out24[3*i + 1] = _p3[1]; out24[3*i + 2] = _p3[2];
4740
+ }
4741
+
4742
+ /**
4743
+ * The eight world-space corners of a camera's frustum: the near face 0–3
4744
+ * counter-clockwise from bottom-left (BL, BR, TR, TL), then the far face
4745
+ * 4–7 in the same order — so corners 3, 2, 1, 0 are a pane's TL, TR, BR,
4746
+ * BL. `cam` is a camera state (its symmetric extents from fov or
4747
+ * halfHeight and `aspect`), or { mat4Eye, mat4Proj, ndcZMin? } for a
4748
+ * matrix-captured camera (the extents read off the projection). The far
4749
+ * extents follow by similar triangles, or equal the near ones under
4750
+ * orthographic.
4751
+ *
4752
+ * @param {Float64Array|number[]} out24 24-element destination.
4753
+ * @param {object} cam Camera state, or { mat4Eye, mat4Proj, ndcZMin? }.
4754
+ * @param {number} [aspect=1] Viewport width / height (state form).
4755
+ * @param {number} [ndcZMin=WEBGL] NDC-z convention when the matrix form carries none.
4756
+ * @returns {Float64Array|number[]|null} out24, or null when the state's lens is unset.
4757
+ */
4758
+ function frustumCorners(out24, cam, aspect, ndcZMin) {
4759
+ let E, n, f, l, r, t, b, ortho;
4760
+ if (_isMat(cam)) {
4761
+ E = cam.mat4Eye;
4762
+ const P = cam.mat4Proj, z = cam.ndcZMin ?? ndcZMin ?? WEBGL;
4763
+ ortho = projIsOrtho(P);
4764
+ n = projNear(P, z); f = projFar(P);
4765
+ l = projLeft(P, z); r = projRight(P, z); t = projTop(P, z); b = projBottom(P, z);
4766
+ } else {
4767
+ ortho = cam.fov == null;
4768
+ if (ortho && cam.halfHeight == null) return null;
4769
+ n = cam.near; f = cam.far;
4770
+ t = ortho ? cam.halfHeight : n * Math.tan(cam.fov / 2);
4771
+ r = t * (aspect ?? 1);
4772
+ b = -t; l = -r;
4773
+ E = cameraEye(_E, cam);
4774
+ }
4775
+ const k = ortho ? 1 : f / n;
4776
+ _corner(out24, 0, E, l, b, -n);
4777
+ _corner(out24, 1, E, r, b, -n);
4778
+ _corner(out24, 2, E, r, t, -n);
4779
+ _corner(out24, 3, E, l, t, -n);
4780
+ _corner(out24, 4, E, k*l, k*b, -f);
4781
+ _corner(out24, 5, E, k*r, k*b, -f);
4782
+ _corner(out24, 6, E, k*r, k*t, -f);
4783
+ _corner(out24, 7, E, k*l, k*t, -f);
4784
+ return out24;
4785
+ }
4786
+
4787
+ /** Line between corners i and j of the scratch corners. */
4788
+ function _edge(i, j) {
4789
+ _line(_c24[3*i], _c24[3*i + 1], _c24[3*i + 2], _c24[3*j], _c24[3*j + 1], _c24[3*j + 2]);
4790
+ }
4791
+
4792
+ /**
4793
+ * A camera's frustum as edges by bit: NEAR and FAR the two rectangles,
4794
+ * BODY the four edges joining them, APEX (perspective only) the eye to
4795
+ * the near corners. `cam` as frustumCorners takes it.
4796
+ *
4797
+ * Count: 8 · (NEAR + FAR + BODY + APEX), at most 32.
4798
+ *
4799
+ * @param {object} out Arrays object.
4800
+ * @param {object} cam Camera state, or { mat4Eye, mat4Proj, ndcZMin? }.
4801
+ * @param {{ aspect?:number, ndcZMin?:number, bits?:number, color?:number[] }} [opts]
4802
+ * @returns {number} Vertices needed (0 when the state's lens is unset).
4803
+ */
4804
+ function frustumLines(out, cam, opts) {
4805
+ const o = opts || {};
4806
+ const bits = o.bits ?? (NEAR | FAR | BODY | APEX);
4807
+ _begin(out);
4808
+ _color(o.color);
4809
+ if (frustumCorners(_c24, cam, o.aspect ?? 1, o.ndcZMin ?? WEBGL) === null) return _end(out);
4810
+ if (bits & NEAR) { _edge(0, 1); _edge(1, 2); _edge(2, 3); _edge(3, 0); }
4811
+ if (bits & FAR) { _edge(4, 5); _edge(5, 6); _edge(6, 7); _edge(7, 4); }
4812
+ if (bits & BODY) { _edge(0, 4); _edge(1, 5); _edge(2, 6); _edge(3, 7); }
4813
+ const persp = _isMat(cam) ? !projIsOrtho(cam.mat4Proj) : cam.fov != null;
4814
+ if ((bits & APEX) && persp) {
4815
+ const E = _isMat(cam) ? cam.mat4Eye : null;
4816
+ const ex = E ? E[12] : cam.eye[0], ey = E ? E[13] : cam.eye[1], ez = E ? E[14] : cam.eye[2];
4817
+ for (let i = 0; i < 4; i++) _line(ex, ey, ez, _c24[3*i], _c24[3*i + 1], _c24[3*i + 2]);
4818
+ }
4819
+ return _end(out);
4820
+ }
4821
+
4822
+ /**
4823
+ * One cubic Hermite segment through hermiteVec3, as a polyline of
4824
+ * `samples` steps.
4825
+ *
4826
+ * Count: 2 · samples.
4827
+ *
4828
+ * @param {object} out Arrays object.
4829
+ * @param {number[]} p0,t0 Start point and its outgoing tangent.
4830
+ * @param {number[]} p1,t1 End point and its incoming tangent.
4831
+ * @param {{ samples?:number, color?:number[] }} [opts]
4832
+ * @returns {number} Vertices needed.
4833
+ */
4834
+ function hermiteLines(out, p0, t0, p1, t1, opts) {
4835
+ const o = opts || {};
4836
+ const N = Math.max(1, (o.samples ?? 32) | 0);
4837
+ _begin(out);
4838
+ _color(o.color);
4839
+ for (let i = 0; i <= N; i++) {
4840
+ hermiteVec3(_p3, p0, t0, p1, t1, i / N);
4841
+ if (i > 0) _line(_q3[0], _q3[1], _q3[2], _p3[0], _p3[1], _p3[2]);
4842
+ _q3[0] = _p3[0]; _q3[1] = _p3[1]; _q3[2] = _p3[2];
4843
+ }
4844
+ return _end(out);
4845
+ }
4846
+
4847
+ // =========================================================================
4848
+ // G5 Path, helm rig, locus, pane
4849
+ // =========================================================================
4850
+
4851
+ /**
4852
+ * A PoseTrack or CameraTrack's path by bit, over the track's own samplers
4853
+ * (samplePos / sampleEye / sampleCenter and the tangent readers), so the
4854
+ * interpolation modes are honoured: PATH the sampled polyline, `samples`
4855
+ * per segment; CONTROLS the straight control polygon; TANGENTS_IN /
4856
+ * TANGENTS_OUT the tangent at each keyframe scaled by `tangentScale`;
4857
+ * CENTER (camera tracks) the gaze line eye → center per keyframe and a
4858
+ * three-axis star of half-size `centerSize` at the center. `target`
4859
+ * ('eye' or 'center') picks a camera track's path for the first three
4860
+ * bits. Markers and handles are the bridge's composition.
4861
+ *
4862
+ * Count: 2 · samples · segments (PATH) + 2 · segments (CONTROLS) +
4863
+ * 2 · keyframes per tangent bit + 8 · keyframes (CENTER).
4864
+ *
4865
+ * @param {object} out Arrays object.
4866
+ * @param {object} track PoseTrack or CameraTrack.
4867
+ * @param {{ bits?:number, samples?:number, tangentScale?:number, target?:string,
4868
+ * centerSize?:number, color?:number[] }} [opts]
4869
+ * @returns {number} Vertices needed.
4870
+ */
4871
+ function pathLines(out, track, opts) {
4872
+ const o = opts || {};
4873
+ const bits = o.bits ?? (PATH | CONTROLS | TANGENTS_IN | TANGENTS_OUT);
4874
+ const N = Math.max(1, (o.samples ?? 32) | 0);
4875
+ const ts = o.tangentScale ?? 0.25;
4876
+ const cs = o.centerSize ?? 4;
4877
+ const kfs = track.keyframes, n = kfs.length;
4878
+ const isCamera = typeof track.sampleEye === 'function';
4879
+ const useCenter = isCamera && o.target === 'center';
4880
+ const field = isCamera ? (useCenter ? 'center' : 'eye') : 'pos';
4881
+ const sampler = isCamera ? (useCenter ? 'sampleCenter' : 'sampleEye') : 'samplePos';
4882
+ const tangents = isCamera ? (useCenter ? 'centerTangents' : 'eyeTangents') : 'tangents';
4883
+ _begin(out);
4884
+ _color(o.color);
4885
+ if ((bits & PATH) && n > 1) {
4886
+ for (let seg = 0; seg < n - 1; seg++) {
4887
+ for (let i = 0; i <= N; i++) {
4888
+ track[sampler](_p3, seg, i / N);
4889
+ if (i > 0) _line(_q3[0], _q3[1], _q3[2], _p3[0], _p3[1], _p3[2]);
4890
+ _q3[0] = _p3[0]; _q3[1] = _p3[1]; _q3[2] = _p3[2];
4891
+ }
4892
+ }
4893
+ }
4894
+ if (bits & CONTROLS) {
4895
+ for (let i = 0; i < n - 1; i++) {
4896
+ const a = kfs[i][field], b = kfs[i + 1][field];
4897
+ _line(a[0], a[1], a[2], b[0], b[1], b[2]);
4898
+ }
4899
+ }
4900
+ if (bits & (TANGENTS_IN | TANGENTS_OUT)) {
4901
+ for (let i = 0; i < n; i++) {
4902
+ track[tangents](_tIn, _tOut, i);
4903
+ const k = kfs[i][field];
4904
+ 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]);
4905
+ 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]);
4906
+ }
4907
+ }
4908
+ if ((bits & CENTER) && isCamera) {
4909
+ for (let i = 0; i < n; i++) {
4910
+ const e = kfs[i].eye, c = kfs[i].center;
4911
+ _line(e[0], e[1], e[2], c[0], c[1], c[2]);
4912
+ _line(c[0] - cs, c[1], c[2], c[0] + cs, c[1], c[2]);
4913
+ _line(c[0], c[1] - cs, c[2], c[0], c[1] + cs, c[2]);
4914
+ _line(c[0], c[1], c[2] - cs, c[0], c[1], c[2] + cs);
4915
+ }
4916
+ }
4917
+ return _end(out);
4918
+ }
4919
+
4920
+ /** An arrow along principal axis `axis` (0 X, 1 Y, 2 Z): signed length L, head size h — 5 lines. */
4921
+ function _arrow(axis, L, h) {
4922
+ const a = (axis + 1) % 3, b = (axis + 2) % 3;
4923
+ _tip[0] = _tip[1] = _tip[2] = 0; _tip[axis] = L;
4924
+ _line(0, 0, 0, _tip[0], _tip[1], _tip[2]);
4925
+ const s = Math.sign(L) || 1;
4926
+ _ha[0] = _ha[1] = _ha[2] = 0; _ha[axis] = L - s*h;
4927
+ _ha[a] = h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4928
+ _ha[a] = -h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4929
+ _ha[a] = 0;
4930
+ _ha[b] = h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4931
+ _ha[b] = -h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
4932
+ }
4933
+
4934
+ /**
4935
+ * A helm's rig, colour always semantic: per translation channel a dim
4936
+ * baseline arrow of signed length sign · size · sens / 0.30 and, while the
4937
+ * channel's activity is non-zero, a bright arrow of length ∝ |activity| /
4938
+ * (sens · fullScale); per rotation channel a dim ring of radius size / 2 ·
4939
+ * sens / 0.0025 and a bright arc sweeping π · f in the live direction.
4940
+ * Dim and bright are the alpha of the written colour (COLOR_DIM, 1). With
4941
+ * `identify`, one anchor per channel goes to out.labels as { x, y, z,
4942
+ * text: 'L' + lane }. Orientation is the caller's M.
4943
+ *
4944
+ * Count: 10 · 3 · 2 (arrows) + 96 · 3 (rings) + 48 · 3 (arcs), at most 492;
4945
+ * the bright half only while a channel is active.
4946
+ *
4947
+ * @param {object} out Arrays object.
4948
+ * @param {object} helm A PoseHelm (profile, fullScale, activity).
4949
+ * @param {{ size?:number, bits?:number, identify?:boolean }} [opts]
4950
+ * @returns {number} Vertices needed.
4951
+ */
4952
+ function helmRigLines(out, helm, opts) {
4953
+ const o = opts || {};
4954
+ const size = o.size ?? 100;
4955
+ const bits = o.bits ?? (TRANSLATE | ROTATE);
4956
+ const identify = o.identify === true;
4957
+ const prof = helm.profile;
4958
+ const head = size * 0.08, ringR0 = size * 0.5;
4959
+ const TREF = 0.30, RREF = 0.0025, FULL = helm.fullScale, ARC_FULL = Math.PI;
4960
+ helm.activity(_act);
4961
+ _begin(out);
4962
+ const labels = identify && out.labels ? out.labels : null;
4963
+ if (bits & TRANSLATE) {
4964
+ const T = [prof.Tx, prof.Ty, prof.Tz];
4965
+ for (let ax = 0; ax < 3; ax++) {
4966
+ const ch = T[ax];
4967
+ const L = ch.sign * size * (ch.sens / TREF);
4968
+ _color(_AXIS_COLORS[ax], COLOR_DIM);
4969
+ _arrow(ax, L, head);
4970
+ const a = _act[ax];
4971
+ if (a !== 0) {
4972
+ const f = Math.min(Math.abs(a) / (ch.sens * FULL), 1);
4973
+ _color(_AXIS_COLORS[ax], 1);
4974
+ _arrow(ax, Math.sign(a) * f * Math.abs(L), head);
4975
+ }
4976
+ if (labels) {
4977
+ const lp = [0, 0, 0]; lp[ax] = L + ch.sign * head * 1.5;
4978
+ labels.push({ x: lp[0], y: lp[1], z: lp[2], text: 'L' + ch.lane });
4979
+ }
4980
+ }
4981
+ }
4982
+ if (bits & ROTATE) {
4983
+ const R = [prof.Rp, prof.Ry, prof.Rr]; // pitch ⊥ X, yaw ⊥ Y, roll ⊥ Z
4984
+ for (let ax = 0; ax < 3; ax++) {
4985
+ const ch = R[ax];
4986
+ const r = ringR0 * (ch.sens / RREF);
4987
+ const u = _AXES[(ax + 1) % 3], v = _AXES[(ax + 2) % 3];
4988
+ _color(_AXIS_COLORS[ax], COLOR_DIM);
4989
+ _ring(0, 0, 0, r, u, v, 48, TWO_PI);
4990
+ const a = _act[3 + ax];
4991
+ if (a !== 0) {
4992
+ const f = Math.min(Math.abs(a) / (ch.sens * FULL), 1);
4993
+ _color(_AXIS_COLORS[ax], 1);
4994
+ _ring(0, 0, 0, r, u, v, 24, Math.sign(a) * f * ARC_FULL);
4995
+ }
4996
+ if (labels) {
4997
+ const lp = [0, 0, 0]; lp[(ax + 1) % 3] = r;
4998
+ labels.push({ x: lp[0], y: lp[1], z: lp[2], text: 'L' + ch.lane });
4999
+ }
5000
+ }
5001
+ }
5002
+ return _end(out);
5003
+ }
5004
+
5005
+ /**
5006
+ * A handle's stroked parts by bit. AIM: anchor → `point` (the handle's
5007
+ * current point, read by the caller with value). LOCUS by constraint.kind:
5008
+ * SPHERE three great circles about the anchor; PLANE a square of
5009
+ * half-extent 100 in the plane's basis; AXIS the segment anchor + [min,
5010
+ * max] · u; DIAL the ring in the dial plane; a constraint flagged `view`
5011
+ * (the host's VIEW) a screen-aligned square of half-extent 100 at `point`,
5012
+ * its basis the camera's right and up read off `mat4View`. RING: SPHERE
5013
+ * the view-facing limb, a ring perpendicular to anchor − eye (the eye from
5014
+ * `mat4View`); PLANE the border of the locus square (written once when
5015
+ * both bits ask for it). A constraint supplying locus(out, opts) is
5016
+ * dispatched to it. The HANDLE dot is not a line and not generated here.
5017
+ *
5018
+ * Count: AIM 2; LOCUS SPHERE 288 · PLANE 8 · AXIS 2 · DIAL 96 · view 8;
5019
+ * RING SPHERE 96 · PLANE 8 (shared with LOCUS); at most 386.
5020
+ *
5021
+ * @param {object} out Arrays object.
5022
+ * @param {object} constraint A contract-conforming constraint.
5023
+ * @param {{ bits?:number, mat4View?:ArrayLike<number>, point?:number[], color?:number[] }} [opts]
5024
+ * @returns {number} Vertices needed.
5025
+ */
5026
+ function locusLines(out, constraint, opts) {
5027
+ const o = opts || {};
5028
+ if (typeof constraint.locus === 'function') return constraint.locus(out, o);
5029
+ const bits = o.bits ?? (AIM | LOCUS);
5030
+ const c = constraint, a = c.anchor, pt = o.point, V = o.mat4View;
5031
+ _begin(out);
5032
+ _color(o.color);
5033
+ if ((bits & AIM) && a && pt) _line(a[0], a[1], a[2], pt[0], pt[1], pt[2]);
5034
+ if (c.view === true) {
5035
+ if ((bits & LOCUS) && pt && V) {
5036
+ _b0[0] = V[0]; _b0[1] = V[4]; _b0[2] = V[8]; // the camera's right
5037
+ _b1[0] = V[1]; _b1[1] = V[5]; _b1[2] = V[9]; // the camera's up
5038
+ _square(pt, _b0, _b1, 100);
5039
+ }
5040
+ } else if (c.kind === SPHERE && a) {
5041
+ if (bits & LOCUS) {
5042
+ _ring(a[0], a[1], a[2], c.radius, _AXES[0], _AXES[1], 48, TWO_PI);
5043
+ _ring(a[0], a[1], a[2], c.radius, _AXES[1], _AXES[2], 48, TWO_PI);
5044
+ _ring(a[0], a[1], a[2], c.radius, _AXES[2], _AXES[0], 48, TWO_PI);
5045
+ }
5046
+ if ((bits & RING) && V) {
5047
+ // eye = −Rᵀ t of the view matrix; the limb is ⊥ anchor − eye
5048
+ const tx = V[12], ty = V[13], tz = V[14];
5049
+ _b2[0] = a[0] + (V[0]*tx + V[1]*ty + V[2]*tz);
5050
+ _b2[1] = a[1] + (V[4]*tx + V[5]*ty + V[6]*tz);
5051
+ _b2[2] = a[2] + (V[8]*tx + V[9]*ty + V[10]*tz);
5052
+ _unit(_b2, 0, 0, 1);
5053
+ _basis(_b2, _b0, _b1);
5054
+ _ring(a[0], a[1], a[2], c.radius, _b0, _b1, 48, TWO_PI);
5055
+ }
5056
+ } else if (c.kind === PLANE && a) {
5057
+ if (bits & (LOCUS | RING)) {
5058
+ _basis(c.n, _b0, _b1);
5059
+ _square(a, _b0, _b1, 100);
5060
+ }
5061
+ } else if (c.kind === AXIS && a) {
5062
+ if (bits & LOCUS) {
5063
+ const u = c.u;
5064
+ _line(a[0] + c.min*u[0], a[1] + c.min*u[1], a[2] + c.min*u[2],
5065
+ a[0] + c.max*u[0], a[1] + c.max*u[1], a[2] + c.max*u[2]);
5066
+ }
5067
+ } else if (c.kind === DIAL && a) {
5068
+ if (bits & LOCUS) _ring(a[0], a[1], a[2], c.radius, c.r0, c.r1, 48, TWO_PI);
5069
+ }
5070
+ return _end(out);
5071
+ }
5072
+
5073
+ /**
5074
+ * A textured quad as two triangles (p0, p1, p2) (p0, p2, p3) — the winding
5075
+ * of the corner order — with texcoord written when the array exists.
5076
+ * Default uvs: p0 (top-left) → (0, 1), p1 → (1, 1), p2 → (1, 0), p3 →
5077
+ * (0, 0), so a texture in GL's bottom-up space reads upright with no
5078
+ * flip; `opts.uvs` overrides, four pairs flat in corner order.
5079
+ *
5080
+ * Count: 6.
5081
+ *
5082
+ * @param {object} out Arrays object.
5083
+ * @param {number[]} p0,p1,p2,p3 Corners, top-left clockwise.
5084
+ * @param {{ uvs?:number[], color?:number[] }} [opts]
5085
+ * @returns {number} Vertices needed.
5086
+ */
5087
+ function paneTris(out, p0, p1, p2, p3, opts) {
5088
+ const o = opts || {};
5089
+ const uv = o.uvs || _UV_DEFAULT;
5090
+ _begin(out);
5091
+ _color(o.color);
5092
+ _vertexUV(p0[0], p0[1], p0[2], uv[0], uv[1]);
5093
+ _vertexUV(p1[0], p1[1], p1[2], uv[2], uv[3]);
5094
+ _vertexUV(p2[0], p2[1], p2[2], uv[4], uv[5]);
5095
+ _vertexUV(p0[0], p0[1], p0[2], uv[0], uv[1]);
5096
+ _vertexUV(p2[0], p2[1], p2[2], uv[4], uv[5]);
5097
+ _vertexUV(p3[0], p3[1], p3[2], uv[6], uv[7]);
5098
+ return _end(out);
5099
+ }
5100
+
5101
+ 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, 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
5102
  //# sourceMappingURL=index.js.map