@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/README.md +112 -14
- package/dist/index.js +1515 -72
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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=
|
|
221
|
-
|
|
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=
|
|
224
|
-
return qFromRotMat3x3(out, rx,ry,
|
|
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
|
-
|
|
478
|
-
const
|
|
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
|
-
|
|
506
|
-
const
|
|
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]
|
|
849
|
-
: projNear(p,ndcZMin)*(
|
|
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]
|
|
858
|
-
: projNear(p,ndcZMin)*(p[9]-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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₃ ·
|
|
909
|
-
*
|
|
910
|
-
*
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
|
2626
|
-
*
|
|
2627
|
-
*
|
|
2628
|
-
*
|
|
2629
|
-
*
|
|
2630
|
-
*
|
|
2631
|
-
*
|
|
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
|
|
3484
|
-
*
|
|
3485
|
-
*
|
|
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
|
-
|
|
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
|