@nakednous/host 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3104 +1,4 @@
1
- /**
2
- * @file Core constants — zero dependencies.
3
- * @module tree/constants
4
- * @license AGPL-3.0-only
5
- */
6
-
7
- // Coordinate spaces
8
- const WORLD = 'WORLD';
9
- const EYE = 'EYE';
10
- const NDC = 'NDC';
11
- const SCREEN = 'SCREEN';
12
- const MATRIX = 'MATRIX';
13
-
14
- // Integrator frame source (helm `from` only): the construct's own current pose
15
- // — body-relative; not a general mapDirection space.
16
- const SELF = 'SELF';
17
-
18
- // NDC Z convention (only difference between backends)
19
- const WEBGL = -1; // z ∈ [−1, 1]
20
-
21
- // Handle constraint kinds
22
- const SPHERE = 0;
23
- const PLANE = 1;
24
- const AXIS = 2;
25
- const DIAL = 3;
26
-
27
- // Handle report modes
28
- const POINT = 0;
29
- const DIRECTION = 1;
30
-
31
- /** Normalise quaternion in-place. @returns {number[]} out */
32
- const qNormalize = (out) => {
33
- const l = Math.sqrt(out[0]*out[0]+out[1]*out[1]+out[2]*out[2]+out[3]*out[3]) || 1;
34
- out[0]/=l; out[1]/=l; out[2]/=l; out[3]/=l; return out;
35
- };
36
-
37
- /** Hamilton product out = a * b. Alias-safe: out may be a or b. @returns {number[]} out */
38
- const qMul = (out, a, b) => {
39
- const ax=a[0],ay=a[1],az=a[2],aw=a[3], bx=b[0],by=b[1],bz=b[2],bw=b[3];
40
- out[0]=aw*bx+ax*bw+ay*bz-az*by;
41
- out[1]=aw*by-ax*bz+ay*bw+az*bx;
42
- out[2]=aw*bz+ax*by-ay*bx+az*bw;
43
- out[3]=aw*bw-ax*bx-ay*by-az*bz;
44
- return out;
45
- };
46
-
47
- // =========================================================================
48
- // Interpolation
49
- // =========================================================================
50
-
51
- /** Spherical linear interpolation. @returns {number[]} out */
52
- const qSlerp = (out, a, b, t) => {
53
- let bx=b[0],by=b[1],bz=b[2],bw=b[3];
54
- let d = a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw;
55
- if (d < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; d=-d; }
56
- let f0, f1;
57
- if (1-d > 1e-10) {
58
- const th=Math.acos(d), st=Math.sin(th);
59
- f0=Math.sin((1-t)*th)/st; f1=Math.sin(t*th)/st;
60
- } else {
61
- f0=1-t; f1=t;
62
- }
63
- out[0]=a[0]*f0+bx*f1; out[1]=a[1]*f0+by*f1;
64
- out[2]=a[2]*f0+bz*f1; out[3]=a[3]*f0+bw*f1;
65
- return qNormalize(out);
66
- };
67
-
68
- /**
69
- * Normalised linear interpolation (nlerp).
70
- * Cheaper than slerp; slightly non-constant angular velocity.
71
- * Handles antipodal quats by flipping b when dot < 0.
72
- * @returns {number[]} out
73
- */
74
- const qNlerp = (out, a, b, t) => {
75
- let bx=b[0],by=b[1],bz=b[2],bw=b[3];
76
- if (a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; }
77
- out[0]=a[0]+t*(bx-a[0]); out[1]=a[1]+t*(by-a[1]);
78
- out[2]=a[2]+t*(bz-a[2]); out[3]=a[3]+t*(bw-a[3]);
79
- return qNormalize(out);
80
- };
81
-
82
- /**
83
- * Build a quaternion from axis-angle.
84
- * @param {number[]} out
85
- * @param {number} ax @param {number} ay @param {number} az Axis (need not be unit).
86
- * @param {number} angle Radians.
87
- * @returns {number[]} out
88
- */
89
- const qFromAxisAngle = (out, ax, ay, az, angle) => {
90
- const half = angle * 0.5;
91
- const s = Math.sin(half);
92
- const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
93
- out[0] = s * ax / len; out[1] = s * ay / len; out[2] = s * az / len;
94
- out[3] = Math.cos(half);
95
- return out;
96
- };
97
-
98
- /**
99
- * Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
100
- * `dir` need not be unit. Right = dir × up, up re-orthogonalised as right × dir;
101
- * when `dir` is parallel to `up` the up hint is re-seeded from the world axis
102
- * least aligned with `dir` (the same seed qFromUnitVectors uses), so every
103
- * direction yields a proper rotation.
104
- * @param {number[]} out
105
- * @param {number[]} dir Forward direction [x,y,z].
106
- * @param {number[]} [up] Up vector [x,y,z].
107
- * @returns {number[]} out
108
- */
109
- const qFromLookDir = (out, dir, up) => {
110
- let fx=dir[0],fy=dir[1],fz=dir[2];
111
- const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
112
- fx/=fl; fy/=fl; fz/=fl;
113
- let ux=up?up[0]:0, uy=up?up[1]:1, uz=up?up[2]:0;
114
- let rx=fy*uz-fz*uy, ry=fz*ux-fx*uz, rz=fx*uy-fy*ux; // right = dir × up
115
- let rl=Math.sqrt(rx*rx+ry*ry+rz*rz);
116
- if (rl < 1e-8) { // dir ∥ up: re-seed up
117
- const ax=Math.abs(fx), ay=Math.abs(fy), az=Math.abs(fz);
118
- ux=0; uy=0; uz=0;
119
- if (ax <= ay && ax <= az) ux=1; else if (ay <= az) uy=1; else uz=1;
120
- rx=fy*uz-fz*uy; ry=fz*ux-fx*uz; rz=fx*uy-fy*ux;
121
- rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
122
- }
123
- rx/=rl; ry/=rl; rz/=rl;
124
- ux=ry*fz-rz*fy; uy=rz*fx-rx*fz; uz=rx*fy-ry*fx; // up = right × dir
125
- return qFromRotMat3x3(out, rx,ux,-fx, ry,uy,-fy, rz,uz,-fz); // columns: right, up, back
126
- };
127
-
128
- /**
129
- * Build a quaternion from a 3×3 rotation matrix (9 row-major scalars).
130
- * @returns {number[]} out (normalised)
131
- */
132
- const qFromRotMat3x3 = (out, m00,m01,m02, m10,m11,m12, m20,m21,m22) => {
133
- const tr = m00+m11+m22;
134
- if (tr > 0) {
135
- const s=0.5/Math.sqrt(tr+1);
136
- out[3]=0.25/s; out[0]=(m21-m12)*s; out[1]=(m02-m20)*s; out[2]=(m10-m01)*s;
137
- } else if (m00>m11 && m00>m22) {
138
- const s=2*Math.sqrt(1+m00-m11-m22);
139
- out[3]=(m21-m12)/s; out[0]=0.25*s; out[1]=(m01+m10)/s; out[2]=(m02+m20)/s;
140
- } else if (m11>m22) {
141
- const s=2*Math.sqrt(1+m11-m00-m22);
142
- out[3]=(m02-m20)/s; out[0]=(m01+m10)/s; out[1]=0.25*s; out[2]=(m12+m21)/s;
143
- } else {
144
- const s=2*Math.sqrt(1+m22-m00-m11);
145
- out[3]=(m10-m01)/s; out[0]=(m02+m20)/s; out[1]=(m12+m21)/s; out[2]=0.25*s;
146
- }
147
- return qNormalize(out);
148
- };
149
-
150
- /**
151
- * Write a quaternion into the rotation block of a column-major mat4.
152
- * Translation and perspective rows/cols are set to identity values.
153
- * @param {Float32Array|number[]} out 16-element array.
154
- * @param {number[]} q [x,y,z,w].
155
- * @returns {Float32Array|number[]} out
156
- */
157
- const qToMat4 = (out, q) => {
158
- const x=q[0],y=q[1],z=q[2],w=q[3];
159
- const x2=x+x,y2=y+y,z2=z+z;
160
- 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;
161
- out[0]=1-(yy+zz); out[1]=xy+wz; out[2]=xz-wy; out[3]=0;
162
- out[4]=xy-wz; out[5]=1-(xx+zz); out[6]=yz+wx; out[7]=0;
163
- out[8]=xz+wy; out[9]=yz-wx; out[10]=1-(xx+yy); out[11]=0;
164
- out[12]=0; out[13]=0; out[14]=0; out[15]=1;
165
- return out;
166
- };
167
-
168
- // Lookat basis scratch — right (0–2), up (3–5), back (6–8): unit, world space.
169
- const _lb = new Float64Array(9);
170
-
171
- /**
172
- * Orthonormal lookat basis into `_lb`: back = eye − center, right = up × back,
173
- * up = back × right. When the view direction is parallel to the up hint the
174
- * hint is re-seeded from the world axis least aligned with it — the seed
175
- * qFromUnitVectors and qFromLookDir use — so the basis is always a rotation.
176
- */
177
- function _lookBasis(ex,ey,ez, cx,cy,cz, ux,uy,uz) {
178
- let zx=ex-cx, zy=ey-cy, zz=ez-cz;
179
- const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
180
- zx/=zl; zy/=zl; zz/=zl;
181
- let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
182
- let xl=Math.sqrt(xx*xx+xy*xy+xz*xz);
183
- if (xl < 1e-8) { // view ∥ up: re-seed up
184
- const ax=Math.abs(zx), ay=Math.abs(zy), az=Math.abs(zz);
185
- ux=0; uy=0; uz=0;
186
- if (ax <= ay && ax <= az) ux=1; else if (ay <= az) uy=1; else uz=1;
187
- xx=uy*zz-uz*zy; xy=uz*zx-ux*zz; xz=ux*zy-uy*zx;
188
- xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
189
- }
190
- xx/=xl; xy/=xl; xz/=xl;
191
- _lb[0]=xx; _lb[1]=xy; _lb[2]=xz;
192
- _lb[3]=zy*xz-zz*xy; _lb[4]=zz*xx-zx*xz; _lb[5]=zx*xy-zy*xx;
193
- _lb[6]=zx; _lb[7]=zy; _lb[8]=zz;
194
- }
195
-
196
- /**
197
- * View matrix (world→eye) from lookat parameters.
198
- * Camera looks along −Z in eye space; right = normalize(up × (−Z)).
199
- * Cheaper than building the eye matrix and inverting.
200
- *
201
- * @param {Float32Array|number[]} out 16-element destination.
202
- * @param {number} ex,ey,ez Eye (camera) position.
203
- * @param {number} cx,cy,cz Look-at target.
204
- * @param {number} ux,uy,uz World up hint (need not be unit; re-seeded when
205
- * parallel to the view direction).
206
- */
207
- function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
208
- _lookBasis(ex,ey,ez, cx,cy,cz, ux,uy,uz);
209
- 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];
210
- out[0]=xx; out[1]=yx; out[2]=zx; out[3]=0;
211
- out[4]=xy; out[5]=yy; out[6]=zy; out[7]=0;
212
- out[8]=xz; out[9]=yz; out[10]=zz; out[11]=0;
213
- out[12]=-(xx*ex+xy*ey+xz*ez);
214
- out[13]=-(yx*ex+yy*ey+yz*ez);
215
- out[14]=-(zx*ex+zy*ey+zz*ez);
216
- out[15]=1;
217
- return out;
218
- }
219
-
220
- /**
221
- * Eye matrix (eye→world) from lookat parameters.
222
- * Transpose of the rotation block + direct translation column.
223
- * Same parameters as mat4View.
224
- *
225
- * @param {Float32Array|number[]} out 16-element destination.
226
- * @param {number} ex,ey,ez Eye position.
227
- * @param {number} cx,cy,cz Look-at target.
228
- * @param {number} ux,uy,uz World up hint (need not be unit; re-seeded when
229
- * parallel to the view direction).
230
- */
231
- function mat4Eye(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
232
- _lookBasis(ex,ey,ez, cx,cy,cz, ux,uy,uz);
233
- 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];
234
- out[0]=xx; out[1]=xy; out[2]=xz; out[3]=0;
235
- out[4]=yx; out[5]=yy; out[6]=yz; out[7]=0;
236
- out[8]=zx; out[9]=zy; out[10]=zz; out[11]=0;
237
- out[12]=ex; out[13]=ey; out[14]=ez; out[15]=1;
238
- return out;
239
- }
240
-
241
- // =========================================================================
242
- // Projection construction
243
- // =========================================================================
244
-
245
- /**
246
- * Orthographic projection matrix.
247
- *
248
- * @param {Float32Array|number[]} out 16-element destination.
249
- * @param {number} left,right,bottom,top Frustum extents.
250
- * @param {number} near,far Clip plane distances (positive).
251
- * @param {number} ndcZMin −1 (WEBGL) or 0 (WEBGPU).
252
- * @param {number} [ndcYSign=1] +1 = NDC y-up (default); −1 = NDC y-down (native Vulkan).
253
- */
254
- function mat4Ortho(out, left, right, bottom, top, near, far, ndcZMin, ndcYSign=1) {
255
- const rl=1/(right-left), tb=1/(top-bottom), fn=1/(far-near);
256
- out[0]=2*rl; out[1]=0; out[2]=0; out[3]=0;
257
- out[4]=0; out[5]=ndcYSign*2*tb; out[6]=0; out[7]=0;
258
- out[8]=0; out[9]=0; out[10]=(ndcZMin-1)*fn; out[11]=0;
259
- out[12]=-(right+left)*rl; out[13]=ndcYSign*(-(top+bottom)*tb);
260
- out[14]=(ndcZMin*far-near)*fn;
261
- out[15]=1;
262
- return out;
263
- }
264
-
265
- /**
266
- * Perspective projection matrix (general / off-centre frustum).
267
- * Symmetric case: left=-right, bottom=-top — derive from fov+aspect in user space:
268
- * top = near * Math.tan(fov / 2); right = top * aspect
269
- * mat4Persp(out, -right, right, -top, top, near, far, ndcZMin)
270
- *
271
- * @param {Float32Array|number[]} out 16-element destination.
272
- * @param {number} left,right,bottom,top Near-plane extents (signed, y-up: top>0, bottom<0).
273
- * @param {number} near,far Clip plane distances (positive).
274
- * @param {number} ndcZMin −1 (WEBGL) or 0 (WEBGPU).
275
- * @param {number} [ndcYSign=1] +1 = NDC y-up (default); −1 = NDC y-down (native Vulkan).
276
- */
277
- function mat4Persp(out, left, right, bottom, top, near, far, ndcZMin, ndcYSign=1) {
278
- const rl=1/(right-left), tb=1/(top-bottom);
279
- out[0]=2*near*rl; out[1]=0; out[2]=0; out[3]=0;
280
- out[4]=0; out[5]=ndcYSign*2*near*tb; out[6]=0; out[7]=0;
281
- out[8]=(right+left)*rl; out[9]=ndcYSign*(top+bottom)*tb;
282
- out[10]=(ndcZMin*near-far)/(far-near);
283
- out[11]=-1;
284
- out[12]=0; out[13]=0;
285
- out[14]=(ndcZMin-1)*far*near/(far-near);
286
- out[15]=0;
287
- return out;
288
- }
289
-
290
- /**
291
- * @file Matrix arithmetic, space-transform dispatch, and projection queries.
292
- * @module tree/query
293
- * @license AGPL-3.0-only
294
- *
295
- * The operative layer — receives matrices and extracts information.
296
- * Contrast with form.js which constructs matrices from specs.
297
- *
298
- * form.js — specs → matrix
299
- * query.js — matrix → information
300
- *
301
- * Storage: column-major Float32Array / ArrayLike<number>.
302
- * Multiply: mat4Mul(out, A, B) = A · B (standard math order).
303
- * Pipeline: clip = P · V · M · v
304
- *
305
- * ── NDC Z convention ──────────────────────────────────────────────────────
306
- * Passed as `ndcZMin` to every space-transform function:
307
- * WEBGL = −1 z ∈ [−1, 1]
308
- * WEBGPU = 0 z ∈ [ 0, 1]
309
- *
310
- * ── NDC Y convention ──────────────────────────────────────────────────────
311
- * Standard (OpenGL / WebGL / WebGPU browser / Three.js / p5v2):
312
- * NDC y-up — y = +1 at top, y = −1 at bottom.
313
- * Native Vulkan: NDC y-down — projections constructed with ndcYSign = −1
314
- * (see form.js). Query functions are convention-agnostic: they work on
315
- * whatever matrices are passed in.
316
- *
317
- * ── Viewport convention ───────────────────────────────────────────────────
318
- * vp = [x, y, w, h] — w and h are SIGNED.
319
- *
320
- * The sign of h encodes the relationship between NDC y and screen y:
321
- * h < 0 (e.g. −canvasH): screen y-DOWN (DOM / p5 mouseX·mouseY / Vulkan surface)
322
- * NDC y=+1 → screen y=0 (top)
323
- * NDC y=−1 → screen y=H (bottom)
324
- * h > 0 (e.g. +canvasH): screen y-UP (OpenGL desktop / WebGL gl_FragCoord)
325
- * NDC y=−1 → screen y=0 (bottom)
326
- * NDC y=+1 → screen y=H (top)
327
- *
328
- * Pass [0, canvasH, canvasW, −canvasH] for p5/DOM coordinates.
329
- * Pass [0, 0, canvasW, canvasH] for WebGL gl_FragCoord / OpenGL bottom-left.
330
- * All helpers use vp[2]/vp[3] signed — no Math.abs — so both conventions
331
- * work automatically without any branching.
332
- *
333
- * All functions follow the out-first, zero-allocation contract.
334
- * Returns null on degeneracy (singular matrix, etc.).
335
- */
336
-
337
-
338
- // ═══════════════════════════════════════════════════════════════════════════
339
- // Mat4 arithmetic
340
- // ═══════════════════════════════════════════════════════════════════════════
341
-
342
- /** out = A · B (column-major) */
343
- function mat4Mul(out, A, B) {
344
- const a0=A[0],a1=A[1],a2=A[2],a3=A[3],
345
- a4=A[4],a5=A[5],a6=A[6],a7=A[7],
346
- a8=A[8],a9=A[9],a10=A[10],a11=A[11],
347
- a12=A[12],a13=A[13],a14=A[14],a15=A[15];
348
- let b0=B[0],b1=B[1],b2=B[2],b3=B[3];
349
- out[0]=a0*b0+a4*b1+a8*b2+a12*b3;
350
- out[1]=a1*b0+a5*b1+a9*b2+a13*b3;
351
- out[2]=a2*b0+a6*b1+a10*b2+a14*b3;
352
- out[3]=a3*b0+a7*b1+a11*b2+a15*b3;
353
- b0=B[4];b1=B[5];b2=B[6];b3=B[7];
354
- out[4]=a0*b0+a4*b1+a8*b2+a12*b3;
355
- out[5]=a1*b0+a5*b1+a9*b2+a13*b3;
356
- out[6]=a2*b0+a6*b1+a10*b2+a14*b3;
357
- out[7]=a3*b0+a7*b1+a11*b2+a15*b3;
358
- b0=B[8];b1=B[9];b2=B[10];b3=B[11];
359
- out[8]=a0*b0+a4*b1+a8*b2+a12*b3;
360
- out[9]=a1*b0+a5*b1+a9*b2+a13*b3;
361
- out[10]=a2*b0+a6*b1+a10*b2+a14*b3;
362
- out[11]=a3*b0+a7*b1+a11*b2+a15*b3;
363
- b0=B[12];b1=B[13];b2=B[14];b3=B[15];
364
- out[12]=a0*b0+a4*b1+a8*b2+a12*b3;
365
- out[13]=a1*b0+a5*b1+a9*b2+a13*b3;
366
- out[14]=a2*b0+a6*b1+a10*b2+a14*b3;
367
- out[15]=a3*b0+a7*b1+a11*b2+a15*b3;
368
- return out;
369
- }
370
-
371
- /** out = inverse(src). Returns null if singular (|det| < 1e-12). */
372
- function mat4Invert(out, src) {
373
- const s0=src[0],s1=src[1],s2=src[2],s3=src[3],
374
- s4=src[4],s5=src[5],s6=src[6],s7=src[7],
375
- s8=src[8],s9=src[9],s10=src[10],s11=src[11],
376
- s12=src[12],s13=src[13],s14=src[14],s15=src[15];
377
- const b0=s0*s5-s1*s4, b1=s0*s6-s2*s4, b2=s0*s7-s3*s4,
378
- b3=s1*s6-s2*s5, b4=s1*s7-s3*s5, b5=s2*s7-s3*s6,
379
- b6=s8*s13-s9*s12, b7=s8*s14-s10*s12, b8=s8*s15-s11*s12,
380
- b9=s9*s14-s10*s13, b10=s9*s15-s11*s13, b11=s10*s15-s11*s14;
381
- let det=b0*b11-b1*b10+b2*b9+b3*b8-b4*b7+b5*b6;
382
- if (Math.abs(det) < 1e-12) return null;
383
- det = 1/det;
384
- out[0]=(s5*b11-s6*b10+s7*b9)*det;
385
- out[1]=(s2*b10-s1*b11-s3*b9)*det;
386
- out[2]=(s13*b5-s14*b4+s15*b3)*det;
387
- out[3]=(s10*b4-s9*b5-s11*b3)*det;
388
- out[4]=(s6*b8-s4*b11-s7*b7)*det;
389
- out[5]=(s0*b11-s2*b8+s3*b7)*det;
390
- out[6]=(s14*b2-s12*b5-s15*b1)*det;
391
- out[7]=(s8*b5-s10*b2+s11*b1)*det;
392
- out[8]=(s4*b10-s5*b8+s7*b6)*det;
393
- out[9]=(s1*b8-s0*b10-s3*b6)*det;
394
- out[10]=(s12*b4-s13*b2+s15*b0)*det;
395
- out[11]=(s9*b2-s8*b4-s11*b0)*det;
396
- out[12]=(s5*b7-s4*b9-s6*b6)*det;
397
- out[13]=(s0*b9-s1*b7+s2*b6)*det;
398
- out[14]=(s13*b1-s12*b3-s14*b0)*det;
399
- out[15]=(s8*b3-s9*b1+s10*b0)*det;
400
- return out;
401
- }
402
-
403
- /** out = mat4 * [x,y,z,1], perspective-divides, writes xyz. */
404
- function mat4MulPoint(out, m, x, y, z) {
405
- const rx=m[0]*x+m[4]*y+m[8]*z+m[12], ry=m[1]*x+m[5]*y+m[9]*z+m[13],
406
- rz=m[2]*x+m[6]*y+m[10]*z+m[14], rw=m[3]*x+m[7]*y+m[11]*z+m[15];
407
- if (rw!==0&&rw!==1) { out[0]=rx/rw; out[1]=ry/rw; out[2]=rz/rw; }
408
- else { out[0]=rx; out[1]=ry; out[2]=rz; }
409
- return out;
410
- }
411
-
412
- // ═══════════════════════════════════════════════════════════════════════════
413
- // Projection queries
414
- // ═══════════════════════════════════════════════════════════════════════════
415
-
416
- /** @returns {boolean} true if orthographic. */
417
- function projIsOrtho(p) { return p[15] !== 0; }
418
-
419
- /**
420
- * Near plane distance.
421
- * @param {ArrayLike<number>} p Projection mat4.
422
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
423
- */
424
- function projNear(p, ndcZMin) {
425
- return p[15]===0 ? p[14]/(p[10]+ndcZMin) : (p[14]-ndcZMin)/p[10];
426
- }
427
-
428
- /**
429
- * Top extent of the near plane in camera space (y_max, positive for standard y-up camera).
430
- * Sign-normalized: returns the larger of the two y boundaries regardless of whether
431
- * the projection was built with ndcYSign = +1 or −1.
432
- */
433
- function projTop(p, ndcZMin) {
434
- return p[15]===1
435
- ? ( Math.sign(p[5]) - p[13]) / p[5] // ortho
436
- : projNear(p,ndcZMin)*(Math.sign(p[5])+p[9])/p[5]; // perspective
437
- }
438
-
439
- /**
440
- * Bottom extent of the near plane in camera space (y_min, negative for standard y-up camera).
441
- */
442
- function projBottom(p, ndcZMin) {
443
- return p[15]===1
444
- ? (-Math.sign(p[5]) - p[13]) / p[5] // ortho
445
- : projNear(p,ndcZMin)*(p[9]-Math.sign(p[5]))/p[5]; // perspective
446
- }
447
-
448
- /** Vertical field of view in radians (perspective only). */
449
- function projFov (p) { return Math.abs(2*Math.atan(1/p[5])); }
450
-
451
- // ═══════════════════════════════════════════════════════════════════════════
452
- // Space transforms — mapLocation / mapDirection
453
- // ═══════════════════════════════════════════════════════════════════════════
454
- //
455
- // Flat dispatch: every from→to pair is a self-contained leaf with only stack
456
- // locals — no reentrancy, no shared state between calls.
457
- //
458
- // Matrices bag `m`:
459
- // mat4Proj Float32Array(16) projection (eye → clip)
460
- // mat4View Float32Array(16) view (world → eye)
461
- // mat4Eye? Float32Array(16) eye (eye → world); caller fills before passing
462
- // mat4PV? Float32Array(16) P · V; caller fills or _ensurePV allocates once
463
- // mat4PVInv? Float32Array(16) inv(P · V); caller fills
464
- // fromFrame? Float32Array(16) MATRIX source frame
465
- // toFrameInv? Float32Array(16) inv(MATRIX dest frame)
466
- //
467
- // Viewport `vp` = [x, y, w, h]:
468
- // Use SIGNED h to encode screen-y direction (see module header).
469
- // Core formula: screen = (ndc*0.5+0.5)*vp[k] + vp[k-2] (k=2 for x, k=3 for y)
470
- // Inverse: ndc = ((screen-vp[k-2])/vp[k])*2 - 1
471
- // Negative vp[3] flips NDC y-up to screen y-down automatically.
472
- //
473
-
474
- // ── Location helpers ─────────────────────────────────────────────────────
475
-
476
- function _worldToScreen(out, px, py, pz, pv, vp, ndcZMin) {
477
- const x=pv[0]*px+pv[4]*py+pv[8]*pz+pv[12], y=pv[1]*px+pv[5]*py+pv[9]*pz+pv[13],
478
- z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14], w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
479
- const xi=(w!==0&&w!==1)?1/w:1;
480
- out[0]=(x*xi*0.5+0.5)*vp[2]+vp[0];
481
- out[1]=(y*xi*0.5+0.5)*vp[3]+vp[1];
482
- out[2]=(z*xi-ndcZMin)/(1-ndcZMin);
483
- return out;
484
- }
485
-
486
- function _screenToWorld(out, sx, sy, sz, ipv, vp, ndcZMin) {
487
- return mat4MulPoint(out, ipv,
488
- ((sx-vp[0])/vp[2])*2-1,
489
- ((sy-vp[1])/vp[3])*2-1,
490
- sz*(1-ndcZMin)+ndcZMin);
491
- }
492
-
493
- function _worldToNDC(out, px, py, pz, pv) {
494
- const x=pv[0]*px+pv[4]*py+pv[8]*pz+pv[12], y=pv[1]*px+pv[5]*py+pv[9]*pz+pv[13],
495
- z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14], w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
496
- const xi=(w!==0&&w!==1)?1/w:1;
497
- out[0]=x*xi; out[1]=y*xi; out[2]=z*xi;
498
- return out;
499
- }
500
-
501
- function _ensurePV(m) {
502
- if (m.mat4PV) return m.mat4PV;
503
- m.mat4PV = new Float32Array(16);
504
- mat4Mul(m.mat4PV, m.mat4Proj, m.mat4View);
505
- return m.mat4PV;
506
- }
507
-
508
- /**
509
- * Map a point between named coordinate spaces.
510
- *
511
- * @param {number[]} out 3-element destination — written and returned.
512
- * @param {number} px,py,pz Input point.
513
- * @param {string} from Source space (WORLD, EYE, SCREEN, NDC, MATRIX).
514
- * @param {string} to Destination space.
515
- * @param {object} m Matrices bag — see module header.
516
- * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
517
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
518
- * @returns {number[]} out
519
- */
520
- function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
521
- if (to===SCREEN) return _worldToScreen(out,px,py,pz,_ensurePV(m),vp,ndcZMin);
522
-
523
- if (to===NDC) return _worldToNDC(out,px,py,pz,_ensurePV(m));
524
-
525
- if (to===EYE) return mat4MulPoint(out,m.mat4View,px,py,pz);
526
- if (to===MATRIX) return mat4MulPoint(out,m.toFrameInv,px,py,pz);
527
-
528
- out[0]=px; out[1]=py; out[2]=pz;
529
- return out;
530
- }
531
-
532
- // ── Direction helpers ────────────────────────────────────────────────────
533
- //
534
- // Directions use only the linear 3×3 block — no translation, no w-divide.
535
- // The signed vp[2]/vp[3] carries the y-convention automatically.
536
- //
537
-
538
- function _applyDir(out, m, dx, dy, dz) {
539
- out[0]=m[0]*dx+m[4]*dy+m[8]*dz;
540
- out[1]=m[1]*dx+m[5]*dy+m[9]*dz;
541
- out[2]=m[2]*dx+m[6]*dy+m[10]*dz;
542
- return out;
543
- }
544
-
545
- function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
546
- const vx=view[0]*dx+view[4]*dy+view[8]*dz,
547
- vy=view[1]*dx+view[5]*dy+view[9]*dz,
548
- vz=view[2]*dx+view[6]*dy+view[10]*dz;
549
- // vpH is signed — negative flips y component automatically.
550
- out[0]=(proj[0]*vx+proj[4]*vy+proj[8]*vz)*vpW*0.5;
551
- out[1]=(proj[1]*vx+proj[5]*vy+proj[9]*vz)*vpH*0.5;
552
- out[2]=(proj[2]*vx+proj[6]*vy+proj[10]*vz)*(1-ndcZMin)*0.5;
553
- return out;
554
- }
555
-
556
- function _screenToWorldDir(out, dx, dy, dz, proj, eye, vpW, vpH, ndcZMin) {
557
- // Inverse of _worldToScreenDir: undo the viewport scale, then the projection's
558
- // upper-triangular 3×3 block (p[8], p[9] carry an off-centre frustum), then
559
- // rotate eye→world. Signed vpW/vpH cancel the y-flip.
560
- const cz = dz/((1-ndcZMin)*0.5)/proj[10];
561
- const cy = (dy/(vpH*0.5) - proj[9]*cz)/proj[5];
562
- const cx = (dx/(vpW*0.5) - proj[8]*cz)/proj[0];
563
- return _applyDir(out, eye, cx, cy, cz);
564
- }
565
-
566
- function _screenToNDCDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
567
- out[0]=dx/(vpW*0.5); out[1]=dy/(vpH*0.5); out[2]=dz/((1-ndcZMin)*0.5);
568
- return out;
569
- }
570
-
571
- function _ndcToScreenDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
572
- out[0]=dx*vpW*0.5; out[1]=dy*vpH*0.5; out[2]=dz*(1-ndcZMin)*0.5;
573
- return out;
574
- }
575
-
576
- /**
577
- * Map a direction between named coordinate spaces.
578
- * Same bag and viewport contract as mapLocation.
579
- *
580
- * @param {number[]} out 3-element destination.
581
- * @param {number} dx,dy,dz Input direction.
582
- * @param {string} from Source space.
583
- * @param {string} to Destination space.
584
- * @param {object} m Matrices bag — see module header.
585
- * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
586
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
587
- * @returns {number[]} out
588
- */
589
- function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
590
- const vpW=vp[2], vpH=vp[3]; // signed — carry y-convention through all helpers
591
-
592
- if (from===EYE && to===WORLD) return _applyDir(out,m.mat4Eye, dx,dy,dz);
593
- if (from===WORLD && to===EYE) return _applyDir(out,m.mat4View,dx,dy,dz);
594
-
595
- if (from===WORLD && to===SCREEN) return _worldToScreenDir(out,dx,dy,dz,m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
596
- if (from===SCREEN && to===WORLD) return _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye, vpW,vpH,ndcZMin);
597
-
598
- if (from===SCREEN && to===NDC) return _screenToNDCDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
599
- if (from===NDC && to===SCREEN) return _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
600
-
601
- if (from===WORLD && to===NDC) {
602
- _worldToScreenDir(out,dx,dy,dz,m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
603
- return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
604
- }
605
- if (from===NDC && to===WORLD) {
606
- _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
607
- return _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
608
- }
609
-
610
- if (from===EYE && to===SCREEN) {
611
- _applyDir(out,m.mat4Eye,dx,dy,dz);
612
- return _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
613
- }
614
- if (from===SCREEN && to===EYE) {
615
- _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
616
- return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
617
- }
618
-
619
- if (from===EYE && to===NDC) {
620
- _applyDir(out,m.mat4Eye,dx,dy,dz);
621
- _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
622
- return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
623
- }
624
- if (from===NDC && to===EYE) {
625
- _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
626
- _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
627
- return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
628
- }
629
-
630
- if (from===MATRIX && to===WORLD) return _applyDir(out,m.fromFrame, dx,dy,dz);
631
- if (from===WORLD && to===MATRIX) return _applyDir(out,m.toFrameInv,dx,dy,dz);
632
-
633
- if (from===MATRIX && to===EYE) {
634
- _applyDir(out,m.fromFrame,dx,dy,dz);
635
- return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
636
- }
637
- if (from===EYE && to===MATRIX) {
638
- _applyDir(out,m.mat4Eye,dx,dy,dz);
639
- return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
640
- }
641
-
642
- if (from===MATRIX && to===SCREEN) {
643
- _applyDir(out,m.fromFrame,dx,dy,dz);
644
- return _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
645
- }
646
- if (from===SCREEN && to===MATRIX) {
647
- _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
648
- return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
649
- }
650
-
651
- if (from===MATRIX && to===NDC) {
652
- _applyDir(out,m.fromFrame,dx,dy,dz);
653
- _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
654
- return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
655
- }
656
- if (from===NDC && to===MATRIX) {
657
- _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
658
- _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
659
- return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
660
- }
661
-
662
- if (from===MATRIX && to===MATRIX) {
663
- _applyDir(out,m.fromFrame,dx,dy,dz);
664
- return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
665
- }
666
-
667
- out[0]=dx; out[1]=dy; out[2]=dz;
668
- return out;
669
- }
670
-
671
- // ═══════════════════════════════════════════════════════════════════════════
672
- // Rays and pointer hits
673
- // ═══════════════════════════════════════════════════════════════════════════
674
-
675
- /**
676
- * Screen point → world ray: origin on the near plane (screen depth 0), unit
677
- * direction toward the far plane (depth 1). Same bag and viewport contract as
678
- * mapLocation; `m.mat4PVInv` must be filled by the caller.
679
- *
680
- * @param {number[]} outO 3-element origin.
681
- * @param {number[]} outD 3-element unit direction.
682
- * @param {number} sx,sy Screen point.
683
- * @param {object} m Matrices bag — see module header.
684
- * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
685
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
686
- * @returns {number[]|null} outD, or null when the bag carries no mat4PVInv
687
- * (singular P·V) or the ray has no length.
688
- */
689
- function unproject(outO, outD, sx, sy, m, vp, ndcZMin) {
690
- const ipv = m.mat4PVInv;
691
- if (!ipv) return null;
692
- _screenToWorld(outO, sx, sy, 0, ipv, vp, ndcZMin);
693
- _screenToWorld(outD, sx, sy, 1, ipv, vp, ndcZMin);
694
- const dx=outD[0]-outO[0], dy=outD[1]-outO[1], dz=outD[2]-outO[2];
695
- const l=Math.sqrt(dx*dx+dy*dy+dz*dz);
696
- if (!(l > 0)) return null;
697
- outD[0]=dx/l; outD[1]=dy/l; outD[2]=dz/l;
698
- return outD;
699
- }
700
-
701
- // ═══════════════════════════════════════════════════════════════════════════
702
- // pixelRatio
703
- // ═══════════════════════════════════════════════════════════════════════════
704
-
705
- /**
706
- * World-units-per-pixel at a given eye-space Z depth.
707
- * @param {ArrayLike<number>} proj Projection mat4.
708
- * @param {number} vpH Viewport height in pixels (positive).
709
- * @param {number} eyeZ Eye-space Z — negative means in front of camera.
710
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
711
- */
712
- function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
713
- return projIsOrtho(proj)
714
- ? Math.abs(projTop(proj,ndcZMin)-projBottom(proj,ndcZMin)) / vpH
715
- : 2*Math.abs(eyeZ)*Math.tan(projFov(proj)/2) / vpH;
716
- }
717
-
718
- /**
719
- * @file Spline math and keyframe animation state machines.
720
- * @module tree/track
721
- * @license AGPL-3.0-only
722
- *
723
- * Quaternion algebra is provided by quat.js; projection matrix construction
724
- * by form.js. Spline helpers (hermiteVec3, lerpVec3) and TRS↔mat4 conversions
725
- * (transformToMat4, mat4ToTransform) live here because they are tightly
726
- * coupled to the PoseTrack keyframe shape.
727
- *
728
- * Zero dependencies on p5, DOM, WebGL, or WebGPU.
729
- *
730
- * ── Exports ──────────────────────────────────────────────────────────────
731
- * Quaternion helpers (re-exported from quat.js)
732
- * qSet qCopy qDot qNormalize qNegate qMul qConjugate qRotateVec3
733
- * qSlerp qNlerp qFromUnitVectors qFromAxisAngle qFromLookDir
734
- * qFromRotMat3x3 qFromMat4 qToMat4 qToAxisAngle
735
- * Spline / vector helpers
736
- * hermiteVec3 lerpVec3
737
- * Transform / mat4 helpers
738
- * transformToMat4 mat4ToTransform
739
- * Tracks
740
- * PoseTrack — { pos, rot, scl } TRS keyframes
741
- * CameraTrack — { eye, center, up, fov?, halfHeight?, near, far }
742
- * lookat keyframes
743
- *
744
- * ── Public path access (all zero-alloc, no cursor side effects) ──────────
745
- * PoseTrack
746
- * samplePos (out) | (out, seg, t) vec3
747
- * mat4Model (out) | (out, seg, t) mat4 — TRS model
748
- * tangents (outIn, outOut, index) vec3 × 2 at keyframe
749
- * eval (out?) TRS object at cursor
750
- *
751
- * CameraTrack
752
- * sampleEye (out) | (out, seg, t) vec3
753
- * sampleCenter (out) | (out, seg, t) vec3
754
- * mat4Eye (out) | (out, seg, t) mat4 — lookat frame
755
- * eyeTangents (outIn, outOut, index) vec3 × 2 at keyframe
756
- * centerTangents (outIn, outOut, index) vec3 × 2 at keyframe
757
- * eval (out?) { eye, center, up,
758
- * fov, halfHeight,
759
- * near, far }
760
- *
761
- * Two arities for the continuous family:
762
- * (out) cursor form — reads track.seg / track.f. Useful when the
763
- * track's own transport is driving the animation.
764
- * (out, seg, t) explicit form — continuous (seg, t) coordinate, no cursor
765
- * side effects. seg ∈ [0, segments−1] integer, t ∈ [0, 1]
766
- * local to that segment. As a convenience, seg === segments
767
- * is accepted and rewritten to (segments−1, 1) — so the
768
- * idiom sampleX(out, i, 0) uniformly addresses keyframe i
769
- * for every i ∈ [0, keyframes.length−1].
770
- *
771
- * All honour the track's interpolation mode (hermite / linear / step) and
772
- * the same stored-tangent → centripetal-CR fallback chain that eval() uses.
773
- *
774
- * `tangents` / `eyeTangents` / `centerTangents` are keyframe-indexed — a
775
- * keyframe's incoming tangent belongs to the previous segment, outgoing to
776
- * the next. At boundary keyframes the missing side mirrors the present one
777
- * so drawing arrows at endpoints always yields a visible vector.
778
- *
779
- * Projection matrices are deliberately not exposed as a track method. Each
780
- * CameraTrack keyframe stores `fov` (perspective) or `halfHeight` (ortho)
781
- * directly on track.keyframes[i] — callers wanting a projection build one
782
- * with the free mat4Persp / mat4Ortho constructors. Animated fov in
783
- * sketches flows through the bridge's camera-binding via eval().fov and
784
- * eval().halfHeight (the bridge calls cam.perspective() / cam.ortho() each
785
- * frame).
786
- *
787
- * ── Class hierarchy ──────────────────────────────────────────────────────
788
- * Track (unexported, never instantiated directly)
789
- * └── PoseTrack (exported)
790
- * └── CameraTrack (exported)
791
- *
792
- * ── Hook architecture ────────────────────────────────────────────────────
793
- * Lib-space hooks (underscore prefix — host layer / UI layer):
794
- * _onActivate / _onDeactivate — fire on playing false→true / true→false.
795
- * _onPlay / _onEnd / _onStop — mirror the user-space hooks.
796
- *
797
- * User-space hooks:
798
- * onPlay : fires in play() on false→true transition.
799
- * onEnd : fires in tick() at natural boundary (once mode only).
800
- * onStop : fires in stop() / reset().
801
- *
802
- * Firing order:
803
- * play() → onPlay → _onPlay → _onActivate
804
- * tick() → onEnd → _onEnd → _onDeactivate
805
- * stop() → onStop → _onStop → _onDeactivate
806
- * reset() → onStop → _onStop → _onDeactivate
807
- *
808
- * ── Loop modes ───────────────────────────────────────────────────────────
809
- * loop:false, bounce:false — play once, stop at end (fires onEnd)
810
- * loop:true, bounce:false — repeat, wrap back to start
811
- * loop:true, bounce:true — bounce forever at boundaries
812
- * loop:false, bounce:true — bounce once: flip at far boundary, stop at origin
813
- *
814
- * ── Playback semantics (rate + _dir) ─────────────────────────────────────
815
- * rate > 0 forward rate < 0 backward rate === 0 frozen
816
- *
817
- * play() is the sole setter of playing = true.
818
- * stop() is the sole setter of playing = false.
819
- * Assigning rate never starts or stops playback.
820
- *
821
- * _dir (internal, ±1) tracks the current bounce travel direction.
822
- * tick() advances by rate * _dir and flips _dir at boundaries.
823
- * _dir is reset to 1 only in reset().
824
- *
825
- * ── One-keyframe behaviour ───────────────────────────────────────────────
826
- * play() with exactly one keyframe snaps eval() to that keyframe without
827
- * setting playing = true and without firing hooks.
828
- */
829
-
830
-
831
- // =========================================================================
832
- // S2 Spline / vector helpers
833
- // =========================================================================
834
-
835
- function _dist3(a, b) {
836
- const dx=a[0]-b[0], dy=a[1]-b[1], dz=a[2]-b[2];
837
- return Math.sqrt(dx*dx+dy*dy+dz*dz);
838
- }
839
-
840
- /**
841
- * Cubic Hermite interpolation between p0 and p1 with explicit tangents.
842
- * @param {number[]} out 3-element result.
843
- * @param {number[]} p0 Segment start.
844
- * @param {number[]} m0 Outgoing tangent at p0.
845
- * @param {number[]} p1 Segment end.
846
- * @param {number[]} m1 Incoming tangent at p1.
847
- * @param {number} t Blend [0, 1].
848
- * @returns {number[]} out
849
- */
850
- const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
851
- const t2=t*t, t3=t2*t;
852
- const h00=2*t3-3*t2+1, h10=t3-2*t2+t, h01=-2*t3+3*t2, h11=t3-t2;
853
- out[0]=h00*p0[0]+h10*m0[0]+h01*p1[0]+h11*m1[0];
854
- out[1]=h00*p0[1]+h10*m0[1]+h01*p1[1]+h11*m1[1];
855
- out[2]=h00*p0[2]+h10*m0[2]+h01*p1[2]+h11*m1[2];
856
- return out;
857
- };
858
-
859
- // Centripetal CR outgoing tangent at p1 for segment p1→p2.
860
- // Signature: (out, p0, p1, p2). Returns tangent AT p1 (the middle point).
861
- const _crTanOut = (out, p0, p1, p2) => {
862
- const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
863
- for (let i=0;i<3;i++) out[i]=((p1[i]-p0[i])/dt0-(p2[i]-p0[i])/(dt0+dt1)+(p2[i]-p1[i])/dt1)*dt1;
864
- return out;
865
- };
866
-
867
- // Centripetal CR incoming tangent at p2 for segment p1→p2.
868
- // Signature: (out, p1, p2, p3). Returns tangent AT p2 (the middle point).
869
- const _crTanIn = (out, p1, p2, p3) => {
870
- const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
871
- for (let i=0;i<3;i++) out[i]=((p2[i]-p1[i])/dt1-(p3[i]-p1[i])/(dt1+dt2)+(p3[i]-p2[i])/dt2)*dt1;
872
- return out;
873
- };
874
-
875
- // Module-level scratch — shared across track instances (non-reentrant hot path).
876
- const _m0=[0,0,0], _m1=[0,0,0];
877
- const _trsScratch = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
878
- const _eyeScratch = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] };
879
-
880
- /**
881
- * Linear interpolation between two vec3s.
882
- */
883
- const lerpVec3 = (out, a, b, t) => {
884
- out[0]=a[0]+t*(b[0]-a[0]);
885
- out[1]=a[1]+t*(b[1]-a[1]);
886
- out[2]=a[2]+t*(b[2]-a[2]);
887
- return out;
888
- };
889
-
890
- // =========================================================================
891
- // S2b Path samplers — shared core
892
- // =========================================================================
893
-
894
- /** @private — sample interpolated vec3 path at (seg, t) into out. */
895
- function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t) {
896
- const n = kfs.length;
897
- if (n === 0) { out[0]=0; out[1]=0; out[2]=0; return out; }
898
- if (n === 1) {
899
- const p = kfs[0][field];
900
- out[0]=p[0]; out[1]=p[1]; out[2]=p[2];
901
- return out;
902
- }
903
- const nSeg = n - 1;
904
- seg = seg | 0;
905
- if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
906
- else if (seg < 0) { seg = 0; t = 0; }
907
- t = _clamp01(t);
908
- const k0 = kfs[seg];
909
- const k1 = kfs[seg + 1];
910
-
911
- if (interp === 'step') {
912
- const p = k0[field];
913
- out[0]=p[0]; out[1]=p[1]; out[2]=p[2];
914
- return out;
915
- }
916
- if (interp === 'linear') {
917
- return lerpVec3(out, k0[field], k1[field], t);
918
- }
919
-
920
- const p0 = seg > 0 ? kfs[seg - 1][field] : k0[field];
921
- const p3 = seg + 2 < n ? kfs[seg + 2][field] : k1[field];
922
- const m0 = k0[tanOutName] != null ? k0[tanOutName]
923
- : k0[tanInName] != null ? k0[tanInName]
924
- : _crTanOut(_m0, p0, k0[field], k1[field]);
925
- const m1 = k1[tanInName] != null ? k1[tanInName]
926
- : k1[tanOutName] != null ? k1[tanOutName]
927
- : _crTanIn(_m1, k0[field], k1[field], p3);
928
- return hermiteVec3(out, k0[field], m0, k1[field], m1, t);
929
- }
930
-
931
- /** @private — write effective in/out tangents at keyframe i. */
932
- function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i) {
933
- const n = kfs.length;
934
- if (n === 0) {
935
- outIn[0]=outIn[1]=outIn[2]=0; outOut[0]=outOut[1]=outOut[2]=0;
936
- return;
937
- }
938
- i = _clampS(i | 0, 0, n - 1);
939
- const ki = kfs[i];
940
- const hasTI = ki[tanInName] != null;
941
- const hasTO = ki[tanOutName] != null;
942
-
943
- if (hasTO) {
944
- outOut[0]=ki[tanOutName][0]; outOut[1]=ki[tanOutName][1]; outOut[2]=ki[tanOutName][2];
945
- } else if (hasTI) {
946
- outOut[0]=ki[tanInName][0]; outOut[1]=ki[tanInName][1]; outOut[2]=ki[tanInName][2];
947
- } else if (i < n - 1) {
948
- const k1 = kfs[i + 1];
949
- const p0 = i > 0 ? kfs[i - 1][field] : ki[field];
950
- _crTanOut(outOut, p0, ki[field], k1[field]);
951
- } else {
952
- outOut[0]=0; outOut[1]=0; outOut[2]=0;
953
- }
954
-
955
- if (hasTI) {
956
- outIn[0]=ki[tanInName][0]; outIn[1]=ki[tanInName][1]; outIn[2]=ki[tanInName][2];
957
- } else if (hasTO) {
958
- outIn[0]=ki[tanOutName][0]; outIn[1]=ki[tanOutName][1]; outIn[2]=ki[tanOutName][2];
959
- } else if (i > 0) {
960
- const k0 = kfs[i - 1];
961
- const p3 = i + 1 < n ? kfs[i + 1][field] : ki[field];
962
- _crTanIn(outIn, k0[field], ki[field], p3);
963
- } else {
964
- outIn[0]=outOut[0]; outIn[1]=outOut[1]; outIn[2]=outOut[2];
965
- }
966
-
967
- if (i === n - 1 && !hasTO && !hasTI) {
968
- outOut[0]=outIn[0]; outOut[1]=outIn[1]; outOut[2]=outIn[2];
969
- }
970
- }
971
-
972
- // =========================================================================
973
- // S3 Transform <-> Mat4
974
- // =========================================================================
975
-
976
- /**
977
- * Write a TRS transform into a column-major mat4.
978
- */
979
- const transformToMat4 = (out, xform) => {
980
- qToMat4(out, xform.rot);
981
- const sx=xform.scl[0], sy=xform.scl[1], sz=xform.scl[2];
982
- out[0]*=sx; out[1]*=sx; out[2]*=sx;
983
- out[4]*=sy; out[5]*=sy; out[6]*=sy;
984
- out[8]*=sz; out[9]*=sz; out[10]*=sz;
985
- out[12]=xform.pos[0]; out[13]=xform.pos[1]; out[14]=xform.pos[2];
986
- return out;
987
- };
988
-
989
- /**
990
- * Decompose a column-major mat4 into a TRS transform.
991
- */
992
- const mat4ToTransform = (out, m) => {
993
- out.pos[0]=m[12]; out.pos[1]=m[13]; out.pos[2]=m[14];
994
- const sx=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2]);
995
- const sy=Math.sqrt(m[4]*m[4]+m[5]*m[5]+m[6]*m[6]);
996
- const sz=Math.sqrt(m[8]*m[8]+m[9]*m[9]+m[10]*m[10]);
997
- out.scl[0]=sx; out.scl[1]=sy; out.scl[2]=sz;
998
- qFromRotMat3x3(out.rot,
999
- m[0]/sx,m[4]/sy,m[8]/sz,
1000
- m[1]/sx,m[5]/sy,m[9]/sz,
1001
- m[2]/sx,m[6]/sy,m[10]/sz);
1002
- return out;
1003
- };
1004
-
1005
- // =========================================================================
1006
- // S4a Spec parser — PoseTrack
1007
- // =========================================================================
1008
-
1009
- const _isNum$1 = (x) => typeof x === 'number' && Number.isFinite(x);
1010
- const _clamp01 = (x) => x < 0 ? 0 : (x > 1 ? 1 : x);
1011
- const _clampS = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
1012
-
1013
- function _parseVec3(v) {
1014
- if (!v) return null;
1015
- if (ArrayBuffer.isView(v) && v.length >= 3) return [v[0], v[1], v[2]];
1016
- if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0], v[1], v[2]];
1017
- if (typeof v === 'object' && 'x' in v) return [v.x || 0, v.y || 0, v.z || 0];
1018
- return null;
1019
- }
1020
-
1021
- const _EULER_AXES = { X:[1,0,0], Y:[0,1,0], Z:[0,0,1] };
1022
- const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
1023
-
1024
- /**
1025
- * Parse any rotation representation into a unit quaternion [x,y,z,w].
1026
- *
1027
- * Accepted forms:
1028
- * [x,y,z,w] — raw quaternion array
1029
- * { axis:[x,y,z], angle } — axis-angle (angle in radians)
1030
- * { dir:[x,y,z], up?:[x,y,z] } — forward direction (−Z) with optional up
1031
- * { mat4Eye: mat4 } — rotation block of an eye matrix
1032
- * { mat3: mat3 } — column-major 3×3 rotation matrix
1033
- * { euler:[rx,ry,rz], order? } — intrinsic Euler (default order: YXZ)
1034
- * { from:[x,y,z], to:[x,y,z] } — shortest-arc rotation between two vectors
1035
- *
1036
- * @param {*} v
1037
- * @returns {number[]|null} [x,y,z,w] or null if unparseable.
1038
- */
1039
- function _parseQuat(v) {
1040
- if (!v) return null;
1041
-
1042
- // [x,y,z,w] — normalised like every other form
1043
- if (Array.isArray(v) && v.length === 4) return qNormalize([v[0],v[1],v[2],v[3]]);
1044
- if (ArrayBuffer.isView(v) && v.length >= 4) return qNormalize([v[0],v[1],v[2],v[3]]);
1045
-
1046
- if (typeof v !== 'object') return null;
1047
-
1048
- // { axis, angle }
1049
- if (v.axis != null && v.angle != null) {
1050
- const ax = Array.isArray(v.axis) ? v.axis : [v.axis.x||0, v.axis.y||0, v.axis.z||0];
1051
- return qFromAxisAngle([0,0,0,1], ax[0],ax[1],ax[2], v.angle);
1052
- }
1053
-
1054
- // { dir, up? }
1055
- if (v.dir != null) {
1056
- const d = Array.isArray(v.dir) ? v.dir : [v.dir.x||0, v.dir.y||0, v.dir.z||0];
1057
- const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x||0, v.up.y||0, v.up.z||0]) : null;
1058
- return qFromLookDir([0,0,0,1], d, u);
1059
- }
1060
-
1061
- // { mat4Eye }
1062
- if (v.mat4Eye != null) {
1063
- const m = (ArrayBuffer.isView(v.mat4Eye) || Array.isArray(v.mat4Eye))
1064
- ? v.mat4Eye : (v.mat4Eye.mat4 ?? null);
1065
- if (!m || m.length < 16) return null;
1066
- return qFromRotMat3x3([0,0,0,1], m[0],m[4],m[8], m[1],m[5],m[9], m[2],m[6],m[10]);
1067
- }
1068
-
1069
- // { mat3 }
1070
- if (v.mat3 != null) {
1071
- const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3)) ? v.mat3 : null;
1072
- if (!m || m.length < 9) return null;
1073
- return qFromRotMat3x3([0,0,0,1], m[0],m[3],m[6], m[1],m[4],m[7], m[2],m[5],m[8]);
1074
- }
1075
-
1076
- // { euler, order? }
1077
- if (v.euler != null) {
1078
- const e = v.euler;
1079
- if (!Array.isArray(e) || e.length < 3) return null;
1080
- const order = (v.order && _EULER_ORDERS.has(v.order)) ? v.order : 'YXZ';
1081
- const q = [0,0,0,1], s = [0,0,0,1];
1082
- for (let i = 0; i < 3; i++) {
1083
- const ax = _EULER_AXES[order[i]];
1084
- qMul(q, q, qFromAxisAngle(s, ax[0],ax[1],ax[2], e[i]));
1085
- }
1086
- return q;
1087
- }
1088
-
1089
- // { from, to }
1090
- if (v.from != null && v.to != null) {
1091
- const f = Array.isArray(v.from) ? v.from : [v.from.x||0, v.from.y||0, v.from.z||0];
1092
- const t = Array.isArray(v.to) ? v.to : [v.to.x||0, v.to.y||0, v.to.z||0];
1093
- const fl = Math.sqrt(f[0]*f[0]+f[1]*f[1]+f[2]*f[2]) || 1;
1094
- const tl = Math.sqrt(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]) || 1;
1095
- const fx=f[0]/fl, fy=f[1]/fl, fz=f[2]/fl;
1096
- const tx=t[0]/tl, ty=t[1]/tl, tz=t[2]/tl;
1097
- const dot = fx*tx + fy*ty + fz*tz;
1098
- if (dot >= 1 - 1e-8) return [0,0,0,1];
1099
- if (dot <= -1 + 1e-8) {
1100
- let px=0, py=fz, pz=-fy;
1101
- let pl = Math.sqrt(px*px+py*py+pz*pz);
1102
- if (pl < 1e-8) { px=fy; py=-fx; pz=0; pl = Math.sqrt(px*px+py*py+pz*pz); }
1103
- if (pl < 1e-8) return [0,0,0,1];
1104
- return qFromAxisAngle([0,0,0,1], px/pl,py/pl,pz/pl, Math.PI);
1105
- }
1106
- let ax=fy*tz-fz*ty, ay=fz*tx-fx*tz, az=fx*ty-fy*tx;
1107
- const al = Math.sqrt(ax*ax+ay*ay+az*az) || 1;
1108
- return qFromAxisAngle([0,0,0,1], ax/al,ay/al,az/al,
1109
- Math.acos(Math.max(-1, Math.min(1, dot))));
1110
- }
1111
-
1112
- return null;
1113
- }
1114
-
1115
- /**
1116
- * Parse a PoseTrack keyframe spec into internal form.
1117
- *
1118
- * Accepted forms:
1119
- * { mat4Model }
1120
- * Decompose a column-major mat4 into TRS via mat4ToTransform.
1121
- * Float32Array(16), plain Array, or { mat4 } wrapper.
1122
- *
1123
- * { pos?, rot?, scl?, tanIn?, tanOut? }
1124
- * Explicit TRS. pos and scl are vec3; rot accepts any form from _parseQuat.
1125
- * All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
1126
- * missing rot defaults to identity.
1127
- * tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
1128
- *
1129
- * @param {Object} spec
1130
- * @returns {{ pos:number[], rot:number[], scl:number[],
1131
- * tanIn:number[]|null, tanOut:number[]|null } | null}
1132
- */
1133
- function _parseSpec(spec) {
1134
- if (!spec || typeof spec !== 'object') return null;
1135
-
1136
- // { mat4Model } — full TRS decomposition from model matrix
1137
- if (spec.mat4Model != null) {
1138
- const m = (ArrayBuffer.isView(spec.mat4Model) || Array.isArray(spec.mat4Model))
1139
- ? spec.mat4Model : (spec.mat4Model.mat4 ?? null);
1140
- if (!m || m.length < 16) return null;
1141
- const kf = mat4ToTransform({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }, m);
1142
- kf.tanIn = null; kf.tanOut = null;
1143
- return kf;
1144
- }
1145
-
1146
- // { pos?, rot?, scl?, tanIn?, tanOut? } — explicit TRS
1147
- const pos = _parseVec3(spec.pos) || [0,0,0];
1148
- const rot = _parseQuat(spec.rot) || [0,0,0,1];
1149
- const scl = _parseVec3(spec.scl) || [1,1,1];
1150
- const tanIn = _parseVec3(spec.tanIn) || null;
1151
- const tanOut = _parseVec3(spec.tanOut) || null;
1152
- return { pos, rot, scl, tanIn, tanOut };
1153
- }
1154
-
1155
- function _sameTransform(a, b) {
1156
- for (let i=0;i<3;i++) if (a.pos[i]!==b.pos[i]||a.scl[i]!==b.scl[i]) return false;
1157
- for (let i=0;i<4;i++) if (a.rot[i]!==b.rot[i]) return false;
1158
- return true;
1159
- }
1160
-
1161
- // =========================================================================
1162
- // S4b Spec parser — CameraTrack
1163
- // =========================================================================
1164
-
1165
- // Lens defaults used when a spec omits near/far. Match the three.js /
1166
- // Bevy conventions and are safe for typical p5 v2 scene scales.
1167
- const _DEFAULT_NEAR = 0.1;
1168
- const _DEFAULT_FAR = 1000;
1169
-
1170
- /**
1171
- * Parse a CameraTrack keyframe spec into internal form.
1172
- *
1173
- * Required: eye (vec3). Everything else is optional.
1174
- * center defaults to [0, 0, 0]
1175
- * up defaults to [0, 1, 0] and is normalised
1176
- * fov vertical fov (radians), perspective only — null if absent
1177
- * halfHeight world-unit half-height of ortho frustum — null if absent
1178
- * near near clip distance (positive) — defaults to 0.1
1179
- * far far clip distance (positive) — defaults to 1000
1180
- * eyeTanIn/Out optional Hermite tangents for the eye path
1181
- * centerTanIn/Out optional Hermite tangents for the center path
1182
- *
1183
- * fov and halfHeight are mutually exclusive (perspective xor ortho) and
1184
- * therefore left nullable; eval() lerps each only when both adjacent
1185
- * keyframes carry a non-null value, passing null through otherwise.
1186
- *
1187
- * near and far are always meaningful regardless of projection type, so
1188
- * they receive real defaults and are linearly interpolated unconditionally.
1189
- *
1190
- * @param {Object} spec
1191
- * @returns {Object|null} Parsed keyframe or null if eye is missing/malformed.
1192
- */
1193
- function _parseCameraSpec(spec) {
1194
- if (!spec || typeof spec !== 'object') return null;
1195
- const eye = _parseVec3(spec.eye);
1196
- if (!eye) return null;
1197
- const center = _parseVec3(spec.center) || [0,0,0];
1198
- const upRaw = spec.up ? _parseVec3(spec.up) : null;
1199
- const up = upRaw || [0,1,0];
1200
- const ul = Math.sqrt(up[0]*up[0]+up[1]*up[1]+up[2]*up[2]) || 1;
1201
- return {
1202
- eye, center,
1203
- up: [up[0]/ul, up[1]/ul, up[2]/ul],
1204
- fov: typeof spec.fov === 'number' ? spec.fov : null,
1205
- halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
1206
- near: typeof spec.near === 'number' ? spec.near : _DEFAULT_NEAR,
1207
- far: typeof spec.far === 'number' ? spec.far : _DEFAULT_FAR,
1208
- eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
1209
- eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
1210
- centerTanIn: _parseVec3(spec.centerTanIn) || null,
1211
- centerTanOut: _parseVec3(spec.centerTanOut)|| null,
1212
- };
1213
- }
1214
-
1215
- function _sameCameraKeyframe(a, b) {
1216
- for (let i=0;i<3;i++) {
1217
- if (a.eye[i]!==b.eye[i]) return false;
1218
- if (a.center[i]!==b.center[i]) return false;
1219
- if (a.up[i]!==b.up[i]) return false;
1220
- }
1221
- if (a.fov !== b.fov) return false;
1222
- if (a.halfHeight !== b.halfHeight) return false;
1223
- if (a.near !== b.near) return false;
1224
- if (a.far !== b.far) return false;
1225
- return true;
1226
- }
1227
-
1228
- // =========================================================================
1229
- // S5 Track — unexported base class (transport machinery only)
1230
- // =========================================================================
1231
-
1232
- class Track {
1233
- constructor() {
1234
- /** @type {Array} Keyframe array — shape depends on subclass. */
1235
- this.keyframes = [];
1236
- /** Whether playback is currently active. @type {boolean} */
1237
- this.playing = false;
1238
- /** Loop at boundaries. @type {boolean} */
1239
- this.loop = false;
1240
- /** Ping-pong bounce at boundaries (independent of loop). @type {boolean} */
1241
- this.bounce = false;
1242
- /** Frames per segment (≥1). @type {number} */
1243
- this.duration = 30;
1244
- /** Current segment index. @type {number} */
1245
- this.seg = 0;
1246
- /** Frame offset within current segment (can be fractional). @type {number} */
1247
- this.f = 0;
1248
-
1249
- // Playback rate (signed: negative = reverse; 0 = frozen).
1250
- this._rate = 1;
1251
- // Current bounce travel direction (±1). Reset to 1 only on reset().
1252
- this._dir = 1;
1253
- // Whether a bounce-once has already flipped direction.
1254
- this._bounced = false;
1255
-
1256
- /** User hook: fires on play() false→true transition. @type {Function|null} */
1257
- this.onPlay = null;
1258
- /** User hook: fires on natural boundary in once mode. @type {Function|null} */
1259
- this.onEnd = null;
1260
- /** User hook: fires on stop() / reset() / end-of-bounce-once. @type {Function|null} */
1261
- this.onStop = null;
1262
-
1263
- // Lib-space hooks (underscore prefix — host layer / UI layer only).
1264
- this._onActivate = null; this._onDeactivate = null;
1265
- this._onPlay = null; this._onEnd = null; this._onStop = null;
1266
- }
1267
-
1268
- /** Playback rate. Signed: negative reverses, 0 freezes. Assigning never starts or stops playback. @type {number} */
1269
- get rate() { return this._rate; }
1270
- set rate(v) { this._rate = (_isNum$1(v)) ? v : 1; }
1271
-
1272
- /** Number of interpolatable segments (keyframes.length − 1, min 0). @type {number} */
1273
- get segments() { return Math.max(0, this.keyframes.length - 1); }
1274
-
1275
- /**
1276
- * @private — resolve cursor (seg, f) into continuous (seg, t).
1277
- * Shared backing of every cursor-form sampler / matrix method.
1278
- */
1279
- _cursorSegT() {
1280
- const nSeg = this.segments;
1281
- const dur = Math.max(1, this.duration | 0);
1282
- const seg = nSeg > 0 ? _clampS(this.seg, 0, nSeg - 1) : 0;
1283
- const t = _clamp01(this.f / dur);
1284
- return [seg, t];
1285
- }
1286
-
1287
- /**
1288
- * Start or update playback. Sole setter of `playing = true`.
1289
- *
1290
- * Fires `onPlay → _onPlay → _onActivate` only on a false→true transition.
1291
- * Zero keyframes: no-op. Exactly one keyframe: snaps eval() to it but does
1292
- * not set `playing` and fires no hooks.
1293
- *
1294
- * @param {number|{duration?:number,loop?:boolean,bounce?:boolean,
1295
- * rate?:number,onPlay?:Function,onEnd?:Function,
1296
- * onStop?:Function}} [rateOrOpts]
1297
- * A bare number is taken as `rate`; an object configures multiple
1298
- * fields in one call.
1299
- * @returns {Track} this
1300
- */
1301
- play(rateOrOpts) {
1302
- if (this.keyframes.length === 0) return this;
1303
- if (this.keyframes.length === 1) { this.seg = 0; this.f = 0; return this; }
1304
-
1305
- if (typeof rateOrOpts === 'number' && Number.isFinite(rateOrOpts)) {
1306
- this._rate = rateOrOpts;
1307
- } else if (rateOrOpts && typeof rateOrOpts === 'object') {
1308
- const o = rateOrOpts;
1309
- if (_isNum$1(o.duration)) this.duration = Math.max(1, o.duration | 0);
1310
- if ('loop' in o) this.loop = !!o.loop;
1311
- if ('bounce' in o) this.bounce = !!o.bounce;
1312
- if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
1313
- if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
1314
- if (typeof o.onStop === 'function') this.onStop = o.onStop;
1315
- if (_isNum$1(o.rate)) this._rate = o.rate;
1316
- }
1317
-
1318
- const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
1319
- if (this.seg < 0) this.seg = 0;
1320
- if (this.seg >= nSeg) this.seg = nSeg - 1;
1321
- if (this.f < 0) this.f = 0;
1322
- if (this.f > dur) this.f = dur;
1323
-
1324
- const wasPlaying = this.playing;
1325
- this.playing = true;
1326
- if (!wasPlaying) {
1327
- this._bounced = false;
1328
- if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
1329
- this._onPlay?.();
1330
- this._onActivate?.();
1331
- }
1332
- return this;
1333
- }
1334
-
1335
- /**
1336
- * Stop playback. Sole setter of `playing = false`. Fires
1337
- * `onStop → _onStop → _onDeactivate` on a true→false transition.
1338
- *
1339
- * @param {boolean} [rewind] If true, seek to the origin end after stopping
1340
- * (0 when playing forward, 1 when playing backward).
1341
- * @returns {Track} this
1342
- */
1343
- stop(rewind) {
1344
- const wasPlaying = this.playing;
1345
- this.playing = false;
1346
- if (wasPlaying) {
1347
- this._bounced = false;
1348
- if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
1349
- this._onStop?.();
1350
- this._onDeactivate?.();
1351
- if (rewind && this.keyframes.length > 1) this.seek(this._rate * this._dir < 0 ? 1 : 0);
1352
- }
1353
- return this;
1354
- }
1355
-
1356
- /**
1357
- * Clear all keyframes and stop. Fires stop-side hooks if it was playing.
1358
- * Unlike stop(), this also resets `_dir` to +1.
1359
- * @returns {Track} this
1360
- */
1361
- reset() {
1362
- const wasPlaying = this.playing;
1363
- this.playing = false;
1364
- if (wasPlaying) {
1365
- if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
1366
- this._onStop?.();
1367
- this._onDeactivate?.();
1368
- }
1369
- this.keyframes.length = 0;
1370
- this.seg = 0; this.f = 0; this._dir = 1; this._bounced = false;
1371
- return this;
1372
- }
1373
-
1374
- /**
1375
- * Remove the keyframe at `index`. Adjusts the cursor if the removal shrinks
1376
- * the track below the current segment.
1377
- * @param {number} index
1378
- * @returns {boolean} true if removed; false if index was invalid.
1379
- */
1380
- remove(index) {
1381
- if (!_isNum$1(index)) return false;
1382
- const i = index | 0;
1383
- if (i < 0 || i >= this.keyframes.length) return false;
1384
- this.keyframes.splice(i, 1);
1385
- const nSeg = this.segments;
1386
- if (nSeg === 0) { this.seg = 0; this.f = 0; }
1387
- else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
1388
- return true;
1389
- }
1390
-
1391
- /**
1392
- * Move the cursor.
1393
- * seek(t) Scrub to normalised position t ∈ [0, 1] across the
1394
- * whole track.
1395
- * seek(t, segIndex) Position within a specific segment — t is local to
1396
- * that segment.
1397
- * Does not affect `playing`.
1398
- *
1399
- * @param {number} t
1400
- * @param {number} [segIndex]
1401
- * @returns {Track} this
1402
- */
1403
- seek(t, segIndex) {
1404
- const nSeg = this.segments;
1405
- if (nSeg === 0) { this.seg = 0; this.f = 0; return this; }
1406
- const dur = Math.max(1, this.duration | 0);
1407
- if (_isNum$1(segIndex)) {
1408
- this.seg = _clampS(segIndex | 0, 0, nSeg - 1);
1409
- this.f = _clamp01(t) * dur;
1410
- } else {
1411
- this._setCursorFromScalar(_clamp01(t) * nSeg * dur);
1412
- }
1413
- return this;
1414
- }
1415
-
1416
- /**
1417
- * Normalised cursor position across the whole track.
1418
- * @returns {number} ∈ [0, 1]; 0 when there are no segments.
1419
- */
1420
- time() {
1421
- const nSeg = this.segments;
1422
- if (nSeg === 0) return 0;
1423
- const dur = Math.max(1, this.duration | 0);
1424
- return _clamp01((this.seg * dur + this.f) / (nSeg * dur));
1425
- }
1426
-
1427
- /**
1428
- * Snapshot of transport state. Allocates a new object per call — intended
1429
- * for UI / debugging, not hot loops.
1430
- * @returns {{keyframes:number, segments:number, seg:number, f:number,
1431
- * playing:boolean, loop:boolean, bounce:boolean, rate:number,
1432
- * duration:number, time:number}}
1433
- */
1434
- info() {
1435
- return {
1436
- keyframes: this.keyframes.length,
1437
- segments: this.segments,
1438
- seg: this.seg,
1439
- f: this.f,
1440
- playing: this.playing,
1441
- loop: this.loop,
1442
- bounce: this.bounce,
1443
- rate: this._rate,
1444
- duration: this.duration,
1445
- time: this.segments > 0 ? this.time() : 0
1446
- };
1447
- }
1448
-
1449
- /**
1450
- * Advance the cursor by `rate * _dir` in frames. Handles loop / bounce /
1451
- * once modes per the table in the module header. Fires `onEnd → _onEnd →
1452
- * _onDeactivate` at a natural boundary in once mode.
1453
- *
1454
- * Intended to be called once per animation frame by the bridge / UI layer.
1455
- * rate === 0 freezes the cursor but keeps `playing` unchanged.
1456
- *
1457
- * @returns {boolean} Current `playing` state after advancing.
1458
- */
1459
- tick() {
1460
- if (!this.playing) return false;
1461
- const nSeg = this.segments;
1462
- if (nSeg === 0) { this.playing = false; this._onDeactivate?.(); return false; }
1463
- if (this._rate === 0) return true;
1464
-
1465
- const dur = Math.max(1, this.duration | 0);
1466
- const total = nSeg * dur;
1467
- const s = _clampS(this.seg * dur + this.f, 0, total);
1468
- const next = s + this._rate * this._dir;
1469
-
1470
- if (this.loop && this.bounce) {
1471
- let pos = next, flips = 0;
1472
- while (pos < 0 || pos > total) {
1473
- if (pos < 0) { pos = -pos; flips++; }
1474
- else { pos = 2 * total - pos; flips++; }
1475
- }
1476
- if (flips & 1) this._dir = -this._dir;
1477
- this._setCursorFromScalar(pos);
1478
- return true;
1479
- }
1480
-
1481
- if (!this.loop && this.bounce) {
1482
- if (next >= total) {
1483
- this._setCursorFromScalar(Math.min(total, 2 * total - next));
1484
- this._dir = -this._dir;
1485
- this._bounced = true;
1486
- return true;
1487
- }
1488
- if (next <= 0) {
1489
- this._setCursorFromScalar(0);
1490
- this.playing = false;
1491
- this._dir = 1; this._bounced = false;
1492
- if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
1493
- this._onEnd?.();
1494
- this._onDeactivate?.();
1495
- return false;
1496
- }
1497
- this._setCursorFromScalar(next);
1498
- return true;
1499
- }
1500
-
1501
- if (this.loop) {
1502
- this._setCursorFromScalar(((next % total) + total) % total);
1503
- return true;
1504
- }
1505
-
1506
- if (next <= 0) {
1507
- this._setCursorFromScalar(0);
1508
- this.playing = false;
1509
- if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
1510
- this._onEnd?.();
1511
- this._onDeactivate?.();
1512
- return false;
1513
- }
1514
- if (next >= total) {
1515
- this._setCursorFromScalar(total);
1516
- this.playing = false;
1517
- if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
1518
- this._onEnd?.();
1519
- this._onDeactivate?.();
1520
- return false;
1521
- }
1522
-
1523
- this._setCursorFromScalar(next);
1524
- return true;
1525
- }
1526
-
1527
- /** @private */
1528
- _setCursorFromScalar(s) {
1529
- const dur = Math.max(1, this.duration | 0);
1530
- const nSeg = this.segments;
1531
- this.seg = Math.floor(s / dur);
1532
- this.f = s - this.seg * dur;
1533
- if (this.seg >= nSeg) { this.seg = nSeg - 1; this.f = dur; }
1534
- if (this.seg < 0) { this.seg = 0; this.f = 0; }
1535
- }
1536
- }
1537
-
1538
- // =========================================================================
1539
- // S6 PoseTrack
1540
- // =========================================================================
1541
-
1542
- /**
1543
- * Renderer-agnostic TRS keyframe track.
1544
- *
1545
- * Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z],
1546
- * tanIn?:[x,y,z], tanOut?:[x,y,z] }
1547
- *
1548
- * tanIn — incoming position tangent at this keyframe (Hermite mode).
1549
- * tanOut — outgoing position tangent at this keyframe (Hermite mode).
1550
- * When only one is supplied, the other mirrors it at sample time.
1551
- * When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
1552
- */
1553
- class PoseTrack extends Track {
1554
- constructor() {
1555
- super();
1556
- /**
1557
- * Position interpolation mode.
1558
- * - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
1559
- * tangents when none are stored (default)
1560
- * - 'linear' — lerp
1561
- * - 'step' — snap to k0; useful for discrete state changes
1562
- * @type {'hermite'|'linear'|'step'}
1563
- */
1564
- this.posInterp = 'hermite';
1565
- /**
1566
- * Rotation interpolation mode.
1567
- * - 'slerp' — constant angular velocity (default)
1568
- * - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
1569
- * - 'step' — snap to k0 quaternion
1570
- * @type {'slerp'|'nlerp'|'step'}
1571
- */
1572
- this.rotInterp = 'slerp';
1573
- }
1574
-
1575
- /**
1576
- * Append one or more keyframes. Adjacent duplicates are skipped by default.
1577
- * Accepts any spec form understood by _parseSpec, or an array of them.
1578
- * @param {Object|Object[]} spec
1579
- * @param {{ deduplicate?: boolean }} [opts]
1580
- */
1581
- add(spec, opts) {
1582
- if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
1583
- const kf = _parseSpec(spec);
1584
- if (!kf) return;
1585
- const dedup = !opts || opts.deduplicate !== false;
1586
- if (dedup && this.keyframes.length > 0) {
1587
- if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
1588
- }
1589
- this.keyframes.push(kf);
1590
- }
1591
-
1592
- /**
1593
- * Replace the keyframe at `index`, or append at the end if `index` equals
1594
- * the current keyframe count.
1595
- * @param {number} index
1596
- * @param {Object} spec Any spec form understood by _parseSpec.
1597
- * @returns {boolean} true on success; false for invalid index or spec.
1598
- */
1599
- set(index, spec) {
1600
- if (!_isNum$1(index)) return false;
1601
- const i = index | 0, kf = _parseSpec(spec);
1602
- if (!kf || i < 0 || i > this.keyframes.length) return false;
1603
- if (i === this.keyframes.length) this.keyframes.push(kf);
1604
- else this.keyframes[i] = kf;
1605
- return true;
1606
- }
1607
-
1608
- /**
1609
- * Sample the position path. Zero-alloc, no cursor side effects. Honours
1610
- * posInterp and the stored-tangent → auto-CR fallback chain.
1611
- *
1612
- * Two signatures:
1613
- * samplePos(out) cursor form — reads current seg/f
1614
- * samplePos(out, seg, t) explicit (seg, t), continuous on the path
1615
- *
1616
- * @param {number[]} out 3-element result buffer.
1617
- * @param {number} [seg] Segment index in [0, segments−1]. Omit for cursor.
1618
- * @param {number} [t] Local parameter in [0, 1]. Omit for cursor.
1619
- * @returns {number[]} out
1620
- */
1621
- samplePos(out, seg, t) {
1622
- if (arguments.length < 3) [seg, t] = this._cursorSegT();
1623
- return _samplePathCore(out, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
1624
- }
1625
-
1626
- /**
1627
- * Write the TRS pose as a column-major model mat4. Zero-alloc, no cursor
1628
- * side effects.
1629
- *
1630
- * Two signatures:
1631
- * mat4Model(out) cursor form — reads current seg/f
1632
- * mat4Model(out, seg, t) explicit (seg, t), continuous on the path
1633
- *
1634
- * Replaces the previous toMatrix() method.
1635
- *
1636
- * @param {Float32Array|number[]} out 16-element result buffer.
1637
- * @param {number} [seg] Segment index in [0, segments−1]. Omit for cursor.
1638
- * @param {number} [t] Local parameter in [0, 1]. Omit for cursor.
1639
- * @returns {Float32Array|number[]} out
1640
- */
1641
- mat4Model(out, seg, t) {
1642
- if (arguments.length < 3) [seg, t] = this._cursorSegT();
1643
- this._sampleTRS(_trsScratch, seg, t);
1644
- return transformToMat4(out, _trsScratch);
1645
- }
1646
-
1647
- /**
1648
- * Effective incoming / outgoing position tangents at keyframe `index`.
1649
- * Stored tanIn/tanOut take precedence; each mirrors the other when only
1650
- * one is stored; else centripetal Catmull-Rom tangents are auto-computed
1651
- * from neighbours. Boundary keyframes mirror across the missing side.
1652
- *
1653
- * @param {number[]} outIn 3-element result — incoming tangent.
1654
- * @param {number[]} outOut 3-element result — outgoing tangent.
1655
- * @param {number} index Keyframe index.
1656
- * @returns {PoseTrack} this
1657
- */
1658
- tangents(outIn, outOut, index) {
1659
- _sampleTangentsCore(outIn, outOut, this.keyframes, 'pos', 'tanIn', 'tanOut', index);
1660
- return this;
1661
- }
1662
-
1663
- /** @private — write interpolated TRS at (seg, t). */
1664
- _sampleTRS(out, seg, t) {
1665
- const n = this.keyframes.length;
1666
- if (n === 0) return out;
1667
- if (n === 1) {
1668
- const k = this.keyframes[0];
1669
- out.pos[0]=k.pos[0]; out.pos[1]=k.pos[1]; out.pos[2]=k.pos[2];
1670
- out.rot[0]=k.rot[0]; out.rot[1]=k.rot[1]; out.rot[2]=k.rot[2]; out.rot[3]=k.rot[3];
1671
- out.scl[0]=k.scl[0]; out.scl[1]=k.scl[1]; out.scl[2]=k.scl[2];
1672
- return out;
1673
- }
1674
- const nSeg = n - 1;
1675
- seg = seg | 0;
1676
- if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
1677
- else if (seg < 0) { seg = 0; t = 0; }
1678
- t = _clamp01(t);
1679
- const k0 = this.keyframes[seg];
1680
- const k1 = this.keyframes[seg + 1];
1681
-
1682
- _samplePathCore(out.pos, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
1683
-
1684
- if (this.rotInterp === 'step') {
1685
- out.rot[0]=k0.rot[0]; out.rot[1]=k0.rot[1]; out.rot[2]=k0.rot[2]; out.rot[3]=k0.rot[3];
1686
- } else if (this.rotInterp === 'nlerp') {
1687
- qNlerp(out.rot, k0.rot, k1.rot, t);
1688
- } else {
1689
- qSlerp(out.rot, k0.rot, k1.rot, t);
1690
- }
1691
-
1692
- lerpVec3(out.scl, k0.scl, k1.scl, t);
1693
- return out;
1694
- }
1695
-
1696
- /**
1697
- * Evaluate interpolated TRS pose at current cursor.
1698
- * @param {{ pos:number[], rot:number[], scl:number[] }} [out]
1699
- * @returns {{ pos:number[], rot:number[], scl:number[] }} out
1700
- */
1701
- eval(out) {
1702
- out = out || { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
1703
- const n = this.keyframes.length;
1704
- if (n === 0) return out;
1705
- if (n === 1) return this._sampleTRS(out, 0, 0);
1706
- const [seg, t] = this._cursorSegT();
1707
- return this._sampleTRS(out, seg, t);
1708
- }
1709
- }
1710
-
1711
- // =========================================================================
1712
- // S7 CameraTrack
1713
- // =========================================================================
1714
-
1715
- /**
1716
- * Lookat camera keyframe track.
1717
- *
1718
- * Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z],
1719
- * fov?:number, halfHeight?:number,
1720
- * near:number, far:number,
1721
- * eyeTanIn?:[x,y,z], eyeTanOut?:[x,y,z],
1722
- * centerTanIn?:[x,y,z], centerTanOut?:[x,y,z] }
1723
- *
1724
- * fov — vertical fov (radians) for perspective cameras; null for ortho.
1725
- * halfHeight — world-unit half-height of ortho frustum; null for perspective.
1726
- * Both are optional and nullable because exactly one is meaningful per
1727
- * keyframe (perspective xor ortho). eval() lerps each only when both
1728
- * adjacent keyframes carry a non-null value for that field; mixed or
1729
- * missing entries pass `null` through.
1730
- *
1731
- * near, far — clip plane distances (positive, world units). Always real
1732
- * numbers. Defaults: near = 0.1, far = 1000 (three.js / Bevy convention).
1733
- * Linearly interpolated between keyframes without null-passthrough.
1734
- *
1735
- * eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite
1736
- * interpolation of the eye and center paths respectively. When absent,
1737
- * centripetal Catmull-Rom tangents are auto-computed at sample time.
1738
- *
1739
- * Missing fields default to: center → [0,0,0], up → [0,1,0],
1740
- * near → 0.1, far → 1000.
1741
- *
1742
- * For matrix-based capture of a camera-like pose use
1743
- * PoseTrack.add({ mat4Model: mat4Eye }) for full TRS fidelity including roll,
1744
- * or a lookat spec here for camera-style interpolation.
1745
- */
1746
- class CameraTrack extends Track {
1747
- constructor() {
1748
- super();
1749
- /**
1750
- * Eye-path interpolation mode.
1751
- * @type {'hermite'|'linear'|'step'}
1752
- */
1753
- this.eyeInterp = 'hermite';
1754
- /**
1755
- * Center-path interpolation mode.
1756
- * @type {'hermite'|'linear'|'step'}
1757
- */
1758
- this.centerInterp = 'linear';
1759
- }
1760
-
1761
- /**
1762
- * Append one or more camera keyframes. Adjacent duplicates are skipped by
1763
- * default.
1764
- * @param {Object|Object[]} spec
1765
- * @param {{ deduplicate?: boolean }} [opts]
1766
- */
1767
- add(spec, opts) {
1768
- if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
1769
- const kf = _parseCameraSpec(spec);
1770
- if (!kf) return;
1771
- const dedup = !opts || opts.deduplicate !== false;
1772
- if (dedup && this.keyframes.length > 0) {
1773
- if (_sameCameraKeyframe(this.keyframes[this.keyframes.length - 1], kf)) return;
1774
- }
1775
- this.keyframes.push(kf);
1776
- }
1777
-
1778
- /**
1779
- * Replace the camera keyframe at `index`, or append at the end if `index`
1780
- * equals the current keyframe count.
1781
- * @param {number} index
1782
- * @param {Object} spec
1783
- * @returns {boolean}
1784
- */
1785
- set(index, spec) {
1786
- if (!_isNum$1(index)) return false;
1787
- const i = index | 0, kf = _parseCameraSpec(spec);
1788
- if (!kf || i < 0 || i > this.keyframes.length) return false;
1789
- if (i === this.keyframes.length) this.keyframes.push(kf);
1790
- else this.keyframes[i] = kf;
1791
- return true;
1792
- }
1793
-
1794
- /**
1795
- * Sample the eye path. Zero-alloc, no cursor side effects.
1796
- *
1797
- * Two signatures:
1798
- * sampleEye(out) cursor form — reads current seg/f
1799
- * sampleEye(out, seg, t) explicit (seg, t), continuous on the path
1800
- *
1801
- * @param {number[]} out
1802
- * @param {number} [seg]
1803
- * @param {number} [t]
1804
- * @returns {number[]} out
1805
- */
1806
- sampleEye(out, seg, t) {
1807
- if (arguments.length < 3) [seg, t] = this._cursorSegT();
1808
- return _samplePathCore(out, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
1809
- }
1810
-
1811
- /**
1812
- * Sample the center path. Zero-alloc, no cursor side effects.
1813
- *
1814
- * Two signatures:
1815
- * sampleCenter(out) cursor form — reads current seg/f
1816
- * sampleCenter(out, seg, t) explicit (seg, t)
1817
- *
1818
- * @param {number[]} out
1819
- * @param {number} [seg]
1820
- * @param {number} [t]
1821
- * @returns {number[]} out
1822
- */
1823
- sampleCenter(out, seg, t) {
1824
- if (arguments.length < 3) [seg, t] = this._cursorSegT();
1825
- return _samplePathCore(out, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
1826
- }
1827
-
1828
- /**
1829
- * Write the interpolated lookat eye matrix as a column-major mat4
1830
- * (eye→world rigid frame). Zero-alloc, no cursor side effects.
1831
- *
1832
- * Two signatures:
1833
- * mat4Eye(out) cursor form — reads current seg/f
1834
- * mat4Eye(out, seg, t) explicit (seg, t), continuous on the path
1835
- *
1836
- * @param {Float32Array|number[]} out 16-element result buffer.
1837
- * @param {number} [seg]
1838
- * @param {number} [t]
1839
- * @returns {Float32Array|number[]} out
1840
- */
1841
- mat4Eye(out, seg, t) {
1842
- if (arguments.length < 3) [seg, t] = this._cursorSegT();
1843
- this._sampleEyePose(_eyeScratch, seg, t);
1844
- const e = _eyeScratch;
1845
- return mat4Eye(out,
1846
- e.eye[0], e.eye[1], e.eye[2],
1847
- e.center[0], e.center[1], e.center[2],
1848
- e.up[0], e.up[1], e.up[2]);
1849
- }
1850
-
1851
- /**
1852
- * Effective in/out eye tangents at keyframe `index`.
1853
- */
1854
- eyeTangents(outIn, outOut, index) {
1855
- _sampleTangentsCore(outIn, outOut, this.keyframes, 'eye', 'eyeTanIn', 'eyeTanOut', index);
1856
- return this;
1857
- }
1858
-
1859
- /**
1860
- * Effective in/out center tangents at keyframe `index`.
1861
- */
1862
- centerTangents(outIn, outOut, index) {
1863
- _sampleTangentsCore(outIn, outOut, this.keyframes, 'center', 'centerTanIn', 'centerTanOut', index);
1864
- return this;
1865
- }
1866
-
1867
- /** @private — write interpolated { eye, center, up } at (seg, t). */
1868
- _sampleEyePose(out, seg, t) {
1869
- const n = this.keyframes.length;
1870
- if (n === 0) return out;
1871
- if (n === 1) {
1872
- const k = this.keyframes[0];
1873
- out.eye[0]=k.eye[0]; out.eye[1]=k.eye[1]; out.eye[2]=k.eye[2];
1874
- out.center[0]=k.center[0]; out.center[1]=k.center[1]; out.center[2]=k.center[2];
1875
- out.up[0]=k.up[0]; out.up[1]=k.up[1]; out.up[2]=k.up[2];
1876
- return out;
1877
- }
1878
- const nSeg = n - 1;
1879
- seg = seg | 0;
1880
- if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
1881
- else if (seg < 0) { seg = 0; t = 0; }
1882
- t = _clamp01(t);
1883
- const k0 = this.keyframes[seg];
1884
- const k1 = this.keyframes[seg + 1];
1885
-
1886
- _samplePathCore(out.eye, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
1887
- _samplePathCore(out.center, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
1888
-
1889
- lerpVec3(out.up, k0.up, k1.up, t);
1890
- const ul = Math.sqrt(out.up[0]*out.up[0]+out.up[1]*out.up[1]+out.up[2]*out.up[2]) || 1;
1891
- out.up[0]/=ul; out.up[1]/=ul; out.up[2]/=ul;
1892
- return out;
1893
- }
1894
-
1895
- /**
1896
- * Evaluate interpolated camera pose at current cursor.
1897
- *
1898
- * `fov` / `halfHeight` are lerped only when both adjacent keyframes carry
1899
- * a non-null value; mixed entries pass `null` through so the bridge can
1900
- * leave the projection unchanged.
1901
- *
1902
- * `near` / `far` are always real numbers and are linearly interpolated
1903
- * unconditionally.
1904
- *
1905
- * @param {{ eye:number[], center:number[], up:number[],
1906
- * fov:number|null, halfHeight:number|null,
1907
- * near:number, far:number }} [out]
1908
- * @returns {{ eye:number[], center:number[], up:number[],
1909
- * fov:number|null, halfHeight:number|null,
1910
- * near:number, far:number }} out
1911
- */
1912
- eval(out) {
1913
- out = out || { eye:[0,0,0], center:[0,0,0], up:[0,1,0],
1914
- fov:null, halfHeight:null,
1915
- near:_DEFAULT_NEAR, far:_DEFAULT_FAR };
1916
- const n = this.keyframes.length;
1917
- if (n === 0) return out;
1918
- if (n === 1) {
1919
- const k = this.keyframes[0];
1920
- this._sampleEyePose(out, 0, 0);
1921
- out.fov = k.fov; out.halfHeight = k.halfHeight;
1922
- out.near = k.near; out.far = k.far;
1923
- return out;
1924
- }
1925
- const [seg, t] = this._cursorSegT();
1926
- const k0 = this.keyframes[seg];
1927
- const k1 = this.keyframes[seg + 1];
1928
-
1929
- this._sampleEyePose(out, seg, t);
1930
-
1931
- out.fov = (k0.fov != null && k1.fov != null)
1932
- ? k0.fov + t * (k1.fov - k0.fov) : (k0.fov ?? k1.fov ?? null);
1933
- out.halfHeight = (k0.halfHeight != null && k1.halfHeight != null)
1934
- ? k0.halfHeight + t * (k1.halfHeight - k0.halfHeight)
1935
- : (k0.halfHeight ?? k1.halfHeight ?? null);
1936
-
1937
- // near / far carry real defaults on every keyframe — always lerp.
1938
- out.near = k0.near + t * (k1.near - k0.near);
1939
- out.far = k0.far + t * (k1.far - k0.far);
1940
-
1941
- return out;
1942
- }
1943
- }
1944
-
1945
- /**
1946
- * @file Constraint solver, canonical handle state, and ray-primitive
1947
- * intersections for interactive manipulators.
1948
- * @module tree/handle
1949
- * @license AGPL-3.0-only
1950
- *
1951
- * The numeric brain of the manipulator handle. Renderer- and frame-agnostic:
1952
- * inputs are plain numbers in ONE working space chosen by the caller (the
1953
- * bridge), and the solver never learns world vs eye — it solves in whatever
1954
- * space the ray and geometry are expressed in. The bridge converts the pointer
1955
- * ray into the working frame before calling solve(), and converts the value
1956
- * back out via mapLocation / mapDirection.
1957
- *
1958
- * Zero dependencies on p5, DOM, WebGL, or WebGPU. Out-first throughout; no
1959
- * allocation in solve() / value(). Vectors are passed as flat scalars (matching
1960
- * form.js), state is held as plain number[] (matching track.js keyframes).
1961
- *
1962
- * ── Storage convention ─────────────────────────────────────────────────────
1963
- * The core distinguishes two value shapes deliberately:
1964
- * mat4 — 16-element ArrayLike, typically Float32Array, because matrices
1965
- * cross the GL boundary (contiguous f32 is what the renderer wants).
1966
- * vec3 / quat — plain number[] (f64), because they are authoring/state values
1967
- * and because the frozen basis-vector constants (i, j, k, ORIGIN)
1968
- * cannot be expressed as frozen typed arrays. Handle state follows
1969
- * the vec3 rule.
1970
- *
1971
- * ── Constraint kinds ───────────────────────────────────────────────────────
1972
- * SPHERE 2-DOF heading on a sphere of radius r about an anchor.
1973
- * Canonical state is a UNIT DIRECTION (renormalised every solve) —
1974
- * no stored Euler angles, so no gimbal degeneracy at the poles.
1975
- * az/el are derived on request via azEl().
1976
- * PLANE 2-DOF point on a fixed plane (anchor + unit normal).
1977
- * AXIS 1-DOF point on a line (anchor + unit dir), scalar t clamped to extent.
1978
- * DIAL 1-DOF angle on a circle of radius r in the plane (anchor, axis).
1979
- * Canonical state is an ACCUMULATED angle θ (multi-turn winding is
1980
- * preserved: each solve applies the smaller signed arc, so θ counts
1981
- * full turns — essential for bind targets like "rotate this 720°").
1982
- * θ=0 sits at the reference direction `zero` (derived from the axis
1983
- * when not supplied); θ grows right-handed about the axis. Near
1984
- * edge-on view (the ray almost parallel to the dial plane, where a
1985
- * plane hit races to infinity — the classic rotate-gizmo failure)
1986
- * the solve falls back to the TANGENT LINE of the circle at the
1987
- * current angle, converting linear drag to dθ: bounded, monotone,
1988
- * and still pure ray geometry.
1989
- *
1990
- * VIEW (the camera-facing free-translate constraint) is NOT a core kind: the
1991
- * bridge implements it with PLANE, feeding a fresh camera-derived normal and
1992
- * anchor each frame. The core stays oblivious to the camera.
1993
- *
1994
- * ── Report modes ───────────────────────────────────────────────────────────
1995
- * DIRECTION value() writes the unit direction: SPHERE → the heading;
1996
- * DIAL → the radial unit at θ.
1997
- * POINT value() writes a position: SPHERE → anchor + dir·radius;
1998
- * DIAL → the point on the circle; PLANE / AXIS → the constrained
1999
- * point.
2000
- *
2001
- * ── State ownership & portability ──────────────────────────────────────────
2002
- * Each Constraint owns its own state; there is no module-level scratch. solve()
2003
- * writes into instance fields, never shared globals. This keeps the type a
2004
- * plain-data state machine: it ports 1:1 to a Rust enum + impl that is
2005
- * `Send + Sync` for free, with `&mut self` on solve() making concurrent
2006
- * mutation a compile error. The binding closure deliberately lives in the
2007
- * bridge, not here — a stored callback would forfeit that guarantee.
2008
- *
2009
- * ── Extension contract ─────────────────────────────────────────────────────
2010
- * A constraint is any object exposing: `kind` (integer discriminant),
2011
- * `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and
2012
- * optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])` — the
2013
- * basis re-aim seam the bridge's deferred `from` frame drives — and
2014
- * `proxy(ox,oy,oz, dx,dy,dz, radius)`, the analytic pick: the ray parameter
2015
- * at which a ray in the working space meets the grab proxy of `radius`
2016
- * working units, or Infinity (a kind without one gets a sphere at its
2017
- * POINT). The handle controller drives any conforming constraint
2018
- * (lifecycle, frame conversion, bind, hooks, pick); a new kind — 6-DOF, or
2019
- * app-specific — implements this contract here (portable, draw-free) plus a
2020
- * bridge-side locus draw, rather than forking the controller. The classes
2021
- * below are the reference implementation. See handle-design.md §9–§10.
2022
- *
2023
- * ── Conventions ────────────────────────────────────────────────────────────
2024
- * Ray direction `d` is assumed unit (the bridge normalises). Plane / axis
2025
- * normals and directions are normalised at construction. The angular utilities
2026
- * use a right-handed convention: az about +Y, el measured from the XZ plane.
2027
- * solve() does NOT depend on this convention — it stores the hit direction
2028
- * directly; az/el are a readout/authoring convenience only. DIAL's θ is
2029
- * right-handed about its own axis, measured from `zero`.
2030
- */
2031
-
2032
-
2033
- const EPS = 1e-6;
2034
- const TWO_PI$1 = Math.PI * 2;
2035
-
2036
- // Edge-on threshold for DIAL: below this |d·n| the plane hit is ill-conditioned
2037
- // (dθ per pixel diverges) and the solve switches to the tangent-line fallback.
2038
- // ≈ 4.6° of incidence. Tune empirically (handle-experiments/e2).
2039
- const DIAL_EDGE = 0.08;
2040
-
2041
- // =========================================================================
2042
- // H1 Small private helpers
2043
- // =========================================================================
2044
-
2045
- const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
2046
- const _clamp = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
2047
- const _num = (x, d) => _isNum(x) ? x : d;
2048
-
2049
- /** Wrap an angle to (−π, π]. */
2050
- const _wrapPi = (a) => a - TWO_PI$1 * Math.round(a / TWO_PI$1);
2051
-
2052
- /** Parse a vec3 from array / typed array / {x,y,z}. Returns a fresh [x,y,z] or null. */
2053
- function _vec3$1(v) {
2054
- if (!v) return null;
2055
- if (ArrayBuffer.isView(v) && v.length >= 3) return [v[0], v[1], v[2]];
2056
- if (Array.isArray(v) && v.length >= 3) return [v[0], v[1], v[2]];
2057
- if (typeof v === 'object' && 'x' in v) return [v.x || 0, v.y || 0, v.z || 0];
2058
- return null;
2059
- }
2060
-
2061
- /** Normalise a vec3 in place; zero-length falls back to the given default axis. */
2062
- function _unit$1(v, dx, dy, dz) {
2063
- const l = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
2064
- if (l < EPS) { v[0]=dx; v[1]=dy; v[2]=dz; return v; }
2065
- v[0]/=l; v[1]/=l; v[2]/=l;
2066
- return v;
2067
- }
2068
-
2069
- /**
2070
- * Orthonormal in-plane basis for a unit normal n, written into ub, vb. Seeds
2071
- * from the world axis least aligned with n so the first cross can't degenerate.
2072
- * (Same derivation the p5 bridge uses for its plane quad — duplicated here
2073
- * because the core cannot depend on the bridge.)
2074
- */
2075
- function _basis$1(n, ub, vb) {
2076
- const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
2077
- let rx = 0, ry = 0, rz = 0;
2078
- if (ax <= ay && ax <= az) rx = 1; else if (ay <= az) ry = 1; else rz = 1;
2079
- ub[0] = ry*n[2] - rz*n[1]; ub[1] = rz*n[0] - rx*n[2]; ub[2] = rx*n[1] - ry*n[0];
2080
- _unit$1(ub, 1, 0, 0);
2081
- 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];
2082
- }
2083
-
2084
- // =========================================================================
2085
- // H2 Ray-primitive intersections (pure, out-first, scalar params)
2086
- // =========================================================================
2087
-
2088
- /**
2089
- * Nearest ray–sphere intersection. The ray direction is assumed unit, so the
2090
- * quadratic's leading coefficient is 1. On a miss, writes the closest-approach
2091
- * point projected onto the sphere (so the handle tracks the limb gracefully
2092
- * rather than snapping away).
2093
- *
2094
- * @param {number[]} out 3-element destination (hit point).
2095
- * @param {number} ox,oy,oz Ray origin.
2096
- * @param {number} dx,dy,dz Ray direction (unit).
2097
- * @param {number} cx,cy,cz Sphere centre.
2098
- * @param {number} r Sphere radius.
2099
- * @returns {number} Ray parameter t at the written hit.
2100
- */
2101
- function raySphere(out, ox,oy,oz, dx,dy,dz, cx,cy,cz, r) {
2102
- const lx=ox-cx, ly=oy-cy, lz=oz-cz;
2103
- const b = lx*dx + ly*dy + lz*dz;
2104
- const cc = lx*lx + ly*ly + lz*lz - r*r;
2105
- const disc = b*b - cc;
2106
- let t;
2107
- if (disc >= 0) {
2108
- const s = Math.sqrt(disc);
2109
- t = -b - s;
2110
- if (t < 0) t = -b + s; // origin inside the sphere: take far root
2111
- out[0]=ox+t*dx; out[1]=oy+t*dy; out[2]=oz+t*dz;
2112
- } else {
2113
- t = -b; // closest approach along the ray
2114
- let hx=ox+t*dx-cx, hy=oy+t*dy-cy, hz=oz+t*dz-cz;
2115
- const hl = Math.sqrt(hx*hx + hy*hy + hz*hz) || 1;
2116
- const k = r/hl;
2117
- out[0]=cx+hx*k; out[1]=cy+hy*k; out[2]=cz+hz*k;
2118
- }
2119
- return t;
2120
- }
2121
-
2122
- /**
2123
- * Ray–plane intersection. A near-parallel ray (|d·n| < EPS) returns Infinity
2124
- * and leaves `out` untouched — the caller keeps the previous value.
2125
- *
2126
- * @param {number[]} out 3-element destination (hit point).
2127
- * @param {number} ox,oy,oz Ray origin.
2128
- * @param {number} dx,dy,dz Ray direction (unit).
2129
- * @param {number} px,py,pz A point on the plane.
2130
- * @param {number} nx,ny,nz Plane normal (unit).
2131
- * @returns {number} Ray parameter t, or Infinity if parallel.
2132
- */
2133
- function rayPlane(out, ox,oy,oz, dx,dy,dz, px,py,pz, nx,ny,nz) {
2134
- const den = dx*nx + dy*ny + dz*nz;
2135
- if (den < EPS && den > -EPS) return Infinity;
2136
- const t = ((px-ox)*nx + (py-oy)*ny + (pz-oz)*nz) / den;
2137
- out[0]=ox+t*dx; out[1]=oy+t*dy; out[2]=oz+t*dz;
2138
- return t;
2139
- }
2140
-
2141
- /**
2142
- * Closest point on the infinite line (p, u) to the ray (o, d). Both `d` and
2143
- * `u` are assumed unit. For a ray parallel to the line, projects the ray
2144
- * origin onto the line.
2145
- *
2146
- * @param {number[]} out 3-element destination (closest point on the line).
2147
- * @param {number} ox,oy,oz Ray origin.
2148
- * @param {number} dx,dy,dz Ray direction (unit).
2149
- * @param {number} px,py,pz A point on the line.
2150
- * @param {number} ux,uy,uz Line direction (unit).
2151
- * @returns {number} Signed parameter s along u at the written point (unclamped).
2152
- */
2153
- function rayClosestPointOnAxis(out, ox,oy,oz, dx,dy,dz, px,py,pz, ux,uy,uz) {
2154
- const wx=ox-px, wy=oy-py, wz=oz-pz;
2155
- const b = dx*ux + dy*uy + dz*uz; // d·u
2156
- const dd = dx*wx + dy*wy + dz*wz; // d·w
2157
- const e = ux*wx + uy*wy + uz*wz; // u·w
2158
- const den = 1 - b*b; // (d·d)(u·u) − (d·u)² with d,u unit
2159
- const s = (den > EPS) ? (e - b*dd) / den : e;
2160
- out[0]=px+s*ux; out[1]=py+s*uy; out[2]=pz+s*uz;
2161
- return s;
2162
- }
2163
-
2164
- // =========================================================================
2165
- // H2b Ray-primitive hit tests (pure, no out: the nearest t, or Infinity)
2166
- // =========================================================================
2167
- //
2168
- // Beside the solve primitives above, which always write a point, these are
2169
- // TESTS: they write nothing and return the ray parameter of the nearest hit
2170
- // with t ≥ 0, or Infinity on a miss. A ray whose origin lies inside the
2171
- // primitive hits at its exit, so a press from inside a proxy still grabs.
2172
- // The ray direction is assumed unit.
2173
-
2174
- /**
2175
- * Ray–sphere hit test.
2176
- * @param {number} ox,oy,oz Ray origin.
2177
- * @param {number} dx,dy,dz Ray direction (unit).
2178
- * @param {number} cx,cy,cz Sphere centre.
2179
- * @param {number} r Sphere radius.
2180
- * @returns {number} The nearest t ≥ 0, or Infinity.
2181
- */
2182
- function rayHitSphere(ox,oy,oz, dx,dy,dz, cx,cy,cz, r) {
2183
- const lx=ox-cx, ly=oy-cy, lz=oz-cz;
2184
- const b = lx*dx + ly*dy + lz*dz;
2185
- const cc = lx*lx + ly*ly + lz*lz - r*r;
2186
- const disc = b*b - cc;
2187
- if (disc < 0) return Infinity;
2188
- const s = Math.sqrt(disc);
2189
- let t = -b - s;
2190
- if (t < 0) t = -b + s; // origin inside: the exit
2191
- return t < 0 ? Infinity : t; // both roots behind the origin
2192
- }
2193
-
2194
- // One end cap of a capsule: the sphere at (cx,cy,cz), a root accepted only
2195
- // where the hit's axial coordinate h = wu + t·du lies beyond the segment on
2196
- // that cap's side (h ≤ 0 at A, h ≥ L at B), so the point is on the capsule's
2197
- // surface and not inside its cylinder.
2198
- function _capHit(ox,oy,oz, dx,dy,dz, cx,cy,cz, r, wu, du, lim, endB) {
2199
- const lx=ox-cx, ly=oy-cy, lz=oz-cz;
2200
- const b = lx*dx + ly*dy + lz*dz;
2201
- const cc = lx*lx + ly*ly + lz*lz - r*r;
2202
- const disc = b*b - cc;
2203
- if (disc < 0) return Infinity;
2204
- const s = Math.sqrt(disc);
2205
- let t = -b - s, h = wu + t*du;
2206
- if (t < 0 || (endB ? h < lim : h > lim)) { t = -b + s; h = wu + t*du; }
2207
- return (t >= 0 && (endB ? h >= lim : h <= lim)) ? t : Infinity;
2208
- }
2209
-
2210
- /**
2211
- * Ray–capsule hit test: the segment A→B swept by radius r. The cylinder wall
2212
- * counts within the segment's extent, each end sphere beyond it; the nearest
2213
- * of the three wins. A zero-length segment is the sphere at A.
2214
- * @param {number} ox,oy,oz Ray origin.
2215
- * @param {number} dx,dy,dz Ray direction (unit).
2216
- * @param {number} ax,ay,az Segment start.
2217
- * @param {number} bx,by,bz Segment end.
2218
- * @param {number} r Capsule radius.
2219
- * @returns {number} The nearest t ≥ 0, or Infinity.
2220
- */
2221
- function rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r) {
2222
- let ux=bx-ax, uy=by-ay, uz=bz-az;
2223
- const L = Math.sqrt(ux*ux + uy*uy + uz*uz);
2224
- if (L < EPS) return rayHitSphere(ox,oy,oz, dx,dy,dz, ax,ay,az, r);
2225
- ux/=L; uy/=L; uz/=L;
2226
- const wx=ox-ax, wy=oy-ay, wz=oz-az;
2227
- const du = dx*ux + dy*uy + dz*uz; // d·u
2228
- const wu = wx*ux + wy*uy + wz*uz; // w·u
2229
- let best = Infinity;
2230
- // Cylinder wall: the quadratic in the components perpendicular to u.
2231
- const a = 1 - du*du;
2232
- if (a > EPS) {
2233
- const b = (dx*wx + dy*wy + dz*wz) - du*wu;
2234
- const c = (wx*wx + wy*wy + wz*wz) - wu*wu - r*r;
2235
- const disc = b*b - a*c;
2236
- if (disc >= 0) {
2237
- const s = Math.sqrt(disc);
2238
- let t = (-b - s)/a, h = wu + t*du;
2239
- if (t < 0 || h < 0 || h > L) { t = (-b + s)/a; h = wu + t*du; }
2240
- if (t >= 0 && h >= 0 && h <= L) best = t;
2241
- }
2242
- }
2243
- const ta = _capHit(ox,oy,oz, dx,dy,dz, ax,ay,az, r, wu, du, 0, false);
2244
- if (ta < best) best = ta;
2245
- const tb = _capHit(ox,oy,oz, dx,dy,dz, bx,by,bz, r, wu, du, L, true);
2246
- if (tb < best) best = tb;
2247
- return best;
2248
- }
2249
-
2250
- // Ring scratch — the plane normal and its in-plane basis. Function-local
2251
- // working values, never state: rayHitRing runs to completion.
2252
- const _rn = [0, 0, 1], _r0 = [1, 0, 0], _r1 = [0, 1, 0];
2253
-
2254
- /**
2255
- * Ray–ring hit test: the circle of radius R about c in the plane
2256
- * perpendicular to u, swept by tube radius r — the analytic torus proxy as a
2257
- * capsule chain, the circle polygonised into `detail` segments and each
2258
- * tested with rayHitCapsule. The chain has volume in every direction, so it
2259
- * never degenerates edge-on; the chordal error is R · (1 − cos(π / detail)).
2260
- * @param {number} ox,oy,oz Ray origin.
2261
- * @param {number} dx,dy,dz Ray direction (unit).
2262
- * @param {number} cx,cy,cz Ring centre.
2263
- * @param {number} ux,uy,uz Ring plane normal (normalised here).
2264
- * @param {number} R Ring radius.
2265
- * @param {number} r Tube radius.
2266
- * @param {number} [detail=32] Chain segments (at least 3).
2267
- * @returns {number} The nearest t ≥ 0, or Infinity.
2268
- */
2269
- function rayHitRing(ox,oy,oz, dx,dy,dz, cx,cy,cz, ux,uy,uz, R, r, detail = 32) {
2270
- const n = _isNum(detail) && detail >= 3 ? Math.floor(detail) : 32;
2271
- _rn[0]=ux; _rn[1]=uy; _rn[2]=uz;
2272
- _unit$1(_rn, 0, 0, 1);
2273
- _basis$1(_rn, _r0, _r1);
2274
- const step = TWO_PI$1 / n;
2275
- let best = Infinity;
2276
- let ax = cx + R*_r0[0], ay = cy + R*_r0[1], az = cz + R*_r0[2]; // vertex at angle 0
2277
- for (let i = 1; i <= n; i++) {
2278
- const th = i === n ? 0 : i*step; // close the chain exactly
2279
- const cs = Math.cos(th)*R, sn = Math.sin(th)*R;
2280
- const bx = cx + cs*_r0[0] + sn*_r1[0];
2281
- const by = cy + cs*_r0[1] + sn*_r1[1];
2282
- const bz = cz + cs*_r0[2] + sn*_r1[2];
2283
- const t = rayHitCapsule(ox,oy,oz, dx,dy,dz, ax,ay,az, bx,by,bz, r);
2284
- if (t < best) best = t;
2285
- ax = bx; ay = by; az = bz;
2286
- }
2287
- return best;
2288
- }
2289
-
2290
- // =========================================================================
2291
- // H3 Angular utilities (readout / authoring convenience)
2292
- // =========================================================================
2293
-
2294
- /**
2295
- * Unit direction from azimuth/elevation (right-handed: az about +Y, el from XZ).
2296
- * @param {number[]} out 3-element destination.
2297
- * @param {number} az Azimuth (radians).
2298
- * @param {number} el Elevation (radians).
2299
- * @returns {number[]} out
2300
- */
2301
- function dirFromAzEl(out, az, el) {
2302
- const ce = Math.cos(el);
2303
- out[0]=ce*Math.cos(az); out[1]=Math.sin(el); out[2]=ce*Math.sin(az);
2304
- return out;
2305
- }
2306
-
2307
- /**
2308
- * Azimuth/elevation from a unit direction. Inverse of dirFromAzEl.
2309
- * @param {number[]} out2 2-element destination [az, el] (radians).
2310
- * @param {number} dx,dy,dz Unit direction.
2311
- * @returns {number[]} out2
2312
- */
2313
- function azElFromDir(out2, dx, dy, dz) {
2314
- out2[0] = Math.atan2(dz, dx); // az
2315
- out2[1] = Math.asin(_clamp(dy, -1, 1)); // el
2316
- return out2;
2317
- }
2318
-
2319
- // =========================================================================
2320
- // H4 Constraint — tagged state machine (one class, kind discriminant)
2321
- // =========================================================================
2322
-
2323
- /**
2324
- * A draggable constraint: maps a ray to a constrained value and holds the
2325
- * canonical state between drags. One class with a `kind` discriminant (never
2326
- * per-constraint subclasses) so it maps cleanly to a Rust enum.
2327
- *
2328
- * Construction options (all optional unless noted):
2329
- * SPHERE — { radius = 1, report = DIRECTION, anchor = [0,0,0] }
2330
- * PLANE — { anchor = [0,0,0], normal = [0,1,0] }
2331
- * AXIS — { anchor = [0,0,0], axis = [1,0,0], extent = [-1, 1] }
2332
- * DIAL — { anchor = [0,0,0], axis = [0,1,0], radius = 1, zero?,
2333
- * extent = [-∞, ∞] }
2334
- * `axis` is the dial-plane normal (θ is right-handed about it);
2335
- * `zero` is the in-plane reference direction for θ=0 (projected
2336
- * onto the plane and normalised; derived from the axis when
2337
- * absent); `extent` clamps θ in radians — unbounded by default,
2338
- * so the dial winds multiple turns.
2339
- *
2340
- * @param {number} kind SPHERE | PLANE | AXIS | DIAL.
2341
- * @param {Object} [opts]
2342
- */
2343
- class Constraint {
2344
- constructor(kind, opts = {}) {
2345
- /** Constraint kind discriminant. @type {number} */
2346
- this.kind = kind;
2347
-
2348
- /** Constraint origin (sphere centre / plane point / axis anchor / dial centre). @type {number[]} */
2349
- this.anchor = _vec3$1(opts.anchor) || [0, 0, 0];
2350
- /** Canonical unit direction — SPHERE. @type {number[]} */
2351
- this.dir = [0, 0, 1];
2352
- /** Constrained point — PLANE / AXIS / DIAL (and SPHERE scratch). @type {number[]} */
2353
- this.pt = [this.anchor[0], this.anchor[1], this.anchor[2]];
2354
- /** Plane normal (unit) — PLANE. @type {number[]} */
2355
- this.n = _unit$1(_vec3$1(opts.normal) || [0, 1, 0], 0, 1, 0);
2356
- /** Axis / dial-plane normal (unit) — AXIS / DIAL. @type {number[]} */
2357
- this.u = kind === DIAL
2358
- ? _unit$1(_vec3$1(opts.axis) || [0, 1, 0], 0, 1, 0)
2359
- : _unit$1(_vec3$1(opts.axis) || [1, 0, 0], 1, 0, 0);
2360
- /** Current scalar parameter — AXIS: t along the line; DIAL: accumulated θ. @type {number} */
2361
- this.s = 0;
2362
-
2363
- // DIAL in-plane basis: r0 (the θ=0 reference) and r1 = u × r0, so the
2364
- // point at θ is anchor + radius·(cosθ·r0 + sinθ·r1) — right-handed about u.
2365
- /** θ=0 reference direction (unit, in-plane) — DIAL. @type {number[]} */
2366
- this.r0 = [1, 0, 0];
2367
- /** In-plane binormal u × r0 (unit) — DIAL. @type {number[]} */
2368
- this.r1 = [0, 0, 1];
2369
- if (kind === DIAL) {
2370
- const z = _vec3$1(opts.zero);
2371
- this._dialBasis(z ? z[0] : NaN, z ? z[1] : NaN, z ? z[2] : NaN);
2372
- }
2373
-
2374
- // Extent: AXIS clamps t (default [-1, 1]); DIAL clamps θ in radians
2375
- // (default unbounded, so the dial winds freely).
2376
- const ext = Array.isArray(opts.extent) ? opts.extent : null;
2377
- const dial = kind === DIAL;
2378
- /** Minimum scalar parameter. @type {number} */
2379
- this.min = ext && _isNum(ext[0]) ? ext[0] : (dial ? -Infinity : -1);
2380
- /** Maximum scalar parameter. @type {number} */
2381
- this.max = ext && _isNum(ext[1]) ? ext[1] : (dial ? Infinity : 1);
2382
-
2383
- // Sphere / dial radius (private backing — see radius getter/setter).
2384
- this._radius = _num(opts.radius, 1);
2385
-
2386
- /**
2387
- * Default report mode. SPHERE defaults to DIRECTION; PLANE / AXIS / DIAL
2388
- * report a POINT.
2389
- * @type {number}
2390
- */
2391
- this.report = (opts.report === POINT || opts.report === DIRECTION)
2392
- ? opts.report
2393
- : (kind === SPHERE ? DIRECTION : POINT);
2394
-
2395
- if (kind === DIAL) this._dialPoint();
2396
- }
2397
-
2398
- /** Sphere / dial radius. @type {number} */
2399
- get radius() { return this._radius; }
2400
- set radius(r) { this._radius = _isNum(r) ? r : this._radius; if (this.kind === DIAL) this._dialPoint(); }
2401
-
2402
- // Recompute the DIAL point from θ (anchor + ρ(cosθ·r0 + sinθ·r1)).
2403
- _dialPoint() {
2404
- const c = Math.cos(this.s) * this._radius, sn = Math.sin(this.s) * this._radius;
2405
- this.pt[0] = this.anchor[0] + c*this.r0[0] + sn*this.r1[0];
2406
- this.pt[1] = this.anchor[1] + c*this.r0[1] + sn*this.r1[1];
2407
- this.pt[2] = this.anchor[2] + c*this.r0[2] + sn*this.r1[2];
2408
- }
2409
-
2410
- // Build the DIAL in-plane basis (r0, r1) from the current axis u and an
2411
- // optional θ=0 reference (zx,zy,zz): the reference is projected onto the
2412
- // dial plane and normalised; absent (NaN) or degenerate, r0 derives from u
2413
- // via the least-aligned-axis seed. r1 = u × r0, right-handed about u.
2414
- _dialBasis(zx, zy, zz) {
2415
- if (_isNum(zx) && _isNum(zy) && _isNum(zz)) {
2416
- const d = zx*this.u[0] + zy*this.u[1] + zz*this.u[2];
2417
- this.r0[0] = zx - d*this.u[0];
2418
- this.r0[1] = zy - d*this.u[1];
2419
- this.r0[2] = zz - d*this.u[2];
2420
- const l = Math.sqrt(this.r0[0]**2 + this.r0[1]**2 + this.r0[2]**2);
2421
- if (l < EPS) _basis$1(this.u, this.r0, this.r1);
2422
- else { this.r0[0]/=l; this.r0[1]/=l; this.r0[2]/=l; }
2423
- } else {
2424
- _basis$1(this.u, this.r0, this.r1);
2425
- }
2426
- // r1 = u × r0 (recomputed even when _basis ran — same result, one rule).
2427
- this.r1[0] = this.u[1]*this.r0[2] - this.u[2]*this.r0[1];
2428
- this.r1[1] = this.u[2]*this.r0[0] - this.u[0]*this.r0[2];
2429
- this.r1[2] = this.u[0]*this.r0[1] - this.u[1]*this.r0[0];
2430
- }
2431
-
2432
- /**
2433
- * Update the canonical state from a ray in the working space. The ray
2434
- * direction is assumed unit. Chainable.
2435
- *
2436
- * @param {number} ox,oy,oz Ray origin.
2437
- * @param {number} dx,dy,dz Ray direction (unit).
2438
- * @returns {Constraint} this
2439
- */
2440
- solve(ox, oy, oz, dx, dy, dz) {
2441
- if (this.kind === SPHERE) {
2442
- // Hit the sphere into pt, then derive the unit heading from the anchor.
2443
- raySphere(this.pt, ox,oy,oz, dx,dy,dz,
2444
- this.anchor[0], this.anchor[1], this.anchor[2], this._radius);
2445
- this.dir[0] = this.pt[0] - this.anchor[0];
2446
- this.dir[1] = this.pt[1] - this.anchor[1];
2447
- this.dir[2] = this.pt[2] - this.anchor[2];
2448
- _unit$1(this.dir, this.dir[0], this.dir[1], this.dir[2]);
2449
- } else if (this.kind === PLANE) {
2450
- // Parallel ray returns Infinity and leaves pt unchanged (keep last).
2451
- rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
2452
- this.anchor[0], this.anchor[1], this.anchor[2],
2453
- this.n[0], this.n[1], this.n[2]);
2454
- } else if (this.kind === AXIS) {
2455
- let s = rayClosestPointOnAxis(this.pt, ox,oy,oz, dx,dy,dz,
2456
- this.anchor[0], this.anchor[1], this.anchor[2],
2457
- this.u[0], this.u[1], this.u[2]);
2458
- s = _clamp(s, this.min, this.max);
2459
- this.s = s;
2460
- this.pt[0] = this.anchor[0] + s*this.u[0]; // clamped point
2461
- this.pt[1] = this.anchor[1] + s*this.u[1];
2462
- this.pt[2] = this.anchor[2] + s*this.u[2];
2463
- } else if (this.kind === DIAL) {
2464
- this._solveDial(ox, oy, oz, dx, dy, dz);
2465
- }
2466
- return this;
2467
- }
2468
-
2469
- // DIAL solve. Face-on: hit the dial plane, derive the raw angle of the hit
2470
- // about the anchor, and accumulate the smaller signed arc into θ (winding
2471
- // preserved). Edge-on (|d·u| < DIAL_EDGE): the plane hit diverges, so solve
2472
- // the ray against the TANGENT LINE of the circle at the current θ instead —
2473
- // the signed tangent parameter over the radius is dθ, bounded and monotone.
2474
- _solveDial(ox, oy, oz, dx, dy, dz) {
2475
- const u = this.u, r0 = this.r0, r1 = this.r1, a = this.anchor;
2476
- const den = dx*u[0] + dy*u[1] + dz*u[2];
2477
- let dth;
2478
- if (den >= DIAL_EDGE || den <= -DIAL_EDGE) {
2479
- // Well-conditioned: plane hit → raw angle → smaller signed arc.
2480
- const t = rayPlane(this.pt, ox,oy,oz, dx,dy,dz,
2481
- a[0], a[1], a[2], u[0], u[1], u[2]);
2482
- if (t === Infinity) return; // keep last (paranoia; den guards)
2483
- const vx = this.pt[0]-a[0], vy = this.pt[1]-a[1], vz = this.pt[2]-a[2];
2484
- const raw = Math.atan2(vx*r1[0] + vy*r1[1] + vz*r1[2],
2485
- vx*r0[0] + vy*r0[1] + vz*r0[2]);
2486
- dth = _wrapPi(raw - _wrapPi(this.s));
2487
- } else {
2488
- // Edge-on fallback: tangent at the current point, w = −sinθ·r0 + cosθ·r1.
2489
- const cs = Math.cos(this.s), sn = Math.sin(this.s);
2490
- const wx = -sn*r0[0] + cs*r1[0];
2491
- const wy = -sn*r0[1] + cs*r1[1];
2492
- const wz = -sn*r0[2] + cs*r1[2];
2493
- this._dialPoint(); // tangent anchored at the current point
2494
- const sl = rayClosestPointOnAxis(this.pt, ox,oy,oz, dx,dy,dz,
2495
- this.pt[0], this.pt[1], this.pt[2],
2496
- wx, wy, wz);
2497
- dth = sl / (this._radius || 1);
2498
- }
2499
- this.s = _clamp(this.s + dth, this.min, this.max);
2500
- this._dialPoint();
2501
- }
2502
-
2503
- /**
2504
- * Write the current value into `out`. `report` overrides the default for
2505
- * this call (e.g. read a SPHERE's point even when its default is DIRECTION).
2506
- * SPHERE+POINT and DIAL+POINT are derived from the angle/heading and the
2507
- * live radius, so they stay correct after a radius change without
2508
- * re-solving. DIAL+DIRECTION writes the radial unit at θ.
2509
- *
2510
- * @param {number[]} out 3-element destination.
2511
- * @param {number} [report] POINT | DIRECTION override.
2512
- * @returns {number[]} out
2513
- */
2514
- value(out, report) {
2515
- const r = (report === POINT || report === DIRECTION) ? report : this.report;
2516
- if (this.kind === SPHERE) {
2517
- if (r === DIRECTION) {
2518
- out[0]=this.dir[0]; out[1]=this.dir[1]; out[2]=this.dir[2];
2519
- } else {
2520
- out[0]=this.anchor[0]+this.dir[0]*this._radius;
2521
- out[1]=this.anchor[1]+this.dir[1]*this._radius;
2522
- out[2]=this.anchor[2]+this.dir[2]*this._radius;
2523
- }
2524
- } else if (this.kind === DIAL) {
2525
- const cs = Math.cos(this.s), sn = Math.sin(this.s);
2526
- if (r === DIRECTION) {
2527
- out[0]=cs*this.r0[0]+sn*this.r1[0];
2528
- out[1]=cs*this.r0[1]+sn*this.r1[1];
2529
- out[2]=cs*this.r0[2]+sn*this.r1[2];
2530
- } else {
2531
- out[0]=this.anchor[0]+(cs*this.r0[0]+sn*this.r1[0])*this._radius;
2532
- out[1]=this.anchor[1]+(cs*this.r0[1]+sn*this.r1[1])*this._radius;
2533
- out[2]=this.anchor[2]+(cs*this.r0[2]+sn*this.r1[2])*this._radius;
2534
- }
2535
- } else {
2536
- out[0]=this.pt[0]; out[1]=this.pt[1]; out[2]=this.pt[2];
2537
- }
2538
- return out;
2539
- }
2540
-
2541
- /**
2542
- * Current scalar parameter: AXIS → signed t along the line; DIAL →
2543
- * accumulated angle θ in radians (multi-turn).
2544
- * @returns {number} The parameter, or NaN for non-scalar constraints.
2545
- */
2546
- scalar() {
2547
- return (this.kind === AXIS || this.kind === DIAL) ? this.s : NaN;
2548
- }
2549
-
2550
- /**
2551
- * Derive [az, el] from the current SPHERE direction (SPHERE only).
2552
- * @param {number[]} out2 2-element destination [az, el].
2553
- * @returns {number[]} out2
2554
- */
2555
- azEl(out2) {
2556
- return azElFromDir(out2, this.dir[0], this.dir[1], this.dir[2]);
2557
- }
2558
-
2559
- /**
2560
- * Seed the canonical state from a value (used by the bridge on bind() so the
2561
- * handle starts at the bound target's value). Chainable.
2562
- * SPHERE — sets the unit direction (a point value recovers its heading).
2563
- * PLANE — projects the value onto the plane.
2564
- * AXIS — projects the value onto the line, clamped to extent.
2565
- * DIAL — projects the value onto the dial plane, derives the wrapped
2566
- * angle, and picks the WINDING NEAREST the current θ (so an
2567
- * external sync doesn't unwind accumulated turns), clamped.
2568
- *
2569
- * @param {number} x,y,z Seed value.
2570
- * @returns {Constraint} this
2571
- */
2572
- seed(x, y, z) {
2573
- if (this.kind === SPHERE) {
2574
- const vx=x-this.anchor[0], vy=y-this.anchor[1], vz=z-this.anchor[2];
2575
- const l = Math.sqrt(vx*vx + vy*vy + vz*vz);
2576
- if (l >= EPS) { this.dir[0]=vx/l; this.dir[1]=vy/l; this.dir[2]=vz/l; }
2577
- } else if (this.kind === PLANE) {
2578
- const wx=x-this.anchor[0], wy=y-this.anchor[1], wz=z-this.anchor[2];
2579
- const d = wx*this.n[0] + wy*this.n[1] + wz*this.n[2];
2580
- this.pt[0]=x-d*this.n[0]; this.pt[1]=y-d*this.n[1]; this.pt[2]=z-d*this.n[2];
2581
- } else if (this.kind === AXIS) {
2582
- const wx=x-this.anchor[0], wy=y-this.anchor[1], wz=z-this.anchor[2];
2583
- const s = _clamp(wx*this.u[0] + wy*this.u[1] + wz*this.u[2], this.min, this.max);
2584
- this.s = s;
2585
- this.pt[0]=this.anchor[0]+s*this.u[0];
2586
- this.pt[1]=this.anchor[1]+s*this.u[1];
2587
- this.pt[2]=this.anchor[2]+s*this.u[2];
2588
- } else if (this.kind === DIAL) {
2589
- const wx=x-this.anchor[0], wy=y-this.anchor[1], wz=z-this.anchor[2];
2590
- const px = wx*this.r0[0] + wy*this.r0[1] + wz*this.r0[2];
2591
- const py = wx*this.r1[0] + wy*this.r1[1] + wz*this.r1[2];
2592
- if (px*px + py*py >= EPS*EPS) {
2593
- const a = Math.atan2(py, px);
2594
- // Nearest winding to the current θ preserves accumulated turns.
2595
- this.s = _clamp(a + TWO_PI$1 * Math.round((this.s - a) / TWO_PI$1),
2596
- this.min, this.max);
2597
- }
2598
- this._dialPoint();
2599
- }
2600
- return this;
2601
- }
2602
-
2603
- /**
2604
- * Re-aim the constraint basis in the working space — the deferred-frame
2605
- * seam (the p5.tree bridge's `from` opt resolves its symbolic basis through
2606
- * mapDirection and calls this). Per kind:
2607
- * PLANE — new normal; the point is re-projected onto the new plane.
2608
- * AXIS — new direction; the scalar t is preserved, the point recomputed.
2609
- * DIAL — new plane normal + optional θ=0 reference; θ is preserved, the
2610
- * in-plane basis rebuilt (reference re-derived when omitted),
2611
- * the point recomputed.
2612
- * SPHERE — no basis; no-op.
2613
- * Inputs are normalised; a zero-length axis keeps the previous one.
2614
- * Chainable.
2615
- *
2616
- * @param {number} ax,ay,az New normal (PLANE) / direction (AXIS) / dial-plane normal (DIAL).
2617
- * @param {number} [zx,zy,zz] DIAL only — θ=0 reference (re-derived when omitted).
2618
- * @returns {Constraint} this
2619
- */
2620
- aim(ax, ay, az, zx, zy, zz) {
2621
- if (this.kind === PLANE) {
2622
- const px = this.n[0], py = this.n[1], pz = this.n[2];
2623
- this.n[0] = ax; this.n[1] = ay; this.n[2] = az;
2624
- _unit$1(this.n, px, py, pz);
2625
- this.seed(this.pt[0], this.pt[1], this.pt[2]);
2626
- } else if (this.kind === AXIS) {
2627
- const px = this.u[0], py = this.u[1], pz = this.u[2];
2628
- this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
2629
- _unit$1(this.u, px, py, pz);
2630
- this.pt[0] = this.anchor[0] + this.s*this.u[0];
2631
- this.pt[1] = this.anchor[1] + this.s*this.u[1];
2632
- this.pt[2] = this.anchor[2] + this.s*this.u[2];
2633
- } else if (this.kind === DIAL) {
2634
- const px = this.u[0], py = this.u[1], pz = this.u[2];
2635
- this.u[0] = ax; this.u[1] = ay; this.u[2] = az;
2636
- _unit$1(this.u, px, py, pz);
2637
- this._dialBasis(zx, zy, zz);
2638
- this._dialPoint();
2639
- }
2640
- return this;
2641
- }
2642
-
2643
- /**
2644
- * The analytic pick proxy: the ray parameter t at which a ray in the
2645
- * working space meets the grab proxy, or Infinity. `radius` is the grab
2646
- * size in working-space units — a constant pixel size the caller converted
2647
- * through pixelRatio at the proxy's depth. SPHERE / PLANE / AXIS: a sphere
2648
- * of `radius` at the reported POINT. DIAL: the ring at the anchor with
2649
- * tube radius `radius` (rayHitRing), so a grab lands anywhere on the ring.
2650
- *
2651
- * @param {number} ox,oy,oz Ray origin.
2652
- * @param {number} dx,dy,dz Ray direction (unit).
2653
- * @param {number} radius Grab radius, working units.
2654
- * @returns {number} The nearest t ≥ 0, or Infinity.
2655
- */
2656
- proxy(ox, oy, oz, dx, dy, dz, radius) {
2657
- const a = this.anchor;
2658
- if (this.kind === DIAL) {
2659
- return rayHitRing(ox,oy,oz, dx,dy,dz, a[0], a[1], a[2],
2660
- this.u[0], this.u[1], this.u[2], this._radius, radius);
2661
- }
2662
- let px, py, pz;
2663
- if (this.kind === SPHERE) {
2664
- px = a[0] + this.dir[0]*this._radius;
2665
- py = a[1] + this.dir[1]*this._radius;
2666
- pz = a[2] + this.dir[2]*this._radius;
2667
- } else {
2668
- px = this.pt[0]; py = this.pt[1]; pz = this.pt[2];
2669
- }
2670
- return rayHitSphere(ox,oy,oz, dx,dy,dz, px,py,pz, radius);
2671
- }
2672
- }
2673
-
2674
- /**
2675
- * Convenience factory mirroring the constructor. Handy for headless tests and
2676
- * for the bridge, which otherwise calls `new Constraint(...)` directly.
2677
- * @param {number} kind SPHERE | PLANE | AXIS | DIAL.
2678
- * @param {Object} [opts]
2679
- * @returns {Constraint}
2680
- */
2681
- function createConstraint(kind, opts) {
2682
- return new Constraint(kind, opts);
2683
- }
2684
-
2685
- /**
2686
- * @file Source-agnostic 6-DOF pose helm — integrates a live rate stream into a
2687
- * { pos, rot } pose. Renderer- and transport-agnostic. Zero dependencies.
2688
- * @module tree/helm
2689
- * @license AGPL-3.0-only
2690
- *
2691
- * A helm is the rate-stream sibling of the Track family: where a Track produces
2692
- * a pose from keyframes over time, a helm produces a pose from a live 6-DOF
2693
- * delta stream (a SpaceNavigator, a tracked hand, an agent policy). It is NOT a
2694
- * handle — a handle reports a `vec3` solved from a pointer ray; a helm reports a
2695
- * pose (position + orientation).
2696
- *
2697
- * Family placement (peer of PoseTrack, NOT a Track subclass — no timeline):
2698
- * feed(translation, rotation) push the latest raw device rate (input, as Track.add)
2699
- * step(out, dt, basis) integrate the rate by dt into the pose (BRIDGE-DRIVEN,
2700
- * as Track.tick) — writes the new pose into out
2701
- * eval(out) read the current { pos, rot } (zero-alloc, as Track.eval)
2702
- * home(pose?) re-home the integrated pose (NOT reset — no keyframes to clear)
2703
- *
2704
- * Value layer (opt-in, §10): a per-helm `fullScale` keeps the read-outs honest
2705
- * across input scales; an opt-in `filter` (oneEuro) conditions the fed rates
2706
- * before the deadzone inside step; `poseDelta` differences two absolute poses
2707
- * into a feedable rate. With `filter` null and the default `fullScale`, the
2708
- * clean rate-native path is unchanged.
2709
- *
2710
- * Quaternion algebra is provided by quat.js. Out-first throughout; no allocation
2711
- * in feed/step/eval. Storage convention (matches the rest of the core): vec3 and
2712
- * quat state are plain number[] (f64) — the same shape as track.js keyframes and
2713
- * the frozen basis-vector constants.
2714
- *
2715
- * ── Frame discipline (`from` / `basis`) ──────────────────────────────────────
2716
- * The space the rates are interpreted in is named by `from` (WORLD | EYE | mat4),
2717
- * a declaration the BRIDGE reads — the core never learns about a camera. The
2718
- * resolved orientation arrives per-step as `basis`: an eye→world mat4 (or null
2719
- * for WORLD ≡ the identity basis). Both the linear and angular rates are rotated
2720
- * through that basis, then the quaternion is composed world-frame. This is the
2721
- * single code path that covers both manipulation conventions:
2722
- *
2723
- * - camera body-fly — basis is the driven camera's own eye matrix (which this
2724
- * helm produced last frame ⇒ equals the integrated `q`,
2725
- * zero staleness); rotating a body delta through `q` then
2726
- * world-composing is algebraically a body-frame compose.
2727
- * - screen-relative — basis is the *viewing* camera's eye matrix (an external
2728
- * frame); a push moves the target relative to the screen.
2729
- *
2730
- * Per-channel `from` ({ translation, rotation }) is a deferred, non-breaking
2731
- * extension (one basis suffices today — pose-helm-design.md §8.3).
2732
- */
2733
-
2734
-
2735
- // =========================================================================
2736
- // Module-level scratch — shared across helm instances (non-reentrant; step
2737
- // is synchronous and called at most once per helm per frame).
2738
- // =========================================================================
2739
-
2740
- const _dq = [0, 0, 0, 1];
2741
-
2742
- // Scratch for the optional input filter (helm.filter). The fed rates are packed
2743
- // into one 6-vector, conditioned, and unpacked to lin/ang triples before the
2744
- // deadzone — so a single filter carries one state per helm. Shared across
2745
- // instances (step is synchronous, at most once per helm per frame).
2746
- const _f6raw = [0, 0, 0, 0, 0, 0];
2747
- const _f6out = [0, 0, 0, 0, 0, 0];
2748
- const _fLin = [0, 0, 0];
2749
- const _fAng = [0, 0, 0];
2750
-
2751
- /**
2752
- * Default full-deflection input magnitude — the fed rate a read-out shows as
2753
- * full (the SpaceNavigator's saturated lane). Private to core: a transport on a
2754
- * different raw scale sets helm.fullScale and never reads this, so there is no
2755
- * public HELM_FULL export. Display-only; integration uses profile.sens directly.
2756
- */
2757
- const HELM_FULL_SCALE = 500;
2758
-
2759
- // Deadzone gate — |v| > dz keeps v, else 0. Module-level so step allocates no
2760
- // closure. Strictly-greater matches the e7 reference (rest reads exact 0).
2761
- const _dz = (v, dz) => (v > dz || v < -dz) ? v : 0;
2762
-
2763
- /**
2764
- * Build the canonical default profile. SpaceNavigator-tuned lane permutation
2765
- * and the eye-frame manipulation feel found in the e7 experiments; it is the
2766
- * per-app / per-device mapping layer and is meant to be overwritten wholesale
2767
- * (`helm.profile = { … }`) for a different transport.
2768
- *
2769
- * sign — per-app direction (camera-fly vs object-grab invert; flip live).
2770
- * sens — per-axis sensitivity (tame roll without touching the rest).
2771
- * lane — input-channel permutation: which fed channel drives this DOF.
2772
- * T* lanes index the translation triple; R* lanes the rotation triple.
2773
- */
2774
- function _defaultProfile() {
2775
- return {
2776
- Tx: { sign: 1, sens: 0.30, lane: 0 },
2777
- Ty: { sign: 1, sens: 0.30, lane: 2 },
2778
- Tz: { sign: -1, sens: 0.30, lane: 1 },
2779
- Rp: { sign: -1, sens: 0.0025, lane: 0 },
2780
- Ry: { sign: -1, sens: 0.0025, lane: 2 },
2781
- Rr: { sign: 1, sens: 0.0018, lane: 1 },
2782
- };
2783
- }
2784
-
2785
- // =========================================================================
2786
- // PoseHelm
2787
- // =========================================================================
2788
-
2789
- /**
2790
- * Source-agnostic 6-DOF pose producer. Holds a profile + the integrated pose;
2791
- * no timeline (no keyframes, no play/seek/loop).
2792
- */
2793
- class PoseHelm {
2794
- constructor() {
2795
- /**
2796
- * DOF mapping table. Six channels, each `{ sign, sens, lane }`.
2797
- * Public and mutable — set wholesale to re-map a device or app.
2798
- * @type {{ Tx:Object, Ty:Object, Tz:Object, Rp:Object, Ry:Object, Rr:Object }}
2799
- */
2800
- this.profile = _defaultProfile();
2801
-
2802
- /**
2803
- * Rest-drift floor. A fed rate with |value| ≤ deadzone reads as 0. Devices
2804
- * that rest at exact 0 need only a small floor. @type {number}
2805
- */
2806
- this.deadzone = 8;
2807
-
2808
- /**
2809
- * Full-deflection input magnitude for the read-outs (gizmo overlay + panel
2810
- * meters): a fed rate of |fullScale| reads as a full bar / arrow. Set by a
2811
- * transport whose saturated rate differs from the default (a gamepad stick
2812
- * that saturates at 1 sets fullScale = 1). Display-only — integration uses
2813
- * profile.sens directly, so this never affects flight. @type {number}
2814
- */
2815
- this.fullScale = HELM_FULL_SCALE;
2816
-
2817
- /**
2818
- * Optional input conditioner applied to the fed rates BEFORE the deadzone,
2819
- * inside step — a oneEuro filter, or any f(out, raw, dt) carrying function
2820
- * with a reset(). null (the default) is the clean rate-native path: the
2821
- * filter branch is skipped, so a clean device pays nothing. Set for noisy /
2822
- * absolute sources; home() resets it. @type {?Function}
2823
- */
2824
- this.filter = null;
2825
-
2826
- /**
2827
- * The space fed rates are interpreted in: a space constant (WORLD | EYE) or
2828
- * a mat4 frame. Declarative only — the bridge reads this to resolve the
2829
- * per-step `basis`; the core stays camera-agnostic. @type {string|ArrayLike<number>}
2830
- */
2831
- this.from = 'EYE';
2832
-
2833
- // Latest fed raw rates (refreshed by feed; persist until the next feed —
2834
- // the transport is responsible for feeding 0 when motion should stop).
2835
- this._lin = [0, 0, 0];
2836
- this._ang = [0, 0, 0];
2837
-
2838
- // Integrated pose — the source of truth.
2839
- this._pos = [0, 0, 0];
2840
- this._q = [0, 0, 0, 1];
2841
- }
2842
-
2843
- /**
2844
- * Push the latest raw 6-DOF device rate. Either argument may be omitted to
2845
- * leave that half unchanged (devices report the two halves on separate
2846
- * frames). The value persists until the next feed. Raw device units — the
2847
- * profile's `sens` does the scaling, so the same feed() suits any transport.
2848
- *
2849
- * @param {ArrayLike<number>} [translation] [tx, ty, tz] raw lane rates.
2850
- * @param {ArrayLike<number>} [rotation] [rx, ry, rz] raw lane rates.
2851
- * @returns {PoseHelm} this
2852
- */
2853
- feed(translation, rotation) {
2854
- if (translation) {
2855
- this._lin[0] = translation[0] || 0;
2856
- this._lin[1] = translation[1] || 0;
2857
- this._lin[2] = translation[2] || 0;
2858
- }
2859
- if (rotation) {
2860
- this._ang[0] = rotation[0] || 0;
2861
- this._ang[1] = rotation[1] || 0;
2862
- this._ang[2] = rotation[2] || 0;
2863
- }
2864
- return this;
2865
- }
2866
-
2867
- /**
2868
- * Integrate the latest fed rate by `dt` seconds into the pose, then write the
2869
- * pose into `out`. Bridge-driven (the player calls it each frame; a sketch
2870
- * never does, as it never calls track.tick()).
2871
- *
2872
- * `basis` is the resolved `from` frame: an eye→world column-major mat4
2873
- * (16-element ArrayLike), or null/omitted for WORLD (the identity basis). Its
2874
- * columns supply the right/up/back axes; forward is −col2. Both the linear and
2875
- * angular rates are rotated through it, then `q` is composed world-frame.
2876
- *
2877
- * Zero-allocation. `out` is `{ pos:number[3], rot:number[4] }`; omit it for a
2878
- * fresh object.
2879
- *
2880
- * @param {{ pos:number[], rot:number[] }} [out]
2881
- * @param {number} dt Elapsed time in seconds.
2882
- * @param {ArrayLike<number>} [basis] Eye→world mat4, or null for WORLD.
2883
- * @returns {{ pos:number[], rot:number[] }} out
2884
- */
2885
- step(out, dt, basis) {
2886
- const p = this.profile, dz = this.deadzone;
2887
- let lin = this._lin, ang = this._ang;
2888
-
2889
- // Value layer — condition the fed rates BEFORE the deadzone (the filter
2890
- // narrows the jitter band; the deadzone then gates the residual to exact
2891
- // zero). The filter carries state, so it is ticked exactly once per step;
2892
- // activity() reads the raw fed rate and is left unfiltered (the read-out
2893
- // shows device input, normalized by fullScale). Skipped when filter is null.
2894
- const filter = this.filter;
2895
- if (filter) {
2896
- _f6raw[0] = lin[0]; _f6raw[1] = lin[1]; _f6raw[2] = lin[2];
2897
- _f6raw[3] = ang[0]; _f6raw[4] = ang[1]; _f6raw[5] = ang[2];
2898
- filter(_f6out, _f6raw, dt);
2899
- _fLin[0] = _f6out[0]; _fLin[1] = _f6out[1]; _fLin[2] = _f6out[2];
2900
- _fAng[0] = _f6out[3]; _fAng[1] = _f6out[4]; _fAng[2] = _f6out[5];
2901
- lin = _fLin; ang = _fAng;
2902
- }
2903
-
2904
- // Basis axes (right / up / forward). No basis ⇒ the identity eye matrix:
2905
- // right +X, up +Y, forward −Z. Forward is −col2 (an eye→world matrix stores
2906
- // the camera's BACK in col2), and the identity's col2 is +Z, so forward is
2907
- // −Z — making step(out, dt, null) identical to step(out, dt, IDENTITY_MAT4),
2908
- // as the `from` contract ("null ≡ the identity basis") promises.
2909
- let rX = 1, rY = 0, rZ = 0;
2910
- let uX = 0, uY = 1, uZ = 0;
2911
- let fX = 0, fY = 0, fZ = -1;
2912
- if (basis) {
2913
- rX = basis[0]; rY = basis[1]; rZ = basis[2];
2914
- uX = basis[4]; uY = basis[5]; uZ = basis[6];
2915
- fX = -basis[8]; fY = -basis[9]; fZ = -basis[10];
2916
- }
2917
-
2918
- // Angular: three rates → one delta quat, rotated into world, world-composed.
2919
- const wx = _dz(ang[p.Rp.lane], dz) * p.Rp.sign * p.Rp.sens;
2920
- const wy = _dz(ang[p.Ry.lane], dz) * p.Ry.sign * p.Ry.sens;
2921
- const wz = _dz(ang[p.Rr.lane], dz) * p.Rr.sign * p.Rr.sens;
2922
- const w = Math.sqrt(wx * wx + wy * wy + wz * wz);
2923
- if (w > 0) {
2924
- // Axis in world = basis · (wx,wy,wz); its length is w (basis orthonormal),
2925
- // so the rotation angle is w·dt. qFromAxisAngle renormalizes the axis.
2926
- const ax = wx * rX + wy * uX + wz * fX;
2927
- const ay = wx * rY + wy * uY + wz * fY;
2928
- const az = wx * rZ + wy * uZ + wz * fZ;
2929
- qFromAxisAngle(_dq, ax, ay, az, w * dt);
2930
- qMul(this._q, _dq, this._q); // delta already in world → world-frame compose
2931
- qNormalize(this._q);
2932
- }
2933
-
2934
- // Linear: three rates rotated into world, integrated by dt.
2935
- const tx = _dz(lin[p.Tx.lane], dz) * p.Tx.sign * p.Tx.sens;
2936
- const ty = _dz(lin[p.Ty.lane], dz) * p.Ty.sign * p.Ty.sens;
2937
- const tz = _dz(lin[p.Tz.lane], dz) * p.Tz.sign * p.Tz.sens;
2938
- this._pos[0] += (tx * rX + ty * uX + tz * fX) * dt;
2939
- this._pos[1] += (tx * rY + ty * uY + tz * fY) * dt;
2940
- this._pos[2] += (tx * rZ + ty * uZ + tz * fZ) * dt;
2941
-
2942
- return this.eval(out);
2943
- }
2944
-
2945
- /**
2946
- * Read the current pose into `out` (zero-alloc). The shared output contract
2947
- * that feeds applyPose — mirrors Track.eval.
2948
- *
2949
- * @param {{ pos:number[], rot:number[] }} [out]
2950
- * @returns {{ pos:number[], rot:number[] }} out
2951
- */
2952
- eval(out) {
2953
- out = out || { pos: [0, 0, 0], rot: [0, 0, 0, 1] };
2954
- out.pos[0] = this._pos[0]; out.pos[1] = this._pos[1]; out.pos[2] = this._pos[2];
2955
- out.rot[0] = this._q[0]; out.rot[1] = this._q[1];
2956
- out.rot[2] = this._q[2]; out.rot[3] = this._q[3];
2957
- return out;
2958
- }
2959
-
2960
- /**
2961
- * Write the six effective channel rates (post deadzone·sign·sens) into `out6`,
2962
- * in channel order [Tx, Ty, Tz, Rp, Ry, Rr]. The gizmo reads this to light the
2963
- * DOFs being driven; readouts can show signed magnitudes. Zero-alloc.
2964
- *
2965
- * @param {number[]} out6 6-element destination.
2966
- * @returns {number[]} out6
2967
- */
2968
- activity(out6) {
2969
- const p = this.profile, dz = this.deadzone, lin = this._lin, ang = this._ang;
2970
- out6[0] = _dz(lin[p.Tx.lane], dz) * p.Tx.sign * p.Tx.sens;
2971
- out6[1] = _dz(lin[p.Ty.lane], dz) * p.Ty.sign * p.Ty.sens;
2972
- out6[2] = _dz(lin[p.Tz.lane], dz) * p.Tz.sign * p.Tz.sens;
2973
- out6[3] = _dz(ang[p.Rp.lane], dz) * p.Rp.sign * p.Rp.sens;
2974
- out6[4] = _dz(ang[p.Ry.lane], dz) * p.Ry.sign * p.Ry.sens;
2975
- out6[5] = _dz(ang[p.Rr.lane], dz) * p.Rr.sign * p.Rr.sens;
2976
- return out6;
2977
- }
2978
-
2979
- /**
2980
- * Re-home the integrated pose. Sets position + orientation to `pose` (or the
2981
- * identity pose when omitted) and clears any pending rate so the helm rests at
2982
- * a clean, known state. NOT a reset — there are no keyframes to clear.
2983
- *
2984
- * @param {{ pos?:ArrayLike<number>, rot?:ArrayLike<number> }} [pose]
2985
- * @returns {PoseHelm} this
2986
- */
2987
- home(pose) {
2988
- if (pose && pose.pos) {
2989
- this._pos[0] = pose.pos[0]; this._pos[1] = pose.pos[1]; this._pos[2] = pose.pos[2];
2990
- } else {
2991
- this._pos[0] = 0; this._pos[1] = 0; this._pos[2] = 0;
2992
- }
2993
- if (pose && pose.rot) {
2994
- this._q[0] = pose.rot[0]; this._q[1] = pose.rot[1];
2995
- this._q[2] = pose.rot[2]; this._q[3] = pose.rot[3];
2996
- } else {
2997
- this._q[0] = 0; this._q[1] = 0; this._q[2] = 0; this._q[3] = 1;
2998
- }
2999
- this._lin[0] = this._lin[1] = this._lin[2] = 0;
3000
- this._ang[0] = this._ang[1] = this._ang[2] = 0;
3001
- if (this.filter) this.filter.reset(); // drop filter state at the discontinuity
3002
- return this;
3003
- }
3004
- }
3005
- const _d = [0, 0, 0]; // view-direction scratch
3006
-
3007
- /** Gaze distance |center − eye|, or 1 when the state is degenerate. */
3008
- function _gaze(cam) {
3009
- const dx = cam.center[0]-cam.eye[0], dy = cam.center[1]-cam.eye[1], dz = cam.center[2]-cam.eye[2];
3010
- const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
3011
- return d > 0 ? d : 1;
3012
- }
3013
-
3014
- // =========================================================================
3015
- // Builders — state → matrices
3016
- // =========================================================================
3017
-
3018
- /**
3019
- * View matrix (world→eye) from the state's lookat.
3020
- * @param {Float32Array|number[]} out 16-element destination.
3021
- * @param {object} cam
3022
- * @returns {Float32Array|number[]} out
3023
- */
3024
- function cameraView(out, cam) {
3025
- const e = cam.eye, c = cam.center, u = cam.up;
3026
- return mat4View(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
3027
- }
3028
-
3029
- /**
3030
- * Eye matrix (eye→world) from the state's lookat.
3031
- * @param {Float32Array|number[]} out 16-element destination.
3032
- * @param {object} cam
3033
- * @returns {Float32Array|number[]} out
3034
- */
3035
- function cameraEye(out, cam) {
3036
- const e = cam.eye, c = cam.center, u = cam.up;
3037
- return mat4Eye(out, e[0],e[1],e[2], c[0],c[1],c[2], u[0],u[1],u[2]);
3038
- }
3039
-
3040
- /**
3041
- * Projection matrix from the state's lens: mat4Persp from fov, or mat4Ortho
3042
- * from halfHeight, with symmetric extents (top = near · tan(fov / 2) or
3043
- * halfHeight; right = top · aspect).
3044
- *
3045
- * @param {Float32Array|number[]} out 16-element destination.
3046
- * @param {object} cam
3047
- * @param {number} aspect Viewport width / height.
3048
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
3049
- * @param {number} [ndcYSign=1] +1 NDC y-up; −1 NDC y-down.
3050
- * @returns {Float32Array|number[]|null} out, or null (out untouched) when
3051
- * both fov and halfHeight are null.
3052
- */
3053
- function cameraProj(out, cam, aspect, ndcZMin, ndcYSign = 1) {
3054
- const near = cam.near, far = cam.far;
3055
- if (cam.fov != null) {
3056
- const top = near * Math.tan(cam.fov / 2), right = top * aspect;
3057
- return mat4Persp(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
3058
- }
3059
- if (cam.halfHeight != null) {
3060
- const top = cam.halfHeight, right = top * aspect;
3061
- return mat4Ortho(out, -right, right, -top, top, near, far, ndcZMin, ndcYSign);
3062
- }
3063
- return null;
3064
- }
3065
-
3066
- /**
3067
- * Drive the lookat from a TRS pose: eye ← pos, up and forward from the
3068
- * rotation's columns 1 and −2, center ← eye + forward · d with d the current
3069
- * gaze distance (1 when degenerate). The lens is untouched; `scl` is ignored.
3070
- *
3071
- * @param {object} cam State written in place.
3072
- * @param {{ pos:number[], rot:number[] }} pose rot is a unit quaternion [x,y,z,w].
3073
- * @returns {object} cam
3074
- */
3075
- function cameraFromPose(cam, pose) {
3076
- const d = _gaze(cam);
3077
- const q = pose.rot, x=q[0], y=q[1], z=q[2], w=q[3];
3078
- const x2=x+x, y2=y+y, z2=z+z;
3079
- 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;
3080
- const ux=xy-wz, uy=1-(xx+zz), uz=yz+wx; // column 1 of qToMat4(rot): up
3081
- const bx=xz+wy, by=yz-wx, bz=1-(xx+yy); // column 2: back
3082
- cam.eye[0]=pose.pos[0]; cam.eye[1]=pose.pos[1]; cam.eye[2]=pose.pos[2];
3083
- cam.up[0]=ux; cam.up[1]=uy; cam.up[2]=uz;
3084
- cam.center[0]=cam.eye[0]-bx*d; cam.center[1]=cam.eye[1]-by*d; cam.center[2]=cam.eye[2]-bz*d;
3085
- return cam;
3086
- }
3087
-
3088
- /**
3089
- * The lookat as a TRS pose: pos ← eye, rot ← qFromLookDir(center − eye, up) —
3090
- * the rotation of cameraEye, so a helm seeded from it continues the frame.
3091
- *
3092
- * @param {{ pos:number[], rot:number[] }} pose Written in place.
3093
- * @param {object} cam
3094
- * @returns {{ pos:number[], rot:number[] }} pose
3095
- */
3096
- function cameraToPose(pose, cam) {
3097
- pose.pos[0]=cam.eye[0]; pose.pos[1]=cam.eye[1]; pose.pos[2]=cam.eye[2];
3098
- _d[0]=cam.center[0]-cam.eye[0]; _d[1]=cam.center[1]-cam.eye[1]; _d[2]=cam.center[2]-cam.eye[2];
3099
- qFromLookDir(pose.rot, _d, cam.up);
3100
- return pose;
3101
- }
1
+ import { WEBGL, cameraView, cameraProj, mat4Mul, mat4Invert, PLANE, createConstraint, WORLD, AXIS, DIAL, POINT, mapDirection, pixelRatio, rayHitSphere, unproject, SPHERE, dirFromAzEl, DIRECTION, mapLocation, MATRIX, PoseHelm, cameraToPose, EYE, SELF, qToMat4, cameraEye, cameraFromPose, CameraTrack, PoseTrack, qFromAxisAngle, SCREEN, cameraCopy, createCamera, cameraOrbit, cameraPan, cameraDolly } from '@nakednous/tree';
3102
2
 
3103
3
  /**
3104
4
  * @file The view bag — the matrices every pick, solve, label and orbit reads.
@@ -4690,6 +1590,819 @@ class TrackHandles {
4690
1590
  }
4691
1591
  }
4692
1592
 
1593
+ /**
1594
+ * @file Device streams — WebHID and Gamepad rate sources that feed a helm.
1595
+ * @module host/stream
1596
+ * @license AGPL-3.0-only
1597
+ *
1598
+ * The measure-by-subscription transports. Each yields lin[3] / ang[3] in raw
1599
+ * device units and, once bind(helm) ran, calls helm.feed(lin, ang) every
1600
+ * tick of the host's players — the helm's profile does all scaling, so a
1601
+ * stream carries no configuration beyond identification and decoding.
1602
+ *
1603
+ * hid — WebHID. connect() must run from a user gesture; reports land
1604
+ * out of band into the stream's buffers and the frame reads the
1605
+ * latest (last-write-wins). Permissions-Policy gates
1606
+ * requestDevice to top-level documents: a stream in an iframe
1607
+ * without allow="hid" reports available === false and never
1608
+ * prompts.
1609
+ * gamepad — the Gamepad API, polled once per tick from
1610
+ * navigator.getGamepads(); an absent axis reads exactly 0.
1611
+ *
1612
+ * Both expose available and connected for a UI to reflect, and bind(helm),
1613
+ * unbind(), dispose().
1614
+ */
1615
+
1616
+
1617
+ const _nav = () => (typeof navigator !== 'undefined' ? navigator : null);
1618
+
1619
+ /** The 3Dconnexion vendor ids: the SpaceNavigator and its successors. */
1620
+ const HID_FILTERS = [{ vendorId: 0x046d }, { vendorId: 0x256f }];
1621
+
1622
+ /**
1623
+ * The SpaceNavigator decode: report 1 carries translation, report 2
1624
+ * rotation, each three int16 little-endian lanes; a firmware that packs all
1625
+ * six lanes into one report fills both halves from it.
1626
+ * @param {DataView} report The report's data (the id byte excluded).
1627
+ * @param {number} reportId
1628
+ * @param {number[]} lin Translation lanes, written.
1629
+ * @param {number[]} ang Rotation lanes, written.
1630
+ */
1631
+ function decodeSpaceNavigator(report, reportId, lin, ang) {
1632
+ if (report.byteLength >= 12) {
1633
+ for (let i = 0; i < 3; i++) { lin[i] = report.getInt16(2 * i, true); ang[i] = report.getInt16(6 + 2 * i, true); }
1634
+ return;
1635
+ }
1636
+ if (report.byteLength < 6) return;
1637
+ const t = reportId === 1 ? lin : reportId === 2 ? ang : null;
1638
+ if (!t) return;
1639
+ for (let i = 0; i < 3; i++) t[i] = report.getInt16(2 * i, true);
1640
+ }
1641
+
1642
+ /** Whether WebHID's requestDevice is allowed in this document. */
1643
+ function _hidAllowed() {
1644
+ if (typeof document !== 'undefined' && document.featurePolicy && typeof document.featurePolicy.allowsFeature === 'function') {
1645
+ return document.featurePolicy.allowsFeature('hid');
1646
+ }
1647
+ if (typeof window !== 'undefined' && 'top' in window) return window.top === window.self;
1648
+ return true;
1649
+ }
1650
+
1651
+ function _stream(host, lin, ang) {
1652
+ let helm = null;
1653
+ const s = {
1654
+ /** The latest translation lanes, raw device units. */
1655
+ lin,
1656
+ /** The latest rotation lanes, raw device units. */
1657
+ ang,
1658
+ /** The bound helm, or null. */
1659
+ get helm() { return helm; },
1660
+ /**
1661
+ * Feed a helm every tick. Chainable.
1662
+ * @param {object} target A PoseHelm (anything with feed(lin, ang)).
1663
+ * @returns {object} this
1664
+ */
1665
+ bind(target) {
1666
+ if (!target || typeof target.feed !== 'function') {
1667
+ console.error('[host] stream: bind() target must have feed(lin, ang). Leaving unbound.');
1668
+ return s;
1669
+ }
1670
+ helm = target;
1671
+ return s;
1672
+ },
1673
+ /** Stop feeding; the helm sees its last rate until it is fed again. */
1674
+ unbind() { helm = null; return s; },
1675
+ };
1676
+ const feed = () => { if (helm) helm.feed(lin, ang); };
1677
+ return { s, feed };
1678
+ }
1679
+
1680
+ /**
1681
+ * A WebHID rate stream.
1682
+ *
1683
+ * @param {object} host
1684
+ * @param {{ filters?:object[], decode?:function, bind?:object, resume?:boolean,
1685
+ * hid?:object }} [opts]
1686
+ * filters: the requestDevice filters (default HID_FILTERS). decode:
1687
+ * decode(report, reportId, lin, ang) (default decodeSpaceNavigator).
1688
+ * bind: a helm to bind at once. resume (default true): attach a device
1689
+ * this origin was already granted, without a prompt. hid: the WebHID
1690
+ * interface (default navigator.hid) — the seam a test fills.
1691
+ * @returns {object} The stream: { available, connected, device, lin, ang,
1692
+ * connect(), bind(helm), unbind(), dispose() }.
1693
+ */
1694
+ function createHid(host, opts) {
1695
+ const o = opts || {};
1696
+ const hid = o.hid || (_nav() && _nav().hid) || null;
1697
+ const filters = o.filters || HID_FILTERS;
1698
+ const decode = typeof o.decode === 'function' ? o.decode : decodeSpaceNavigator;
1699
+ const lin = [0, 0, 0], ang = [0, 0, 0];
1700
+ const { s, feed } = _stream(host, lin, ang);
1701
+ let device = null;
1702
+
1703
+ const onReport = (e) => decode(e.data, e.reportId, lin, ang);
1704
+ const onDisconnect = (e) => { if (device && e.device === device) detach(); };
1705
+ const matches = (d) => filters.length === 0 || filters.some((f) =>
1706
+ (f.vendorId == null || f.vendorId === d.vendorId) && (f.productId == null || f.productId === d.productId));
1707
+
1708
+ function detach() {
1709
+ if (!device) return;
1710
+ device.removeEventListener('inputreport', onReport);
1711
+ if (device.opened && typeof device.close === 'function') device.close().catch(() => {});
1712
+ device = null;
1713
+ s.connected = false;
1714
+ lin[0] = lin[1] = lin[2] = ang[0] = ang[1] = ang[2] = 0;
1715
+ }
1716
+
1717
+ async function attach(d) {
1718
+ detach();
1719
+ try { if (!d.opened) await d.open(); }
1720
+ catch (e) { console.error('[host] hid: open() failed: ' + e.message + '.'); return false; }
1721
+ device = d;
1722
+ d.addEventListener('inputreport', onReport);
1723
+ s.connected = true;
1724
+ return true;
1725
+ }
1726
+
1727
+ Object.assign(s, {
1728
+ /** Whether WebHID exists and requestDevice is allowed here. */
1729
+ available: !!(hid && typeof hid.requestDevice === 'function') && _hidAllowed(),
1730
+ /** Whether a device is open and reporting. */
1731
+ connected: false,
1732
+
1733
+ /**
1734
+ * Prompt for a device (a user gesture is required) and attach the choice.
1735
+ * @returns {Promise<boolean>} attached
1736
+ */
1737
+ async connect() {
1738
+ if (!s.available) { console.error('[host] hid: WebHID is unavailable here (a top-level Chromium document over https or localhost).'); return false; }
1739
+ let devices;
1740
+ try { devices = await hid.requestDevice({ filters }); }
1741
+ catch (e) { console.error('[host] hid: requestDevice rejected: ' + e.message + '.'); return false; }
1742
+ if (!devices || !devices.length) return false;
1743
+ return attach(devices[0]);
1744
+ },
1745
+
1746
+ /**
1747
+ * Attach a device this origin was already granted, without a prompt.
1748
+ * @returns {Promise<boolean>} attached
1749
+ */
1750
+ async resume() {
1751
+ if (!hid || typeof hid.getDevices !== 'function') return false;
1752
+ let devices;
1753
+ try { devices = await hid.getDevices(); } catch (_) { return false; }
1754
+ const d = (devices || []).find(matches);
1755
+ return d ? attach(d) : false;
1756
+ },
1757
+
1758
+ /** Close the device, remove every listener, unregister from the host. */
1759
+ dispose() {
1760
+ detach();
1761
+ if (hid && typeof hid.removeEventListener === 'function') hid.removeEventListener('disconnect', onDisconnect);
1762
+ host.players.remove(player);
1763
+ host.unregister(s);
1764
+ return s;
1765
+ },
1766
+ });
1767
+
1768
+ /** The open HIDDevice, or null. */
1769
+ Object.defineProperty(s, 'device', { get: () => device, enumerable: true });
1770
+
1771
+ const player = { tick() { feed(); return true; } };
1772
+ host.players.add(player);
1773
+ if (hid && typeof hid.addEventListener === 'function') hid.addEventListener('disconnect', onDisconnect);
1774
+ if (o.bind) s.bind(o.bind);
1775
+ if (s.available && o.resume !== false) s.resume();
1776
+ return host.register(s);
1777
+ }
1778
+
1779
+ /**
1780
+ * The default gamepad map, the standard layout: left stick → lin[0] / lin[1],
1781
+ * right stick → ang[1] / ang[0], the triggers → lin[2] (right minus left).
1782
+ */
1783
+ const GAMEPAD_MAP = { lin: [0, 1, { buttons: [6, 7] }], ang: [3, 2, null] };
1784
+
1785
+ function _lane(gp, spec) {
1786
+ if (spec == null) return 0;
1787
+ if (typeof spec === 'number') { const v = gp.axes[spec]; return typeof v === 'number' ? v : 0; }
1788
+ if (spec.buttons) {
1789
+ const n = gp.buttons[spec.buttons[0]], p = gp.buttons[spec.buttons[1]];
1790
+ return (p ? p.value : 0) - (n ? n.value : 0);
1791
+ }
1792
+ if (spec.button) { const b = gp.buttons[spec.button]; return b ? b.value : 0; }
1793
+ return 0;
1794
+ }
1795
+
1796
+ /**
1797
+ * A Gamepad rate stream, polled once per tick.
1798
+ *
1799
+ * @param {object} host
1800
+ * @param {{ index?:number, map?:object, bind?:object, gamepads?:function }} [opts]
1801
+ * index: which gamepad (default: the first connected). map: { lin,
1802
+ * ang }, each three lane specs — an axis index, { buttons: [neg,
1803
+ * pos] }, { button } or null (default GAMEPAD_MAP). bind: a helm to
1804
+ * bind at once. gamepads: the poll (default navigator.getGamepads) —
1805
+ * the seam a test fills.
1806
+ * @returns {object} The stream: { available, connected, index, lin, ang,
1807
+ * bind(helm), unbind(), dispose() }.
1808
+ */
1809
+ function createGamepad(host, opts) {
1810
+ const o = opts || {};
1811
+ const nav = _nav();
1812
+ const poll = o.gamepads || (nav && typeof nav.getGamepads === 'function' ? () => nav.getGamepads() : null);
1813
+ const map = o.map || GAMEPAD_MAP;
1814
+ const lin = [0, 0, 0], ang = [0, 0, 0];
1815
+ const { s, feed } = _stream(host, lin, ang);
1816
+
1817
+ Object.assign(s, {
1818
+ /** Whether the Gamepad API exists. */
1819
+ available: !!poll,
1820
+ /** Whether the last poll found a gamepad. */
1821
+ connected: false,
1822
+ /** The gamepad index polled, or null for the first connected. */
1823
+ index: o.index ?? null,
1824
+ /** Stop polling, unregister from the host. */
1825
+ dispose() { host.players.remove(player); host.unregister(s); return s; },
1826
+ });
1827
+
1828
+ const player = {
1829
+ tick() {
1830
+ let gp = null;
1831
+ if (poll) {
1832
+ const list = poll() || [];
1833
+ if (s.index != null) gp = list[s.index] || null;
1834
+ else for (let i = 0; i < list.length; i++) if (list[i]) { gp = list[i]; break; }
1835
+ }
1836
+ s.connected = !!gp;
1837
+ for (let i = 0; i < 3; i++) {
1838
+ lin[i] = gp ? _lane(gp, map.lin[i]) : 0;
1839
+ ang[i] = gp ? _lane(gp, map.ang[i]) : 0;
1840
+ }
1841
+ feed();
1842
+ return true;
1843
+ },
1844
+ };
1845
+ host.players.add(player);
1846
+ if (o.bind) s.bind(o.bind);
1847
+ return host.register(s);
1848
+ }
1849
+
1850
+ function getDefaultExportFromCjs (x) {
1851
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
1852
+ }
1853
+
1854
+ var webglObjLoader_min$1 = {exports: {}};
1855
+
1856
+ var webglObjLoader_min = webglObjLoader_min$1.exports;
1857
+
1858
+ var hasRequiredWebglObjLoader_min;
1859
+
1860
+ function requireWebglObjLoader_min () {
1861
+ if (hasRequiredWebglObjLoader_min) return webglObjLoader_min$1.exports;
1862
+ hasRequiredWebglObjLoader_min = 1;
1863
+ (function (module, exports) {
1864
+ !function(e,t){module.exports=t();}("undefined"!=typeof self?self:webglObjLoader_min,(function(){return function(e){var t={};function n(a){if(t[a])return t[a].exports;var s=t[a]={i:a,l:false,exports:{}};return e[a].call(s.exports,s,s.exports,n),s.l=true,s.exports}return n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:true,get:a});},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:true});},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:true,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(a,s,function(t){return e[t]}.bind(null,s));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=0)}({"./src/index.ts":
1865
+ /*!**********************!*\
1866
+ !*** ./src/index.ts ***!
1867
+ \**********************/
1868
+ /*! exports provided: OBJ, Attribute, DuplicateAttributeException, Layout, Material, MaterialLibrary, Mesh, TYPES, downloadModels, downloadMeshes, initMeshBuffers, deleteMeshBuffers, version */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OBJ", function() { return OBJ; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "version", function() { return version; });\n/* harmony import */ var _mesh__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./mesh */ "./src/mesh.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Mesh", function() { return _mesh__WEBPACK_IMPORTED_MODULE_0__["default"]; });\n\n/* harmony import */ var _material__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./material */ "./src/material.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Material", function() { return _material__WEBPACK_IMPORTED_MODULE_1__["Material"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "MaterialLibrary", function() { return _material__WEBPACK_IMPORTED_MODULE_1__["MaterialLibrary"]; });\n\n/* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./layout */ "./src/layout.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Attribute", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["Attribute"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "DuplicateAttributeException", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["DuplicateAttributeException"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Layout", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["Layout"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TYPES", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["TYPES"]; });\n\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "downloadModels", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["downloadModels"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "downloadMeshes", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["downloadMeshes"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "initMeshBuffers", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["initMeshBuffers"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "deleteMeshBuffers", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["deleteMeshBuffers"]; });\n\n\n\n\n\nconst version = "2.0.3";\nconst OBJ = {\n Attribute: _layout__WEBPACK_IMPORTED_MODULE_2__["Attribute"],\n DuplicateAttributeException: _layout__WEBPACK_IMPORTED_MODULE_2__["DuplicateAttributeException"],\n Layout: _layout__WEBPACK_IMPORTED_MODULE_2__["Layout"],\n Material: _material__WEBPACK_IMPORTED_MODULE_1__["Material"],\n MaterialLibrary: _material__WEBPACK_IMPORTED_MODULE_1__["MaterialLibrary"],\n Mesh: _mesh__WEBPACK_IMPORTED_MODULE_0__["default"],\n TYPES: _layout__WEBPACK_IMPORTED_MODULE_2__["TYPES"],\n downloadModels: _utils__WEBPACK_IMPORTED_MODULE_3__["downloadModels"],\n downloadMeshes: _utils__WEBPACK_IMPORTED_MODULE_3__["downloadMeshes"],\n initMeshBuffers: _utils__WEBPACK_IMPORTED_MODULE_3__["initMeshBuffers"],\n deleteMeshBuffers: _utils__WEBPACK_IMPORTED_MODULE_3__["deleteMeshBuffers"],\n version,\n};\n/**\n * @namespace\n */\n\n\n\n//# sourceURL=webpack:///./src/index.ts?');},"./src/layout.ts":
1869
+ /*!***********************!*\
1870
+ !*** ./src/layout.ts ***!
1871
+ \***********************/
1872
+ /*! exports provided: TYPES, DuplicateAttributeException, Attribute, Layout */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TYPES", function() { return TYPES; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DuplicateAttributeException", function() { return DuplicateAttributeException; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Attribute", function() { return Attribute; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Layout", function() { return Layout; });\nvar TYPES;\n(function (TYPES) {\n TYPES["BYTE"] = "BYTE";\n TYPES["UNSIGNED_BYTE"] = "UNSIGNED_BYTE";\n TYPES["SHORT"] = "SHORT";\n TYPES["UNSIGNED_SHORT"] = "UNSIGNED_SHORT";\n TYPES["FLOAT"] = "FLOAT";\n})(TYPES || (TYPES = {}));\n/**\n * An exception for when two or more of the same attributes are found in the\n * same layout.\n * @private\n */\nclass DuplicateAttributeException extends Error {\n /**\n * Create a DuplicateAttributeException\n * @param {Attribute} attribute - The attribute that was found more than\n * once in the {@link Layout}\n */\n constructor(attribute) {\n super(`found duplicate attribute: ${attribute.key}`);\n }\n}\n/**\n * Represents how a vertex attribute should be packed into an buffer.\n * @private\n */\nclass Attribute {\n /**\n * Create an attribute. Do not call this directly, use the predefined\n * constants.\n * @param {string} key - The name of this attribute as if it were a key in\n * an Object. Use the camel case version of the upper snake case\n * const name.\n * @param {number} size - The number of components per vertex attribute.\n * Must be 1, 2, 3, or 4.\n * @param {string} type - The data type of each component for this\n * attribute. Possible values:<br/>\n * "BYTE": signed 8-bit integer, with values in [-128, 127]<br/>\n * "SHORT": signed 16-bit integer, with values in\n * [-32768, 32767]<br/>\n * "UNSIGNED_BYTE": unsigned 8-bit integer, with values in\n * [0, 255]<br/>\n * "UNSIGNED_SHORT": unsigned 16-bit integer, with values in\n * [0, 65535]<br/>\n * "FLOAT": 32-bit floating point number\n * @param {boolean} normalized - Whether integer data values should be\n * normalized when being casted to a float.<br/>\n * If true, signed integers are normalized to [-1, 1].<br/>\n * If true, unsigned integers are normalized to [0, 1].<br/>\n * For type "FLOAT", this parameter has no effect.\n */\n constructor(key, size, type, normalized = false) {\n this.key = key;\n this.size = size;\n this.type = type;\n this.normalized = normalized;\n switch (type) {\n case "BYTE":\n case "UNSIGNED_BYTE":\n this.sizeOfType = 1;\n break;\n case "SHORT":\n case "UNSIGNED_SHORT":\n this.sizeOfType = 2;\n break;\n case "FLOAT":\n this.sizeOfType = 4;\n break;\n default:\n throw new Error(`Unknown gl type: ${type}`);\n }\n this.sizeInBytes = this.sizeOfType * size;\n }\n}\n/**\n * A class to represent the memory layout for a vertex attribute array. Used by\n * {@link Mesh}\'s TBD(...) method to generate a packed array from mesh data.\n * <p>\n * Layout can sort of be thought of as a C-style struct declaration.\n * {@link Mesh}\'s TBD(...) method will use the {@link Layout} instance to\n * pack an array in the given attribute order.\n * <p>\n * Layout also is very helpful when calling a WebGL context\'s\n * <code>vertexAttribPointer</code> method. If you\'ve created a buffer using\n * a Layout instance, then the same Layout instance can be used to determine\n * the size, type, normalized, stride, and offset parameters for\n * <code>vertexAttribPointer</code>.\n * <p>\n * For example:\n * <pre><code>\n *\n * const index = glctx.getAttribLocation(shaderProgram, "pos");\n * glctx.vertexAttribPointer(\n * layout.position.size,\n * glctx[layout.position.type],\n * layout.position.normalized,\n * layout.position.stride,\n * layout.position.offset);\n * </code></pre>\n * @see {@link Mesh}\n */\nclass Layout {\n /**\n * Create a Layout object. This constructor will throw if any duplicate\n * attributes are given.\n * @param {Array} ...attributes - An ordered list of attributes that\n * describe the desired memory layout for each vertex attribute.\n * <p>\n *\n * @see {@link Mesh}\n */\n constructor(...attributes) {\n this.attributes = attributes;\n this.attributeMap = {};\n let offset = 0;\n let maxStrideMultiple = 0;\n for (const attribute of attributes) {\n if (this.attributeMap[attribute.key]) {\n throw new DuplicateAttributeException(attribute);\n }\n // Add padding to satisfy WebGL\'s requirement that all\n // vertexAttribPointer calls have an offset that is a multiple of\n // the type size.\n if (offset % attribute.sizeOfType !== 0) {\n offset += attribute.sizeOfType - (offset % attribute.sizeOfType);\n console.warn("Layout requires padding before " + attribute.key + " attribute");\n }\n this.attributeMap[attribute.key] = {\n attribute: attribute,\n size: attribute.size,\n type: attribute.type,\n normalized: attribute.normalized,\n offset: offset,\n };\n offset += attribute.sizeInBytes;\n maxStrideMultiple = Math.max(maxStrideMultiple, attribute.sizeOfType);\n }\n // Add padding to the end to satisfy WebGL\'s requirement that all\n // vertexAttribPointer calls have a stride that is a multiple of the\n // type size. Because we\'re putting differently sized attributes into\n // the same buffer, it must be padded to a multiple of the largest\n // type size.\n if (offset % maxStrideMultiple !== 0) {\n offset += maxStrideMultiple - (offset % maxStrideMultiple);\n console.warn("Layout requires padding at the back");\n }\n this.stride = offset;\n for (const attribute of attributes) {\n this.attributeMap[attribute.key].stride = this.stride;\n }\n }\n}\n// Geometry attributes\n/**\n * Attribute layout to pack a vertex\'s x, y, & z as floats\n *\n * @see {@link Layout}\n */\nLayout.POSITION = new Attribute("position", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s normal\'s x, y, & z as floats\n *\n * @see {@link Layout}\n */\nLayout.NORMAL = new Attribute("normal", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s normal\'s x, y, & z as floats.\n * <p>\n * This value will be computed on-the-fly based on the texture coordinates.\n * If no texture coordinates are available, the generated value will default to\n * 0, 0, 0.\n *\n * @see {@link Layout}\n */\nLayout.TANGENT = new Attribute("tangent", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s normal\'s bitangent x, y, & z as floats.\n * <p>\n * This value will be computed on-the-fly based on the texture coordinates.\n * If no texture coordinates are available, the generated value will default to\n * 0, 0, 0.\n * @see {@link Layout}\n */\nLayout.BITANGENT = new Attribute("bitangent", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s texture coordinates\' u & v as floats\n *\n * @see {@link Layout}\n */\nLayout.UV = new Attribute("uv", 2, TYPES.FLOAT);\n// Material attributes\n/**\n * Attribute layout to pack an unsigned short to be interpreted as a the index\n * into a {@link Mesh}\'s materials list.\n * <p>\n * The intention of this value is to send all of the {@link Mesh}\'s materials\n * into multiple shader uniforms and then reference the current one by this\n * vertex attribute.\n * <p>\n * example glsl code:\n *\n * <pre><code>\n * // this is bound using MATERIAL_INDEX\n * attribute int materialIndex;\n *\n * struct Material {\n * vec3 diffuse;\n * vec3 specular;\n * vec3 specularExponent;\n * };\n *\n * uniform Material materials[MAX_MATERIALS];\n *\n * // ...\n *\n * vec3 diffuse = materials[materialIndex];\n *\n * </code></pre>\n * TODO: More description & test to make sure subscripting by attributes even\n * works for webgl\n *\n * @see {@link Layout}\n */\nLayout.MATERIAL_INDEX = new Attribute("materialIndex", 1, TYPES.SHORT);\nLayout.MATERIAL_ENABLED = new Attribute("materialEnabled", 1, TYPES.UNSIGNED_SHORT);\nLayout.AMBIENT = new Attribute("ambient", 3, TYPES.FLOAT);\nLayout.DIFFUSE = new Attribute("diffuse", 3, TYPES.FLOAT);\nLayout.SPECULAR = new Attribute("specular", 3, TYPES.FLOAT);\nLayout.SPECULAR_EXPONENT = new Attribute("specularExponent", 3, TYPES.FLOAT);\nLayout.EMISSIVE = new Attribute("emissive", 3, TYPES.FLOAT);\nLayout.TRANSMISSION_FILTER = new Attribute("transmissionFilter", 3, TYPES.FLOAT);\nLayout.DISSOLVE = new Attribute("dissolve", 1, TYPES.FLOAT);\nLayout.ILLUMINATION = new Attribute("illumination", 1, TYPES.UNSIGNED_SHORT);\nLayout.REFRACTION_INDEX = new Attribute("refractionIndex", 1, TYPES.FLOAT);\nLayout.SHARPNESS = new Attribute("sharpness", 1, TYPES.FLOAT);\nLayout.MAP_DIFFUSE = new Attribute("mapDiffuse", 1, TYPES.SHORT);\nLayout.MAP_AMBIENT = new Attribute("mapAmbient", 1, TYPES.SHORT);\nLayout.MAP_SPECULAR = new Attribute("mapSpecular", 1, TYPES.SHORT);\nLayout.MAP_SPECULAR_EXPONENT = new Attribute("mapSpecularExponent", 1, TYPES.SHORT);\nLayout.MAP_DISSOLVE = new Attribute("mapDissolve", 1, TYPES.SHORT);\nLayout.ANTI_ALIASING = new Attribute("antiAliasing", 1, TYPES.UNSIGNED_SHORT);\nLayout.MAP_BUMP = new Attribute("mapBump", 1, TYPES.SHORT);\nLayout.MAP_DISPLACEMENT = new Attribute("mapDisplacement", 1, TYPES.SHORT);\nLayout.MAP_DECAL = new Attribute("mapDecal", 1, TYPES.SHORT);\nLayout.MAP_EMISSIVE = new Attribute("mapEmissive", 1, TYPES.SHORT);\n\n\n//# sourceURL=webpack:///./src/layout.ts?');},"./src/material.ts":
1873
+ /*!*************************!*\
1874
+ !*** ./src/material.ts ***!
1875
+ \*************************/
1876
+ /*! exports provided: Material, MaterialLibrary */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Material", function() { return Material; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MaterialLibrary", function() { return MaterialLibrary; });\n/**\n * The Material class.\n */\nclass Material {\n constructor(name) {\n this.name = name;\n /**\n * Constructor\n * @param {String} name the unique name of the material\n */\n // The values for the following attibutes\n // are an array of R, G, B normalized values.\n // Ka - Ambient Reflectivity\n this.ambient = [0, 0, 0];\n // Kd - Defuse Reflectivity\n this.diffuse = [0, 0, 0];\n // Ks\n this.specular = [0, 0, 0];\n // Ke\n this.emissive = [0, 0, 0];\n // Tf\n this.transmissionFilter = [0, 0, 0];\n // d\n this.dissolve = 0;\n // valid range is between 0 and 1000\n this.specularExponent = 0;\n // either d or Tr; valid values are normalized\n this.transparency = 0;\n // illum - the enum of the illumination model to use\n this.illumination = 0;\n // Ni - Set to "normal" (air).\n this.refractionIndex = 1;\n // sharpness\n this.sharpness = 0;\n // map_Kd\n this.mapDiffuse = emptyTextureOptions();\n // map_Ka\n this.mapAmbient = emptyTextureOptions();\n // map_Ks\n this.mapSpecular = emptyTextureOptions();\n // map_Ns\n this.mapSpecularExponent = emptyTextureOptions();\n // map_d\n this.mapDissolve = emptyTextureOptions();\n // map_aat\n this.antiAliasing = false;\n // map_bump or bump\n this.mapBump = emptyTextureOptions();\n // disp\n this.mapDisplacement = emptyTextureOptions();\n // decal\n this.mapDecal = emptyTextureOptions();\n // map_Ke\n this.mapEmissive = emptyTextureOptions();\n // refl - when the reflection type is a cube, there will be multiple refl\n // statements for each side of the cube. If it\'s a spherical\n // reflection, there should only ever be one.\n this.mapReflections = [];\n }\n}\nconst SENTINEL_MATERIAL = new Material("sentinel");\n/**\n * https://en.wikipedia.org/wiki/Wavefront_.obj_file\n * http://paulbourke.net/dataformats/mtl/\n */\nclass MaterialLibrary {\n constructor(data) {\n this.data = data;\n /**\n * Constructs the Material Parser\n * @param mtlData the MTL file contents\n */\n this.currentMaterial = SENTINEL_MATERIAL;\n this.materials = {};\n this.parse();\n }\n /* eslint-disable camelcase */\n /* the function names here disobey camelCase conventions\n to make parsing/routing easier. see the parse function\n documentation for more information. */\n /**\n * Creates a new Material object and adds to the registry.\n * @param tokens the tokens associated with the directive\n */\n parse_newmtl(tokens) {\n const name = tokens[0];\n // console.info(\'Parsing new Material:\', name);\n this.currentMaterial = new Material(name);\n this.materials[name] = this.currentMaterial;\n }\n /**\n * See the documenation for parse_Ka below for a better understanding.\n *\n * Given a list of possible color tokens, returns an array of R, G, and B\n * color values.\n *\n * @param tokens the tokens associated with the directive\n * @return {*} a 3 element array containing the R, G, and B values\n * of the color.\n */\n parseColor(tokens) {\n if (tokens[0] == "spectral") {\n throw new Error("The MTL parser does not support spectral curve files. You will " +\n "need to convert the MTL colors to either RGB or CIEXYZ.");\n }\n if (tokens[0] == "xyz") {\n throw new Error("The MTL parser does not currently support XYZ colors. Either convert the " +\n "XYZ values to RGB or create an issue to add support for XYZ");\n }\n // from my understanding of the spec, RGB values at this point\n // will either be 3 floats or exactly 1 float, so that\'s the check\n // that i\'m going to perform here\n if (tokens.length == 3) {\n const [x, y, z] = tokens;\n return [parseFloat(x), parseFloat(y), parseFloat(z)];\n }\n // Since tokens at this point has a length of 3, we\'re going to assume\n // it\'s exactly 1, skipping the check for 2.\n const value = parseFloat(tokens[0]);\n // in this case, all values are equivalent\n return [value, value, value];\n }\n /**\n * Parse the ambient reflectivity\n *\n * A Ka directive can take one of three forms:\n * - Ka r g b\n * - Ka spectral file.rfl\n * - Ka xyz x y z\n * These three forms are mutually exclusive in that only one\n * declaration can exist per material. It is considered a syntax\n * error otherwise.\n *\n * The "Ka" form specifies the ambient reflectivity using RGB values.\n * The "g" and "b" values are optional. If only the "r" value is\n * specified, then the "g" and "b" values are assigned the value of\n * "r". Values are normally in the range 0.0 to 1.0. Values outside\n * of this range increase or decrease the reflectivity accordingly.\n *\n * The "Ka spectral" form specifies the ambient reflectivity using a\n * spectral curve. "file.rfl" is the name of the ".rfl" file containing\n * the curve data. "factor" is an optional argument which is a multiplier\n * for the values in the .rfl file and defaults to 1.0 if not specified.\n *\n * The "Ka xyz" form specifies the ambient reflectivity using CIEXYZ values.\n * "x y z" are the values of the CIEXYZ color space. The "y" and "z" arguments\n * are optional and take on the value of the "x" component if only "x" is\n * specified. The "x y z" values are normally in the range of 0.0 to 1.0 and\n * increase or decrease ambient reflectivity accordingly outside of that\n * range.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ka(tokens) {\n this.currentMaterial.ambient = this.parseColor(tokens);\n }\n /**\n * Diffuse Reflectivity\n *\n * Similar to the Ka directive. Simply replace "Ka" with "Kd" and the rules\n * are the same\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Kd(tokens) {\n this.currentMaterial.diffuse = this.parseColor(tokens);\n }\n /**\n * Spectral Reflectivity\n *\n * Similar to the Ka directive. Simply replace "Ks" with "Kd" and the rules\n * are the same\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ks(tokens) {\n this.currentMaterial.specular = this.parseColor(tokens);\n }\n /**\n * Emissive\n *\n * The amount and color of light emitted by the object.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ke(tokens) {\n this.currentMaterial.emissive = this.parseColor(tokens);\n }\n /**\n * Transmission Filter\n *\n * Any light passing through the object is filtered by the transmission\n * filter, which only allows specific colors to pass through. For example, Tf\n * 0 1 0 allows all of the green to pass through and filters out all of the\n * red and blue.\n *\n * Similar to the Ka directive. Simply replace "Ks" with "Tf" and the rules\n * are the same\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Tf(tokens) {\n this.currentMaterial.transmissionFilter = this.parseColor(tokens);\n }\n /**\n * Specifies the dissolve for the current material.\n *\n * Statement: d [-halo] `factor`\n *\n * Example: "d 0.5"\n *\n * The factor is the amount this material dissolves into the background. A\n * factor of 1.0 is fully opaque. This is the default when a new material is\n * created. A factor of 0.0 is fully dissolved (completely transparent).\n *\n * Unlike a real transparent material, the dissolve does not depend upon\n * material thickness nor does it have any spectral character. Dissolve works\n * on all illumination models.\n *\n * The dissolve statement allows for an optional "-halo" flag which indicates\n * that a dissolve is dependent on the surface orientation relative to the\n * viewer. For example, a sphere with the following dissolve, "d -halo 0.0",\n * will be fully dissolved at its center and will appear gradually more opaque\n * toward its edge.\n *\n * "factor" is the minimum amount of dissolve applied to the material. The\n * amount of dissolve will vary between 1.0 (fully opaque) and the specified\n * "factor". The formula is:\n *\n * dissolve = 1.0 - (N*v)(1.0-factor)\n *\n * @param tokens the tokens associated with the directive\n */\n parse_d(tokens) {\n // this ignores the -halo option as I can\'t find any documentation on what\n // it\'s supposed to be.\n this.currentMaterial.dissolve = parseFloat(tokens.pop() || "0");\n }\n /**\n * The "illum" statement specifies the illumination model to use in the\n * material. Illumination models are mathematical equations that represent\n * various material lighting and shading effects.\n *\n * The illumination number can be a number from 0 to 10. The following are\n * the list of illumination enumerations and their summaries:\n * 0. Color on and Ambient off\n * 1. Color on and Ambient on\n * 2. Highlight on\n * 3. Reflection on and Ray trace on\n * 4. Transparency: Glass on, Reflection: Ray trace on\n * 5. Reflection: Fresnel on and Ray trace on\n * 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on\n * 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on\n * 8. Reflection on and Ray trace off\n * 9. Transparency: Glass on, Reflection: Ray trace off\n * 10. Casts shadows onto invisible surfaces\n *\n * Example: "illum 2" to specify the "Highlight on" model\n *\n * @param tokens the tokens associated with the directive\n */\n parse_illum(tokens) {\n this.currentMaterial.illumination = parseInt(tokens[0]);\n }\n /**\n * Optical Density (AKA Index of Refraction)\n *\n * Statement: Ni `index`\n *\n * Example: Ni 1.0\n *\n * Specifies the optical density for the surface. `index` is the value\n * for the optical density. The values can range from 0.001 to 10. A value of\n * 1.0 means that light does not bend as it passes through an object.\n * Increasing the optical_density increases the amount of bending. Glass has\n * an index of refraction of about 1.5. Values of less than 1.0 produce\n * bizarre results and are not recommended\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ni(tokens) {\n this.currentMaterial.refractionIndex = parseFloat(tokens[0]);\n }\n /**\n * Specifies the specular exponent for the current material. This defines the\n * focus of the specular highlight.\n *\n * Statement: Ns `exponent`\n *\n * Example: "Ns 250"\n *\n * `exponent` is the value for the specular exponent. A high exponent results\n * in a tight, concentrated highlight. Ns Values normally range from 0 to\n * 1000.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ns(tokens) {\n this.currentMaterial.specularExponent = parseInt(tokens[0]);\n }\n /**\n * Specifies the sharpness of the reflections from the local reflection map.\n *\n * Statement: sharpness `value`\n *\n * Example: "sharpness 100"\n *\n * If a material does not have a local reflection map defined in its material\n * defintions, sharpness will apply to the global reflection map defined in\n * PreView.\n *\n * `value` can be a number from 0 to 1000. The default is 60. A high value\n * results in a clear reflection of objects in the reflection map.\n *\n * Tip: sharpness values greater than 100 introduce aliasing effects in\n * flat surfaces that are viewed at a sharp angle.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_sharpness(tokens) {\n this.currentMaterial.sharpness = parseInt(tokens[0]);\n }\n /**\n * Parses the -cc flag and updates the options object with the values.\n *\n * @param values the values passed to the -cc flag\n * @param options the Object of all image options\n */\n parse_cc(values, options) {\n options.colorCorrection = values[0] == "on";\n }\n /**\n * Parses the -blendu flag and updates the options object with the values.\n *\n * @param values the values passed to the -blendu flag\n * @param options the Object of all image options\n */\n parse_blendu(values, options) {\n options.horizontalBlending = values[0] == "on";\n }\n /**\n * Parses the -blendv flag and updates the options object with the values.\n *\n * @param values the values passed to the -blendv flag\n * @param options the Object of all image options\n */\n parse_blendv(values, options) {\n options.verticalBlending = values[0] == "on";\n }\n /**\n * Parses the -boost flag and updates the options object with the values.\n *\n * @param values the values passed to the -boost flag\n * @param options the Object of all image options\n */\n parse_boost(values, options) {\n options.boostMipMapSharpness = parseFloat(values[0]);\n }\n /**\n * Parses the -mm flag and updates the options object with the values.\n *\n * @param values the values passed to the -mm flag\n * @param options the Object of all image options\n */\n parse_mm(values, options) {\n options.modifyTextureMap.brightness = parseFloat(values[0]);\n options.modifyTextureMap.contrast = parseFloat(values[1]);\n }\n /**\n * Parses and sets the -o, -s, and -t u, v, and w values\n *\n * @param values the values passed to the -o, -s, -t flag\n * @param {Object} option the Object of either the -o, -s, -t option\n * @param {Integer} defaultValue the Object of all image options\n */\n parse_ost(values, option, defaultValue) {\n while (values.length < 3) {\n values.push(defaultValue.toString());\n }\n option.u = parseFloat(values[0]);\n option.v = parseFloat(values[1]);\n option.w = parseFloat(values[2]);\n }\n /**\n * Parses the -o flag and updates the options object with the values.\n *\n * @param values the values passed to the -o flag\n * @param options the Object of all image options\n */\n parse_o(values, options) {\n this.parse_ost(values, options.offset, 0);\n }\n /**\n * Parses the -s flag and updates the options object with the values.\n *\n * @param values the values passed to the -s flag\n * @param options the Object of all image options\n */\n parse_s(values, options) {\n this.parse_ost(values, options.scale, 1);\n }\n /**\n * Parses the -t flag and updates the options object with the values.\n *\n * @param values the values passed to the -t flag\n * @param options the Object of all image options\n */\n parse_t(values, options) {\n this.parse_ost(values, options.turbulence, 0);\n }\n /**\n * Parses the -texres flag and updates the options object with the values.\n *\n * @param values the values passed to the -texres flag\n * @param options the Object of all image options\n */\n parse_texres(values, options) {\n options.textureResolution = parseFloat(values[0]);\n }\n /**\n * Parses the -clamp flag and updates the options object with the values.\n *\n * @param values the values passed to the -clamp flag\n * @param options the Object of all image options\n */\n parse_clamp(values, options) {\n options.clamp = values[0] == "on";\n }\n /**\n * Parses the -bm flag and updates the options object with the values.\n *\n * @param values the values passed to the -bm flag\n * @param options the Object of all image options\n */\n parse_bm(values, options) {\n options.bumpMultiplier = parseFloat(values[0]);\n }\n /**\n * Parses the -imfchan flag and updates the options object with the values.\n *\n * @param values the values passed to the -imfchan flag\n * @param options the Object of all image options\n */\n parse_imfchan(values, options) {\n options.imfChan = values[0];\n }\n /**\n * This only exists for relection maps and denotes the type of reflection.\n *\n * @param values the values passed to the -type flag\n * @param options the Object of all image options\n */\n parse_type(values, options) {\n options.reflectionType = values[0];\n }\n /**\n * Parses the texture\'s options and returns an options object with the info\n *\n * @param tokens all of the option tokens to pass to the texture\n * @return {Object} a complete object of objects to apply to the texture\n */\n parseOptions(tokens) {\n const options = emptyTextureOptions();\n let option;\n let values;\n const optionsToValues = {};\n tokens.reverse();\n while (tokens.length) {\n // token is guaranteed to exists here, hence the explicit "as"\n const token = tokens.pop();\n if (token.startsWith("-")) {\n option = token.substr(1);\n optionsToValues[option] = [];\n }\n else if (option) {\n optionsToValues[option].push(token);\n }\n }\n for (option in optionsToValues) {\n if (!optionsToValues.hasOwnProperty(option)) {\n continue;\n }\n values = optionsToValues[option];\n const optionMethod = this[`parse_${option}`];\n if (optionMethod) {\n optionMethod.bind(this)(values, options);\n }\n }\n return options;\n }\n /**\n * Parses the given texture map line.\n *\n * @param tokens all of the tokens representing the texture\n * @return a complete object of objects to apply to the texture\n */\n parseMap(tokens) {\n // according to wikipedia:\n // (https://en.wikipedia.org/wiki/Wavefront_.obj_file#Vendor_specific_alterations)\n // there is at least one vendor that places the filename before the options\n // rather than after (which is to spec). All options start with a \'-\'\n // so if the first token doesn\'t start with a \'-\', we\'re going to assume\n // it\'s the name of the map file.\n let optionsString;\n let filename = "";\n if (!tokens[0].startsWith("-")) {\n [filename, ...optionsString] = tokens;\n }\n else {\n filename = tokens.pop();\n optionsString = tokens;\n }\n const options = this.parseOptions(optionsString);\n options.filename = filename.replace(/\\\\/g, "/");\n return options;\n }\n /**\n * Parses the ambient map.\n *\n * @param tokens list of tokens for the map_Ka direcive\n */\n parse_map_Ka(tokens) {\n this.currentMaterial.mapAmbient = this.parseMap(tokens);\n }\n /**\n * Parses the diffuse map.\n *\n * @param tokens list of tokens for the map_Kd direcive\n */\n parse_map_Kd(tokens) {\n this.currentMaterial.mapDiffuse = this.parseMap(tokens);\n }\n /**\n * Parses the specular map.\n *\n * @param tokens list of tokens for the map_Ks direcive\n */\n parse_map_Ks(tokens) {\n this.currentMaterial.mapSpecular = this.parseMap(tokens);\n }\n /**\n * Parses the emissive map.\n *\n * @param tokens list of tokens for the map_Ke direcive\n */\n parse_map_Ke(tokens) {\n this.currentMaterial.mapEmissive = this.parseMap(tokens);\n }\n /**\n * Parses the specular exponent map.\n *\n * @param tokens list of tokens for the map_Ns direcive\n */\n parse_map_Ns(tokens) {\n this.currentMaterial.mapSpecularExponent = this.parseMap(tokens);\n }\n /**\n * Parses the dissolve map.\n *\n * @param tokens list of tokens for the map_d direcive\n */\n parse_map_d(tokens) {\n this.currentMaterial.mapDissolve = this.parseMap(tokens);\n }\n /**\n * Parses the anti-aliasing option.\n *\n * @param tokens list of tokens for the map_aat direcive\n */\n parse_map_aat(tokens) {\n this.currentMaterial.antiAliasing = tokens[0] == "on";\n }\n /**\n * Parses the bump map.\n *\n * @param tokens list of tokens for the map_bump direcive\n */\n parse_map_bump(tokens) {\n this.currentMaterial.mapBump = this.parseMap(tokens);\n }\n /**\n * Parses the bump map.\n *\n * @param tokens list of tokens for the bump direcive\n */\n parse_bump(tokens) {\n this.parse_map_bump(tokens);\n }\n /**\n * Parses the disp map.\n *\n * @param tokens list of tokens for the disp direcive\n */\n parse_disp(tokens) {\n this.currentMaterial.mapDisplacement = this.parseMap(tokens);\n }\n /**\n * Parses the decal map.\n *\n * @param tokens list of tokens for the map_decal direcive\n */\n parse_decal(tokens) {\n this.currentMaterial.mapDecal = this.parseMap(tokens);\n }\n /**\n * Parses the refl map.\n *\n * @param tokens list of tokens for the refl direcive\n */\n parse_refl(tokens) {\n this.currentMaterial.mapReflections.push(this.parseMap(tokens));\n }\n /**\n * Parses the MTL file.\n *\n * Iterates line by line parsing each MTL directive.\n *\n * This function expects the first token in the line\n * to be a valid MTL directive. That token is then used\n * to try and run a method on this class. parse_[directive]\n * E.g., the `newmtl` directive would try to call the method\n * parse_newmtl. Each parsing function takes in the remaining\n * list of tokens and updates the currentMaterial class with\n * the attributes provided.\n */\n parse() {\n const lines = this.data.split(/\\r?\\n/);\n for (let line of lines) {\n line = line.trim();\n if (!line || line.startsWith("#")) {\n continue;\n }\n const [directive, ...tokens] = line.split(/\\s/);\n const parseMethod = this[`parse_${directive}`];\n if (!parseMethod) {\n console.warn(`Don\'t know how to parse the directive: "${directive}"`);\n continue;\n }\n // console.log(`Parsing "${directive}" with tokens: ${tokens}`);\n parseMethod.bind(this)(tokens);\n }\n // some cleanup. These don\'t need to be exposed as public data.\n delete this.data;\n this.currentMaterial = SENTINEL_MATERIAL;\n }\n}\nfunction emptyTextureOptions() {\n return {\n colorCorrection: false,\n horizontalBlending: true,\n verticalBlending: true,\n boostMipMapSharpness: 0,\n modifyTextureMap: {\n brightness: 0,\n contrast: 1,\n },\n offset: { u: 0, v: 0, w: 0 },\n scale: { u: 1, v: 1, w: 1 },\n turbulence: { u: 0, v: 0, w: 0 },\n clamp: false,\n textureResolution: null,\n bumpMultiplier: 1,\n imfChan: null,\n filename: "",\n };\n}\n\n\n//# sourceURL=webpack:///./src/material.ts?');},"./src/mesh.ts":
1877
+ /*!*********************!*\
1878
+ !*** ./src/mesh.ts ***!
1879
+ \*********************/
1880
+ /*! exports provided: default */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Mesh; });\n/* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./layout */ "./src/layout.ts");\n\n/**\n * The main Mesh class. The constructor will parse through the OBJ file data\n * and collect the vertex, vertex normal, texture, and face information. This\n * information can then be used later on when creating your VBOs. See\n * OBJ.initMeshBuffers for an example of how to use the newly created Mesh\n */\nclass Mesh {\n /**\n * Create a Mesh\n * @param {String} objectData - a string representation of an OBJ file with\n * newlines preserved.\n * @param {Object} options - a JS object containing valid options. See class\n * documentation for options.\n * @param {bool} options.enableWTextureCoord - Texture coordinates can have\n * an optional "w" coordinate after the u and v coordinates. This extra\n * value can be used in order to perform fancy transformations on the\n * textures themselves. Default is to truncate to only the u an v\n * coordinates. Passing true will provide a default value of 0 in the\n * event that any or all texture coordinates don\'t provide a w value.\n * Always use the textureStride attribute in order to determine the\n * stride length of the texture coordinates when rendering the element\n * array.\n * @param {bool} options.calcTangentsAndBitangents - Calculate the tangents\n * and bitangents when loading of the OBJ is completed. This adds two new\n * attributes to the Mesh instance: `tangents` and `bitangents`.\n */\n constructor(objectData, options) {\n this.name = "";\n this.indicesPerMaterial = [];\n this.materialsByIndex = {};\n this.tangents = [];\n this.bitangents = [];\n options = options || {};\n options.materials = options.materials || {};\n options.enableWTextureCoord = !!options.enableWTextureCoord;\n // the list of unique vertex, normal, texture, attributes\n this.vertexNormals = [];\n this.textures = [];\n // the indicies to draw the faces\n this.indices = [];\n this.textureStride = options.enableWTextureCoord ? 3 : 2;\n /*\n The OBJ file format does a sort of compression when saving a model in a\n program like Blender. There are at least 3 sections (4 including textures)\n within the file. Each line in a section begins with the same string:\n * \'v\': indicates vertex section\n * \'vn\': indicates vertex normal section\n * \'f\': indicates the faces section\n * \'vt\': indicates vertex texture section (if textures were used on the model)\n Each of the above sections (except for the faces section) is a list/set of\n unique vertices.\n\n Each line of the faces section contains a list of\n (vertex, [texture], normal) groups.\n\n **Note:** The following documentation will use a capital "V" Vertex to\n denote the above (vertex, [texture], normal) groups whereas a lowercase\n "v" vertex is used to denote an X, Y, Z coordinate.\n\n Some examples:\n // the texture index is optional, both formats are possible for models\n // without a texture applied\n f 1/25 18/46 12/31\n f 1//25 18//46 12//31\n\n // A 3 vertex face with texture indices\n f 16/92/11 14/101/22 1/69/1\n\n // A 4 vertex face\n f 16/92/11 40/109/40 38/114/38 14/101/22\n\n The first two lines are examples of a 3 vertex face without a texture applied.\n The second is an example of a 3 vertex face with a texture applied.\n The third is an example of a 4 vertex face. Note: a face can contain N\n number of vertices.\n\n Each number that appears in one of the groups is a 1-based index\n corresponding to an item from the other sections (meaning that indexing\n starts at one and *not* zero).\n\n For example:\n `f 16/92/11` is saying to\n - take the 16th element from the [v] vertex array\n - take the 92nd element from the [vt] texture array\n - take the 11th element from the [vn] normal array\n and together they make a unique vertex.\n Using all 3+ unique Vertices from the face line will produce a polygon.\n\n Now, you could just go through the OBJ file and create a new vertex for\n each face line and WebGL will draw what appears to be the same model.\n However, vertices will be overlapped and duplicated all over the place.\n\n Consider a cube in 3D space centered about the origin and each side is\n 2 units long. The front face (with the positive Z-axis pointing towards\n you) would have a Top Right vertex (looking orthogonal to its normal)\n mapped at (1,1,1) The right face would have a Top Left vertex (looking\n orthogonal to its normal) at (1,1,1) and the top face would have a Bottom\n Right vertex (looking orthogonal to its normal) at (1,1,1). Each face\n has a vertex at the same coordinates, however, three distinct vertices\n will be drawn at the same spot.\n\n To solve the issue of duplicate Vertices (the `(vertex, [texture], normal)`\n groups), while iterating through the face lines, when a group is encountered\n the whole group string (\'16/92/11\') is checked to see if it exists in the\n packed.hashindices object, and if it doesn\'t, the indices it specifies\n are used to look up each attribute in the corresponding attribute arrays\n already created. The values are then copied to the corresponding unpacked\n array (flattened to play nice with WebGL\'s ELEMENT_ARRAY_BUFFER indexing),\n the group string is added to the hashindices set and the current unpacked\n index is used as this hashindices value so that the group of elements can\n be reused. The unpacked index is incremented. If the group string already\n exists in the hashindices object, its corresponding value is the index of\n that group and is appended to the unpacked indices array.\n */\n const verts = [];\n const vertNormals = [];\n const textures = [];\n const materialNamesByIndex = [];\n const materialIndicesByName = {};\n // keep track of what material we\'ve seen last\n let currentMaterialIndex = -1;\n let currentObjectByMaterialIndex = 0;\n // unpacking stuff\n const unpacked = {\n verts: [],\n norms: [],\n textures: [],\n hashindices: {},\n indices: [[]],\n materialIndices: [],\n index: 0,\n };\n const VERTEX_RE = /^v\\s/;\n const NORMAL_RE = /^vn\\s/;\n const TEXTURE_RE = /^vt\\s/;\n const FACE_RE = /^f\\s/;\n const WHITESPACE_RE = /\\s+/;\n const USE_MATERIAL_RE = /^usemtl/;\n // array of lines separated by the newline\n const lines = objectData.split("\\n");\n for (let line of lines) {\n line = line.trim();\n if (!line || line.startsWith("#")) {\n continue;\n }\n const elements = line.split(WHITESPACE_RE);\n elements.shift();\n if (VERTEX_RE.test(line)) {\n // if this is a vertex\n verts.push(...elements);\n }\n else if (NORMAL_RE.test(line)) {\n // if this is a vertex normal\n vertNormals.push(...elements);\n }\n else if (TEXTURE_RE.test(line)) {\n let coords = elements;\n // by default, the loader will only look at the U and V\n // coordinates of the vt declaration. So, this truncates the\n // elements to only those 2 values. If W texture coordinate\n // support is enabled, then the texture coordinate is\n // expected to have three values in it.\n if (elements.length > 2 && !options.enableWTextureCoord) {\n coords = elements.slice(0, 2);\n }\n else if (elements.length === 2 && options.enableWTextureCoord) {\n // If for some reason W texture coordinate support is enabled\n // and only the U and V coordinates are given, then we supply\n // the default value of 0 so that the stride length is correct\n // when the textures are unpacked below.\n coords.push("0");\n }\n textures.push(...coords);\n }\n else if (USE_MATERIAL_RE.test(line)) {\n const materialName = elements[0];\n // check to see if we\'ve ever seen it before\n if (!(materialName in materialIndicesByName)) {\n // new material we\'ve never seen\n materialNamesByIndex.push(materialName);\n materialIndicesByName[materialName] = materialNamesByIndex.length - 1;\n // push new array into indices\n // already contains an array at index zero, don\'t add\n if (materialIndicesByName[materialName] > 0) {\n unpacked.indices.push([]);\n }\n }\n // keep track of the current material index\n currentMaterialIndex = materialIndicesByName[materialName];\n // update current index array\n currentObjectByMaterialIndex = currentMaterialIndex;\n }\n else if (FACE_RE.test(line)) {\n // if this is a face\n /*\n split this face into an array of Vertex groups\n for example:\n f 16/92/11 14/101/22 1/69/1\n becomes:\n [\'16/92/11\', \'14/101/22\', \'1/69/1\'];\n */\n const triangles = triangulate(elements);\n for (const triangle of triangles) {\n for (let j = 0, eleLen = triangle.length; j < eleLen; j++) {\n const hash = triangle[j] + "," + currentMaterialIndex;\n if (hash in unpacked.hashindices) {\n unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]);\n }\n else {\n /*\n Each element of the face line array is a Vertex which has its\n attributes delimited by a forward slash. This will separate\n each attribute into another array:\n \'19/92/11\'\n becomes:\n Vertex = [\'19\', \'92\', \'11\'];\n where\n Vertex[0] is the vertex index\n Vertex[1] is the texture index\n Vertex[2] is the normal index\n Think of faces having Vertices which are comprised of the\n attributes location (v), texture (vt), and normal (vn).\n */\n const vertex = triangle[j].split("/");\n // it\'s possible for faces to only specify the vertex\n // and the normal. In this case, vertex will only have\n // a length of 2 and not 3 and the normal will be the\n // second item in the list with an index of 1.\n const normalIndex = vertex.length - 1;\n /*\n The verts, textures, and vertNormals arrays each contain a\n flattend array of coordinates.\n\n Because it gets confusing by referring to Vertex and then\n vertex (both are different in my descriptions) I will explain\n what\'s going on using the vertexNormals array:\n\n vertex[2] will contain the one-based index of the vertexNormals\n section (vn). One is subtracted from this index number to play\n nice with javascript\'s zero-based array indexing.\n\n Because vertexNormal is a flattened array of x, y, z values,\n simple pointer arithmetic is used to skip to the start of the\n vertexNormal, then the offset is added to get the correct\n component: +0 is x, +1 is y, +2 is z.\n\n This same process is repeated for verts and textures.\n */\n // Vertex position\n unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 0]);\n unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 1]);\n unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 2]);\n // Vertex textures\n if (textures.length) {\n const stride = options.enableWTextureCoord ? 3 : 2;\n unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 0]);\n unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 1]);\n if (options.enableWTextureCoord) {\n unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 2]);\n }\n }\n // Vertex normals\n unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 0]);\n unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 1]);\n unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 2]);\n // Vertex material indices\n unpacked.materialIndices.push(currentMaterialIndex);\n // add the newly created Vertex to the list of indices\n unpacked.hashindices[hash] = unpacked.index;\n unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]);\n // increment the counter\n unpacked.index += 1;\n }\n }\n }\n }\n }\n this.vertices = unpacked.verts;\n this.vertexNormals = unpacked.norms;\n this.textures = unpacked.textures;\n this.vertexMaterialIndices = unpacked.materialIndices;\n this.indices = unpacked.indices[currentObjectByMaterialIndex];\n this.indicesPerMaterial = unpacked.indices;\n this.materialNames = materialNamesByIndex;\n this.materialIndices = materialIndicesByName;\n this.materialsByIndex = {};\n if (options.calcTangentsAndBitangents) {\n this.calculateTangentsAndBitangents();\n }\n }\n /**\n * Calculates the tangents and bitangents of the mesh that forms an orthogonal basis together with the\n * normal in the direction of the texture coordinates. These are useful for setting up the TBN matrix\n * when distorting the normals through normal maps.\n * Method derived from: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/\n *\n * This method requires the normals and texture coordinates to be parsed and set up correctly.\n * Adds the tangents and bitangents as members of the class instance.\n */\n calculateTangentsAndBitangents() {\n console.assert(!!(this.vertices &&\n this.vertices.length &&\n this.vertexNormals &&\n this.vertexNormals.length &&\n this.textures &&\n this.textures.length), "Missing attributes for calculating tangents and bitangents");\n const unpacked = {\n tangents: [...new Array(this.vertices.length)].map(_ => 0),\n bitangents: [...new Array(this.vertices.length)].map(_ => 0),\n };\n // Loop through all faces in the whole mesh\n const indices = this.indices;\n const vertices = this.vertices;\n const normals = this.vertexNormals;\n const uvs = this.textures;\n for (let i = 0; i < indices.length; i += 3) {\n const i0 = indices[i + 0];\n const i1 = indices[i + 1];\n const i2 = indices[i + 2];\n const x_v0 = vertices[i0 * 3 + 0];\n const y_v0 = vertices[i0 * 3 + 1];\n const z_v0 = vertices[i0 * 3 + 2];\n const x_uv0 = uvs[i0 * 2 + 0];\n const y_uv0 = uvs[i0 * 2 + 1];\n const x_v1 = vertices[i1 * 3 + 0];\n const y_v1 = vertices[i1 * 3 + 1];\n const z_v1 = vertices[i1 * 3 + 2];\n const x_uv1 = uvs[i1 * 2 + 0];\n const y_uv1 = uvs[i1 * 2 + 1];\n const x_v2 = vertices[i2 * 3 + 0];\n const y_v2 = vertices[i2 * 3 + 1];\n const z_v2 = vertices[i2 * 3 + 2];\n const x_uv2 = uvs[i2 * 2 + 0];\n const y_uv2 = uvs[i2 * 2 + 1];\n const x_deltaPos1 = x_v1 - x_v0;\n const y_deltaPos1 = y_v1 - y_v0;\n const z_deltaPos1 = z_v1 - z_v0;\n const x_deltaPos2 = x_v2 - x_v0;\n const y_deltaPos2 = y_v2 - y_v0;\n const z_deltaPos2 = z_v2 - z_v0;\n const x_uvDeltaPos1 = x_uv1 - x_uv0;\n const y_uvDeltaPos1 = y_uv1 - y_uv0;\n const x_uvDeltaPos2 = x_uv2 - x_uv0;\n const y_uvDeltaPos2 = y_uv2 - y_uv0;\n const rInv = x_uvDeltaPos1 * y_uvDeltaPos2 - y_uvDeltaPos1 * x_uvDeltaPos2;\n const r = 1.0 / Math.abs(rInv < 0.0001 ? 1.0 : rInv);\n // Tangent\n const x_tangent = (x_deltaPos1 * y_uvDeltaPos2 - x_deltaPos2 * y_uvDeltaPos1) * r;\n const y_tangent = (y_deltaPos1 * y_uvDeltaPos2 - y_deltaPos2 * y_uvDeltaPos1) * r;\n const z_tangent = (z_deltaPos1 * y_uvDeltaPos2 - z_deltaPos2 * y_uvDeltaPos1) * r;\n // Bitangent\n const x_bitangent = (x_deltaPos2 * x_uvDeltaPos1 - x_deltaPos1 * x_uvDeltaPos2) * r;\n const y_bitangent = (y_deltaPos2 * x_uvDeltaPos1 - y_deltaPos1 * x_uvDeltaPos2) * r;\n const z_bitangent = (z_deltaPos2 * x_uvDeltaPos1 - z_deltaPos1 * x_uvDeltaPos2) * r;\n // Gram-Schmidt orthogonalize\n //t = glm::normalize(t - n * glm:: dot(n, t));\n const x_n0 = normals[i0 * 3 + 0];\n const y_n0 = normals[i0 * 3 + 1];\n const z_n0 = normals[i0 * 3 + 2];\n const x_n1 = normals[i1 * 3 + 0];\n const y_n1 = normals[i1 * 3 + 1];\n const z_n1 = normals[i1 * 3 + 2];\n const x_n2 = normals[i2 * 3 + 0];\n const y_n2 = normals[i2 * 3 + 1];\n const z_n2 = normals[i2 * 3 + 2];\n // Tangent\n const n0_dot_t = x_tangent * x_n0 + y_tangent * y_n0 + z_tangent * z_n0;\n const n1_dot_t = x_tangent * x_n1 + y_tangent * y_n1 + z_tangent * z_n1;\n const n2_dot_t = x_tangent * x_n2 + y_tangent * y_n2 + z_tangent * z_n2;\n const x_resTangent0 = x_tangent - x_n0 * n0_dot_t;\n const y_resTangent0 = y_tangent - y_n0 * n0_dot_t;\n const z_resTangent0 = z_tangent - z_n0 * n0_dot_t;\n const x_resTangent1 = x_tangent - x_n1 * n1_dot_t;\n const y_resTangent1 = y_tangent - y_n1 * n1_dot_t;\n const z_resTangent1 = z_tangent - z_n1 * n1_dot_t;\n const x_resTangent2 = x_tangent - x_n2 * n2_dot_t;\n const y_resTangent2 = y_tangent - y_n2 * n2_dot_t;\n const z_resTangent2 = z_tangent - z_n2 * n2_dot_t;\n const magTangent0 = Math.sqrt(x_resTangent0 * x_resTangent0 + y_resTangent0 * y_resTangent0 + z_resTangent0 * z_resTangent0);\n const magTangent1 = Math.sqrt(x_resTangent1 * x_resTangent1 + y_resTangent1 * y_resTangent1 + z_resTangent1 * z_resTangent1);\n const magTangent2 = Math.sqrt(x_resTangent2 * x_resTangent2 + y_resTangent2 * y_resTangent2 + z_resTangent2 * z_resTangent2);\n // Bitangent\n const n0_dot_bt = x_bitangent * x_n0 + y_bitangent * y_n0 + z_bitangent * z_n0;\n const n1_dot_bt = x_bitangent * x_n1 + y_bitangent * y_n1 + z_bitangent * z_n1;\n const n2_dot_bt = x_bitangent * x_n2 + y_bitangent * y_n2 + z_bitangent * z_n2;\n const x_resBitangent0 = x_bitangent - x_n0 * n0_dot_bt;\n const y_resBitangent0 = y_bitangent - y_n0 * n0_dot_bt;\n const z_resBitangent0 = z_bitangent - z_n0 * n0_dot_bt;\n const x_resBitangent1 = x_bitangent - x_n1 * n1_dot_bt;\n const y_resBitangent1 = y_bitangent - y_n1 * n1_dot_bt;\n const z_resBitangent1 = z_bitangent - z_n1 * n1_dot_bt;\n const x_resBitangent2 = x_bitangent - x_n2 * n2_dot_bt;\n const y_resBitangent2 = y_bitangent - y_n2 * n2_dot_bt;\n const z_resBitangent2 = z_bitangent - z_n2 * n2_dot_bt;\n const magBitangent0 = Math.sqrt(x_resBitangent0 * x_resBitangent0 +\n y_resBitangent0 * y_resBitangent0 +\n z_resBitangent0 * z_resBitangent0);\n const magBitangent1 = Math.sqrt(x_resBitangent1 * x_resBitangent1 +\n y_resBitangent1 * y_resBitangent1 +\n z_resBitangent1 * z_resBitangent1);\n const magBitangent2 = Math.sqrt(x_resBitangent2 * x_resBitangent2 +\n y_resBitangent2 * y_resBitangent2 +\n z_resBitangent2 * z_resBitangent2);\n unpacked.tangents[i0 * 3 + 0] += x_resTangent0 / magTangent0;\n unpacked.tangents[i0 * 3 + 1] += y_resTangent0 / magTangent0;\n unpacked.tangents[i0 * 3 + 2] += z_resTangent0 / magTangent0;\n unpacked.tangents[i1 * 3 + 0] += x_resTangent1 / magTangent1;\n unpacked.tangents[i1 * 3 + 1] += y_resTangent1 / magTangent1;\n unpacked.tangents[i1 * 3 + 2] += z_resTangent1 / magTangent1;\n unpacked.tangents[i2 * 3 + 0] += x_resTangent2 / magTangent2;\n unpacked.tangents[i2 * 3 + 1] += y_resTangent2 / magTangent2;\n unpacked.tangents[i2 * 3 + 2] += z_resTangent2 / magTangent2;\n unpacked.bitangents[i0 * 3 + 0] += x_resBitangent0 / magBitangent0;\n unpacked.bitangents[i0 * 3 + 1] += y_resBitangent0 / magBitangent0;\n unpacked.bitangents[i0 * 3 + 2] += z_resBitangent0 / magBitangent0;\n unpacked.bitangents[i1 * 3 + 0] += x_resBitangent1 / magBitangent1;\n unpacked.bitangents[i1 * 3 + 1] += y_resBitangent1 / magBitangent1;\n unpacked.bitangents[i1 * 3 + 2] += z_resBitangent1 / magBitangent1;\n unpacked.bitangents[i2 * 3 + 0] += x_resBitangent2 / magBitangent2;\n unpacked.bitangents[i2 * 3 + 1] += y_resBitangent2 / magBitangent2;\n unpacked.bitangents[i2 * 3 + 2] += z_resBitangent2 / magBitangent2;\n // TODO: check handedness\n }\n this.tangents = unpacked.tangents;\n this.bitangents = unpacked.bitangents;\n }\n /**\n * @param layout - A {@link Layout} object that describes the\n * desired memory layout of the generated buffer\n * @return The packed array in the ... TODO\n */\n makeBufferData(layout) {\n const numItems = this.vertices.length / 3;\n const buffer = new ArrayBuffer(layout.stride * numItems);\n buffer.numItems = numItems;\n const dataView = new DataView(buffer);\n for (let i = 0, vertexOffset = 0; i < numItems; i++) {\n vertexOffset = i * layout.stride;\n // copy in the vertex data in the order and format given by the\n // layout param\n for (const attribute of layout.attributes) {\n const offset = vertexOffset + layout.attributeMap[attribute.key].offset;\n switch (attribute.key) {\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].POSITION.key:\n dataView.setFloat32(offset, this.vertices[i * 3], true);\n dataView.setFloat32(offset + 4, this.vertices[i * 3 + 1], true);\n dataView.setFloat32(offset + 8, this.vertices[i * 3 + 2], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].UV.key:\n dataView.setFloat32(offset, this.textures[i * 2], true);\n dataView.setFloat32(offset + 4, this.textures[i * 2 + 1], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].NORMAL.key:\n dataView.setFloat32(offset, this.vertexNormals[i * 3], true);\n dataView.setFloat32(offset + 4, this.vertexNormals[i * 3 + 1], true);\n dataView.setFloat32(offset + 8, this.vertexNormals[i * 3 + 2], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].MATERIAL_INDEX.key:\n dataView.setInt16(offset, this.vertexMaterialIndices[i], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].AMBIENT.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.ambient[0], true);\n dataView.setFloat32(offset + 4, material.ambient[1], true);\n dataView.setFloat32(offset + 8, material.ambient[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].DIFFUSE.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.diffuse[0], true);\n dataView.setFloat32(offset + 4, material.diffuse[1], true);\n dataView.setFloat32(offset + 8, material.diffuse[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].SPECULAR.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.specular[0], true);\n dataView.setFloat32(offset + 4, material.specular[1], true);\n dataView.setFloat32(offset + 8, material.specular[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].SPECULAR_EXPONENT.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.specularExponent, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].EMISSIVE.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.emissive[0], true);\n dataView.setFloat32(offset + 4, material.emissive[1], true);\n dataView.setFloat32(offset + 8, material.emissive[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].TRANSMISSION_FILTER.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.transmissionFilter[0], true);\n dataView.setFloat32(offset + 4, material.transmissionFilter[1], true);\n dataView.setFloat32(offset + 8, material.transmissionFilter[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].DISSOLVE.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.dissolve, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].ILLUMINATION.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setInt16(offset, material.illumination, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].REFRACTION_INDEX.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.refractionIndex, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].SHARPNESS.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.sharpness, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].ANTI_ALIASING.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setInt16(offset, material.antiAliasing ? 1 : 0, true);\n break;\n }\n }\n }\n }\n return buffer;\n }\n makeIndexBufferData() {\n const buffer = new Uint16Array(this.indices);\n buffer.numItems = this.indices.length;\n return buffer;\n }\n makeIndexBufferDataForMaterials(...materialIndices) {\n const indices = new Array().concat(...materialIndices.map(mtlIdx => this.indicesPerMaterial[mtlIdx]));\n const buffer = new Uint16Array(indices);\n buffer.numItems = indices.length;\n return buffer;\n }\n addMaterialLibrary(mtl) {\n for (const name in mtl.materials) {\n if (!(name in this.materialIndices)) {\n // This material is not referenced by the mesh\n continue;\n }\n const material = mtl.materials[name];\n // Find the material index for this material\n const materialIndex = this.materialIndices[material.name];\n // Put the material into the materialsByIndex object at the right\n // spot as determined when the obj file was parsed\n this.materialsByIndex[materialIndex] = material;\n }\n }\n}\nfunction* triangulate(elements) {\n if (elements.length <= 3) {\n yield elements;\n }\n else if (elements.length === 4) {\n yield [elements[0], elements[1], elements[2]];\n yield [elements[2], elements[3], elements[0]];\n }\n else {\n for (let i = 1; i < elements.length - 1; i++) {\n yield [elements[0], elements[i], elements[i + 1]];\n }\n }\n}\n\n\n//# sourceURL=webpack:///./src/mesh.ts?');},"./src/utils.ts":
1881
+ /*!**********************!*\
1882
+ !*** ./src/utils.ts ***!
1883
+ \**********************/
1884
+ /*! exports provided: downloadModels, downloadMeshes, initMeshBuffers, deleteMeshBuffers */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "downloadModels", function() { return downloadModels; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "downloadMeshes", function() { return downloadMeshes; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "initMeshBuffers", function() { return initMeshBuffers; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "deleteMeshBuffers", function() { return deleteMeshBuffers; });\n/* harmony import */ var _mesh__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./mesh */ "./src/mesh.ts");\n/* harmony import */ var _material__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./material */ "./src/material.ts");\n\n\nfunction downloadMtlTextures(mtl, root) {\n const mapAttributes = [\n "mapDiffuse",\n "mapAmbient",\n "mapSpecular",\n "mapDissolve",\n "mapBump",\n "mapDisplacement",\n "mapDecal",\n "mapEmissive",\n ];\n if (!root.endsWith("/")) {\n root += "/";\n }\n const textures = [];\n for (const materialName in mtl.materials) {\n if (!mtl.materials.hasOwnProperty(materialName)) {\n continue;\n }\n const material = mtl.materials[materialName];\n for (const attr of mapAttributes) {\n const mapData = material[attr];\n if (!mapData || !mapData.filename) {\n continue;\n }\n const url = root + mapData.filename;\n textures.push(fetch(url)\n .then(response => {\n if (!response.ok) {\n throw new Error();\n }\n return response.blob();\n })\n .then(function (data) {\n const image = new Image();\n image.src = URL.createObjectURL(data);\n mapData.texture = image;\n return new Promise(resolve => (image.onload = resolve));\n })\n .catch(() => {\n console.error(`Unable to download texture: ${url}`);\n }));\n }\n }\n return Promise.all(textures);\n}\nfunction getMtl(modelOptions) {\n if (!(typeof modelOptions.mtl === "string")) {\n return modelOptions.obj.replace(/\\.obj$/, ".mtl");\n }\n return modelOptions.mtl;\n}\n/**\n * Accepts a list of model request objects and returns a Promise that\n * resolves when all models have been downloaded and parsed.\n *\n * The list of model objects follow this interface:\n * {\n * obj: \'path/to/model.obj\',\n * mtl: true | \'path/to/model.mtl\',\n * downloadMtlTextures: true | false\n * mtlTextureRoot: \'/models/suzanne/maps\'\n * name: \'suzanne\'\n * }\n *\n * The `obj` attribute is required and should be the path to the\n * model\'s .obj file relative to the current repo (absolute URLs are\n * suggested).\n *\n * The `mtl` attribute is optional and can either be a boolean or\n * a path to the model\'s .mtl file relative to the current URL. If\n * the value is `true`, then the path and basename given for the `obj`\n * attribute is used replacing the .obj suffix for .mtl\n * E.g.: {obj: \'models/foo.obj\', mtl: true} would search for \'models/foo.mtl\'\n *\n * The `name` attribute is optional and is a human friendly name to be\n * included with the parsed OBJ and MTL files. If not given, the base .obj\n * filename will be used.\n *\n * The `downloadMtlTextures` attribute is a flag for automatically downloading\n * any images found in the MTL file and attaching them to each Material\n * created from that file. For example, if material.mapDiffuse is set (there\n * was data in the MTL file), then material.mapDiffuse.texture will contain\n * the downloaded image. This option defaults to `true`. By default, the MTL\'s\n * URL will be used to determine the location of the images.\n *\n * The `mtlTextureRoot` attribute is optional and should point to the location\n * on the server that this MTL\'s texture files are located. The default is to\n * use the MTL file\'s location.\n *\n * @returns {Promise} the result of downloading the given list of models. The\n * promise will resolve with an object whose keys are the names of the models\n * and the value is its Mesh object. Each Mesh object will automatically\n * have its addMaterialLibrary() method called to set the given MTL data (if given).\n */\nfunction downloadModels(models) {\n const finished = [];\n for (const model of models) {\n if (!model.obj) {\n throw new Error(\'"obj" attribute of model object not set. The .obj file is required to be set \' +\n "in order to use downloadModels()");\n }\n const options = {\n indicesPerMaterial: !!model.indicesPerMaterial,\n calcTangentsAndBitangents: !!model.calcTangentsAndBitangents,\n };\n // if the name is not provided, dervive it from the given OBJ\n let name = model.name;\n if (!name) {\n const parts = model.obj.split("/");\n name = parts[parts.length - 1].replace(".obj", "");\n }\n const namePromise = Promise.resolve(name);\n const meshPromise = fetch(model.obj)\n .then(response => response.text())\n .then(data => {\n return new _mesh__WEBPACK_IMPORTED_MODULE_0__["default"](data, options);\n });\n let mtlPromise;\n // Download MaterialLibrary file?\n if (model.mtl) {\n const mtl = getMtl(model);\n mtlPromise = fetch(mtl)\n .then(response => response.text())\n .then((data) => {\n const material = new _material__WEBPACK_IMPORTED_MODULE_1__["MaterialLibrary"](data);\n if (model.downloadMtlTextures !== false) {\n let root = model.mtlTextureRoot;\n if (!root) {\n // get the directory of the MTL file as default\n root = mtl.substr(0, mtl.lastIndexOf("/"));\n }\n // downloadMtlTextures returns a Promise that\n // is resolved once all of the images it\n // contains are downloaded. These are then\n // attached to the map data objects\n return Promise.all([Promise.resolve(material), downloadMtlTextures(material, root)]);\n }\n return Promise.all([Promise.resolve(material), undefined]);\n })\n .then((value) => {\n return value[0];\n });\n }\n const parsed = [namePromise, meshPromise, mtlPromise];\n finished.push(Promise.all(parsed));\n }\n return Promise.all(finished).then(ms => {\n // the "finished" promise is a list of name, Mesh instance,\n // and MaterialLibary instance. This unpacks and returns an\n // object mapping name to Mesh (Mesh points to MTL).\n const models = {};\n for (const model of ms) {\n const [name, mesh, mtl] = model;\n mesh.name = name;\n if (mtl) {\n mesh.addMaterialLibrary(mtl);\n }\n models[name] = mesh;\n }\n return models;\n });\n}\n/**\n * Takes in an object of `mesh_name`, `\'/url/to/OBJ/file\'` pairs and a callback\n * function. Each OBJ file will be ajaxed in and automatically converted to\n * an OBJ.Mesh. When all files have successfully downloaded the callback\n * function provided will be called and passed in an object containing\n * the newly created meshes.\n *\n * **Note:** In order to use this function as a way to download meshes, a\n * webserver of some sort must be used.\n *\n * @param {Object} nameAndAttrs an object where the key is the name of the mesh and the value is the url to that mesh\'s OBJ file\n *\n * @param {Function} completionCallback should contain a function that will take one parameter: an object array where the keys will be the unique object name and the value will be a Mesh object\n *\n * @param {Object} meshes In case other meshes are loaded separately or if a previously declared variable is desired to be used, pass in a (possibly empty) json object of the pattern: { \'<mesh_name>\': OBJ.Mesh }\n *\n */\nfunction downloadMeshes(nameAndURLs, completionCallback, meshes) {\n if (meshes === undefined) {\n meshes = {};\n }\n const completed = [];\n for (const mesh_name in nameAndURLs) {\n if (!nameAndURLs.hasOwnProperty(mesh_name)) {\n continue;\n }\n const url = nameAndURLs[mesh_name];\n completed.push(fetch(url)\n .then(response => response.text())\n .then(data => {\n return [mesh_name, new _mesh__WEBPACK_IMPORTED_MODULE_0__["default"](data)];\n }));\n }\n Promise.all(completed).then(ms => {\n for (const [name, mesh] of ms) {\n meshes[name] = mesh;\n }\n return completionCallback(meshes);\n });\n}\nfunction _buildBuffer(gl, type, data, itemSize) {\n const buffer = gl.createBuffer();\n const arrayView = type === gl.ARRAY_BUFFER ? Float32Array : Uint16Array;\n gl.bindBuffer(type, buffer);\n gl.bufferData(type, new arrayView(data), gl.STATIC_DRAW);\n buffer.itemSize = itemSize;\n buffer.numItems = data.length / itemSize;\n return buffer;\n}\n/**\n * Takes in the WebGL context and a Mesh, then creates and appends the buffers\n * to the mesh object as attributes.\n *\n * @param {WebGLRenderingContext} gl the `canvas.getContext(\'webgl\')` context instance\n * @param {Mesh} mesh a single `OBJ.Mesh` instance\n *\n * The newly created mesh attributes are:\n *\n * Attrbute | Description\n * :--- | ---\n * **normalBuffer** |contains the model&#39;s Vertex Normals\n * normalBuffer.itemSize |set to 3 items\n * normalBuffer.numItems |the total number of vertex normals\n * |\n * **textureBuffer** |contains the model&#39;s Texture Coordinates\n * textureBuffer.itemSize |set to 2 items\n * textureBuffer.numItems |the number of texture coordinates\n * |\n * **vertexBuffer** |contains the model&#39;s Vertex Position Coordinates (does not include w)\n * vertexBuffer.itemSize |set to 3 items\n * vertexBuffer.numItems |the total number of vertices\n * |\n * **indexBuffer** |contains the indices of the faces\n * indexBuffer.itemSize |is set to 1\n * indexBuffer.numItems |the total number of indices\n *\n * A simple example (a lot of steps are missing, so don\'t copy and paste):\n *\n * const gl = canvas.getContext(\'webgl\'),\n * mesh = OBJ.Mesh(obj_file_data);\n * // compile the shaders and create a shader program\n * const shaderProgram = gl.createProgram();\n * // compilation stuff here\n * ...\n * // make sure you have vertex, vertex normal, and texture coordinate\n * // attributes located in your shaders and attach them to the shader program\n * shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");\n * gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);\n *\n * shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal");\n * gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute);\n *\n * shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");\n * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);\n *\n * // create and initialize the vertex, vertex normal, and texture coordinate buffers\n * // and save on to the mesh object\n * OBJ.initMeshBuffers(gl, mesh);\n *\n * // now to render the mesh\n * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer);\n * gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, mesh.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0);\n * // it\'s possible that the mesh doesn\'t contain\n * // any texture coordinates (e.g. suzanne.obj in the development branch).\n * // in this case, the texture vertexAttribArray will need to be disabled\n * // before the call to drawElements\n * if(!mesh.textures.length){\n * gl.disableVertexAttribArray(shaderProgram.textureCoordAttribute);\n * }\n * else{\n * // if the texture vertexAttribArray has been previously\n * // disabled, then it needs to be re-enabled\n * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);\n * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.textureBuffer);\n * gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, mesh.textureBuffer.itemSize, gl.FLOAT, false, 0, 0);\n * }\n *\n * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer);\n * gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, mesh.normalBuffer.itemSize, gl.FLOAT, false, 0, 0);\n *\n * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer);\n * gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0);\n */\nfunction initMeshBuffers(gl, mesh) {\n mesh.normalBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.vertexNormals, 3);\n mesh.textureBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.textures, mesh.textureStride);\n mesh.vertexBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.vertices, 3);\n mesh.indexBuffer = _buildBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, mesh.indices, 1);\n return mesh;\n}\nfunction deleteMeshBuffers(gl, mesh) {\n gl.deleteBuffer(mesh.normalBuffer);\n gl.deleteBuffer(mesh.textureBuffer);\n gl.deleteBuffer(mesh.vertexBuffer);\n gl.deleteBuffer(mesh.indexBuffer);\n}\n\n\n//# sourceURL=webpack:///./src/utils.ts?');},0:
1885
+ /*!****************************!*\
1886
+ !*** multi ./src/index.ts ***!
1887
+ \****************************/
1888
+ /*! no static exports found */function(module,exports,__webpack_require__){eval('module.exports = __webpack_require__(/*! /home/aaron/google_drive/projects/webgl-obj-loader/src/index.ts */"./src/index.ts");\n\n\n//# sourceURL=webpack:///multi_./src/index.ts?');}})}));
1889
+ } (webglObjLoader_min$1));
1890
+ return webglObjLoader_min$1.exports;
1891
+ }
1892
+
1893
+ var webglObjLoader_minExports = requireWebglObjLoader_min();
1894
+ var OBJ = /*@__PURE__*/getDefaultExportFromCjs(webglObjLoader_minExports);
1895
+
1896
+ /**
1897
+ * @file Media sources — images, video and Canvas2D rasters for a bridge's textures, OBJ models for its buffers.
1898
+ * @module host/media
1899
+ * @license AGPL-3.0-only
1900
+ *
1901
+ * A model arrives as arrays for a bridge's buffer upload: loadModel fetches an
1902
+ * OBJ file and adapts webgl-obj-loader's mesh, no parser of its own.
1903
+ *
1904
+ * What a texture upload reads from: an ImageBitmap fetched from a URL, a
1905
+ * hidden <video> element from a file or the user's camera, a bitmap drawn
1906
+ * through a 2D context (the text path — glyph strips, billboard tags,
1907
+ * anything Canvas2D can typeset). No orientation option anywhere: whether
1908
+ * rows flip is the bridge's upload, since it depends on the GPU API's
1909
+ * texture space.
1910
+ */
1911
+
1912
+
1913
+ /**
1914
+ * Fetch an image into an ImageBitmap.
1915
+ * @param {string} url
1916
+ * @param {{ fetch?:object, bitmap?:object }} [opts]
1917
+ * fetch: the fetch init (credentials, mode, …). bitmap: the
1918
+ * createImageBitmap options (premultiplyAlpha, colorSpaceConversion, …).
1919
+ * @returns {Promise<ImageBitmap>}
1920
+ */
1921
+ async function loadImage(url, opts) {
1922
+ const o = opts || {};
1923
+ const res = await fetch(url, o.fetch);
1924
+ if (!res.ok) throw new Error('[host] image: ' + url + ' → ' + res.status);
1925
+ const blob = await res.blob();
1926
+ return o.bitmap ? createImageBitmap(blob, o.bitmap) : createImageBitmap(blob);
1927
+ }
1928
+
1929
+ /**
1930
+ * Fetch an OBJ model into the arrays shape: position and indices, plus normal
1931
+ * and texcoord when the file carries them. twgl's createBufferInfoFromArrays
1932
+ * takes the result as it is.
1933
+ *
1934
+ * Parsing is webgl-obj-loader's: faces are triangulated and each distinct
1935
+ * position / uv / normal triple becomes one vertex. Materials are ignored.
1936
+ *
1937
+ * @param {string} url
1938
+ * @param {{ fetch?:object }} [opts] fetch: the fetch init (credentials, mode, …).
1939
+ * @returns {Promise<{ position:{numComponents:number,data:Float32Array},
1940
+ * normal?:{numComponents:number,data:Float32Array},
1941
+ * texcoord?:{numComponents:number,data:Float32Array},
1942
+ * indices:{numComponents:number,data:Uint32Array} }>}
1943
+ */
1944
+ async function loadModel(url, opts) {
1945
+ const o = opts || {};
1946
+ const res = await fetch(url, o.fetch);
1947
+ if (!res.ok) throw new Error('[host] model: ' + url + ' → ' + res.status);
1948
+ const mesh = new OBJ.Mesh(await res.text());
1949
+ const out = {
1950
+ position: { numComponents: 3, data: new Float32Array(mesh.vertices) },
1951
+ indices: { numComponents: 3, data: new Uint32Array(mesh.indices) },
1952
+ };
1953
+ // a file without normals leaves the loader's normals unset, one per component
1954
+ const n = mesh.vertexNormals;
1955
+ if (n.length && n.every(Number.isFinite)) out.normal = { numComponents: 3, data: new Float32Array(n) };
1956
+ if (mesh.textures.length) out.texcoord = { numComponents: mesh.textureStride, data: new Float32Array(mesh.textures) };
1957
+ return out;
1958
+ }
1959
+
1960
+ /**
1961
+ * A hidden, muted, inline <video> element from a file or the user's camera.
1962
+ * `ready` resolves once metadata arrived (width / height known); a camera
1963
+ * denial rejects it. Inside an iframe a camera source needs allow="camera"
1964
+ * on the frame.
1965
+ *
1966
+ * @param {{ src?:string, camera?:boolean|object, loop?:boolean, autoplay?:boolean,
1967
+ * document?:object, media?:object }} opts
1968
+ * src: a media URL. camera: true or getUserMedia video constraints
1969
+ * ({ facingMode, width, height }). loop (default true for src).
1970
+ * autoplay (default true): start() once ready. document / media: the
1971
+ * Document and MediaDevices to use (defaults: the globals) — the seam
1972
+ * a test fills.
1973
+ * @returns {{ el:HTMLVideoElement, ready:Promise<object>, width:number, height:number,
1974
+ * start():Promise<void>, stop():object, dispose():object }}
1975
+ */
1976
+ function createVideo(opts) {
1977
+ const o = opts || {};
1978
+ const doc = o.document || (typeof document !== 'undefined' ? document : null);
1979
+ if (!doc) { console.error('[host] video: no document.'); return null; }
1980
+ const el = doc.createElement('video');
1981
+ el.muted = true;
1982
+ el.playsInline = true;
1983
+ el.setAttribute('playsinline', '');
1984
+ el.setAttribute('muted', '');
1985
+ el.style.display = 'none';
1986
+ let stream = null;
1987
+
1988
+ const v = {
1989
+ /** The <video> element — the bridge's upload source, refreshed each frame. */
1990
+ el,
1991
+ /** Intrinsic size, known once `ready` resolved. */
1992
+ width: 0, height: 0,
1993
+ /** Resolves with this source once metadata arrived; rejects on a camera denial or a load error. */
1994
+ ready: null,
1995
+ /** Play; resolves when playback began. */
1996
+ start() { const p = el.play(); return p && typeof p.catch === 'function' ? p.catch((e) => console.error('[host] video: play() failed: ' + e.message + '.')) : Promise.resolve(); },
1997
+ /** Pause. Chainable. */
1998
+ stop() { el.pause(); return v; },
1999
+ /** Stop, release the camera tracks, detach the element. */
2000
+ dispose() {
2001
+ el.pause();
2002
+ if (stream) { for (const t of stream.getTracks()) t.stop(); stream = null; }
2003
+ el.removeAttribute('src');
2004
+ el.srcObject = null;
2005
+ if (typeof el.remove === 'function') el.remove();
2006
+ return v;
2007
+ },
2008
+ };
2009
+
2010
+ v.ready = new Promise((resolve, reject) => {
2011
+ const unlisten = () => { el.removeEventListener('loadedmetadata', onMeta); el.removeEventListener('error', onErr); };
2012
+ const fail = (msg) => { unlisten(); reject(new Error('[host] video: ' + msg)); };
2013
+ const onMeta = () => {
2014
+ v.width = el.videoWidth || 0; v.height = el.videoHeight || 0;
2015
+ unlisten();
2016
+ if (o.autoplay !== false) v.start();
2017
+ resolve(v);
2018
+ };
2019
+ const onErr = () => fail('failed to load ' + (o.src || 'the source') + '.');
2020
+ el.addEventListener('loadedmetadata', onMeta);
2021
+ el.addEventListener('error', onErr);
2022
+ if (o.camera) {
2023
+ const media = o.media || (typeof navigator !== 'undefined' && navigator.mediaDevices) || null;
2024
+ if (!media || typeof media.getUserMedia !== 'function') { fail('getUserMedia is unavailable here.'); return; }
2025
+ media.getUserMedia({ video: o.camera === true ? true : o.camera, audio: false })
2026
+ .then((s) => { stream = s; el.srcObject = s; })
2027
+ .catch((e) => fail('camera denied: ' + e.message + '.'));
2028
+ } else if (o.src) {
2029
+ el.loop = o.loop !== false;
2030
+ el.src = o.src;
2031
+ } else {
2032
+ fail('needs { src } or { camera }.');
2033
+ }
2034
+ });
2035
+ v.ready.catch(() => {}); // an unobserved rejection is the caller's to observe, not a crash
2036
+ return v;
2037
+ }
2038
+
2039
+ /**
2040
+ * Draw through a 2D context and hand the result back as a texture source: an
2041
+ * ImageBitmap where OffscreenCanvas exists, else the canvas element itself.
2042
+ *
2043
+ * @param {function(CanvasRenderingContext2D, number, number):void} draw
2044
+ * @param {number} w Pixel width.
2045
+ * @param {number} h Pixel height.
2046
+ * @param {{ document?:object }} [opts] The Document for the fallback canvas.
2047
+ * @returns {ImageBitmap|HTMLCanvasElement|null}
2048
+ */
2049
+ function raster(draw, w, h, opts) {
2050
+ const o = opts || {};
2051
+ if (typeof OffscreenCanvas !== 'undefined') {
2052
+ const c = new OffscreenCanvas(w, h);
2053
+ draw(c.getContext('2d'), w, h);
2054
+ return c.transferToImageBitmap();
2055
+ }
2056
+ const doc = o.document || (typeof document !== 'undefined' ? document : null);
2057
+ if (!doc) { console.error('[host] raster: no OffscreenCanvas and no document.'); return null; }
2058
+ const c = doc.createElement('canvas');
2059
+ c.width = w; c.height = h;
2060
+ draw(c.getContext('2d'), w, h);
2061
+ return c;
2062
+ }
2063
+
2064
+ /**
2065
+ * @file The label layer — DOM text over the canvas, anchored in world or screen space.
2066
+ * @module host/labels
2067
+ * @license AGPL-3.0-only
2068
+ *
2069
+ * The replacement for WEBGL text() in HUDs, gizmo labels and formula
2070
+ * readouts, with the browser's own shaping. One absolutely positioned <div>
2071
+ * overlays the canvas as its next sibling, pointer-events: none, sized by
2072
+ * the canvas observer; the host sets the canvas parent to position:
2073
+ * relative when it is static, and says so in the console once.
2074
+ *
2075
+ * labels.set(id, text, x, y, z, opts) // a world anchor, projected each tick()
2076
+ * labels.setScreen(id, text, sx, sy, opts) // canvas px, the HUD form
2077
+ * labels.remove(id) · labels.clear() · labels.visible
2078
+ * labels.tick() // after the view bag is current
2079
+ *
2080
+ * opts: dx / dy pixel offsets, anchor ('center' default, 'left', 'right'),
2081
+ * class — a CSS class beside the layer's own 'host-label', frame — a
2082
+ * transient label that lives for the frame it was set in: a gizmo re-sets
2083
+ * it every draw and tick() removes it once a frame passes without. A label
2084
+ * whose screen depth leaves [0, 1] or whose anchor leaves the canvas is
2085
+ * hidden, not clamped. Elements are reused by id; tick() writes transforms
2086
+ * only, text when the string changed.
2087
+ */
2088
+
2089
+
2090
+ const _p = [0, 0, 0];
2091
+ const _ANCHOR = { center: -50, left: 0, right: -100 };
2092
+ const _NO_OPTS = {};
2093
+
2094
+ /**
2095
+ * Create the label layer of a canvas. The layer is inserted at once; it
2096
+ * needs a parent, so create it after the canvas is in the document.
2097
+ *
2098
+ * @param {object} host Its canvas, view bag and size are read.
2099
+ * @param {{ document?:object }} [opts] The Document (default: the global).
2100
+ * @returns {object|null} The layer, or null without a document or a parent.
2101
+ */
2102
+ function createLabels(host, opts) {
2103
+ const o = opts || {};
2104
+ const doc = o.document || (typeof document !== 'undefined' ? document : null);
2105
+ const canvas = host.canvas;
2106
+ const parent = canvas && canvas.parentNode;
2107
+ if (!doc || !parent) { console.error('[host] labels: the canvas needs a parent element.'); return null; }
2108
+
2109
+ const el = doc.createElement('div');
2110
+ el.className = 'host-labels';
2111
+ Object.assign(el.style, {
2112
+ position: 'absolute', left: '0px', top: '0px', width: '0px', height: '0px',
2113
+ pointerEvents: 'none', overflow: 'hidden',
2114
+ });
2115
+ const cs = typeof getComputedStyle === 'function' ? getComputedStyle(parent) : null;
2116
+ if (cs && cs.position === 'static') {
2117
+ parent.style.position = 'relative';
2118
+ console.info('[host] labels: set the canvas parent to position: relative for the label layer.');
2119
+ }
2120
+ parent.insertBefore(el, canvas.nextSibling);
2121
+
2122
+ const entries = new Map(); // id → { el, text, x, y, z, dx, dy, ax, screen, cls, sx, sy, shown }
2123
+ let width = 0, height = 0, left = -1, top = -1, visible = true;
2124
+
2125
+ const entry = (id, text, o) => {
2126
+ let e = entries.get(id);
2127
+ if (!e) {
2128
+ const span = doc.createElement('span');
2129
+ Object.assign(span.style, { position: 'absolute', left: '0px', top: '0px', whiteSpace: 'nowrap', willChange: 'transform' });
2130
+ span.style.visibility = 'hidden';
2131
+ el.appendChild(span);
2132
+ e = { el: span, text: null, x: 0, y: 0, z: 0, dx: 0, dy: 0, ax: -50, screen: false, cls: null, sx: NaN, sy: NaN, shown: false, frame: false, fresh: false };
2133
+ entries.set(id, e);
2134
+ }
2135
+ if (e.text !== text) { e.text = text; e.el.textContent = text; }
2136
+ const c = o.class || null;
2137
+ if (e.cls !== c) { e.cls = c; e.el.className = c ? 'host-label ' + c : 'host-label'; }
2138
+ e.ax = _ANCHOR[o.anchor] ?? -50;
2139
+ e.dx = o.dx || 0; e.dy = o.dy || 0;
2140
+ e.frame = !!o.frame; e.fresh = true;
2141
+ return e;
2142
+ };
2143
+ const place = (e, sx, sy) => {
2144
+ if (sx === e.sx && sy === e.sy) return;
2145
+ e.sx = sx; e.sy = sy;
2146
+ e.el.style.transform = 'translate(' + sx + 'px, ' + sy + 'px) translate(' + e.ax + '%, -50%)';
2147
+ };
2148
+ const show = (e, on) => {
2149
+ if (e.shown === on) return;
2150
+ e.shown = on;
2151
+ e.el.style.visibility = on ? '' : 'hidden';
2152
+ };
2153
+
2154
+ const labels = {
2155
+ /** The layer element. */
2156
+ el,
2157
+ /** Whether the layer shows; false hides every label at once. */
2158
+ get visible() { return visible; },
2159
+ set visible(v) { visible = !!v; el.style.display = visible ? '' : 'none'; },
2160
+ /** Live label count. */
2161
+ get size() { return entries.size; },
2162
+
2163
+ /**
2164
+ * Place a label at a world anchor, projected each tick().
2165
+ * @param {string} id
2166
+ * @param {string} text
2167
+ * @param {number} x, y, z World anchor.
2168
+ * @param {{ dx?:number, dy?:number, anchor?:string, class?:string, frame?:boolean }} [opts]
2169
+ * @returns {object} this
2170
+ */
2171
+ set(id, text, x, y, z, opts) {
2172
+ const e = entry(id, text, opts || _NO_OPTS);
2173
+ e.screen = false; e.x = x; e.y = y; e.z = z;
2174
+ return labels;
2175
+ },
2176
+
2177
+ /**
2178
+ * Place a label in canvas px — the HUD form.
2179
+ * @param {string} id
2180
+ * @param {string} text
2181
+ * @param {number} sx, sy Canvas px, y down.
2182
+ * @param {{ dx?:number, dy?:number, anchor?:string, class?:string, frame?:boolean }} [opts]
2183
+ * @returns {object} this
2184
+ */
2185
+ setScreen(id, text, sx, sy, opts) {
2186
+ const e = entry(id, text, opts || _NO_OPTS);
2187
+ e.screen = true; e.x = sx; e.y = sy; e.z = 0;
2188
+ return labels;
2189
+ },
2190
+
2191
+ /** Remove a label. */
2192
+ remove(id) {
2193
+ const e = entries.get(id);
2194
+ if (e) { el.removeChild(e.el); entries.delete(id); }
2195
+ return labels;
2196
+ },
2197
+
2198
+ /** Remove every label. */
2199
+ clear() { for (const id of [...entries.keys()]) labels.remove(id); return labels; },
2200
+
2201
+ /**
2202
+ * Project every world label through the view bag and write the
2203
+ * transforms; hide what falls outside the canvas or the depth range.
2204
+ * Call after the view bag is current for the frame.
2205
+ * @returns {object} this
2206
+ */
2207
+ tick() {
2208
+ const ol = canvas.offsetLeft || 0, ot = canvas.offsetTop || 0;
2209
+ if (ol !== left || ot !== top) { left = ol; top = ot; el.style.left = ol + 'px'; el.style.top = ot + 'px'; }
2210
+ const view = host.view;
2211
+ for (const [id, e] of entries) {
2212
+ if (e.frame && !e.fresh) { labels.remove(id); continue; }
2213
+ e.fresh = false;
2214
+ let sx, sy, on;
2215
+ if (e.screen) { sx = e.x; sy = e.y; on = true; }
2216
+ else if (!view || view.stale) { on = false; }
2217
+ else {
2218
+ mapLocation(_p, e.x, e.y, e.z, WORLD, SCREEN, view, view.vp, view.ndcZMin);
2219
+ sx = _p[0]; sy = _p[1];
2220
+ on = _p[2] >= 0 && _p[2] <= 1 && sx >= 0 && sx <= width && sy >= 0 && sy <= height;
2221
+ }
2222
+ if (on) place(e, sx + e.dx, sy + e.dy);
2223
+ show(e, on);
2224
+ }
2225
+ return labels;
2226
+ },
2227
+
2228
+ /**
2229
+ * The observer's write: the layer's size in CSS px.
2230
+ * @param {number} w
2231
+ * @param {number} h
2232
+ * @returns {object} this
2233
+ */
2234
+ resize(w, h) {
2235
+ width = w; height = h;
2236
+ el.style.width = w + 'px'; el.style.height = h + 'px';
2237
+ return labels;
2238
+ },
2239
+
2240
+ /** Remove the layer and its labels. */
2241
+ dispose() {
2242
+ entries.clear();
2243
+ if (el.parentNode) el.parentNode.removeChild(el);
2244
+ host.unregister(labels);
2245
+ return labels;
2246
+ },
2247
+ };
2248
+ labels.resize(host.width || 0, host.height || 0);
2249
+ return host.register(labels);
2250
+ }
2251
+
2252
+ /**
2253
+ * @file The orbit — the fall-through camera gesture on unclaimed pointers.
2254
+ * @module host/orbit
2255
+ * @license AGPL-3.0-only
2256
+ *
2257
+ * What the handle gate yields to: one pointer orbits the camera state about
2258
+ * its center, two pointers dolly by their distance and pan by their
2259
+ * midpoint, the wheel dollies. update() consumes only unclaimed pointers and
2260
+ * returns true when it moved the camera, so
2261
+ *
2262
+ * if (!h.update()) orbit.update()
2263
+ *
2264
+ * reads as the gate. It writes cam only; the next setCamera(cam) installs it.
2265
+ * Two-pointer motion is computed from the pointers' own coordinates, never
2266
+ * from a browser's pinch synthesis, so it reads the same on every platform;
2267
+ * the canvas gets touch-action: none while the orbit lives so the browser
2268
+ * does not scroll or zoom the page instead. No damping, no inertia: direct
2269
+ * manipulation wants exactness, and a sketch that wants smoothing lerps the
2270
+ * camera it owns.
2271
+ *
2272
+ * Screen y runs down; which world direction that is depends on the
2273
+ * projection installed in the view bag: a y-up projection (GL's, tree's
2274
+ * default) maps a downward drag to the eye's −up, a y-flipped one (p5's,
2275
+ * mat4Proj[5] < 0) to +up. The orbit reads that sign off the bag each
2276
+ * update, so a drag down always brings the scene down.
2277
+ */
2278
+
2279
+
2280
+ const _clamp = { min: 0, max: Infinity };
2281
+
2282
+ /**
2283
+ * Create an orbit on a camera state.
2284
+ *
2285
+ * @param {object} host
2286
+ * @param {object} cam The camera state written.
2287
+ * @param {{ rotate?:number, pan?:number, zoom?:number, wheel?:number,
2288
+ * minDistance?:number, maxDistance?:number, enabled?:boolean }} [opts]
2289
+ * rotate: radians per pixel (default 0.005). pan / zoom / wheel:
2290
+ * scalars on the two-pointer pan, the pinch and the wheel dolly
2291
+ * (default 1). minDistance / maxDistance: gaze-distance clamps.
2292
+ * @returns {object} The orbit: { cam, enabled, rotate, pan, zoom, wheel,
2293
+ * minDistance, maxDistance, update(), home(), dispose() }.
2294
+ */
2295
+ function createOrbit(host, cam, opts) {
2296
+ const o = opts || {};
2297
+ const canvas = host.canvas;
2298
+ const home = cameraCopy(createCamera(), cam);
2299
+ const tracked = new Map(); // pointer id → { x, y } as last consumed
2300
+ let wheelAcc = 0;
2301
+
2302
+ const onWheel = (e) => {
2303
+ if (typeof e.preventDefault === 'function') e.preventDefault();
2304
+ const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? (host.height || 1) : 1;
2305
+ wheelAcc += (e.deltaY || 0) * unit;
2306
+ };
2307
+ canvas.addEventListener('wheel', onWheel, { passive: false });
2308
+ const touchAction = canvas.style ? canvas.style.touchAction : undefined;
2309
+ if (canvas.style) canvas.style.touchAction = 'none';
2310
+
2311
+ const track = (p) => {
2312
+ let t = tracked.get(p.id);
2313
+ if (!t) { t = { x: p.x, y: p.y }; tracked.set(p.id, t); }
2314
+ else { t.x = p.x; t.y = p.y; }
2315
+ };
2316
+ const distance = () => {
2317
+ const e = cam.eye, c = cam.center;
2318
+ return Math.hypot(e[0] - c[0], e[1] - c[1], e[2] - c[2]);
2319
+ };
2320
+ const dolly = (factor) => {
2321
+ _clamp.min = orbit.minDistance; _clamp.max = orbit.maxDistance;
2322
+ cameraDolly(cam, factor, _clamp);
2323
+ };
2324
+
2325
+ const orbit = {
2326
+ /** The camera state written. */
2327
+ cam,
2328
+ /** False makes update() a no-op that drops its pointer memory. */
2329
+ enabled: o.enabled !== false,
2330
+ /** Radians per pixel of one-pointer drag. */
2331
+ rotate: o.rotate ?? 0.005,
2332
+ /** Scalar on the two-pointer pan. */
2333
+ pan: o.pan ?? 1,
2334
+ /** Scalar on the pinch dolly's exponent. */
2335
+ zoom: o.zoom ?? 1,
2336
+ /** Scalar on the wheel dolly. */
2337
+ wheel: o.wheel ?? 1,
2338
+ /** Gaze-distance clamps for every dolly. */
2339
+ minDistance: o.minDistance ?? 0,
2340
+ maxDistance: o.maxDistance ?? Infinity,
2341
+
2342
+ /**
2343
+ * Consume this frame's unclaimed pointer motion and wheel into cam.
2344
+ * @returns {boolean} Whether the camera moved.
2345
+ */
2346
+ update() {
2347
+ if (!orbit.enabled) { tracked.clear(); wheelAcc = 0; return false; }
2348
+ const src = host.pointer;
2349
+ let a = null, b = null;
2350
+ for (const p of src.pointers.values()) {
2351
+ // a hovering mouse has an entry without a press; only pressed, unclaimed, live pointers drive
2352
+ if (!p.down || p.owner !== null || p.up || p.cancel) { tracked.delete(p.id); continue; }
2353
+ if (a === null || p.id < a.id) { b = a; a = p; }
2354
+ else if (b === null || p.id < b.id) b = p;
2355
+ }
2356
+ for (const id of tracked.keys()) {
2357
+ if ((a === null || id !== a.id) && (b === null || id !== b.id)) tracked.delete(id);
2358
+ }
2359
+ let moved = false;
2360
+ const view = host.view;
2361
+ const ys = view.mat4Proj[5] < 0 ? -1 : 1; // screen-down is the eye's −up under a y-up projection, +up under p5's flip
2362
+ if (a !== null && b === null) {
2363
+ const t = tracked.get(a.id);
2364
+ if (t) {
2365
+ const dx = a.x - t.x, dy = a.y - t.y;
2366
+ if (dx !== 0 || dy !== 0) { cameraOrbit(cam, -dx * orbit.rotate, ys * dy * orbit.rotate); moved = true; }
2367
+ }
2368
+ track(a);
2369
+ } else if (a !== null && b !== null) {
2370
+ const ta = tracked.get(a.id), tb = tracked.get(b.id);
2371
+ if (ta && tb) {
2372
+ const dmx = (a.x + b.x - ta.x - tb.x) / 2, dmy = (a.y + b.y - ta.y - tb.y) / 2;
2373
+ if (dmx !== 0 || dmy !== 0) {
2374
+ const ratio = pixelRatio(view.mat4Proj, -view.vp[3] || 1, -distance(), view.ndcZMin);
2375
+ cameraPan(cam, -dmx * ratio * orbit.pan, ys * dmy * ratio * orbit.pan);
2376
+ moved = true;
2377
+ }
2378
+ const d0 = Math.hypot(ta.x - tb.x, ta.y - tb.y), d1 = Math.hypot(a.x - b.x, a.y - b.y);
2379
+ if (d0 > 0 && d1 > 0 && d0 !== d1) { dolly(Math.pow(d0 / d1, orbit.zoom)); moved = true; }
2380
+ }
2381
+ track(a); track(b);
2382
+ }
2383
+ if (wheelAcc !== 0) {
2384
+ dolly(Math.exp(wheelAcc * 0.001 * orbit.wheel));
2385
+ wheelAcc = 0;
2386
+ moved = true;
2387
+ }
2388
+ return moved;
2389
+ },
2390
+
2391
+ /** Restore the pose captured at creation. */
2392
+ home() { cameraCopy(cam, home); return orbit; },
2393
+
2394
+ /** Remove the wheel listener, restore touch-action, unregister from the host. */
2395
+ dispose() {
2396
+ canvas.removeEventListener('wheel', onWheel);
2397
+ if (canvas.style) canvas.style.touchAction = touchAction;
2398
+ tracked.clear();
2399
+ host.unregister(orbit);
2400
+ return orbit;
2401
+ },
2402
+ };
2403
+ return host.register(orbit);
2404
+ }
2405
+
4693
2406
  /**
4694
2407
  * @file The host context — one per canvas: pointer, view bag, players, loop, observer.
4695
2408
  * @module host
@@ -4706,6 +2419,10 @@ class TrackHandles {
4706
2419
  * host.handle(opts) · host.router(handles, opts)
4707
2420
  * host.cameraHelm(cam, opts) · host.poseHelm(opts)
4708
2421
  * host.poseTrack(opts) · host.cameraTrack(cam, opts)
2422
+ * host.hid(opts) · host.gamepad(opts)
2423
+ * host.image(url) · host.video(opts) · host.raster(draw, w, h) · host.model(url)
2424
+ * host.labels // the DOM label layer, created on first access
2425
+ * host.orbit(cam, opts)
4709
2426
  * host.tick(dt) // external-loop mode
4710
2427
  * host.dispose()
4711
2428
  *
@@ -4740,7 +2457,7 @@ function createHost(canvas, opts) {
4740
2457
  const o = opts || {};
4741
2458
  const t0 = _now();
4742
2459
  const constructs = new Set();
4743
- let loop = null, observer = null;
2460
+ let loop = null, observer = null, labels = null;
4744
2461
  const host = {
4745
2462
  canvas,
4746
2463
  width: 0, height: 0, dpr: 1,
@@ -4788,6 +2505,30 @@ function createHost(canvas, opts) {
4788
2505
  poseTrack(opts) { return poseTrack(host, opts); },
4789
2506
  /** A core CameraTrack evaluating into a camera state while it plays — see host/track. */
4790
2507
  cameraTrack(cam, opts) { return cameraTrack(host, cam, opts); },
2508
+ /** A WebHID 6-DOF rate stream — see host/stream. */
2509
+ hid(opts) { return createHid(host, opts); },
2510
+ /** A Gamepad rate stream, polled each tick — see host/stream. */
2511
+ gamepad(opts) { return createGamepad(host, opts); },
2512
+ /** Fetch an image into an ImageBitmap — see host/media. */
2513
+ image(url, opts) { return loadImage(url, opts); },
2514
+ /** A hidden <video> from a file or the camera, disposed with the host — see host/media. */
2515
+ video(opts) { return host.register(createVideo(opts)); },
2516
+ /** Draw through a 2D context into a texture source — see host/media. */
2517
+ raster(draw, w, h, opts) { return raster(draw, w, h, opts); },
2518
+ /** Fetch an OBJ model into the arrays shape — see host/media. */
2519
+ model(url, opts) { return loadModel(url, opts); },
2520
+ /**
2521
+ * The label layer — see host/labels. Created on first access, so a
2522
+ * canvas that never labels never gets a layer nor a repositioned parent.
2523
+ */
2524
+ get labels() {
2525
+ if (labels === null) labels = createLabels(host);
2526
+ return labels;
2527
+ },
2528
+ /** Whether the label layer exists, without creating it. */
2529
+ get hasLabels() { return labels !== null; },
2530
+ /** The fall-through camera gesture on unclaimed pointers — see host/orbit. */
2531
+ orbit(cam, opts) { return createOrbit(host, cam, opts); },
4791
2532
 
4792
2533
  /** Register a construct with dispose() so host.dispose() releases it. */
4793
2534
  register(c) { if (c) constructs.add(c); return c; },
@@ -4799,6 +2540,7 @@ function createHost(canvas, opts) {
4799
2540
  if (loop) loop.stop();
4800
2541
  for (const c of [...constructs]) { if (typeof c.dispose === 'function') c.dispose(); }
4801
2542
  constructs.clear();
2543
+ labels = null;
4802
2544
  observer.dispose();
4803
2545
  host.pointer.dispose();
4804
2546
  host.players.clear();
@@ -4811,6 +2553,7 @@ function createHost(canvas, opts) {
4811
2553
  observer = observeCanvas(canvas, (w, h, dpr) => {
4812
2554
  host.width = w; host.height = h; host.dpr = dpr;
4813
2555
  host.view.resize(w, h);
2556
+ if (labels) labels.resize(w, h);
4814
2557
  if (typeof o.onSize === 'function') o.onSize(w, h, dpr);
4815
2558
  });
4816
2559
 
@@ -4829,5 +2572,5 @@ function createHost(canvas, opts) {
4829
2572
  return host;
4830
2573
  }
4831
2574
 
4832
- export { Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createHost, createLoop, createPlayers, createPointer, createView, helmBasis, isConstraint, measureCanvas, observeCanvas, poseHelm, poseTrack, validConstraint };
2575
+ export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeSpaceNavigator, helmBasis, isConstraint, loadImage, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
4833
2576
  //# sourceMappingURL=index.js.map