@nakednous/tree 0.0.26 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -12
- package/dist/index.js +1717 -70
- 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
|
/**
|
|
@@ -293,6 +357,117 @@ const qToAxisAngle = (q, out) => {
|
|
|
293
357
|
return out;
|
|
294
358
|
};
|
|
295
359
|
|
|
360
|
+
/**
|
|
361
|
+
* @file 1€ input filter — speed-adaptive first-order low-pass for noisy input.
|
|
362
|
+
* @module tree/filter
|
|
363
|
+
* @license AGPL-3.0-only
|
|
364
|
+
*
|
|
365
|
+
* A jitter conditioner for noisy / absolute input streams (the helm's optional
|
|
366
|
+
* `filter` slot; sketch-side handle conditioning). It is NOT a deadzone
|
|
367
|
+
* replacement: a low-pass passes DC, so a constant rest bias survives it and
|
|
368
|
+
* still integrates to creep — only the deadzone's exact-zero clamp removes that.
|
|
369
|
+
* The 1€ removes zero-mean jitter; the two are orthogonal and coexist, applied
|
|
370
|
+
* filter → deadzone (condition the continuous signal first, then gate to zero).
|
|
371
|
+
*
|
|
372
|
+
* First-order low-pass whose cutoff rises with the signal's speed: slow (rest)
|
|
373
|
+
* → low cutoff → heavy smoothing; fast (motion) → high cutoff → low lag. Tuned
|
|
374
|
+
* by `minCutoff` (Hz, the rest cutoff) and `beta` (how fast the cutoff opens
|
|
375
|
+
* with speed). Reimplemented from the paper's equations — `tree` is zero-dep, so
|
|
376
|
+
* the ~one-page primitive is vendored, not depended on (the same call made for
|
|
377
|
+
* quaternions over gl-matrix).
|
|
378
|
+
*
|
|
379
|
+
* Reference: Casiez, G., Roussel, N., & Vogel, D. (2012). 1€ Filter: A Simple
|
|
380
|
+
* Speed-based Low-pass Filter for Noisy Input in Interactive Systems. CHI '12,
|
|
381
|
+
* 2527–2530. DOI 10.1145/2207676.2208639 · https://gery.casiez.net/1euro/
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Build a stateful 1€ filter as an out-first carrying function.
|
|
387
|
+
*
|
|
388
|
+
* The returned `f` carries the previous filtered value and derivative across
|
|
389
|
+
* calls (one state slot per vector component), and is used in one of two forms:
|
|
390
|
+
*
|
|
391
|
+
* - vec form `f(out, raw, dt)` — `out`/`raw` are equal-length ArrayLikes;
|
|
392
|
+
* writes the filtered vector into `out` and returns it.
|
|
393
|
+
* Zero-allocation after the first call (state sized to `raw`).
|
|
394
|
+
* - scalar form `f(raw, dt)` — `raw` is a number; returns the filtered number.
|
|
395
|
+
*
|
|
396
|
+
* The form is chosen by the first argument's type (number ⇒ scalar). `minCutoff`,
|
|
397
|
+
* `beta`, and `dCutoff` are live-mutable on the returned function (`f.minCutoff`,
|
|
398
|
+
* `f.beta`, `f.dCutoff`) and read every call, so a panel can tune them against
|
|
399
|
+
* live noise. `f.reset()` drops the carried state at a discontinuity (a
|
|
400
|
+
* re-acquired source, a helm `home()`); smear across one and the output lurches.
|
|
401
|
+
*
|
|
402
|
+
* @param {Object} [opts]
|
|
403
|
+
* @param {number} [opts.minCutoff=1] Minimum (rest) cutoff frequency, in Hz.
|
|
404
|
+
* @param {number} [opts.beta=0] Speed coefficient (cutoff = minCutoff +
|
|
405
|
+
* beta·|filtered derivative|); unitless.
|
|
406
|
+
* @param {number} [opts.dCutoff=1] Derivative cutoff frequency, in Hz.
|
|
407
|
+
* @returns {Function} A carrying filter `f(out, raw, dt)` / `f(raw, dt)` with a
|
|
408
|
+
* `reset()` method and live `minCutoff` / `beta` / `dCutoff`.
|
|
409
|
+
*/
|
|
410
|
+
function oneEuro(opts) {
|
|
411
|
+
opts = opts || {};
|
|
412
|
+
|
|
413
|
+
// Carried state: previous filtered value + previous filtered derivative.
|
|
414
|
+
// number (scalar form) | number[] (vec form). primed = state is seeded.
|
|
415
|
+
let xPrev = null;
|
|
416
|
+
let dxPrev = null;
|
|
417
|
+
let primed = false;
|
|
418
|
+
|
|
419
|
+
// Low-pass smoothing factor for a sample period `dt` and a cutoff frequency
|
|
420
|
+
// (the paper's α = 1 / (1 + τ/Te), τ = 1 / (2π·cutoff)).
|
|
421
|
+
const alpha = (dt, cutoff) => {
|
|
422
|
+
const tau = 1 / (2 * Math.PI * cutoff);
|
|
423
|
+
return 1 / (1 + tau / dt);
|
|
424
|
+
};
|
|
425
|
+
const smooth = (a, x, xp) => a * x + (1 - a) * xp;
|
|
426
|
+
|
|
427
|
+
const scalar = (x, dt) => {
|
|
428
|
+
if (!primed) { xPrev = x; dxPrev = 0; primed = true; return x; }
|
|
429
|
+
const dx = (x - xPrev) / dt;
|
|
430
|
+
const edx = smooth(alpha(dt, f.dCutoff), dx, dxPrev);
|
|
431
|
+
const a = alpha(dt, f.minCutoff + f.beta * Math.abs(edx));
|
|
432
|
+
const xh = smooth(a, x, xPrev);
|
|
433
|
+
xPrev = xh; dxPrev = edx;
|
|
434
|
+
return xh;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const vec = (out, raw, dt) => {
|
|
438
|
+
const n = raw.length;
|
|
439
|
+
if (!primed) {
|
|
440
|
+
if (!Array.isArray(xPrev) || xPrev.length !== n) {
|
|
441
|
+
xPrev = new Array(n); dxPrev = new Array(n);
|
|
442
|
+
}
|
|
443
|
+
for (let i = 0; i < n; i++) { xPrev[i] = raw[i]; dxPrev[i] = 0; out[i] = raw[i]; }
|
|
444
|
+
primed = true;
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
for (let i = 0; i < n; i++) {
|
|
448
|
+
const dx = (raw[i] - xPrev[i]) / dt;
|
|
449
|
+
const edx = smooth(alpha(dt, f.dCutoff), dx, dxPrev[i]);
|
|
450
|
+
const a = alpha(dt, f.minCutoff + f.beta * Math.abs(edx));
|
|
451
|
+
const xh = smooth(a, raw[i], xPrev[i]);
|
|
452
|
+
xPrev[i] = xh; dxPrev[i] = edx;
|
|
453
|
+
out[i] = xh;
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const f = (out, raw, dt) =>
|
|
459
|
+
(typeof out === 'number') ? scalar(out, raw) : vec(out, raw, dt);
|
|
460
|
+
|
|
461
|
+
f.minCutoff = (opts.minCutoff != null) ? opts.minCutoff : 1;
|
|
462
|
+
f.beta = (opts.beta != null) ? opts.beta : 0;
|
|
463
|
+
f.dCutoff = (opts.dCutoff != null) ? opts.dCutoff : 1;
|
|
464
|
+
|
|
465
|
+
/** Drop the carried state — call at a discontinuity (re-acquire / home). */
|
|
466
|
+
f.reset = () => { primed = false; return f; };
|
|
467
|
+
|
|
468
|
+
return f;
|
|
469
|
+
}
|
|
470
|
+
|
|
296
471
|
/**
|
|
297
472
|
* @file Matrix construction from geometric specs and partial decomposition.
|
|
298
473
|
* @module tree/form
|
|
@@ -352,6 +527,34 @@ function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
|
|
|
352
527
|
return out;
|
|
353
528
|
}
|
|
354
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
|
+
|
|
355
558
|
/**
|
|
356
559
|
* View matrix (world→eye) from lookat parameters.
|
|
357
560
|
* Camera looks along −Z in eye space; right = normalize(up × (−Z)).
|
|
@@ -360,16 +563,12 @@ function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
|
|
|
360
563
|
* @param {Float32Array|number[]} out 16-element destination.
|
|
361
564
|
* @param {number} ex,ey,ez Eye (camera) position.
|
|
362
565
|
* @param {number} cx,cy,cz Look-at target.
|
|
363
|
-
* @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).
|
|
364
568
|
*/
|
|
365
569
|
function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
zx/=zl; zy/=zl; zz/=zl;
|
|
369
|
-
let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
|
|
370
|
-
const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
|
|
371
|
-
xx/=xl; xy/=xl; xz/=xl;
|
|
372
|
-
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];
|
|
373
572
|
out[0]=xx; out[1]=yx; out[2]=zx; out[3]=0;
|
|
374
573
|
out[4]=xy; out[5]=yy; out[6]=zy; out[7]=0;
|
|
375
574
|
out[8]=xz; out[9]=yz; out[10]=zz; out[11]=0;
|
|
@@ -388,16 +587,12 @@ function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
|
|
|
388
587
|
* @param {Float32Array|number[]} out 16-element destination.
|
|
389
588
|
* @param {number} ex,ey,ez Eye position.
|
|
390
589
|
* @param {number} cx,cy,cz Look-at target.
|
|
391
|
-
* @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).
|
|
392
592
|
*/
|
|
393
593
|
function mat4Eye(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
zx/=zl; zy/=zl; zz/=zl;
|
|
397
|
-
let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
|
|
398
|
-
const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
|
|
399
|
-
xx/=xl; xy/=xl; xz/=xl;
|
|
400
|
-
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];
|
|
401
596
|
out[0]=xx; out[1]=xy; out[2]=xz; out[3]=0;
|
|
402
597
|
out[4]=yx; out[5]=yy; out[6]=yz; out[7]=0;
|
|
403
598
|
out[8]=zx; out[9]=zy; out[10]=zz; out[11]=0;
|
|
@@ -734,8 +929,8 @@ function projRight (p, ndcZMin) { return p[15]===1 ? (1-p[12])/p[0] : projNear
|
|
|
734
929
|
*/
|
|
735
930
|
function projTop(p, ndcZMin) {
|
|
736
931
|
return p[15]===1
|
|
737
|
-
? ( Math.sign(p[5]) - p[13]) / p[5]
|
|
738
|
-
: 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
|
|
739
934
|
}
|
|
740
935
|
|
|
741
936
|
/**
|
|
@@ -743,8 +938,8 @@ function projTop(p, ndcZMin) {
|
|
|
743
938
|
*/
|
|
744
939
|
function projBottom(p, ndcZMin) {
|
|
745
940
|
return p[15]===1
|
|
746
|
-
? (-Math.sign(p[5]) - p[13]) / p[5]
|
|
747
|
-
: 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
|
|
748
943
|
}
|
|
749
944
|
|
|
750
945
|
/** Vertical field of view in radians (perspective only). */
|
|
@@ -772,9 +967,10 @@ function mat4MV(out, model, view) { return mat4Mul(out, view, model); }
|
|
|
772
967
|
*/
|
|
773
968
|
function mat4Location(out, from, to) {
|
|
774
969
|
// Same as: return mat4Invert(out, to) && mat4Mul(out, out, from);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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];
|
|
778
974
|
const b01=a22*a11-a12*a21, b11=a12*a20-a22*a10, b21=a21*a10-a11*a20;
|
|
779
975
|
let det=a00*b01+a01*b11+a02*b21;
|
|
780
976
|
if (Math.abs(det) < 1e-12) return null;
|
|
@@ -794,14 +990,17 @@ function mat4Location(out, from, to) {
|
|
|
794
990
|
}
|
|
795
991
|
|
|
796
992
|
/**
|
|
797
|
-
* Direction transform between frames: out = to₃ ·
|
|
798
|
-
*
|
|
799
|
-
*
|
|
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.
|
|
800
998
|
*/
|
|
801
999
|
function mat3Direction(out, from, to) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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];
|
|
805
1004
|
const b01=a22*a11-a12*a21, b11=a12*a20-a22*a10, b21=a21*a10-a11*a20;
|
|
806
1005
|
let det=a00*b01+a01*b11+a02*b21;
|
|
807
1006
|
if (Math.abs(det) < 1e-12) return null;
|
|
@@ -809,10 +1008,13 @@ function mat3Direction(out, from, to) {
|
|
|
809
1008
|
const i00=b01*det, i01=(a02*a21-a22*a01)*det, i02=(a12*a01-a02*a11)*det;
|
|
810
1009
|
const i10=b11*det, i11=(a22*a00-a02*a20)*det, i12=(a02*a10-a12*a00)*det;
|
|
811
1010
|
const i20=b21*det, i21=(a01*a20-a21*a00)*det, i22=(a11*a00-a01*a10)*det;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
+
}
|
|
816
1018
|
return out;
|
|
817
1019
|
}
|
|
818
1020
|
|
|
@@ -1010,9 +1212,13 @@ function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
|
|
|
1010
1212
|
}
|
|
1011
1213
|
|
|
1012
1214
|
function _screenToWorldDir(out, dx, dy, dz, proj, eye, vpW, vpH, ndcZMin) {
|
|
1013
|
-
// Inverse of _worldToScreenDir
|
|
1014
|
-
|
|
1015
|
-
|
|
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);
|
|
1016
1222
|
}
|
|
1017
1223
|
|
|
1018
1224
|
function _screenToNDCDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
|
|
@@ -1120,6 +1326,63 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
1120
1326
|
return out;
|
|
1121
1327
|
}
|
|
1122
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
|
+
|
|
1123
1386
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1124
1387
|
// pixelRatio
|
|
1125
1388
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1173,6 +1436,34 @@ function mat4Pick(proj, px, py, vp) {
|
|
|
1173
1436
|
}
|
|
1174
1437
|
}
|
|
1175
1438
|
|
|
1439
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1440
|
+
// Pick-id codec
|
|
1441
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Pick id → colour: the 24-bit id packed into r, g, b as normalised floats,
|
|
1445
|
+
* R the low byte, alpha 1. Id 0 is the background; ids run 1 … 2²⁴ − 1.
|
|
1446
|
+
* @param {number[]} out 4-element destination.
|
|
1447
|
+
* @param {number} id Integer id.
|
|
1448
|
+
* @returns {number[]} out
|
|
1449
|
+
*/
|
|
1450
|
+
function idToRgba(out, id) {
|
|
1451
|
+
out[0] = (id & 255) / 255;
|
|
1452
|
+
out[1] = ((id >> 8) & 255) / 255;
|
|
1453
|
+
out[2] = ((id >> 16) & 255) / 255;
|
|
1454
|
+
out[3] = 1;
|
|
1455
|
+
return out;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Colour bytes → pick id, the inverse of idToRgba on a readback.
|
|
1460
|
+
* @param {number} r,g,b Bytes 0 … 255 (fractions truncated).
|
|
1461
|
+
* @returns {number} id
|
|
1462
|
+
*/
|
|
1463
|
+
function rgbaToId(r, g, b) {
|
|
1464
|
+
return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1176
1467
|
// =========================================================================
|
|
1177
1468
|
// Decomposition
|
|
1178
1469
|
// =========================================================================
|
|
@@ -1540,9 +1831,9 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1540
1831
|
function _parseQuat(v) {
|
|
1541
1832
|
if (!v) return null;
|
|
1542
1833
|
|
|
1543
|
-
// [x,y,z,w]
|
|
1544
|
-
if (Array.isArray(v) && v.length === 4) return [v[0],v[1],v[2],v[3]];
|
|
1545
|
-
if (ArrayBuffer.isView(v) && v.length >= 4) return [v[0],v[1],v[2],v[3]];
|
|
1834
|
+
// [x,y,z,w] — normalised like every other form
|
|
1835
|
+
if (Array.isArray(v) && v.length === 4) return qNormalize([v[0],v[1],v[2],v[3]]);
|
|
1836
|
+
if (ArrayBuffer.isView(v) && v.length >= 4) return qNormalize([v[0],v[1],v[2],v[3]]);
|
|
1546
1837
|
|
|
1547
1838
|
if (typeof v !== 'object') return null;
|
|
1548
1839
|
|
|
@@ -2511,13 +2802,15 @@ class CameraTrack extends Track {
|
|
|
2511
2802
|
* A constraint is any object exposing: `kind` (integer discriminant),
|
|
2512
2803
|
* `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
|
|
2513
2804
|
* optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])` — the
|
|
2514
|
-
* basis re-aim seam the bridge's deferred `from` frame drives
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2519
|
-
*
|
|
2520
|
-
*
|
|
2805
|
+
* basis re-aim seam the bridge's deferred `from` frame drives — and
|
|
2806
|
+
* `proxy(ox,oy,oz, dx,dy,dz, radius)`, the analytic pick: the ray parameter
|
|
2807
|
+
* at which a ray in the working space meets the grab proxy of `radius`
|
|
2808
|
+
* working units, or Infinity (a kind without one gets a sphere at its
|
|
2809
|
+
* POINT). The handle controller drives any conforming constraint
|
|
2810
|
+
* (lifecycle, frame conversion, bind, hooks, pick); a new kind — 6-DOF, or
|
|
2811
|
+
* app-specific — implements this contract here (portable, draw-free) plus a
|
|
2812
|
+
* bridge-side locus draw, rather than forking the controller. The classes
|
|
2813
|
+
* below are the reference implementation. See handle-design.md §9–§10.
|
|
2521
2814
|
*
|
|
2522
2815
|
* ── Conventions ────────────────────────────────────────────────────────────
|
|
2523
2816
|
* Ray direction `d` is assumed unit (the bridge normalises). Plane / axis
|
|
@@ -2530,7 +2823,7 @@ class CameraTrack extends Track {
|
|
|
2530
2823
|
|
|
2531
2824
|
|
|
2532
2825
|
const EPS = 1e-6;
|
|
2533
|
-
const TWO_PI = Math.PI * 2;
|
|
2826
|
+
const TWO_PI$1 = Math.PI * 2;
|
|
2534
2827
|
|
|
2535
2828
|
// Edge-on threshold for DIAL: below this |d·n| the plane hit is ill-conditioned
|
|
2536
2829
|
// (dθ per pixel diverges) and the solve switches to the tangent-line fallback.
|
|
@@ -2546,10 +2839,10 @@ const _clamp = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
|
|
|
2546
2839
|
const _num = (x, d) => _isNum(x) ? x : d;
|
|
2547
2840
|
|
|
2548
2841
|
/** Wrap an angle to (−π, π]. */
|
|
2549
|
-
const _wrapPi = (a) => a - TWO_PI * Math.round(a / TWO_PI);
|
|
2842
|
+
const _wrapPi = (a) => a - TWO_PI$1 * Math.round(a / TWO_PI$1);
|
|
2550
2843
|
|
|
2551
2844
|
/** Parse a vec3 from array / typed array / {x,y,z}. Returns a fresh [x,y,z] or null. */
|
|
2552
|
-
function _vec3(v) {
|
|
2845
|
+
function _vec3$1(v) {
|
|
2553
2846
|
if (!v) return null;
|
|
2554
2847
|
if (ArrayBuffer.isView(v) && v.length >= 3) return [v[0], v[1], v[2]];
|
|
2555
2848
|
if (Array.isArray(v) && v.length >= 3) return [v[0], v[1], v[2]];
|
|
@@ -2558,7 +2851,7 @@ function _vec3(v) {
|
|
|
2558
2851
|
}
|
|
2559
2852
|
|
|
2560
2853
|
/** Normalise a vec3 in place; zero-length falls back to the given default axis. */
|
|
2561
|
-
function _unit(v, dx, dy, dz) {
|
|
2854
|
+
function _unit$1(v, dx, dy, dz) {
|
|
2562
2855
|
const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
|
|
2563
2856
|
if (l < EPS) { v[0]=dx; v[1]=dy; v[2]=dz; return v; }
|
|
2564
2857
|
v[0]/=l; v[1]/=l; v[2]/=l;
|
|
@@ -2571,12 +2864,12 @@ function _unit(v, dx, dy, dz) {
|
|
|
2571
2864
|
* (Same derivation the p5 bridge uses for its plane quad — duplicated here
|
|
2572
2865
|
* because the core cannot depend on the bridge.)
|
|
2573
2866
|
*/
|
|
2574
|
-
function _basis(n, ub, vb) {
|
|
2867
|
+
function _basis$1(n, ub, vb) {
|
|
2575
2868
|
const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
|
|
2576
2869
|
let rx = 0, ry = 0, rz = 0;
|
|
2577
2870
|
if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
|
|
2578
2871
|
ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
|
|
2579
|
-
_unit(ub, 1, 0, 0);
|
|
2872
|
+
_unit$1(ub, 1, 0, 0);
|
|
2580
2873
|
vb[0] = n[1]*ub[2] - n[2]*ub[1]; vb[1] = n[2]*ub[0] - n[0]*ub[2]; vb[2] = n[0]*ub[1] - n[1]*ub[0];
|
|
2581
2874
|
}
|
|
2582
2875
|
|
|
@@ -2660,6 +2953,132 @@ function rayClosestPointOnAxis(out, ox,oy,oz, dx,dy,dz, px,py,pz, ux,uy,uz) {
|
|
|
2660
2953
|
return s;
|
|
2661
2954
|
}
|
|
2662
2955
|
|
|
2956
|
+
// =========================================================================
|
|
2957
|
+
// H2b Ray-primitive hit tests (pure, no out: the nearest t, or Infinity)
|
|
2958
|
+
// =========================================================================
|
|
2959
|
+
//
|
|
2960
|
+
// Beside the solve primitives above, which always write a point, these are
|
|
2961
|
+
// TESTS: they write nothing and return the ray parameter of the nearest hit
|
|
2962
|
+
// with t ≥ 0, or Infinity on a miss. A ray whose origin lies inside the
|
|
2963
|
+
// primitive hits at its exit, so a press from inside a proxy still grabs.
|
|
2964
|
+
// The ray direction is assumed unit.
|
|
2965
|
+
|
|
2966
|
+
/**
|
|
2967
|
+
* Ray–sphere hit test.
|
|
2968
|
+
* @param {number} ox,oy,oz Ray origin.
|
|
2969
|
+
* @param {number} dx,dy,dz Ray direction (unit).
|
|
2970
|
+
* @param {number} cx,cy,cz Sphere centre.
|
|
2971
|
+
* @param {number} r Sphere radius.
|
|
2972
|
+
* @returns {number} The nearest t ≥ 0, or Infinity.
|
|
2973
|
+
*/
|
|
2974
|
+
function rayHitSphere(ox,oy,oz, dx,dy,dz, cx,cy,cz, r) {
|
|
2975
|
+
const lx=ox-cx, ly=oy-cy, lz=oz-cz;
|
|
2976
|
+
const b = lx*dx + ly*dy + lz*dz;
|
|
2977
|
+
const cc = lx*lx + ly*ly + lz*lz - r*r;
|
|
2978
|
+
const disc = b*b - cc;
|
|
2979
|
+
if (disc < 0) return Infinity;
|
|
2980
|
+
const s = Math.sqrt(disc);
|
|
2981
|
+
let t = -b - s;
|
|
2982
|
+
if (t < 0) t = -b + s; // origin inside: the exit
|
|
2983
|
+
return t < 0 ? Infinity : t; // both roots behind the origin
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// One end cap of a capsule: the sphere at (cx,cy,cz), a root accepted only
|
|
2987
|
+
// where the hit's axial coordinate h = wu + t·du lies beyond the segment on
|
|
2988
|
+
// that cap's side (h ≤ 0 at A, h ≥ L at B), so the point is on the capsule's
|
|
2989
|
+
// surface and not inside its cylinder.
|
|
2990
|
+
function _capHit(ox,oy,oz, dx,dy,dz, cx,cy,cz, r, wu, du, lim, endB) {
|
|
2991
|
+
const lx=ox-cx, ly=oy-cy, lz=oz-cz;
|
|
2992
|
+
const b = lx*dx + ly*dy + lz*dz;
|
|
2993
|
+
const cc = lx*lx + ly*ly + lz*lz - r*r;
|
|
2994
|
+
const disc = b*b - cc;
|
|
2995
|
+
if (disc < 0) return Infinity;
|
|
2996
|
+
const s = Math.sqrt(disc);
|
|
2997
|
+
let t = -b - s, h = wu + t*du;
|
|
2998
|
+
if (t < 0 || (endB ? h < lim : h > lim)) { t = -b + s; h = wu + t*du; }
|
|
2999
|
+
return (t >= 0 && (endB ? h >= lim : h <= lim)) ? t : Infinity;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
/**
|
|
3003
|
+
* Ray–capsule hit test: the segment A→B swept by radius r. The cylinder wall
|
|
3004
|
+
* counts within the segment's extent, each end sphere beyond it; the nearest
|
|
3005
|
+
* of the three wins. A zero-length segment is the sphere at A.
|
|
3006
|
+
* @param {number} ox,oy,oz Ray origin.
|
|
3007
|
+
* @param {number} dx,dy,dz Ray direction (unit).
|
|
3008
|
+
* @param {number} ax,ay,az Segment start.
|
|
3009
|
+
* @param {number} bx,by,bz Segment end.
|
|
3010
|
+
* @param {number} r Capsule radius.
|
|
3011
|
+
* @returns {number} The nearest t ≥ 0, or Infinity.
|
|
3012
|
+
*/
|
|
3013
|
+
function rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r) {
|
|
3014
|
+
let ux=bx-ax, uy=by-ay, uz=bz-az;
|
|
3015
|
+
const L = Math.sqrt(ux*ux + uy*uy + uz*uz);
|
|
3016
|
+
if (L < EPS) return rayHitSphere(ox,oy,oz, dx,dy,dz, ax,ay,az, r);
|
|
3017
|
+
ux/=L; uy/=L; uz/=L;
|
|
3018
|
+
const wx=ox-ax, wy=oy-ay, wz=oz-az;
|
|
3019
|
+
const du = dx*ux + dy*uy + dz*uz; // d·u
|
|
3020
|
+
const wu = wx*ux + wy*uy + wz*uz; // w·u
|
|
3021
|
+
let best = Infinity;
|
|
3022
|
+
// Cylinder wall: the quadratic in the components perpendicular to u.
|
|
3023
|
+
const a = 1 - du*du;
|
|
3024
|
+
if (a > EPS) {
|
|
3025
|
+
const b = (dx*wx + dy*wy + dz*wz) - du*wu;
|
|
3026
|
+
const c = (wx*wx + wy*wy + wz*wz) - wu*wu - r*r;
|
|
3027
|
+
const disc = b*b - a*c;
|
|
3028
|
+
if (disc >= 0) {
|
|
3029
|
+
const s = Math.sqrt(disc);
|
|
3030
|
+
let t = (-b - s)/a, h = wu + t*du;
|
|
3031
|
+
if (t < 0 || h < 0 || h > L) { t = (-b + s)/a; h = wu + t*du; }
|
|
3032
|
+
if (t >= 0 && h >= 0 && h <= L) best = t;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
const ta = _capHit(ox,oy,oz, dx,dy,dz, ax,ay,az, r, wu, du, 0, false);
|
|
3036
|
+
if (ta < best) best = ta;
|
|
3037
|
+
const tb = _capHit(ox,oy,oz, dx,dy,dz, bx,by,bz, r, wu, du, L, true);
|
|
3038
|
+
if (tb < best) best = tb;
|
|
3039
|
+
return best;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// Ring scratch — the plane normal and its in-plane basis. Function-local
|
|
3043
|
+
// working values, never state: rayHitRing runs to completion.
|
|
3044
|
+
const _rn = [0, 0, 1], _r0 = [1, 0, 0], _r1 = [0, 1, 0];
|
|
3045
|
+
|
|
3046
|
+
/**
|
|
3047
|
+
* Ray–ring hit test: the circle of radius R about c in the plane
|
|
3048
|
+
* perpendicular to u, swept by tube radius r — the analytic torus proxy as a
|
|
3049
|
+
* capsule chain, the circle polygonised into `detail` segments and each
|
|
3050
|
+
* tested with rayHitCapsule. The chain has volume in every direction, so it
|
|
3051
|
+
* never degenerates edge-on; the chordal error is R · (1 − cos(π / detail)).
|
|
3052
|
+
* @param {number} ox,oy,oz Ray origin.
|
|
3053
|
+
* @param {number} dx,dy,dz Ray direction (unit).
|
|
3054
|
+
* @param {number} cx,cy,cz Ring centre.
|
|
3055
|
+
* @param {number} ux,uy,uz Ring plane normal (normalised here).
|
|
3056
|
+
* @param {number} R Ring radius.
|
|
3057
|
+
* @param {number} r Tube radius.
|
|
3058
|
+
* @param {number} [detail=32] Chain segments (at least 3).
|
|
3059
|
+
* @returns {number} The nearest t ≥ 0, or Infinity.
|
|
3060
|
+
*/
|
|
3061
|
+
function rayHitRing(ox,oy,oz, dx,dy,dz, cx,cy,cz, ux,uy,uz, R, r, detail = 32) {
|
|
3062
|
+
const n = _isNum(detail) && detail >= 3 ? Math.floor(detail) : 32;
|
|
3063
|
+
_rn[0]=ux; _rn[1]=uy; _rn[2]=uz;
|
|
3064
|
+
_unit$1(_rn, 0, 0, 1);
|
|
3065
|
+
_basis$1(_rn, _r0, _r1);
|
|
3066
|
+
const step = TWO_PI$1 / n;
|
|
3067
|
+
let best = Infinity;
|
|
3068
|
+
let ax = cx + R*_r0[0], ay = cy + R*_r0[1], az = cz + R*_r0[2]; // vertex at angle 0
|
|
3069
|
+
for (let i = 1; i <= n; i++) {
|
|
3070
|
+
const th = i === n ? 0 : i*step; // close the chain exactly
|
|
3071
|
+
const cs = Math.cos(th)*R, sn = Math.sin(th)*R;
|
|
3072
|
+
const bx = cx + cs*_r0[0] + sn*_r1[0];
|
|
3073
|
+
const by = cy + cs*_r0[1] + sn*_r1[1];
|
|
3074
|
+
const bz = cz + cs*_r0[2] + sn*_r1[2];
|
|
3075
|
+
const t = rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r);
|
|
3076
|
+
if (t < best) best = t;
|
|
3077
|
+
ax = bx; ay = by; az = bz;
|
|
3078
|
+
}
|
|
3079
|
+
return best;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
2663
3082
|
// =========================================================================
|
|
2664
3083
|
// H3 Angular utilities (readout / authoring convenience)
|
|
2665
3084
|
// =========================================================================
|
|
@@ -2719,17 +3138,17 @@ class Constraint {
|
|
|
2719
3138
|
this.kind = kind;
|
|
2720
3139
|
|
|
2721
3140
|
/** Constraint origin (sphere centre / plane point / axis anchor / dial centre). @type {number[]} */
|
|
2722
|
-
this.anchor = _vec3(opts.anchor) || [0, 0, 0];
|
|
3141
|
+
this.anchor = _vec3$1(opts.anchor) || [0, 0, 0];
|
|
2723
3142
|
/** Canonical unit direction — SPHERE. @type {number[]} */
|
|
2724
3143
|
this.dir = [0, 0, 1];
|
|
2725
3144
|
/** Constrained point — PLANE / AXIS / DIAL (and SPHERE scratch). @type {number[]} */
|
|
2726
3145
|
this.pt = [this.anchor[0], this.anchor[1], this.anchor[2]];
|
|
2727
3146
|
/** Plane normal (unit) — PLANE. @type {number[]} */
|
|
2728
|
-
this.n = _unit(_vec3(opts.normal) || [0, 1, 0], 0, 1, 0);
|
|
3147
|
+
this.n = _unit$1(_vec3$1(opts.normal) || [0, 1, 0], 0, 1, 0);
|
|
2729
3148
|
/** Axis / dial-plane normal (unit) — AXIS / DIAL. @type {number[]} */
|
|
2730
3149
|
this.u = kind === DIAL
|
|
2731
|
-
? _unit(_vec3(opts.axis) || [0, 1, 0], 0, 1, 0)
|
|
2732
|
-
: _unit(_vec3(opts.axis) || [1, 0, 0], 1, 0, 0);
|
|
3150
|
+
? _unit$1(_vec3$1(opts.axis) || [0, 1, 0], 0, 1, 0)
|
|
3151
|
+
: _unit$1(_vec3$1(opts.axis) || [1, 0, 0], 1, 0, 0);
|
|
2733
3152
|
/** Current scalar parameter — AXIS: t along the line; DIAL: accumulated θ. @type {number} */
|
|
2734
3153
|
this.s = 0;
|
|
2735
3154
|
|
|
@@ -2740,7 +3159,7 @@ class Constraint {
|
|
|
2740
3159
|
/** In-plane binormal u × r0 (unit) — DIAL. @type {number[]} */
|
|
2741
3160
|
this.r1 = [0, 0, 1];
|
|
2742
3161
|
if (kind === DIAL) {
|
|
2743
|
-
const z = _vec3(opts.zero);
|
|
3162
|
+
const z = _vec3$1(opts.zero);
|
|
2744
3163
|
this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
|
|
2745
3164
|
}
|
|
2746
3165
|
|
|
@@ -2791,10 +3210,10 @@ class Constraint {
|
|
|
2791
3210
|
this.r0[1] = zy - d*this.u[1];
|
|
2792
3211
|
this.r0[2] = zz - d*this.u[2];
|
|
2793
3212
|
const l = Math.sqrt(this.r0[0]**2 + this.r0[1]**2 + this.r0[2]**2);
|
|
2794
|
-
if (l < EPS) _basis(this.u, this.r0, this.r1);
|
|
3213
|
+
if (l < EPS) _basis$1(this.u, this.r0, this.r1);
|
|
2795
3214
|
else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
|
|
2796
3215
|
} else {
|
|
2797
|
-
_basis(this.u, this.r0, this.r1);
|
|
3216
|
+
_basis$1(this.u, this.r0, this.r1);
|
|
2798
3217
|
}
|
|
2799
3218
|
// r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
|
|
2800
3219
|
this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
|
|
@@ -2818,7 +3237,7 @@ class Constraint {
|
|
|
2818
3237
|
this.dir[0] = this.pt[0] - this.anchor[0];
|
|
2819
3238
|
this.dir[1] = this.pt[1] - this.anchor[1];
|
|
2820
3239
|
this.dir[2] = this.pt[2] - this.anchor[2];
|
|
2821
|
-
_unit(this.dir, this.dir[0], this.dir[1], this.dir[2]);
|
|
3240
|
+
_unit$1(this.dir, this.dir[0], this.dir[1], this.dir[2]);
|
|
2822
3241
|
} else if (this.kind === PLANE) {
|
|
2823
3242
|
// Parallel ray returns Infinity and leaves pt unchanged (keep last).
|
|
2824
3243
|
rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
|
|
@@ -2965,7 +3384,7 @@ class Constraint {
|
|
|
2965
3384
|
if (px*px + py*py >= EPS*EPS) {
|
|
2966
3385
|
const a = Math.atan2(py, px);
|
|
2967
3386
|
// Nearest winding to the current θ preserves accumulated turns.
|
|
2968
|
-
this.s = _clamp(a + TWO_PI * Math.round((this.s - a) / TWO_PI),
|
|
3387
|
+
this.s = _clamp(a + TWO_PI$1 * Math.round((this.s - a) / TWO_PI$1),
|
|
2969
3388
|
this.min, this.max);
|
|
2970
3389
|
}
|
|
2971
3390
|
this._dialPoint();
|
|
@@ -2994,24 +3413,54 @@ class Constraint {
|
|
|
2994
3413
|
if (this.kind === PLANE) {
|
|
2995
3414
|
const px = this.n[0], py = this.n[1], pz = this.n[2];
|
|
2996
3415
|
this.n[0] = ax; this.n[1] = ay; this.n[2] = az;
|
|
2997
|
-
_unit(this.n, px, py, pz);
|
|
3416
|
+
_unit$1(this.n, px, py, pz);
|
|
2998
3417
|
this.seed(this.pt[0], this.pt[1], this.pt[2]);
|
|
2999
3418
|
} else if (this.kind === AXIS) {
|
|
3000
3419
|
const px = this.u[0], py = this.u[1], pz = this.u[2];
|
|
3001
3420
|
this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
|
|
3002
|
-
_unit(this.u, px, py, pz);
|
|
3421
|
+
_unit$1(this.u, px, py, pz);
|
|
3003
3422
|
this.pt[0] = this.anchor[0] + this.s*this.u[0];
|
|
3004
3423
|
this.pt[1] = this.anchor[1] + this.s*this.u[1];
|
|
3005
3424
|
this.pt[2] = this.anchor[2] + this.s*this.u[2];
|
|
3006
3425
|
} else if (this.kind === DIAL) {
|
|
3007
3426
|
const px = this.u[0], py = this.u[1], pz = this.u[2];
|
|
3008
3427
|
this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
|
|
3009
|
-
_unit(this.u, px, py, pz);
|
|
3428
|
+
_unit$1(this.u, px, py, pz);
|
|
3010
3429
|
this._dialBasis(zx, zy, zz);
|
|
3011
3430
|
this._dialPoint();
|
|
3012
3431
|
}
|
|
3013
3432
|
return this;
|
|
3014
3433
|
}
|
|
3434
|
+
|
|
3435
|
+
/**
|
|
3436
|
+
* The analytic pick proxy: the ray parameter t at which a ray in the
|
|
3437
|
+
* working space meets the grab proxy, or Infinity. `radius` is the grab
|
|
3438
|
+
* size in working-space units — a constant pixel size the caller converted
|
|
3439
|
+
* through pixelRatio at the proxy's depth. SPHERE / PLANE / AXIS: a sphere
|
|
3440
|
+
* of `radius` at the reported POINT. DIAL: the ring at the anchor with
|
|
3441
|
+
* tube radius `radius` (rayHitRing), so a grab lands anywhere on the ring.
|
|
3442
|
+
*
|
|
3443
|
+
* @param {number} ox,oy,oz Ray origin.
|
|
3444
|
+
* @param {number} dx,dy,dz Ray direction (unit).
|
|
3445
|
+
* @param {number} radius Grab radius, working units.
|
|
3446
|
+
* @returns {number} The nearest t ≥ 0, or Infinity.
|
|
3447
|
+
*/
|
|
3448
|
+
proxy(ox, oy, oz, dx, dy, dz, radius) {
|
|
3449
|
+
const a = this.anchor;
|
|
3450
|
+
if (this.kind === DIAL) {
|
|
3451
|
+
return rayHitRing(ox,oy,oz, dx,dy,dz, a[0], a[1], a[2],
|
|
3452
|
+
this.u[0], this.u[1], this.u[2], this._radius, radius);
|
|
3453
|
+
}
|
|
3454
|
+
let px, py, pz;
|
|
3455
|
+
if (this.kind === SPHERE) {
|
|
3456
|
+
px = a[0] + this.dir[0]*this._radius;
|
|
3457
|
+
py = a[1] + this.dir[1]*this._radius;
|
|
3458
|
+
pz = a[2] + this.dir[2]*this._radius;
|
|
3459
|
+
} else {
|
|
3460
|
+
px = this.pt[0]; py = this.pt[1]; pz = this.pt[2];
|
|
3461
|
+
}
|
|
3462
|
+
return rayHitSphere(ox,oy,oz, dx,dy,dz, px,py,pz, radius);
|
|
3463
|
+
}
|
|
3015
3464
|
}
|
|
3016
3465
|
|
|
3017
3466
|
/**
|
|
@@ -3044,6 +3493,12 @@ function createConstraint(kind, opts) {
|
|
|
3044
3493
|
* eval(out) read the current { pos, rot } (zero-alloc, as Track.eval)
|
|
3045
3494
|
* home(pose?) re-home the integrated pose (NOT reset — no keyframes to clear)
|
|
3046
3495
|
*
|
|
3496
|
+
* Value layer (opt-in, §10): a per-helm `fullScale` keeps the read-outs honest
|
|
3497
|
+
* across input scales; an opt-in `filter` (oneEuro) conditions the fed rates
|
|
3498
|
+
* before the deadzone inside step; `poseDelta` differences two absolute poses
|
|
3499
|
+
* into a feedable rate. With `filter` null and the default `fullScale`, the
|
|
3500
|
+
* clean rate-native path is unchanged.
|
|
3501
|
+
*
|
|
3047
3502
|
* Quaternion algebra is provided by quat.js. Out-first throughout; no allocation
|
|
3048
3503
|
* in feed/step/eval. Storage convention (matches the rest of the core): vec3 and
|
|
3049
3504
|
* quat state are plain number[] (f64) — the same shape as track.js keyframes and
|
|
@@ -3076,6 +3531,23 @@ function createConstraint(kind, opts) {
|
|
|
3076
3531
|
|
|
3077
3532
|
const _dq = [0, 0, 0, 1];
|
|
3078
3533
|
|
|
3534
|
+
// Scratch for the optional input filter (helm.filter). The fed rates are packed
|
|
3535
|
+
// into one 6-vector, conditioned, and unpacked to lin/ang triples before the
|
|
3536
|
+
// deadzone — so a single filter carries one state per helm. Shared across
|
|
3537
|
+
// instances (step is synchronous, at most once per helm per frame).
|
|
3538
|
+
const _f6raw = [0, 0, 0, 0, 0, 0];
|
|
3539
|
+
const _f6out = [0, 0, 0, 0, 0, 0];
|
|
3540
|
+
const _fLin = [0, 0, 0];
|
|
3541
|
+
const _fAng = [0, 0, 0];
|
|
3542
|
+
|
|
3543
|
+
/**
|
|
3544
|
+
* Default full-deflection input magnitude — the fed rate a read-out shows as
|
|
3545
|
+
* full (the SpaceNavigator's saturated lane). Private to core: a transport on a
|
|
3546
|
+
* different raw scale sets helm.fullScale and never reads this, so there is no
|
|
3547
|
+
* public HELM_FULL export. Display-only; integration uses profile.sens directly.
|
|
3548
|
+
*/
|
|
3549
|
+
const HELM_FULL_SCALE = 500;
|
|
3550
|
+
|
|
3079
3551
|
// Deadzone gate — |v| > dz keeps v, else 0. Module-level so step allocates no
|
|
3080
3552
|
// closure. Strictly-greater matches the e7 reference (rest reads exact 0).
|
|
3081
3553
|
const _dz = (v, dz) => (v > dz || v < -dz) ? v : 0;
|
|
@@ -3128,6 +3600,24 @@ class PoseHelm {
|
|
|
3128
3600
|
*/
|
|
3129
3601
|
this.deadzone = 8;
|
|
3130
3602
|
|
|
3603
|
+
/**
|
|
3604
|
+
* Full-deflection input magnitude for the read-outs (gizmo overlay + panel
|
|
3605
|
+
* meters): a fed rate of |fullScale| reads as a full bar / arrow. Set by a
|
|
3606
|
+
* transport whose saturated rate differs from the default (a gamepad stick
|
|
3607
|
+
* that saturates at 1 sets fullScale = 1). Display-only — integration uses
|
|
3608
|
+
* profile.sens directly, so this never affects flight. @type {number}
|
|
3609
|
+
*/
|
|
3610
|
+
this.fullScale = HELM_FULL_SCALE;
|
|
3611
|
+
|
|
3612
|
+
/**
|
|
3613
|
+
* Optional input conditioner applied to the fed rates BEFORE the deadzone,
|
|
3614
|
+
* inside step — a oneEuro filter, or any f(out, raw, dt) carrying function
|
|
3615
|
+
* with a reset(). null (the default) is the clean rate-native path: the
|
|
3616
|
+
* filter branch is skipped, so a clean device pays nothing. Set for noisy /
|
|
3617
|
+
* absolute sources; home() resets it. @type {?Function}
|
|
3618
|
+
*/
|
|
3619
|
+
this.filter = null;
|
|
3620
|
+
|
|
3131
3621
|
/**
|
|
3132
3622
|
* The space fed rates are interpreted in: a space constant (WORLD | EYE) or
|
|
3133
3623
|
* a mat4 frame. Declarative only — the bridge reads this to resolve the
|
|
@@ -3189,7 +3679,22 @@ class PoseHelm {
|
|
|
3189
3679
|
*/
|
|
3190
3680
|
step(out, dt, basis) {
|
|
3191
3681
|
const p = this.profile, dz = this.deadzone;
|
|
3192
|
-
|
|
3682
|
+
let lin = this._lin, ang = this._ang;
|
|
3683
|
+
|
|
3684
|
+
// Value layer — condition the fed rates BEFORE the deadzone (the filter
|
|
3685
|
+
// narrows the jitter band; the deadzone then gates the residual to exact
|
|
3686
|
+
// zero). The filter carries state, so it is ticked exactly once per step;
|
|
3687
|
+
// activity() reads the raw fed rate and is left unfiltered (the read-out
|
|
3688
|
+
// shows device input, normalized by fullScale). Skipped when filter is null.
|
|
3689
|
+
const filter = this.filter;
|
|
3690
|
+
if (filter) {
|
|
3691
|
+
_f6raw[0] = lin[0]; _f6raw[1] = lin[1]; _f6raw[2] = lin[2];
|
|
3692
|
+
_f6raw[3] = ang[0]; _f6raw[4] = ang[1]; _f6raw[5] = ang[2];
|
|
3693
|
+
filter(_f6out, _f6raw, dt);
|
|
3694
|
+
_fLin[0] = _f6out[0]; _fLin[1] = _f6out[1]; _fLin[2] = _f6out[2];
|
|
3695
|
+
_fAng[0] = _f6out[3]; _fAng[1] = _f6out[4]; _fAng[2] = _f6out[5];
|
|
3696
|
+
lin = _fLin; ang = _fAng;
|
|
3697
|
+
}
|
|
3193
3698
|
|
|
3194
3699
|
// Basis axes (right / up / forward). No basis ⇒ the identity eye matrix:
|
|
3195
3700
|
// right +X, up +Y, forward −Z. Forward is −col2 (an eye→world matrix stores
|
|
@@ -3288,10 +3793,76 @@ class PoseHelm {
|
|
|
3288
3793
|
}
|
|
3289
3794
|
this._lin[0] = this._lin[1] = this._lin[2] = 0;
|
|
3290
3795
|
this._ang[0] = this._ang[1] = this._ang[2] = 0;
|
|
3796
|
+
if (this.filter) this.filter.reset(); // drop filter state at the discontinuity
|
|
3291
3797
|
return this;
|
|
3292
3798
|
}
|
|
3293
3799
|
}
|
|
3294
3800
|
|
|
3801
|
+
// =========================================================================
|
|
3802
|
+
// poseDelta — absolute pose → 6-DOF rate
|
|
3803
|
+
// =========================================================================
|
|
3804
|
+
|
|
3805
|
+
/**
|
|
3806
|
+
* Difference two consecutive absolute poses into a 6-DOF rate the helm can be
|
|
3807
|
+
* fed — the bridge for absolute-pose transports (gesture / landmark / marker /
|
|
3808
|
+
* IMU) into feed(lin, ang). Out-first, zero-allocation.
|
|
3809
|
+
*
|
|
3810
|
+
* Linear rate is (cur.pos − prev.pos) / dt. Angular rate is the world-frame
|
|
3811
|
+
* angular velocity ω carrying prev.rot onto cur.rot over dt: the relative
|
|
3812
|
+
* rotation r = cur · conj(prev) (so cur = r · prev, the world-frame compose the
|
|
3813
|
+
* step integrates), read as axis · angle / dt.
|
|
3814
|
+
*
|
|
3815
|
+
* The double-cover guard is the whole reason to ship this rather than inline it:
|
|
3816
|
+
* a quaternion and its negation are the same orientation, so when
|
|
3817
|
+
* dot(prev.rot, cur.rot) < 0 the two samples sit on opposite hemispheres and a
|
|
3818
|
+
* naive difference takes the long way round — the angular rate spikes at the
|
|
3819
|
+
* crossing. Flipping cur into prev's hemisphere first keeps r the shortest arc.
|
|
3820
|
+
*
|
|
3821
|
+
* Fed through a helm in WORLD whose profile has unit `sens`, in-order lanes and
|
|
3822
|
+
* `Tz` / `Rr` signs of −1 — the helm's lanes are eye-frame and the null basis
|
|
3823
|
+
* is the identity eye matrix (forward −Z), so the two Z channels flip against
|
|
3824
|
+
* world axes — the integrated pose retraces the source (the round-trip e9
|
|
3825
|
+
* asserts). A 1:1, non-integrated consumer skips the helm and applyPoses the
|
|
3826
|
+
* absolute pose directly.
|
|
3827
|
+
*
|
|
3828
|
+
* @param {{ lin:number[], ang:number[] }} [out] Destination; omit for a fresh one.
|
|
3829
|
+
* @param {{ pos:ArrayLike<number>, rot:ArrayLike<number> }} prev Previous pose.
|
|
3830
|
+
* @param {{ pos:ArrayLike<number>, rot:ArrayLike<number> }} cur Current pose.
|
|
3831
|
+
* @param {number} dt Elapsed seconds between the two samples.
|
|
3832
|
+
* @returns {{ lin:number[], ang:number[] }} out
|
|
3833
|
+
*/
|
|
3834
|
+
function poseDelta(out, prev, cur, dt) {
|
|
3835
|
+
out = out || { lin: [0, 0, 0], ang: [0, 0, 0] };
|
|
3836
|
+
const inv = 1 / dt;
|
|
3837
|
+
|
|
3838
|
+
out.lin[0] = (cur.pos[0] - prev.pos[0]) * inv;
|
|
3839
|
+
out.lin[1] = (cur.pos[1] - prev.pos[1]) * inv;
|
|
3840
|
+
out.lin[2] = (cur.pos[2] - prev.pos[2]) * inv;
|
|
3841
|
+
|
|
3842
|
+
const px = prev.rot[0], py = prev.rot[1], pz = prev.rot[2], pw = prev.rot[3];
|
|
3843
|
+
let cx = cur.rot[0], cy = cur.rot[1], cz = cur.rot[2], cw = cur.rot[3];
|
|
3844
|
+
|
|
3845
|
+
// Double-cover guard: bring cur into prev's hemisphere so r is the short arc.
|
|
3846
|
+
if (px * cx + py * cy + pz * cz + pw * cw < 0) { cx = -cx; cy = -cy; cz = -cz; cw = -cw; }
|
|
3847
|
+
|
|
3848
|
+
// Relative rotation r = cur · conj(prev), conj(prev) = (−px, −py, −pz, pw).
|
|
3849
|
+
const rx = -cw * px + cx * pw - cy * pz + cz * py;
|
|
3850
|
+
const ry = -cw * py + cx * pz + cy * pw - cz * px;
|
|
3851
|
+
const rz = -cw * pz - cx * py + cy * px + cz * pw;
|
|
3852
|
+
const rw = cw * pw + cx * px + cy * py + cz * pz; // = dot(prev, cur) ≥ 0 → angle ≤ π
|
|
3853
|
+
|
|
3854
|
+
// Axis · angle of r → ω = axis · angle / dt. Below the threshold the rotation
|
|
3855
|
+
// is negligible (no well-defined axis) and the angular rate is exact zero.
|
|
3856
|
+
const sinHalf = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
3857
|
+
if (sinHalf < 1e-8) {
|
|
3858
|
+
out.ang[0] = 0; out.ang[1] = 0; out.ang[2] = 0;
|
|
3859
|
+
} else {
|
|
3860
|
+
const k = (2 * Math.atan2(sinHalf, rw)) * inv / sinHalf;
|
|
3861
|
+
out.ang[0] = rx * k; out.ang[1] = ry * k; out.ang[2] = rz * k;
|
|
3862
|
+
}
|
|
3863
|
+
return out;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3295
3866
|
/**
|
|
3296
3867
|
* @file Frustum planes and visibility tests — zero allocations.
|
|
3297
3868
|
* @module tree/visibility
|
|
@@ -3451,5 +4022,1081 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
3451
4022
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
3452
4023
|
}
|
|
3453
4024
|
|
|
3454
|
-
|
|
4025
|
+
/**
|
|
4026
|
+
* @file Camera state ↔ matrices: builders, decomposers, and in-place edits.
|
|
4027
|
+
* @module tree/camera
|
|
4028
|
+
* @license AGPL-3.0-only
|
|
4029
|
+
*
|
|
4030
|
+
* The camera is plain data — the CameraTrack keyframe shape:
|
|
4031
|
+
*
|
|
4032
|
+
* cam = {
|
|
4033
|
+
* eye: [x, y, z], // position
|
|
4034
|
+
* center: [x, y, z], // lookat target; |center − eye| is the gaze distance
|
|
4035
|
+
* up: [x, y, z], // up hint, need not be unit
|
|
4036
|
+
* fov: number | null, // vertical field of view, radians — perspective
|
|
4037
|
+
* halfHeight: number | null, // world-unit half-height at the near plane — orthographic
|
|
4038
|
+
* near: number, // > 0
|
|
4039
|
+
* far: number, // > near
|
|
4040
|
+
* }
|
|
4041
|
+
*
|
|
4042
|
+
* Exactly one of fov / halfHeight is meaningful; fov wins when both are set.
|
|
4043
|
+
* With both null the lens is "unchanged": cameraProj and cameraPlanes return
|
|
4044
|
+
* null and leave their output untouched, so a track that evaluates null into
|
|
4045
|
+
* the state keeps the last projection installed. A track writes the state
|
|
4046
|
+
* directly — `track.eval(cam)` — and a pose drives it through cameraFromPose.
|
|
4047
|
+
*
|
|
4048
|
+
* The state carries no aspect ratio — the viewport owns it — so one state is
|
|
4049
|
+
* portable between targets of different sizes; every builder that needs it
|
|
4050
|
+
* takes it as an argument.
|
|
4051
|
+
*
|
|
4052
|
+
* Vectors are plain number[] (f64, authoring state); matrices are written into
|
|
4053
|
+
* whatever 16-element buffer the caller passes. createCamera is the one
|
|
4054
|
+
* allocating call; everything else is out-first (or in-place) and zero-alloc.
|
|
4055
|
+
*
|
|
4056
|
+
* Every function that needs the camera frame derives it through mat4Eye, so
|
|
4057
|
+
* planes and edits agree with cameraEye / cameraView exactly — including the
|
|
4058
|
+
* up re-seed when the view direction is parallel to the up hint.
|
|
4059
|
+
*/
|
|
4060
|
+
|
|
4061
|
+
|
|
4062
|
+
const _E$1 = new Float64Array(16); // eye→world frame scratch: right 0–2, up 4–6, back 8–10
|
|
4063
|
+
const _d = [0, 0, 0]; // view-direction scratch
|
|
4064
|
+
|
|
4065
|
+
/** Eye→world frame of `cam` into `_E`. */
|
|
4066
|
+
function _frame(cam) {
|
|
4067
|
+
const e = cam.eye, c = cam.center, u = cam.up;
|
|
4068
|
+
mat4Eye(_E$1, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
/** Gaze distance |center − eye|, or 1 when the state is degenerate. */
|
|
4072
|
+
function _gaze(cam) {
|
|
4073
|
+
const dx = cam.center[0]-cam.eye[0], dy = cam.center[1]-cam.eye[1], dz = cam.center[2]-cam.eye[2];
|
|
4074
|
+
const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
|
|
4075
|
+
return d > 0 ? d : 1;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
function _vec3(v, x, y, z) { return v != null ? [v[0], v[1], v[2]] : [x, y, z]; }
|
|
4079
|
+
|
|
4080
|
+
// =========================================================================
|
|
4081
|
+
// State
|
|
4082
|
+
// =========================================================================
|
|
4083
|
+
|
|
4084
|
+
/**
|
|
4085
|
+
* Allocate a camera state — the one allocating call, setup-time.
|
|
4086
|
+
* Defaults: eye [0, 0, 500], center [0, 0, 0], up [0, 1, 0], fov π/3,
|
|
4087
|
+
* halfHeight null, near 0.1, far 1000. Passing halfHeight without fov yields
|
|
4088
|
+
* an orthographic state (fov null).
|
|
4089
|
+
*
|
|
4090
|
+
* @param {{ eye?:number[], center?:number[], up?:number[], fov?:number|null,
|
|
4091
|
+
* halfHeight?:number|null, near?:number, far?:number }} [opts]
|
|
4092
|
+
* @returns {{ eye:number[], center:number[], up:number[], fov:number|null,
|
|
4093
|
+
* halfHeight:number|null, near:number, far:number }}
|
|
4094
|
+
*/
|
|
4095
|
+
function createCamera(opts) {
|
|
4096
|
+
const o = opts || {};
|
|
4097
|
+
const ortho = o.halfHeight != null && o.fov === undefined;
|
|
4098
|
+
return {
|
|
4099
|
+
eye: _vec3(o.eye, 0, 0, 500),
|
|
4100
|
+
center: _vec3(o.center, 0, 0, 0),
|
|
4101
|
+
up: _vec3(o.up, 0, 1, 0),
|
|
4102
|
+
fov: o.fov !== undefined ? o.fov : (ortho ? null : Math.PI / 3),
|
|
4103
|
+
halfHeight: o.halfHeight !== undefined ? o.halfHeight : null,
|
|
4104
|
+
near: typeof o.near === 'number' ? o.near : 0.1,
|
|
4105
|
+
far: typeof o.far === 'number' ? o.far : 1000,
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
/**
|
|
4110
|
+
* Copy one camera state into another (an orbit's home, a track's capture).
|
|
4111
|
+
* @param {object} out Destination state.
|
|
4112
|
+
* @param {object} cam Source state.
|
|
4113
|
+
* @returns {object} out
|
|
4114
|
+
*/
|
|
4115
|
+
function cameraCopy(out, cam) {
|
|
4116
|
+
out.eye[0]=cam.eye[0]; out.eye[1]=cam.eye[1]; out.eye[2]=cam.eye[2];
|
|
4117
|
+
out.center[0]=cam.center[0]; out.center[1]=cam.center[1]; out.center[2]=cam.center[2];
|
|
4118
|
+
out.up[0]=cam.up[0]; out.up[1]=cam.up[1]; out.up[2]=cam.up[2];
|
|
4119
|
+
out.fov = cam.fov; out.halfHeight = cam.halfHeight;
|
|
4120
|
+
out.near = cam.near; out.far = cam.far;
|
|
4121
|
+
return out;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
// =========================================================================
|
|
4125
|
+
// Builders — state → matrices
|
|
4126
|
+
// =========================================================================
|
|
4127
|
+
|
|
4128
|
+
/**
|
|
4129
|
+
* View matrix (world→eye) from the state's lookat.
|
|
4130
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
4131
|
+
* @param {object} cam
|
|
4132
|
+
* @returns {Float32Array|number[]} out
|
|
4133
|
+
*/
|
|
4134
|
+
function cameraView(out, cam) {
|
|
4135
|
+
const e = cam.eye, c = cam.center, u = cam.up;
|
|
4136
|
+
return mat4View(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
/**
|
|
4140
|
+
* Eye matrix (eye→world) from the state's lookat.
|
|
4141
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
4142
|
+
* @param {object} cam
|
|
4143
|
+
* @returns {Float32Array|number[]} out
|
|
4144
|
+
*/
|
|
4145
|
+
function cameraEye(out, cam) {
|
|
4146
|
+
const e = cam.eye, c = cam.center, u = cam.up;
|
|
4147
|
+
return mat4Eye(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
/**
|
|
4151
|
+
* Projection matrix from the state's lens: mat4Persp from fov, or mat4Ortho
|
|
4152
|
+
* from halfHeight, with symmetric extents (top = near · tan(fov / 2) or
|
|
4153
|
+
* halfHeight; right = top · aspect).
|
|
4154
|
+
*
|
|
4155
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
4156
|
+
* @param {object} cam
|
|
4157
|
+
* @param {number} aspect Viewport width / height.
|
|
4158
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
|
|
4159
|
+
* @param {number} [ndcYSign=1] +1 NDC y-up; −1 NDC y-down.
|
|
4160
|
+
* @returns {Float32Array|number[]|null} out, or null (out untouched) when
|
|
4161
|
+
* both fov and halfHeight are null.
|
|
4162
|
+
*/
|
|
4163
|
+
function cameraProj(out, cam, aspect, ndcZMin, ndcYSign = 1) {
|
|
4164
|
+
const near = cam.near, far = cam.far;
|
|
4165
|
+
if (cam.fov != null) {
|
|
4166
|
+
const top = near * Math.tan(cam.fov / 2), right = top * aspect;
|
|
4167
|
+
return mat4Persp(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
|
|
4168
|
+
}
|
|
4169
|
+
if (cam.halfHeight != null) {
|
|
4170
|
+
const top = cam.halfHeight, right = top * aspect;
|
|
4171
|
+
return mat4Ortho(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
|
|
4172
|
+
}
|
|
4173
|
+
return null;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
/**
|
|
4177
|
+
* The six frustum planes of the state, world space — frustumPlanes over the
|
|
4178
|
+
* lookat basis and the symmetric extents cameraProj uses, so visibility tests
|
|
4179
|
+
* run against a camera state without any matrix.
|
|
4180
|
+
*
|
|
4181
|
+
* @param {Float64Array} planes 24-element destination.
|
|
4182
|
+
* @param {object} cam
|
|
4183
|
+
* @param {number} aspect Viewport width / height.
|
|
4184
|
+
* @returns {Float64Array|null} planes, or null (planes untouched) when both
|
|
4185
|
+
* fov and halfHeight are null.
|
|
4186
|
+
*/
|
|
4187
|
+
function cameraPlanes(planes, cam, aspect) {
|
|
4188
|
+
const ortho = cam.fov == null;
|
|
4189
|
+
if (ortho && cam.halfHeight == null) return null;
|
|
4190
|
+
const top = ortho ? cam.halfHeight : cam.near * Math.tan(cam.fov / 2), right = top * aspect;
|
|
4191
|
+
_frame(cam);
|
|
4192
|
+
const e = cam.eye;
|
|
4193
|
+
return frustumPlanes(planes,
|
|
4194
|
+
e[0], e[1], e[2],
|
|
4195
|
+
-_E$1[8], -_E$1[9], -_E$1[10],
|
|
4196
|
+
_E$1[4], _E$1[5], _E$1[6],
|
|
4197
|
+
_E$1[0], _E$1[1], _E$1[2],
|
|
4198
|
+
ortho, cam.near, cam.far, -right, right, top, -top);
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
// =========================================================================
|
|
4202
|
+
// Decomposers — matrices and poses → state
|
|
4203
|
+
// =========================================================================
|
|
4204
|
+
|
|
4205
|
+
/**
|
|
4206
|
+
* Read a state back from an eye matrix and a projection: eye ← column 3,
|
|
4207
|
+
* up ← column 1, forward ← −column 2 of E; center ← eye + forward · d with d
|
|
4208
|
+
* the state's current gaze distance (1 when degenerate), so the distance
|
|
4209
|
+
* survives a round trip. The lens comes from the projection queries: fov or
|
|
4210
|
+
* halfHeight by projIsOrtho, near and far under `ndcZMin`.
|
|
4211
|
+
*
|
|
4212
|
+
* @param {object} cam State written in place.
|
|
4213
|
+
* @param {ArrayLike<number>} E Eye matrix (eye→world), 16 elements.
|
|
4214
|
+
* @param {ArrayLike<number>} P Projection matrix, 16 elements.
|
|
4215
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
|
|
4216
|
+
* @returns {object} cam
|
|
4217
|
+
*/
|
|
4218
|
+
function cameraFromMat4(cam, E, P, ndcZMin) {
|
|
4219
|
+
const d = _gaze(cam);
|
|
4220
|
+
let fx = -E[8], fy = -E[9], fz = -E[10];
|
|
4221
|
+
const fl = Math.sqrt(fx*fx+fy*fy+fz*fz) || 1;
|
|
4222
|
+
fx /= fl; fy /= fl; fz /= fl;
|
|
4223
|
+
cam.eye[0]=E[12]; cam.eye[1]=E[13]; cam.eye[2]=E[14];
|
|
4224
|
+
cam.up[0]=E[4]; cam.up[1]=E[5]; cam.up[2]=E[6];
|
|
4225
|
+
cam.center[0]=cam.eye[0]+fx*d; cam.center[1]=cam.eye[1]+fy*d; cam.center[2]=cam.eye[2]+fz*d;
|
|
4226
|
+
if (projIsOrtho(P)) { cam.fov = null; cam.halfHeight = projTop(P, ndcZMin); }
|
|
4227
|
+
else { cam.fov = projFov(P); cam.halfHeight = null; }
|
|
4228
|
+
cam.near = projNear(P, ndcZMin);
|
|
4229
|
+
cam.far = projFar(P);
|
|
4230
|
+
return cam;
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
/**
|
|
4234
|
+
* Drive the lookat from a TRS pose: eye ← pos, up and forward from the
|
|
4235
|
+
* rotation's columns 1 and −2, center ← eye + forward · d with d the current
|
|
4236
|
+
* gaze distance (1 when degenerate). The lens is untouched; `scl` is ignored.
|
|
4237
|
+
*
|
|
4238
|
+
* @param {object} cam State written in place.
|
|
4239
|
+
* @param {{ pos:number[], rot:number[] }} pose rot is a unit quaternion [x,y,z,w].
|
|
4240
|
+
* @returns {object} cam
|
|
4241
|
+
*/
|
|
4242
|
+
function cameraFromPose(cam, pose) {
|
|
4243
|
+
const d = _gaze(cam);
|
|
4244
|
+
const q = pose.rot, x=q[0], y=q[1], z=q[2], w=q[3];
|
|
4245
|
+
const x2=x+x, y2=y+y, z2=z+z;
|
|
4246
|
+
const xx=x*x2, xy=x*y2, xz=x*z2, yy=y*y2, yz=y*z2, zz=z*z2, wx=w*x2, wy=w*y2, wz=w*z2;
|
|
4247
|
+
const ux=xy-wz, uy=1-(xx+zz), uz=yz+wx; // column 1 of qToMat4(rot): up
|
|
4248
|
+
const bx=xz+wy, by=yz-wx, bz=1-(xx+yy); // column 2: back
|
|
4249
|
+
cam.eye[0]=pose.pos[0]; cam.eye[1]=pose.pos[1]; cam.eye[2]=pose.pos[2];
|
|
4250
|
+
cam.up[0]=ux; cam.up[1]=uy; cam.up[2]=uz;
|
|
4251
|
+
cam.center[0]=cam.eye[0]-bx*d; cam.center[1]=cam.eye[1]-by*d; cam.center[2]=cam.eye[2]-bz*d;
|
|
4252
|
+
return cam;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
/**
|
|
4256
|
+
* The lookat as a TRS pose: pos ← eye, rot ← qFromLookDir(center − eye, up) —
|
|
4257
|
+
* the rotation of cameraEye, so a helm seeded from it continues the frame.
|
|
4258
|
+
*
|
|
4259
|
+
* @param {{ pos:number[], rot:number[] }} pose Written in place.
|
|
4260
|
+
* @param {object} cam
|
|
4261
|
+
* @returns {{ pos:number[], rot:number[] }} pose
|
|
4262
|
+
*/
|
|
4263
|
+
function cameraToPose(pose, cam) {
|
|
4264
|
+
pose.pos[0]=cam.eye[0]; pose.pos[1]=cam.eye[1]; pose.pos[2]=cam.eye[2];
|
|
4265
|
+
_d[0]=cam.center[0]-cam.eye[0]; _d[1]=cam.center[1]-cam.eye[1]; _d[2]=cam.center[2]-cam.eye[2];
|
|
4266
|
+
qFromLookDir(pose.rot, _d, cam.up);
|
|
4267
|
+
return pose;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
// =========================================================================
|
|
4271
|
+
// Edits — in place, chainable, zero-alloc
|
|
4272
|
+
// =========================================================================
|
|
4273
|
+
|
|
4274
|
+
/**
|
|
4275
|
+
* Orbit the eye about the center: `dAz` rotates it about the up hint
|
|
4276
|
+
* (right-handed); `dEl` raises its elevation above the plane through the
|
|
4277
|
+
* center perpendicular to the hint, clamped to ±opts.maxEl so the view
|
|
4278
|
+
* direction never reaches the hint (the pole guard). `up` is left as the hint
|
|
4279
|
+
* it was, so an orbit never rolls. A state already at the pole is pulled
|
|
4280
|
+
* inside the guard by its first non-zero edit; (0, 0) is a no-op.
|
|
4281
|
+
*
|
|
4282
|
+
* @param {object} cam
|
|
4283
|
+
* @param {number} dAz Azimuth delta, radians.
|
|
4284
|
+
* @param {number} dEl Elevation delta, radians.
|
|
4285
|
+
* @param {{ maxEl?:number }} [opts] Elevation limit; default π/2 − 1e-3.
|
|
4286
|
+
* @returns {object} cam
|
|
4287
|
+
*/
|
|
4288
|
+
function cameraOrbit(cam, dAz, dEl, opts) {
|
|
4289
|
+
if (dAz === 0 && dEl === 0) return cam;
|
|
4290
|
+
const maxEl = opts && typeof opts.maxEl === 'number' ? opts.maxEl : Math.PI / 2 - 1e-3;
|
|
4291
|
+
const e = cam.eye, c = cam.center;
|
|
4292
|
+
let ux=cam.up[0], uy=cam.up[1], uz=cam.up[2];
|
|
4293
|
+
const ul = Math.sqrt(ux*ux+uy*uy+uz*uz) || 1;
|
|
4294
|
+
ux/=ul; uy/=ul; uz/=ul;
|
|
4295
|
+
const dx=e[0]-c[0], dy=e[1]-c[1], dz=e[2]-c[2];
|
|
4296
|
+
const r = Math.sqrt(dx*dx+dy*dy+dz*dz);
|
|
4297
|
+
_frame(cam);
|
|
4298
|
+
const px=_E$1[4], py=_E$1[5], pz=_E$1[6]; // the eye's up
|
|
4299
|
+
const bx=_E$1[8], by=_E$1[9], bz=_E$1[10]; // back: center → eye, unit
|
|
4300
|
+
// elevation: raise by θ within the (back, up) plane, clamped
|
|
4301
|
+
const el = Math.asin(Math.max(-1, Math.min(1, bx*ux+by*uy+bz*uz)));
|
|
4302
|
+
const th = Math.max(-maxEl, Math.min(maxEl, el + dEl)) - el;
|
|
4303
|
+
const ct = Math.cos(th), st = Math.sin(th);
|
|
4304
|
+
const ox = r*(bx*ct+px*st), oy = r*(by*ct+py*st), oz = r*(bz*ct+pz*st);
|
|
4305
|
+
// azimuth: rotate about the hint (Rodrigues)
|
|
4306
|
+
const ca = Math.cos(dAz), sa = Math.sin(dAz), k = (ux*ox+uy*oy+uz*oz)*(1-ca);
|
|
4307
|
+
e[0] = c[0] + ox*ca + (uy*oz-uz*oy)*sa + ux*k;
|
|
4308
|
+
e[1] = c[1] + oy*ca + (uz*ox-ux*oz)*sa + uy*k;
|
|
4309
|
+
e[2] = c[2] + oz*ca + (ux*oy-uy*ox)*sa + uz*k;
|
|
4310
|
+
return cam;
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
/**
|
|
4314
|
+
* Dolly: scale the gaze distance by `factor` — the eye moves along the view
|
|
4315
|
+
* direction under perspective; under orthographic (fov null, halfHeight set)
|
|
4316
|
+
* halfHeight is scaled instead, since moving the eye changes nothing on
|
|
4317
|
+
* screen. opts.min / opts.max clamp the scaled quantity. An edit that would
|
|
4318
|
+
* make it non-positive is a no-op.
|
|
4319
|
+
*
|
|
4320
|
+
* @param {object} cam
|
|
4321
|
+
* @param {number} factor
|
|
4322
|
+
* @param {{ min?:number, max?:number }} [opts]
|
|
4323
|
+
* @returns {object} cam
|
|
4324
|
+
*/
|
|
4325
|
+
function cameraDolly(cam, factor, opts) {
|
|
4326
|
+
const min = opts && typeof opts.min === 'number' ? opts.min : 0;
|
|
4327
|
+
const max = opts && typeof opts.max === 'number' ? opts.max : Infinity;
|
|
4328
|
+
if (cam.fov == null && cam.halfHeight != null) {
|
|
4329
|
+
const h = Math.max(min, Math.min(max, cam.halfHeight * factor));
|
|
4330
|
+
if (h > 0) cam.halfHeight = h;
|
|
4331
|
+
return cam;
|
|
4332
|
+
}
|
|
4333
|
+
const e = cam.eye, c = cam.center;
|
|
4334
|
+
const dx=e[0]-c[0], dy=e[1]-c[1], dz=e[2]-c[2];
|
|
4335
|
+
const r = Math.sqrt(dx*dx+dy*dy+dz*dz);
|
|
4336
|
+
if (r === 0) return cam;
|
|
4337
|
+
const r1 = Math.max(min, Math.min(max, r * factor));
|
|
4338
|
+
if (!(r1 > 0)) return cam;
|
|
4339
|
+
const s = r1 / r;
|
|
4340
|
+
e[0]=c[0]+dx*s; e[1]=c[1]+dy*s; e[2]=c[2]+dz*s;
|
|
4341
|
+
return cam;
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
/**
|
|
4345
|
+
* Pan: translate eye and center by `dx` along the eye's right and `dy` along
|
|
4346
|
+
* its up, world units. The caller converts pixels through pixelRatio at the
|
|
4347
|
+
* center's depth so a pan tracks the pointer.
|
|
4348
|
+
*
|
|
4349
|
+
* @param {object} cam
|
|
4350
|
+
* @param {number} dx
|
|
4351
|
+
* @param {number} dy
|
|
4352
|
+
* @returns {object} cam
|
|
4353
|
+
*/
|
|
4354
|
+
function cameraPan(cam, dx, dy) {
|
|
4355
|
+
_frame(cam);
|
|
4356
|
+
const tx=_E$1[0]*dx+_E$1[4]*dy, ty=_E$1[1]*dx+_E$1[5]*dy, tz=_E$1[2]*dx+_E$1[6]*dy;
|
|
4357
|
+
const e = cam.eye, c = cam.center;
|
|
4358
|
+
e[0]+=tx; e[1]+=ty; e[2]+=tz;
|
|
4359
|
+
c[0]+=tx; c[1]+=ty; c[2]+=tz;
|
|
4360
|
+
return cam;
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
/**
|
|
4364
|
+
* @file Gizmo line generators in the arrays shape.
|
|
4365
|
+
* @module tree/gizmo
|
|
4366
|
+
* @license AGPL-3.0-only
|
|
4367
|
+
*
|
|
4368
|
+
* A gizmo is geometry that explains: axes, a grid, a frustum, a path, a rig,
|
|
4369
|
+
* a handle's locus. This module generates its vertices — renderer-free, into
|
|
4370
|
+
* caller-owned arrays shaped the way twgl's createBufferInfoFromArrays
|
|
4371
|
+
* consumes them and a WebGPU vertex buffer is filled from — and nothing
|
|
4372
|
+
* else. Drawing, colour state, HUD mode, textures, the dot at a handle's
|
|
4373
|
+
* point, text: all the bridge's or the host's.
|
|
4374
|
+
*
|
|
4375
|
+
* ── The arrays shape ───────────────────────────────────────────────────────
|
|
4376
|
+
*
|
|
4377
|
+
* out = {
|
|
4378
|
+
* position: { numComponents: 3, data: Float32Array(3 · capacity) },
|
|
4379
|
+
* color: { numComponents: 4, data: Float32Array(4 · capacity) }, // optional
|
|
4380
|
+
* texcoord: { numComponents: 2, data: Float32Array(2 · capacity) }, // optional — panes
|
|
4381
|
+
* count: 0, // vertices written
|
|
4382
|
+
* labels: [], // optional — { x, y, z, text }
|
|
4383
|
+
* }
|
|
4384
|
+
*
|
|
4385
|
+
* Line generators write line lists — vertex pairs, no indices; paneTris
|
|
4386
|
+
* writes two triangles. capacity is position.data.length / 3.
|
|
4387
|
+
*
|
|
4388
|
+
* ── The contract — snprintf-style ──────────────────────────────────────────
|
|
4389
|
+
* Every generator gen(out, …) → n returns the vertex count it needs, writes
|
|
4390
|
+
* min(n, capacity) vertices, and sets out.count to what it wrote. A caller
|
|
4391
|
+
* sizes once and grows never in steady state:
|
|
4392
|
+
*
|
|
4393
|
+
* let n = axesLines(out, opts)
|
|
4394
|
+
* if (n > capacityOf(out)) { growArrays(out, n); axesLines(out, opts) }
|
|
4395
|
+
*
|
|
4396
|
+
* Each generator states its count formula so a caller can pre-size exactly.
|
|
4397
|
+
*
|
|
4398
|
+
* ── Colour ─────────────────────────────────────────────────────────────────
|
|
4399
|
+
* If out.color exists, every vertex written gets a colour: a generator with
|
|
4400
|
+
* semantic colouring (axes, the helm rig) writes its palette, every other
|
|
4401
|
+
* generator writes opts.color (default white). If out.color is absent
|
|
4402
|
+
* nothing is written and the bridge draws with a uniform colour.
|
|
4403
|
+
*
|
|
4404
|
+
* ── Frames ─────────────────────────────────────────────────────────────────
|
|
4405
|
+
* A generator writes in the frame the caller means — model space for scene
|
|
4406
|
+
* gizmos (the bridge's M places them), screen pixels for HUD gizmos.
|
|
4407
|
+
* Nothing here consults a camera except locusLines and frustumLines, which
|
|
4408
|
+
* take what they need explicitly. Signatures: out first, the subject second
|
|
4409
|
+
* where there is one, options last.
|
|
4410
|
+
*/
|
|
4411
|
+
|
|
4412
|
+
|
|
4413
|
+
const TWO_PI = Math.PI * 2;
|
|
4414
|
+
const _AXIS_COLORS = [COLOR_X, COLOR_Y, COLOR_Z];
|
|
4415
|
+
const _U = [1, 0, 0], _V = [0, 1, 0]; // the HUD plane's basis
|
|
4416
|
+
const _E = new Float64Array(16); // a camera state's eye matrix
|
|
4417
|
+
const _p3 = [0, 0, 0]; // a transformed corner / a sampled point
|
|
4418
|
+
const _q3 = [0, 0, 0]; // the previous sampled point
|
|
4419
|
+
const _c24 = new Float64Array(24); // frustum corners scratch
|
|
4420
|
+
const _tIn = [0, 0, 0], _tOut = [0, 0, 0]; // a keyframe's tangents
|
|
4421
|
+
const _act = [0, 0, 0, 0, 0, 0]; // a helm's activity
|
|
4422
|
+
const _tip = [0, 0, 0], _ha = [0, 0, 0]; // an arrow's tip and head base
|
|
4423
|
+
const _AXES = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
|
4424
|
+
const _b0 = [0, 0, 0], _b1 = [0, 0, 0], _b2 = [0, 0, 0]; // a locus basis
|
|
4425
|
+
const _UV_DEFAULT = [0, 1, 1, 1, 1, 0, 0, 0]; // paneTris: p0 top-left → (0, 1), clockwise
|
|
4426
|
+
|
|
4427
|
+
// =========================================================================
|
|
4428
|
+
// G1 Arrays — the one allocating call, growth, capacity
|
|
4429
|
+
// =========================================================================
|
|
4430
|
+
|
|
4431
|
+
/**
|
|
4432
|
+
* Allocate an arrays object of `capacity` vertices — the one allocating
|
|
4433
|
+
* call, setup-time. Flags add the optional attributes and the label list.
|
|
4434
|
+
*
|
|
4435
|
+
* @param {number} capacity Vertices.
|
|
4436
|
+
* @param {{ color?:boolean, texcoord?:boolean, labels?:boolean }} [opts]
|
|
4437
|
+
* @returns {{ position:{numComponents:number,data:Float32Array},
|
|
4438
|
+
* color?:{numComponents:number,data:Float32Array},
|
|
4439
|
+
* texcoord?:{numComponents:number,data:Float32Array},
|
|
4440
|
+
* count:number, labels?:object[] }}
|
|
4441
|
+
*/
|
|
4442
|
+
function createArrays(capacity, opts) {
|
|
4443
|
+
const o = opts || {};
|
|
4444
|
+
const n = Math.max(0, capacity | 0);
|
|
4445
|
+
const out = { position: { numComponents: 3, data: new Float32Array(3 * n) } };
|
|
4446
|
+
if (o.color) out.color = { numComponents: 4, data: new Float32Array(4 * n) };
|
|
4447
|
+
if (o.texcoord) out.texcoord = { numComponents: 2, data: new Float32Array(2 * n) };
|
|
4448
|
+
out.count = 0;
|
|
4449
|
+
if (o.labels) out.labels = [];
|
|
4450
|
+
return out;
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
/**
|
|
4454
|
+
* Reallocate an arrays object to a new capacity, keeping its attribute set;
|
|
4455
|
+
* the data is fresh (a generator refills it) and count is 0.
|
|
4456
|
+
*
|
|
4457
|
+
* @param {object} out An arrays object from createArrays.
|
|
4458
|
+
* @param {number} capacity Vertices.
|
|
4459
|
+
* @returns {object} out
|
|
4460
|
+
*/
|
|
4461
|
+
function growArrays(out, capacity) {
|
|
4462
|
+
const n = Math.max(0, capacity | 0);
|
|
4463
|
+
out.position.data = new Float32Array(3 * n);
|
|
4464
|
+
if (out.color) out.color.data = new Float32Array(4 * n);
|
|
4465
|
+
if (out.texcoord) out.texcoord.data = new Float32Array(2 * n);
|
|
4466
|
+
out.count = 0;
|
|
4467
|
+
if (out.labels) out.labels.length = 0;
|
|
4468
|
+
return out;
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
/**
|
|
4472
|
+
* The vertex capacity of an arrays object: position.data.length / 3.
|
|
4473
|
+
* @param {object} out
|
|
4474
|
+
* @returns {number}
|
|
4475
|
+
*/
|
|
4476
|
+
function capacityOf(out) {
|
|
4477
|
+
return (out.position.data.length / 3) | 0;
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
// =========================================================================
|
|
4481
|
+
// G2 The writer — a cursor over out; counts every vertex, writes those
|
|
4482
|
+
// within capacity. Module-level scratch: a generator runs to completion.
|
|
4483
|
+
// =========================================================================
|
|
4484
|
+
|
|
4485
|
+
const _w = { pos: null, col: null, tex: null, cap: 0, n: 0, r: 1, g: 1, b: 1, a: 1 };
|
|
4486
|
+
const _WHITE = [1, 1, 1, 1];
|
|
4487
|
+
|
|
4488
|
+
function _begin(out) {
|
|
4489
|
+
_w.pos = out.position.data;
|
|
4490
|
+
_w.col = out.color ? out.color.data : null;
|
|
4491
|
+
_w.tex = out.texcoord ? out.texcoord.data : null;
|
|
4492
|
+
_w.cap = (_w.pos.length / 3) | 0;
|
|
4493
|
+
_w.n = 0;
|
|
4494
|
+
if (out.labels) out.labels.length = 0;
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
/** Set the current colour from an RGB(A) array, with an alpha override. */
|
|
4498
|
+
function _color(c, alpha) {
|
|
4499
|
+
const v = c || _WHITE;
|
|
4500
|
+
_w.r = v[0]; _w.g = v[1]; _w.b = v[2];
|
|
4501
|
+
_w.a = alpha != null ? alpha : (v.length > 3 ? v[3] : 1);
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
function _vertex(x, y, z) {
|
|
4505
|
+
const n = _w.n++;
|
|
4506
|
+
if (n >= _w.cap) return;
|
|
4507
|
+
const p = _w.pos, i = 3 * n;
|
|
4508
|
+
p[i] = x; p[i + 1] = y; p[i + 2] = z;
|
|
4509
|
+
const c = _w.col;
|
|
4510
|
+
if (c) { const j = 4 * n; c[j] = _w.r; c[j + 1] = _w.g; c[j + 2] = _w.b; c[j + 3] = _w.a; }
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function _line(x0, y0, z0, x1, y1, z1) {
|
|
4514
|
+
_vertex(x0, y0, z0);
|
|
4515
|
+
_vertex(x1, y1, z1);
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
/** A vertex with a texture coordinate (written when out.texcoord exists). */
|
|
4519
|
+
function _vertexUV(x, y, z, u, v) {
|
|
4520
|
+
const n = _w.n;
|
|
4521
|
+
_vertex(x, y, z);
|
|
4522
|
+
if (_w.tex && n < _w.cap) { _w.tex[2*n] = u; _w.tex[2*n + 1] = v; }
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
/** Normalise v in place; a zero vector becomes (dx, dy, dz). */
|
|
4526
|
+
function _unit(v, dx, dy, dz) {
|
|
4527
|
+
const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
|
|
4528
|
+
if (l < 1e-9) { v[0] = dx; v[1] = dy; v[2] = dz; return v; }
|
|
4529
|
+
v[0] /= l; v[1] /= l; v[2] /= l;
|
|
4530
|
+
return v;
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
/** Orthonormal in-plane basis (ub, vb) for a unit normal n, seeded from the least-aligned axis. */
|
|
4534
|
+
function _basis(n, ub, vb) {
|
|
4535
|
+
const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
|
|
4536
|
+
let rx = 0, ry = 0, rz = 0;
|
|
4537
|
+
if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
|
|
4538
|
+
ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
|
|
4539
|
+
_unit(ub, 1, 0, 0);
|
|
4540
|
+
vb[0] = n[1]*ub[2] - n[2]*ub[1]; vb[1] = n[2]*ub[0] - n[0]*ub[2]; vb[2] = n[0]*ub[1] - n[1]*ub[0];
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
/** The four edges of a square of half-extent h at c, spanned by u, v. */
|
|
4544
|
+
function _square(c, u, v, h) {
|
|
4545
|
+
const x0 = c[0] - h*u[0] - h*v[0], y0 = c[1] - h*u[1] - h*v[1], z0 = c[2] - h*u[2] - h*v[2];
|
|
4546
|
+
const x1 = c[0] + h*u[0] - h*v[0], y1 = c[1] + h*u[1] - h*v[1], z1 = c[2] + h*u[2] - h*v[2];
|
|
4547
|
+
const x2 = c[0] + h*u[0] + h*v[0], y2 = c[1] + h*u[1] + h*v[1], z2 = c[2] + h*u[2] + h*v[2];
|
|
4548
|
+
const x3 = c[0] - h*u[0] + h*v[0], y3 = c[1] - h*u[1] + h*v[1], z3 = c[2] - h*u[2] + h*v[2];
|
|
4549
|
+
_line(x0, y0, z0, x1, y1, z1);
|
|
4550
|
+
_line(x1, y1, z1, x2, y2, z2);
|
|
4551
|
+
_line(x2, y2, z2, x3, y3, z3);
|
|
4552
|
+
_line(x3, y3, z3, x0, y0, z0);
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
/** Close the write: count ← what fits; return what was needed. */
|
|
4556
|
+
function _end(out) {
|
|
4557
|
+
out.count = _w.n < _w.cap ? _w.n : _w.cap;
|
|
4558
|
+
return _w.n;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
/** A sampled circle (or arc of `sweep`) of radius r at c, spanned by u, v: n segments. */
|
|
4562
|
+
function _ring(cx, cy, cz, r, u, v, n, sweep) {
|
|
4563
|
+
let px = 0, py = 0, pz = 0;
|
|
4564
|
+
for (let i = 0; i <= n; i++) {
|
|
4565
|
+
const t = (i / n) * sweep;
|
|
4566
|
+
const ct = Math.cos(t) * r, st = Math.sin(t) * r;
|
|
4567
|
+
const x = cx + ct*u[0] + st*v[0];
|
|
4568
|
+
const y = cy + ct*u[1] + st*v[1];
|
|
4569
|
+
const z = cz + ct*u[2] + st*v[2];
|
|
4570
|
+
if (i > 0) _line(px, py, pz, x, y, z);
|
|
4571
|
+
px = x; py = y; pz = z;
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
// =========================================================================
|
|
4576
|
+
// G3 Axes, grid, cross, bulls-eye, ring
|
|
4577
|
+
// =========================================================================
|
|
4578
|
+
|
|
4579
|
+
/**
|
|
4580
|
+
* A coordinate frame at the origin: six half-axes by bit and, with LABELS,
|
|
4581
|
+
* the X (2 lines) · Y (4) · Z (3) glyphs at 1.04 · size, sized size / 40 ×
|
|
4582
|
+
* size / 30. Semantic colour per axis and its glyph (COLOR_X / Y / Z), or
|
|
4583
|
+
* opts.color when `semantic` is false.
|
|
4584
|
+
*
|
|
4585
|
+
* Count: 2 · axes + 18 · (LABELS ? 1 : 0), at most 30.
|
|
4586
|
+
*
|
|
4587
|
+
* @param {object} out Arrays object.
|
|
4588
|
+
* @param {{ size?:number, bits?:number, semantic?:boolean, color?:number[] }} [opts]
|
|
4589
|
+
* @returns {number} Vertices needed.
|
|
4590
|
+
*/
|
|
4591
|
+
function axesLines(out, opts) {
|
|
4592
|
+
const o = opts || {};
|
|
4593
|
+
const size = o.size ?? 100;
|
|
4594
|
+
const bits = o.bits ?? (LABELS | X | Y | Z);
|
|
4595
|
+
const semantic = o.semantic !== false;
|
|
4596
|
+
const axis = (i) => _color(semantic ? _AXIS_COLORS[i] : o.color);
|
|
4597
|
+
_begin(out);
|
|
4598
|
+
if (bits & LABELS) {
|
|
4599
|
+
const cw = size/40, ch = size/30, cs = 1.04*size;
|
|
4600
|
+
axis(0);
|
|
4601
|
+
_line(cs, cw, -ch, cs, -cw, ch);
|
|
4602
|
+
_line(cs, -cw, -ch, cs, cw, ch);
|
|
4603
|
+
axis(1);
|
|
4604
|
+
_line( cw, cs, ch, 0, cs, 0);
|
|
4605
|
+
_line( 0, cs, 0, -cw, cs, ch);
|
|
4606
|
+
_line(-cw, cs, ch, 0, cs, 0);
|
|
4607
|
+
_line( 0, cs, 0, 0, cs, -ch);
|
|
4608
|
+
axis(2);
|
|
4609
|
+
_line(-cw, -ch, cs, cw, -ch, cs);
|
|
4610
|
+
_line( cw, -ch, cs, -cw, ch, cs);
|
|
4611
|
+
_line(-cw, ch, cs, cw, ch, cs);
|
|
4612
|
+
}
|
|
4613
|
+
axis(0);
|
|
4614
|
+
if (bits & X) _line(0, 0, 0, size, 0, 0);
|
|
4615
|
+
if (bits & _X) _line(0, 0, 0, -size, 0, 0);
|
|
4616
|
+
axis(1);
|
|
4617
|
+
if (bits & Y) _line(0, 0, 0, 0, size, 0);
|
|
4618
|
+
if (bits & _Y) _line(0, 0, 0, 0, -size, 0);
|
|
4619
|
+
axis(2);
|
|
4620
|
+
if (bits & Z) _line(0, 0, 0, 0, 0, size);
|
|
4621
|
+
if (bits & _Z) _line(0, 0, 0, 0, 0, -size);
|
|
4622
|
+
return _end(out);
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
/**
|
|
4626
|
+
* A grid in the XY plane: subdivisions + 1 lines each way spanning ±size.
|
|
4627
|
+
* Orientation is the caller's M (a ground plane is a rotation about X).
|
|
4628
|
+
*
|
|
4629
|
+
* Count: 4 · (subdivisions + 1).
|
|
4630
|
+
*
|
|
4631
|
+
* @param {object} out Arrays object.
|
|
4632
|
+
* @param {{ size?:number, subdivisions?:number, color?:number[] }} [opts]
|
|
4633
|
+
* @returns {number} Vertices needed.
|
|
4634
|
+
*/
|
|
4635
|
+
function gridLines(out, opts) {
|
|
4636
|
+
const o = opts || {};
|
|
4637
|
+
const size = o.size ?? 100;
|
|
4638
|
+
const sub = Math.max(1, (o.subdivisions ?? 10) | 0);
|
|
4639
|
+
_begin(out);
|
|
4640
|
+
_color(o.color);
|
|
4641
|
+
for (let i = 0; i <= sub; i++) {
|
|
4642
|
+
const pos = size * (2*i/sub - 1);
|
|
4643
|
+
_line(pos, -size, 0, pos, size, 0);
|
|
4644
|
+
_line(-size, pos, 0, size, pos, 0);
|
|
4645
|
+
}
|
|
4646
|
+
return _end(out);
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
/**
|
|
4650
|
+
* A crosshair in HUD space (z = 0, target pixels): two lines of `size`
|
|
4651
|
+
* through (x, y). The bridge projects a model origin and converts a world
|
|
4652
|
+
* size to pixels before this call.
|
|
4653
|
+
*
|
|
4654
|
+
* Count: 4.
|
|
4655
|
+
*
|
|
4656
|
+
* @param {object} out Arrays object.
|
|
4657
|
+
* @param {{ x?:number, y?:number, size?:number, color?:number[] }} [opts]
|
|
4658
|
+
* @returns {number} Vertices needed.
|
|
4659
|
+
*/
|
|
4660
|
+
function crossLines(out, opts) {
|
|
4661
|
+
const o = opts || {};
|
|
4662
|
+
const x = o.x ?? 0, y = o.y ?? 0, half = (o.size ?? 50) / 2;
|
|
4663
|
+
_begin(out);
|
|
4664
|
+
_color(o.color);
|
|
4665
|
+
_line(x - half, y, 0, x + half, y, 0);
|
|
4666
|
+
_line(x, y - half, 0, x, y + half, 0);
|
|
4667
|
+
return _end(out);
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
/**
|
|
4671
|
+
* A bulls-eye in HUD space (z = 0, target pixels): a sampled circle of
|
|
4672
|
+
* radius size / 2 (`detail` segments) or the cornered square (8 lines),
|
|
4673
|
+
* plus the central cross at 0.6 · half.
|
|
4674
|
+
*
|
|
4675
|
+
* Count: 2 · detail + 4 (CIRCLE) or 20 (SQUARE).
|
|
4676
|
+
*
|
|
4677
|
+
* @param {object} out Arrays object.
|
|
4678
|
+
* @param {{ x?:number, y?:number, size?:number, shape?:number, detail?:number, color?:number[] }} [opts]
|
|
4679
|
+
* @returns {number} Vertices needed.
|
|
4680
|
+
*/
|
|
4681
|
+
function bullsEyeLines(out, opts) {
|
|
4682
|
+
const o = opts || {};
|
|
4683
|
+
const x = o.x ?? 0, y = o.y ?? 0, half = (o.size ?? 50) / 2;
|
|
4684
|
+
const shape = o.shape ?? CIRCLE;
|
|
4685
|
+
const detail = Math.max(3, (o.detail ?? 50) | 0);
|
|
4686
|
+
_begin(out);
|
|
4687
|
+
_color(o.color);
|
|
4688
|
+
if (shape === CIRCLE) {
|
|
4689
|
+
_ring(x, y, 0, half, _U, _V, detail, TWO_PI);
|
|
4690
|
+
} else {
|
|
4691
|
+
const c = 0.6 * half;
|
|
4692
|
+
_line(x-half, y-half+c, 0, x-half, y-half, 0);
|
|
4693
|
+
_line(x-half, y-half, 0, x-half+c, y-half, 0);
|
|
4694
|
+
_line(x+half-c, y-half, 0, x+half, y-half, 0);
|
|
4695
|
+
_line(x+half, y-half, 0, x+half, y-half+c, 0);
|
|
4696
|
+
_line(x+half, y+half-c, 0, x+half, y+half, 0);
|
|
4697
|
+
_line(x+half, y+half, 0, x+half-c, y+half, 0);
|
|
4698
|
+
_line(x-half+c, y+half, 0, x-half, y+half, 0);
|
|
4699
|
+
_line(x-half, y+half, 0, x-half, y+half-c, 0);
|
|
4700
|
+
}
|
|
4701
|
+
const ch = 0.6 * half;
|
|
4702
|
+
_line(x - ch, y, 0, x + ch, y, 0);
|
|
4703
|
+
_line(x, y - ch, 0, x, y + ch, 0);
|
|
4704
|
+
return _end(out);
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
/**
|
|
4708
|
+
* A sampled circle of radius r at (cx, cy, cz) spanned by the orthonormal
|
|
4709
|
+
* u, v — the shared primitive; a partial `sweep` gives an arc from u.
|
|
4710
|
+
*
|
|
4711
|
+
* Count: 2 · detail.
|
|
4712
|
+
*
|
|
4713
|
+
* @param {object} out Arrays object.
|
|
4714
|
+
* @param {number} cx,cy,cz Centre.
|
|
4715
|
+
* @param {number} r Radius.
|
|
4716
|
+
* @param {number[]} u,v Orthonormal in-plane basis.
|
|
4717
|
+
* @param {{ detail?:number, sweep?:number, color?:number[] }} [opts]
|
|
4718
|
+
* @returns {number} Vertices needed.
|
|
4719
|
+
*/
|
|
4720
|
+
function ringLines(out, cx, cy, cz, r, u, v, opts) {
|
|
4721
|
+
const o = opts || {};
|
|
4722
|
+
const detail = Math.max(1, (o.detail ?? 48) | 0);
|
|
4723
|
+
const sweep = o.sweep ?? TWO_PI;
|
|
4724
|
+
_begin(out);
|
|
4725
|
+
_color(o.color);
|
|
4726
|
+
_ring(cx, cy, cz, r, u, v, detail, sweep);
|
|
4727
|
+
return _end(out);
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
// =========================================================================
|
|
4731
|
+
// G4 Frustum and Hermite
|
|
4732
|
+
// =========================================================================
|
|
4733
|
+
|
|
4734
|
+
const _isMat = (cam) => cam != null && cam.mat4Eye != null && cam.mat4Proj != null;
|
|
4735
|
+
|
|
4736
|
+
/** Corner i of out24 ← E · (x, y, z). */
|
|
4737
|
+
function _corner(out24, i, E, x, y, z) {
|
|
4738
|
+
mat4MulPoint(_p3, E, x, y, z);
|
|
4739
|
+
out24[3*i] = _p3[0]; out24[3*i + 1] = _p3[1]; out24[3*i + 2] = _p3[2];
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
/**
|
|
4743
|
+
* The eight world-space corners of a camera's frustum: the near face 0–3
|
|
4744
|
+
* counter-clockwise from bottom-left (BL, BR, TR, TL), then the far face
|
|
4745
|
+
* 4–7 in the same order — so corners 3, 2, 1, 0 are a pane's TL, TR, BR,
|
|
4746
|
+
* BL. `cam` is a camera state (its symmetric extents from fov or
|
|
4747
|
+
* halfHeight and `aspect`), or { mat4Eye, mat4Proj, ndcZMin? } for a
|
|
4748
|
+
* matrix-captured camera (the extents read off the projection). The far
|
|
4749
|
+
* extents follow by similar triangles, or equal the near ones under
|
|
4750
|
+
* orthographic.
|
|
4751
|
+
*
|
|
4752
|
+
* @param {Float64Array|number[]} out24 24-element destination.
|
|
4753
|
+
* @param {object} cam Camera state, or { mat4Eye, mat4Proj, ndcZMin? }.
|
|
4754
|
+
* @param {number} [aspect=1] Viewport width / height (state form).
|
|
4755
|
+
* @param {number} [ndcZMin=WEBGL] NDC-z convention when the matrix form carries none.
|
|
4756
|
+
* @returns {Float64Array|number[]|null} out24, or null when the state's lens is unset.
|
|
4757
|
+
*/
|
|
4758
|
+
function frustumCorners(out24, cam, aspect, ndcZMin) {
|
|
4759
|
+
let E, n, f, l, r, t, b, ortho;
|
|
4760
|
+
if (_isMat(cam)) {
|
|
4761
|
+
E = cam.mat4Eye;
|
|
4762
|
+
const P = cam.mat4Proj, z = cam.ndcZMin ?? ndcZMin ?? WEBGL;
|
|
4763
|
+
ortho = projIsOrtho(P);
|
|
4764
|
+
n = projNear(P, z); f = projFar(P);
|
|
4765
|
+
l = projLeft(P, z); r = projRight(P, z); t = projTop(P, z); b = projBottom(P, z);
|
|
4766
|
+
} else {
|
|
4767
|
+
ortho = cam.fov == null;
|
|
4768
|
+
if (ortho && cam.halfHeight == null) return null;
|
|
4769
|
+
n = cam.near; f = cam.far;
|
|
4770
|
+
t = ortho ? cam.halfHeight : n * Math.tan(cam.fov / 2);
|
|
4771
|
+
r = t * (aspect ?? 1);
|
|
4772
|
+
b = -t; l = -r;
|
|
4773
|
+
E = cameraEye(_E, cam);
|
|
4774
|
+
}
|
|
4775
|
+
const k = ortho ? 1 : f / n;
|
|
4776
|
+
_corner(out24, 0, E, l, b, -n);
|
|
4777
|
+
_corner(out24, 1, E, r, b, -n);
|
|
4778
|
+
_corner(out24, 2, E, r, t, -n);
|
|
4779
|
+
_corner(out24, 3, E, l, t, -n);
|
|
4780
|
+
_corner(out24, 4, E, k*l, k*b, -f);
|
|
4781
|
+
_corner(out24, 5, E, k*r, k*b, -f);
|
|
4782
|
+
_corner(out24, 6, E, k*r, k*t, -f);
|
|
4783
|
+
_corner(out24, 7, E, k*l, k*t, -f);
|
|
4784
|
+
return out24;
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
/** Line between corners i and j of the scratch corners. */
|
|
4788
|
+
function _edge(i, j) {
|
|
4789
|
+
_line(_c24[3*i], _c24[3*i + 1], _c24[3*i + 2], _c24[3*j], _c24[3*j + 1], _c24[3*j + 2]);
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
/**
|
|
4793
|
+
* A camera's frustum as edges by bit: NEAR and FAR the two rectangles,
|
|
4794
|
+
* BODY the four edges joining them, APEX (perspective only) the eye to
|
|
4795
|
+
* the near corners. `cam` as frustumCorners takes it.
|
|
4796
|
+
*
|
|
4797
|
+
* Count: 8 · (NEAR + FAR + BODY + APEX), at most 32.
|
|
4798
|
+
*
|
|
4799
|
+
* @param {object} out Arrays object.
|
|
4800
|
+
* @param {object} cam Camera state, or { mat4Eye, mat4Proj, ndcZMin? }.
|
|
4801
|
+
* @param {{ aspect?:number, ndcZMin?:number, bits?:number, color?:number[] }} [opts]
|
|
4802
|
+
* @returns {number} Vertices needed (0 when the state's lens is unset).
|
|
4803
|
+
*/
|
|
4804
|
+
function frustumLines(out, cam, opts) {
|
|
4805
|
+
const o = opts || {};
|
|
4806
|
+
const bits = o.bits ?? (NEAR | FAR | BODY | APEX);
|
|
4807
|
+
_begin(out);
|
|
4808
|
+
_color(o.color);
|
|
4809
|
+
if (frustumCorners(_c24, cam, o.aspect ?? 1, o.ndcZMin ?? WEBGL) === null) return _end(out);
|
|
4810
|
+
if (bits & NEAR) { _edge(0, 1); _edge(1, 2); _edge(2, 3); _edge(3, 0); }
|
|
4811
|
+
if (bits & FAR) { _edge(4, 5); _edge(5, 6); _edge(6, 7); _edge(7, 4); }
|
|
4812
|
+
if (bits & BODY) { _edge(0, 4); _edge(1, 5); _edge(2, 6); _edge(3, 7); }
|
|
4813
|
+
const persp = _isMat(cam) ? !projIsOrtho(cam.mat4Proj) : cam.fov != null;
|
|
4814
|
+
if ((bits & APEX) && persp) {
|
|
4815
|
+
const E = _isMat(cam) ? cam.mat4Eye : null;
|
|
4816
|
+
const ex = E ? E[12] : cam.eye[0], ey = E ? E[13] : cam.eye[1], ez = E ? E[14] : cam.eye[2];
|
|
4817
|
+
for (let i = 0; i < 4; i++) _line(ex, ey, ez, _c24[3*i], _c24[3*i + 1], _c24[3*i + 2]);
|
|
4818
|
+
}
|
|
4819
|
+
return _end(out);
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
/**
|
|
4823
|
+
* One cubic Hermite segment through hermiteVec3, as a polyline of
|
|
4824
|
+
* `samples` steps.
|
|
4825
|
+
*
|
|
4826
|
+
* Count: 2 · samples.
|
|
4827
|
+
*
|
|
4828
|
+
* @param {object} out Arrays object.
|
|
4829
|
+
* @param {number[]} p0,t0 Start point and its outgoing tangent.
|
|
4830
|
+
* @param {number[]} p1,t1 End point and its incoming tangent.
|
|
4831
|
+
* @param {{ samples?:number, color?:number[] }} [opts]
|
|
4832
|
+
* @returns {number} Vertices needed.
|
|
4833
|
+
*/
|
|
4834
|
+
function hermiteLines(out, p0, t0, p1, t1, opts) {
|
|
4835
|
+
const o = opts || {};
|
|
4836
|
+
const N = Math.max(1, (o.samples ?? 32) | 0);
|
|
4837
|
+
_begin(out);
|
|
4838
|
+
_color(o.color);
|
|
4839
|
+
for (let i = 0; i <= N; i++) {
|
|
4840
|
+
hermiteVec3(_p3, p0, t0, p1, t1, i / N);
|
|
4841
|
+
if (i > 0) _line(_q3[0], _q3[1], _q3[2], _p3[0], _p3[1], _p3[2]);
|
|
4842
|
+
_q3[0] = _p3[0]; _q3[1] = _p3[1]; _q3[2] = _p3[2];
|
|
4843
|
+
}
|
|
4844
|
+
return _end(out);
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4847
|
+
// =========================================================================
|
|
4848
|
+
// G5 Path, helm rig, locus, pane
|
|
4849
|
+
// =========================================================================
|
|
4850
|
+
|
|
4851
|
+
/**
|
|
4852
|
+
* A PoseTrack or CameraTrack's path by bit, over the track's own samplers
|
|
4853
|
+
* (samplePos / sampleEye / sampleCenter and the tangent readers), so the
|
|
4854
|
+
* interpolation modes are honoured: PATH the sampled polyline, `samples`
|
|
4855
|
+
* per segment; CONTROLS the straight control polygon; TANGENTS_IN /
|
|
4856
|
+
* TANGENTS_OUT the tangent at each keyframe scaled by `tangentScale`;
|
|
4857
|
+
* CENTER (camera tracks) the gaze line eye → center per keyframe and a
|
|
4858
|
+
* three-axis star of half-size `centerSize` at the center. `target`
|
|
4859
|
+
* ('eye' or 'center') picks a camera track's path for the first three
|
|
4860
|
+
* bits. Markers and handles are the bridge's composition.
|
|
4861
|
+
*
|
|
4862
|
+
* Count: 2 · samples · segments (PATH) + 2 · segments (CONTROLS) +
|
|
4863
|
+
* 2 · keyframes per tangent bit + 8 · keyframes (CENTER).
|
|
4864
|
+
*
|
|
4865
|
+
* @param {object} out Arrays object.
|
|
4866
|
+
* @param {object} track PoseTrack or CameraTrack.
|
|
4867
|
+
* @param {{ bits?:number, samples?:number, tangentScale?:number, target?:string,
|
|
4868
|
+
* centerSize?:number, color?:number[] }} [opts]
|
|
4869
|
+
* @returns {number} Vertices needed.
|
|
4870
|
+
*/
|
|
4871
|
+
function pathLines(out, track, opts) {
|
|
4872
|
+
const o = opts || {};
|
|
4873
|
+
const bits = o.bits ?? (PATH | CONTROLS | TANGENTS_IN | TANGENTS_OUT);
|
|
4874
|
+
const N = Math.max(1, (o.samples ?? 32) | 0);
|
|
4875
|
+
const ts = o.tangentScale ?? 0.25;
|
|
4876
|
+
const cs = o.centerSize ?? 4;
|
|
4877
|
+
const kfs = track.keyframes, n = kfs.length;
|
|
4878
|
+
const isCamera = typeof track.sampleEye === 'function';
|
|
4879
|
+
const useCenter = isCamera && o.target === 'center';
|
|
4880
|
+
const field = isCamera ? (useCenter ? 'center' : 'eye') : 'pos';
|
|
4881
|
+
const sampler = isCamera ? (useCenter ? 'sampleCenter' : 'sampleEye') : 'samplePos';
|
|
4882
|
+
const tangents = isCamera ? (useCenter ? 'centerTangents' : 'eyeTangents') : 'tangents';
|
|
4883
|
+
_begin(out);
|
|
4884
|
+
_color(o.color);
|
|
4885
|
+
if ((bits & PATH) && n > 1) {
|
|
4886
|
+
for (let seg = 0; seg < n - 1; seg++) {
|
|
4887
|
+
for (let i = 0; i <= N; i++) {
|
|
4888
|
+
track[sampler](_p3, seg, i / N);
|
|
4889
|
+
if (i > 0) _line(_q3[0], _q3[1], _q3[2], _p3[0], _p3[1], _p3[2]);
|
|
4890
|
+
_q3[0] = _p3[0]; _q3[1] = _p3[1]; _q3[2] = _p3[2];
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
if (bits & CONTROLS) {
|
|
4895
|
+
for (let i = 0; i < n - 1; i++) {
|
|
4896
|
+
const a = kfs[i][field], b = kfs[i + 1][field];
|
|
4897
|
+
_line(a[0], a[1], a[2], b[0], b[1], b[2]);
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
if (bits & (TANGENTS_IN | TANGENTS_OUT)) {
|
|
4901
|
+
for (let i = 0; i < n; i++) {
|
|
4902
|
+
track[tangents](_tIn, _tOut, i);
|
|
4903
|
+
const k = kfs[i][field];
|
|
4904
|
+
if (bits & TANGENTS_IN) _line(k[0] - ts*_tIn[0], k[1] - ts*_tIn[1], k[2] - ts*_tIn[2], k[0], k[1], k[2]);
|
|
4905
|
+
if (bits & TANGENTS_OUT) _line(k[0], k[1], k[2], k[0] + ts*_tOut[0], k[1] + ts*_tOut[1], k[2] + ts*_tOut[2]);
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
if ((bits & CENTER) && isCamera) {
|
|
4909
|
+
for (let i = 0; i < n; i++) {
|
|
4910
|
+
const e = kfs[i].eye, c = kfs[i].center;
|
|
4911
|
+
_line(e[0], e[1], e[2], c[0], c[1], c[2]);
|
|
4912
|
+
_line(c[0] - cs, c[1], c[2], c[0] + cs, c[1], c[2]);
|
|
4913
|
+
_line(c[0], c[1] - cs, c[2], c[0], c[1] + cs, c[2]);
|
|
4914
|
+
_line(c[0], c[1], c[2] - cs, c[0], c[1], c[2] + cs);
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
return _end(out);
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
/** An arrow along principal axis `axis` (0 X, 1 Y, 2 Z): signed length L, head size h — 5 lines. */
|
|
4921
|
+
function _arrow(axis, L, h) {
|
|
4922
|
+
const a = (axis + 1) % 3, b = (axis + 2) % 3;
|
|
4923
|
+
_tip[0] = _tip[1] = _tip[2] = 0; _tip[axis] = L;
|
|
4924
|
+
_line(0, 0, 0, _tip[0], _tip[1], _tip[2]);
|
|
4925
|
+
const s = Math.sign(L) || 1;
|
|
4926
|
+
_ha[0] = _ha[1] = _ha[2] = 0; _ha[axis] = L - s*h;
|
|
4927
|
+
_ha[a] = h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
|
|
4928
|
+
_ha[a] = -h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
|
|
4929
|
+
_ha[a] = 0;
|
|
4930
|
+
_ha[b] = h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
|
|
4931
|
+
_ha[b] = -h*0.5; _line(_tip[0], _tip[1], _tip[2], _ha[0], _ha[1], _ha[2]);
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
/**
|
|
4935
|
+
* A helm's rig, colour always semantic: per translation channel a dim
|
|
4936
|
+
* baseline arrow of signed length sign · size · sens / 0.30 and, while the
|
|
4937
|
+
* channel's activity is non-zero, a bright arrow of length ∝ |activity| /
|
|
4938
|
+
* (sens · fullScale); per rotation channel a dim ring of radius size / 2 ·
|
|
4939
|
+
* sens / 0.0025 and a bright arc sweeping π · f in the live direction.
|
|
4940
|
+
* Dim and bright are the alpha of the written colour (COLOR_DIM, 1). With
|
|
4941
|
+
* `identify`, one anchor per channel goes to out.labels as { x, y, z,
|
|
4942
|
+
* text: 'L' + lane }. Orientation is the caller's M.
|
|
4943
|
+
*
|
|
4944
|
+
* Count: 10 · 3 · 2 (arrows) + 96 · 3 (rings) + 48 · 3 (arcs), at most 492;
|
|
4945
|
+
* the bright half only while a channel is active.
|
|
4946
|
+
*
|
|
4947
|
+
* @param {object} out Arrays object.
|
|
4948
|
+
* @param {object} helm A PoseHelm (profile, fullScale, activity).
|
|
4949
|
+
* @param {{ size?:number, bits?:number, identify?:boolean }} [opts]
|
|
4950
|
+
* @returns {number} Vertices needed.
|
|
4951
|
+
*/
|
|
4952
|
+
function helmRigLines(out, helm, opts) {
|
|
4953
|
+
const o = opts || {};
|
|
4954
|
+
const size = o.size ?? 100;
|
|
4955
|
+
const bits = o.bits ?? (TRANSLATE | ROTATE);
|
|
4956
|
+
const identify = o.identify === true;
|
|
4957
|
+
const prof = helm.profile;
|
|
4958
|
+
const head = size * 0.08, ringR0 = size * 0.5;
|
|
4959
|
+
const TREF = 0.30, RREF = 0.0025, FULL = helm.fullScale, ARC_FULL = Math.PI;
|
|
4960
|
+
helm.activity(_act);
|
|
4961
|
+
_begin(out);
|
|
4962
|
+
const labels = identify && out.labels ? out.labels : null;
|
|
4963
|
+
if (bits & TRANSLATE) {
|
|
4964
|
+
const T = [prof.Tx, prof.Ty, prof.Tz];
|
|
4965
|
+
for (let ax = 0; ax < 3; ax++) {
|
|
4966
|
+
const ch = T[ax];
|
|
4967
|
+
const L = ch.sign * size * (ch.sens / TREF);
|
|
4968
|
+
_color(_AXIS_COLORS[ax], COLOR_DIM);
|
|
4969
|
+
_arrow(ax, L, head);
|
|
4970
|
+
const a = _act[ax];
|
|
4971
|
+
if (a !== 0) {
|
|
4972
|
+
const f = Math.min(Math.abs(a) / (ch.sens * FULL), 1);
|
|
4973
|
+
_color(_AXIS_COLORS[ax], 1);
|
|
4974
|
+
_arrow(ax, Math.sign(a) * f * Math.abs(L), head);
|
|
4975
|
+
}
|
|
4976
|
+
if (labels) {
|
|
4977
|
+
const lp = [0, 0, 0]; lp[ax] = L + ch.sign * head * 1.5;
|
|
4978
|
+
labels.push({ x: lp[0], y: lp[1], z: lp[2], text: 'L' + ch.lane });
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
if (bits & ROTATE) {
|
|
4983
|
+
const R = [prof.Rp, prof.Ry, prof.Rr]; // pitch ⊥ X, yaw ⊥ Y, roll ⊥ Z
|
|
4984
|
+
for (let ax = 0; ax < 3; ax++) {
|
|
4985
|
+
const ch = R[ax];
|
|
4986
|
+
const r = ringR0 * (ch.sens / RREF);
|
|
4987
|
+
const u = _AXES[(ax + 1) % 3], v = _AXES[(ax + 2) % 3];
|
|
4988
|
+
_color(_AXIS_COLORS[ax], COLOR_DIM);
|
|
4989
|
+
_ring(0, 0, 0, r, u, v, 48, TWO_PI);
|
|
4990
|
+
const a = _act[3 + ax];
|
|
4991
|
+
if (a !== 0) {
|
|
4992
|
+
const f = Math.min(Math.abs(a) / (ch.sens * FULL), 1);
|
|
4993
|
+
_color(_AXIS_COLORS[ax], 1);
|
|
4994
|
+
_ring(0, 0, 0, r, u, v, 24, Math.sign(a) * f * ARC_FULL);
|
|
4995
|
+
}
|
|
4996
|
+
if (labels) {
|
|
4997
|
+
const lp = [0, 0, 0]; lp[(ax + 1) % 3] = r;
|
|
4998
|
+
labels.push({ x: lp[0], y: lp[1], z: lp[2], text: 'L' + ch.lane });
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
return _end(out);
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
/**
|
|
5006
|
+
* A handle's stroked parts by bit. AIM: anchor → `point` (the handle's
|
|
5007
|
+
* current point, read by the caller with value). LOCUS by constraint.kind:
|
|
5008
|
+
* SPHERE three great circles about the anchor; PLANE a square of
|
|
5009
|
+
* half-extent 100 in the plane's basis; AXIS the segment anchor + [min,
|
|
5010
|
+
* max] · u; DIAL the ring in the dial plane; a constraint flagged `view`
|
|
5011
|
+
* (the host's VIEW) a screen-aligned square of half-extent 100 at `point`,
|
|
5012
|
+
* its basis the camera's right and up read off `mat4View`. RING: SPHERE
|
|
5013
|
+
* the view-facing limb, a ring perpendicular to anchor − eye (the eye from
|
|
5014
|
+
* `mat4View`); PLANE the border of the locus square (written once when
|
|
5015
|
+
* both bits ask for it). A constraint supplying locus(out, opts) is
|
|
5016
|
+
* dispatched to it. The HANDLE dot is not a line and not generated here.
|
|
5017
|
+
*
|
|
5018
|
+
* Count: AIM 2; LOCUS SPHERE 288 · PLANE 8 · AXIS 2 · DIAL 96 · view 8;
|
|
5019
|
+
* RING SPHERE 96 · PLANE 8 (shared with LOCUS); at most 386.
|
|
5020
|
+
*
|
|
5021
|
+
* @param {object} out Arrays object.
|
|
5022
|
+
* @param {object} constraint A contract-conforming constraint.
|
|
5023
|
+
* @param {{ bits?:number, mat4View?:ArrayLike<number>, point?:number[], color?:number[] }} [opts]
|
|
5024
|
+
* @returns {number} Vertices needed.
|
|
5025
|
+
*/
|
|
5026
|
+
function locusLines(out, constraint, opts) {
|
|
5027
|
+
const o = opts || {};
|
|
5028
|
+
if (typeof constraint.locus === 'function') return constraint.locus(out, o);
|
|
5029
|
+
const bits = o.bits ?? (AIM | LOCUS);
|
|
5030
|
+
const c = constraint, a = c.anchor, pt = o.point, V = o.mat4View;
|
|
5031
|
+
_begin(out);
|
|
5032
|
+
_color(o.color);
|
|
5033
|
+
if ((bits & AIM) && a && pt) _line(a[0], a[1], a[2], pt[0], pt[1], pt[2]);
|
|
5034
|
+
if (c.view === true) {
|
|
5035
|
+
if ((bits & LOCUS) && pt && V) {
|
|
5036
|
+
_b0[0] = V[0]; _b0[1] = V[4]; _b0[2] = V[8]; // the camera's right
|
|
5037
|
+
_b1[0] = V[1]; _b1[1] = V[5]; _b1[2] = V[9]; // the camera's up
|
|
5038
|
+
_square(pt, _b0, _b1, 100);
|
|
5039
|
+
}
|
|
5040
|
+
} else if (c.kind === SPHERE && a) {
|
|
5041
|
+
if (bits & LOCUS) {
|
|
5042
|
+
_ring(a[0], a[1], a[2], c.radius, _AXES[0], _AXES[1], 48, TWO_PI);
|
|
5043
|
+
_ring(a[0], a[1], a[2], c.radius, _AXES[1], _AXES[2], 48, TWO_PI);
|
|
5044
|
+
_ring(a[0], a[1], a[2], c.radius, _AXES[2], _AXES[0], 48, TWO_PI);
|
|
5045
|
+
}
|
|
5046
|
+
if ((bits & RING) && V) {
|
|
5047
|
+
// eye = −Rᵀ t of the view matrix; the limb is ⊥ anchor − eye
|
|
5048
|
+
const tx = V[12], ty = V[13], tz = V[14];
|
|
5049
|
+
_b2[0] = a[0] + (V[0]*tx + V[1]*ty + V[2]*tz);
|
|
5050
|
+
_b2[1] = a[1] + (V[4]*tx + V[5]*ty + V[6]*tz);
|
|
5051
|
+
_b2[2] = a[2] + (V[8]*tx + V[9]*ty + V[10]*tz);
|
|
5052
|
+
_unit(_b2, 0, 0, 1);
|
|
5053
|
+
_basis(_b2, _b0, _b1);
|
|
5054
|
+
_ring(a[0], a[1], a[2], c.radius, _b0, _b1, 48, TWO_PI);
|
|
5055
|
+
}
|
|
5056
|
+
} else if (c.kind === PLANE && a) {
|
|
5057
|
+
if (bits & (LOCUS | RING)) {
|
|
5058
|
+
_basis(c.n, _b0, _b1);
|
|
5059
|
+
_square(a, _b0, _b1, 100);
|
|
5060
|
+
}
|
|
5061
|
+
} else if (c.kind === AXIS && a) {
|
|
5062
|
+
if (bits & LOCUS) {
|
|
5063
|
+
const u = c.u;
|
|
5064
|
+
_line(a[0] + c.min*u[0], a[1] + c.min*u[1], a[2] + c.min*u[2],
|
|
5065
|
+
a[0] + c.max*u[0], a[1] + c.max*u[1], a[2] + c.max*u[2]);
|
|
5066
|
+
}
|
|
5067
|
+
} else if (c.kind === DIAL && a) {
|
|
5068
|
+
if (bits & LOCUS) _ring(a[0], a[1], a[2], c.radius, c.r0, c.r1, 48, TWO_PI);
|
|
5069
|
+
}
|
|
5070
|
+
return _end(out);
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
/**
|
|
5074
|
+
* A textured quad as two triangles (p0, p1, p2) (p0, p2, p3) — the winding
|
|
5075
|
+
* of the corner order — with texcoord written when the array exists.
|
|
5076
|
+
* Default uvs: p0 (top-left) → (0, 1), p1 → (1, 1), p2 → (1, 0), p3 →
|
|
5077
|
+
* (0, 0), so a texture in GL's bottom-up space reads upright with no
|
|
5078
|
+
* flip; `opts.uvs` overrides, four pairs flat in corner order.
|
|
5079
|
+
*
|
|
5080
|
+
* Count: 6.
|
|
5081
|
+
*
|
|
5082
|
+
* @param {object} out Arrays object.
|
|
5083
|
+
* @param {number[]} p0,p1,p2,p3 Corners, top-left clockwise.
|
|
5084
|
+
* @param {{ uvs?:number[], color?:number[] }} [opts]
|
|
5085
|
+
* @returns {number} Vertices needed.
|
|
5086
|
+
*/
|
|
5087
|
+
function paneTris(out, p0, p1, p2, p3, opts) {
|
|
5088
|
+
const o = opts || {};
|
|
5089
|
+
const uv = o.uvs || _UV_DEFAULT;
|
|
5090
|
+
_begin(out);
|
|
5091
|
+
_color(o.color);
|
|
5092
|
+
_vertexUV(p0[0], p0[1], p0[2], uv[0], uv[1]);
|
|
5093
|
+
_vertexUV(p1[0], p1[1], p1[2], uv[2], uv[3]);
|
|
5094
|
+
_vertexUV(p2[0], p2[1], p2[2], uv[4], uv[5]);
|
|
5095
|
+
_vertexUV(p0[0], p0[1], p0[2], uv[0], uv[1]);
|
|
5096
|
+
_vertexUV(p2[0], p2[1], p2[2], uv[4], uv[5]);
|
|
5097
|
+
_vertexUV(p3[0], p3[1], p3[2], uv[6], uv[7]);
|
|
5098
|
+
return _end(out);
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
export { AIM, APEX, AXIS, BODY, BOTTOM, CENTER, CIRCLE, COLOR_DIM, COLOR_X, COLOR_Y, COLOR_Z, CONTROLS, CameraTrack, Constraint, DIAL, DIRECTION, EYE, FAR, HANDLE, HANDLES, HELM_CHANNELS, INVISIBLE, LABELS, LEFT, LOCUS, MATRIX, MODEL, NDC, NEAR, NONE, ORIGIN, PATH, PLANE, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, POINT, PoseHelm, PoseTrack, RIGHT, RING, ROTATE, SCREEN, SELF, SEMIVISIBLE, SPHERE, SQUARE, TANGENTS, TANGENTS_IN, TANGENTS_OUT, TOP, TRANSLATE, VISIBLE, WEBGL, WEBGPU, WORLD, X, Y, Z, _X, _Y, _Z, _i, _j, _k, axesLines, azElFromDir, boxVisibility, bullsEyeLines, cameraCopy, cameraDolly, cameraEye, cameraFromMat4, cameraFromPose, cameraOrbit, cameraPan, cameraPlanes, cameraProj, cameraToPose, cameraView, capacityOf, createArrays, createCamera, createConstraint, crossLines, dirFromAzEl, distanceToPlane, frustumCorners, frustumLines, frustumPlanes, gridLines, growArrays, helmRigLines, hermiteLines, hermiteVec3, i, idToRgba, j, k, lerpVec3, locusLines, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4Eye, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Persp, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4View, oneEuro, paneTris, pathLines, pixelRatio, pointVisibility, pointerHit, poseDelta, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qConjugate, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qFromUnitVectors, qMul, qNegate, qNlerp, qNormalize, qRotateVec3, qSet, qSlerp, qToAxisAngle, qToMat4, rayClosestPointOnAxis, rayHitCapsule, rayHitRing, rayHitSphere, rayPlane, raySphere, rgbaToId, ringLines, sphereVisibility, transformToMat4, unproject };
|
|
3455
5102
|
//# sourceMappingURL=index.js.map
|