@nakednous/tree 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/index.js +1819 -0
- package/dist/index.js.map +1 -0
- package/package.json +22 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1819 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Core constants — zero dependencies.
|
|
3
|
+
* @module tree/constants
|
|
4
|
+
* @license GPL-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 MODEL = 'MODEL';
|
|
13
|
+
const MATRIX = 'MATRIX';
|
|
14
|
+
|
|
15
|
+
// NDC Z convention (only difference between backends)
|
|
16
|
+
const WEBGL = -1; // z ∈ [−1, 1]
|
|
17
|
+
const WEBGPU = 0; // z ∈ [0, 1]
|
|
18
|
+
|
|
19
|
+
// Visibility results
|
|
20
|
+
const INVISIBLE = 0;
|
|
21
|
+
const VISIBLE = 1;
|
|
22
|
+
const SEMIVISIBLE = 2;
|
|
23
|
+
|
|
24
|
+
// Basis vectors (frozen plain arrays — duck-typed Vec3)
|
|
25
|
+
const ORIGIN = Object.freeze([0, 0, 0]);
|
|
26
|
+
const i = Object.freeze([1, 0, 0]);
|
|
27
|
+
const j = Object.freeze([0, 1, 0]);
|
|
28
|
+
const k = Object.freeze([0, 0, 1]);
|
|
29
|
+
const _i = Object.freeze([-1, 0, 0]);
|
|
30
|
+
const _j = Object.freeze([0, -1, 0]);
|
|
31
|
+
const _k = Object.freeze([0, 0, -1]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @file Pure numeric math — mat4, mat3, projection queries, space transforms.
|
|
35
|
+
* @module tree/math
|
|
36
|
+
* @license GPL-3.0-only
|
|
37
|
+
*
|
|
38
|
+
* CONVENTIONS (all functions in this module follow these):
|
|
39
|
+
*
|
|
40
|
+
* Storage: Column-major Float32Array / ArrayLike<number>.
|
|
41
|
+
* Element [col*4 + row] = M[row, col].
|
|
42
|
+
*
|
|
43
|
+
* Multiply: mat4Mul(out, A, B) = A · B (standard math order).
|
|
44
|
+
*
|
|
45
|
+
* Pipeline: clip = P · V · M · v
|
|
46
|
+
* P = projection (eye → clip)
|
|
47
|
+
* V = view (world → eye)
|
|
48
|
+
* M = model (local → world)
|
|
49
|
+
*
|
|
50
|
+
* PV: All functions expecting a "pv" matrix receive P · V.
|
|
51
|
+
* This is what _worldToScreen, _ensurePV, etc. compute.
|
|
52
|
+
*
|
|
53
|
+
* Matrix stack (translate/rotate/scale in p5):
|
|
54
|
+
* Each call post-multiplies: M = M · T, so:
|
|
55
|
+
* translate(tx,ty,tz); rotateY(a); scale(s);
|
|
56
|
+
* yields M = T · R · S. A vertex v is transformed as M·v = T·R·S·v
|
|
57
|
+
* (scaled first, then rotated, then translated — last-written-first-applied).
|
|
58
|
+
*
|
|
59
|
+
* p5 bridge note (for implementors of host layers):
|
|
60
|
+
* p5.Matrix.mult(B) computes B · this (pre-multiply, arg on LEFT).
|
|
61
|
+
* p5 translate/rotate/scale do this · T (post-multiply, GL stack).
|
|
62
|
+
* So p5's pvMatrix() = V.clone().mult(P) = P · V — same as ours.
|
|
63
|
+
* The bridge extracts .mat4 (Float32Array) and feeds it directly,
|
|
64
|
+
* or uses mat4Mul(out, proj, view) for the non-cached path.
|
|
65
|
+
*
|
|
66
|
+
* Every function uses only stack locals for intermediates (zero shared state).
|
|
67
|
+
* Every mutating function writes to a caller-provided `out` and returns `out`.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// Mat4 operations
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
/** out = identity 4×4 */
|
|
76
|
+
function mat4Identity(out) {
|
|
77
|
+
out[0]=1;out[1]=0;out[2]=0;out[3]=0;
|
|
78
|
+
out[4]=0;out[5]=1;out[6]=0;out[7]=0;
|
|
79
|
+
out[8]=0;out[9]=0;out[10]=1;out[11]=0;
|
|
80
|
+
out[12]=0;out[13]=0;out[14]=0;out[15]=1;
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** out = a * b (column-major) */
|
|
85
|
+
function mat4Mul(out, a, b) {
|
|
86
|
+
const a0=a[0],a1=a[1],a2=a[2],a3=a[3],
|
|
87
|
+
a4=a[4],a5=a[5],a6=a[6],a7=a[7],
|
|
88
|
+
a8=a[8],a9=a[9],a10=a[10],a11=a[11],
|
|
89
|
+
a12=a[12],a13=a[13],a14=a[14],a15=a[15];
|
|
90
|
+
let b0,b1,b2,b3;
|
|
91
|
+
b0=b[0];b1=b[1];b2=b[2];b3=b[3];
|
|
92
|
+
out[0]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
93
|
+
out[1]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
94
|
+
out[2]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
95
|
+
out[3]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
96
|
+
b0=b[4];b1=b[5];b2=b[6];b3=b[7];
|
|
97
|
+
out[4]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
98
|
+
out[5]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
99
|
+
out[6]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
100
|
+
out[7]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
101
|
+
b0=b[8];b1=b[9];b2=b[10];b3=b[11];
|
|
102
|
+
out[8]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
103
|
+
out[9]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
104
|
+
out[10]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
105
|
+
out[11]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
106
|
+
b0=b[12];b1=b[13];b2=b[14];b3=b[15];
|
|
107
|
+
out[12]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
108
|
+
out[13]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
109
|
+
out[14]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
110
|
+
out[15]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** out = inverse(src). Returns null if singular. */
|
|
115
|
+
function mat4Invert(out, src) {
|
|
116
|
+
const s=src;
|
|
117
|
+
const a00=s[0],a01=s[1],a02=s[2],a03=s[3],
|
|
118
|
+
a10=s[4],a11=s[5],a12=s[6],a13=s[7],
|
|
119
|
+
a20=s[8],a21=s[9],a22=s[10],a23=s[11],
|
|
120
|
+
a30=s[12],a31=s[13],a32=s[14],a33=s[15];
|
|
121
|
+
const b00=a00*a11-a01*a10,b01=a00*a12-a02*a10,
|
|
122
|
+
b02=a00*a13-a03*a10,b03=a01*a12-a02*a11,
|
|
123
|
+
b04=a01*a13-a03*a11,b05=a02*a13-a03*a12,
|
|
124
|
+
b06=a20*a31-a21*a30,b07=a20*a32-a22*a30,
|
|
125
|
+
b08=a20*a33-a23*a30,b09=a21*a32-a22*a31,
|
|
126
|
+
b10=a21*a33-a23*a31,b11=a22*a33-a23*a32;
|
|
127
|
+
let det=b00*b11-b01*b10+b02*b09+b03*b08-b04*b07+b05*b06;
|
|
128
|
+
if (Math.abs(det) < 1e-12) return null;
|
|
129
|
+
det=1/det;
|
|
130
|
+
out[0]=(a11*b11-a12*b10+a13*b09)*det;
|
|
131
|
+
out[1]=(a02*b10-a01*b11-a03*b09)*det;
|
|
132
|
+
out[2]=(a31*b05-a32*b04+a33*b03)*det;
|
|
133
|
+
out[3]=(a22*b04-a21*b05-a23*b03)*det;
|
|
134
|
+
out[4]=(a12*b08-a10*b11-a13*b07)*det;
|
|
135
|
+
out[5]=(a00*b11-a02*b08+a03*b07)*det;
|
|
136
|
+
out[6]=(a32*b02-a30*b05-a33*b01)*det;
|
|
137
|
+
out[7]=(a20*b05-a22*b02+a23*b01)*det;
|
|
138
|
+
out[8]=(a10*b10-a11*b08+a13*b06)*det;
|
|
139
|
+
out[9]=(a01*b08-a00*b10-a03*b06)*det;
|
|
140
|
+
out[10]=(a30*b04-a31*b02+a33*b00)*det;
|
|
141
|
+
out[11]=(a21*b02-a20*b04-a23*b00)*det;
|
|
142
|
+
out[12]=(a11*b07-a10*b09-a12*b06)*det;
|
|
143
|
+
out[13]=(a00*b09-a01*b07+a02*b06)*det;
|
|
144
|
+
out[14]=(a31*b01-a30*b03-a32*b00)*det;
|
|
145
|
+
out[15]=(a20*b03-a21*b01+a22*b00)*det;
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** out = transpose(src) */
|
|
150
|
+
function mat4Transpose(out, src) {
|
|
151
|
+
if (out === src) {
|
|
152
|
+
let t;
|
|
153
|
+
t=src[1];out[1]=src[4];out[4]=t;
|
|
154
|
+
t=src[2];out[2]=src[8];out[8]=t;
|
|
155
|
+
t=src[3];out[3]=src[12];out[12]=t;
|
|
156
|
+
t=src[6];out[6]=src[9];out[9]=t;
|
|
157
|
+
t=src[7];out[7]=src[13];out[13]=t;
|
|
158
|
+
t=src[11];out[11]=src[14];out[14]=t;
|
|
159
|
+
} else {
|
|
160
|
+
out[0]=src[0];out[1]=src[4];out[2]=src[8];out[3]=src[12];
|
|
161
|
+
out[4]=src[1];out[5]=src[5];out[6]=src[9];out[7]=src[13];
|
|
162
|
+
out[8]=src[2];out[9]=src[6];out[10]=src[10];out[11]=src[14];
|
|
163
|
+
out[12]=src[3];out[13]=src[7];out[14]=src[11];out[15]=src[15];
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** out[0..8] = inverseTranspose(upper3×3(src)) (normal matrix) */
|
|
169
|
+
function mat3NormalFromMat4(out, src) {
|
|
170
|
+
const a00=src[0],a01=src[1],a02=src[2],
|
|
171
|
+
a10=src[4],a11=src[5],a12=src[6],
|
|
172
|
+
a20=src[8],a21=src[9],a22=src[10];
|
|
173
|
+
const b01=a22*a11-a12*a21,
|
|
174
|
+
b11=-a22*a01+a02*a21,
|
|
175
|
+
b21=a12*a01-a02*a11;
|
|
176
|
+
let det=a00*b01+a10*b11+a20*b21;
|
|
177
|
+
if (Math.abs(det) < 1e-12) { for(let i=0;i<9;i++)out[i]=0; return out; }
|
|
178
|
+
det=1/det;
|
|
179
|
+
out[0]=b01*det;
|
|
180
|
+
out[1]=(-a22*a10+a12*a20)*det;
|
|
181
|
+
out[2]=(a21*a10-a11*a20)*det;
|
|
182
|
+
out[3]=b11*det;
|
|
183
|
+
out[4]=(a22*a00-a02*a20)*det;
|
|
184
|
+
out[5]=(-a21*a00+a01*a20)*det;
|
|
185
|
+
out[6]=b21*det;
|
|
186
|
+
out[7]=(-a12*a00+a02*a10)*det;
|
|
187
|
+
out[8]=(a11*a00-a01*a10)*det;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** out = mat4 * [x,y,z,1], perspective-divides, writes xyz */
|
|
192
|
+
function mat4MulPoint(out, m, x, y, z) {
|
|
193
|
+
const rx = m[0]*x + m[4]*y + m[8]*z + m[12];
|
|
194
|
+
const ry = m[1]*x + m[5]*y + m[9]*z + m[13];
|
|
195
|
+
const rz = m[2]*x + m[6]*y + m[10]*z + m[14];
|
|
196
|
+
const rw = m[3]*x + m[7]*y + m[11]*z + m[15];
|
|
197
|
+
if (rw !== 0 && rw !== 1) {
|
|
198
|
+
out[0] = rx/rw; out[1] = ry/rw; out[2] = rz/rw;
|
|
199
|
+
} else {
|
|
200
|
+
out[0] = rx; out[1] = ry; out[2] = rz;
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** out = mat4 * [x,y,z,0] (direction, no translation) */
|
|
206
|
+
function mat4MulDir(out, m, x, y, z) {
|
|
207
|
+
out[0] = m[0]*x + m[4]*y + m[8]*z;
|
|
208
|
+
out[1] = m[1]*x + m[5]*y + m[9]*z;
|
|
209
|
+
out[2] = m[2]*x + m[6]*y + m[10]*z;
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** out = upper-left 3×3 transposed from mat4 (direction / dMatrix extraction) */
|
|
214
|
+
function mat3FromMat4T(out, m) {
|
|
215
|
+
out[0]=m[0]; out[1]=m[4]; out[2]=m[8];
|
|
216
|
+
out[3]=m[1]; out[4]=m[5]; out[5]=m[9];
|
|
217
|
+
out[6]=m[2]; out[7]=m[6]; out[8]=m[10];
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** out = mat3 * vec3 */
|
|
222
|
+
function mat3MulVec3(out, m, x, y, z) {
|
|
223
|
+
out[0] = m[0]*x + m[3]*y + m[6]*z;
|
|
224
|
+
out[1] = m[1]*x + m[4]*y + m[7]*z;
|
|
225
|
+
out[2] = m[2]*x + m[5]*y + m[8]*z;
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
230
|
+
// Projection queries
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
232
|
+
|
|
233
|
+
/** @returns {boolean} true if orthographic */
|
|
234
|
+
function projIsOrtho(p) { return p[15] !== 0; }
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Near plane distance.
|
|
238
|
+
* @param {ArrayLike<number>} p Projection Mat4
|
|
239
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0)
|
|
240
|
+
*/
|
|
241
|
+
function projNear(p, ndcZMin) {
|
|
242
|
+
return p[15] === 0
|
|
243
|
+
? p[14] / (p[10] + ndcZMin)
|
|
244
|
+
: (p[14] - ndcZMin) / p[10];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Far plane distance (convention-independent: far always maps to NDC z=1). */
|
|
248
|
+
function projFar(p) {
|
|
249
|
+
return p[15] === 0
|
|
250
|
+
? p[14] / (1 + p[10])
|
|
251
|
+
: (p[14] - 1) / p[10];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function projLeft(p, ndcZMin) {
|
|
255
|
+
return p[15] === 1
|
|
256
|
+
? -(1 + p[12]) / p[0]
|
|
257
|
+
: projNear(p, ndcZMin) * (p[8] - 1) / p[0];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function projRight(p, ndcZMin) {
|
|
261
|
+
return p[15] === 1
|
|
262
|
+
? (1 - p[12]) / p[0]
|
|
263
|
+
: projNear(p, ndcZMin) * (1 + p[8]) / p[0];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function projTop(p, ndcZMin) {
|
|
267
|
+
return p[15] === 1
|
|
268
|
+
? (p[13] - 1) / p[5]
|
|
269
|
+
: projNear(p, ndcZMin) * (p[9] - 1) / p[5];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function projBottom(p, ndcZMin) {
|
|
273
|
+
return p[15] === 1
|
|
274
|
+
? (1 + p[13]) / p[5]
|
|
275
|
+
: projNear(p, ndcZMin) * (1 + p[9]) / p[5];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Vertical fov (radians, perspective only). */
|
|
279
|
+
function projFov(p) {
|
|
280
|
+
return Math.abs(2 * Math.atan(1 / p[5]));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Horizontal fov (radians, perspective only). */
|
|
284
|
+
function projHfov(p) {
|
|
285
|
+
return Math.abs(2 * Math.atan(1 / p[0]));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
289
|
+
// Derived matrices (convenience)
|
|
290
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
291
|
+
|
|
292
|
+
/** out = proj * view = P · V (standard GL) */
|
|
293
|
+
function mat4PV(out, proj, view) { return mat4Mul(out, proj, view); }
|
|
294
|
+
|
|
295
|
+
/** out = view * model = V · M (standard GL) */
|
|
296
|
+
function mat4MV(out, model, view) { return mat4Mul(out, view, model); }
|
|
297
|
+
|
|
298
|
+
/** out = proj * view * model = P · V · M (standard GL) */
|
|
299
|
+
function mat4PMV(out, proj, model, view) {
|
|
300
|
+
// MV = view * model (V · M)
|
|
301
|
+
const t0=view[0],t1=view[1],t2=view[2],t3=view[3],
|
|
302
|
+
t4=view[4],t5=view[5],t6=view[6],t7=view[7],
|
|
303
|
+
t8=view[8],t9=view[9],t10=view[10],t11=view[11],
|
|
304
|
+
t12=view[12],t13=view[13],t14=view[14],t15=view[15];
|
|
305
|
+
let b0,b1,b2,b3;
|
|
306
|
+
b0=model[0];b1=model[1];b2=model[2];b3=model[3];
|
|
307
|
+
const mv0=t0*b0+t4*b1+t8*b2+t12*b3, mv1=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
308
|
+
mv2=t2*b0+t6*b1+t10*b2+t14*b3, mv3=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
309
|
+
b0=model[4];b1=model[5];b2=model[6];b3=model[7];
|
|
310
|
+
const mv4=t0*b0+t4*b1+t8*b2+t12*b3, mv5=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
311
|
+
mv6=t2*b0+t6*b1+t10*b2+t14*b3, mv7=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
312
|
+
b0=model[8];b1=model[9];b2=model[10];b3=model[11];
|
|
313
|
+
const mv8=t0*b0+t4*b1+t8*b2+t12*b3, mv9=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
314
|
+
mv10=t2*b0+t6*b1+t10*b2+t14*b3, mv11=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
315
|
+
b0=model[12];b1=model[13];b2=model[14];b3=model[15];
|
|
316
|
+
const mv12=t0*b0+t4*b1+t8*b2+t12*b3, mv13=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
317
|
+
mv14=t2*b0+t6*b1+t10*b2+t14*b3, mv15=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
318
|
+
// PMV = proj * MV (P · V · M)
|
|
319
|
+
const p0=proj[0],p1=proj[1],p2=proj[2],p3=proj[3],
|
|
320
|
+
p4=proj[4],p5=proj[5],p6=proj[6],p7=proj[7],
|
|
321
|
+
p8=proj[8],p9=proj[9],p10=proj[10],p11=proj[11],
|
|
322
|
+
p12=proj[12],p13=proj[13],p14=proj[14],p15=proj[15];
|
|
323
|
+
out[0]=p0*mv0+p4*mv1+p8*mv2+p12*mv3;
|
|
324
|
+
out[1]=p1*mv0+p5*mv1+p9*mv2+p13*mv3;
|
|
325
|
+
out[2]=p2*mv0+p6*mv1+p10*mv2+p14*mv3;
|
|
326
|
+
out[3]=p3*mv0+p7*mv1+p11*mv2+p15*mv3;
|
|
327
|
+
out[4]=p0*mv4+p4*mv5+p8*mv6+p12*mv7;
|
|
328
|
+
out[5]=p1*mv4+p5*mv5+p9*mv6+p13*mv7;
|
|
329
|
+
out[6]=p2*mv4+p6*mv5+p10*mv6+p14*mv7;
|
|
330
|
+
out[7]=p3*mv4+p7*mv5+p11*mv6+p15*mv7;
|
|
331
|
+
out[8]=p0*mv8+p4*mv9+p8*mv10+p12*mv11;
|
|
332
|
+
out[9]=p1*mv8+p5*mv9+p9*mv10+p13*mv11;
|
|
333
|
+
out[10]=p2*mv8+p6*mv9+p10*mv10+p14*mv11;
|
|
334
|
+
out[11]=p3*mv8+p7*mv9+p11*mv10+p15*mv11;
|
|
335
|
+
out[12]=p0*mv12+p4*mv13+p8*mv14+p12*mv15;
|
|
336
|
+
out[13]=p1*mv12+p5*mv13+p9*mv14+p13*mv15;
|
|
337
|
+
out[14]=p2*mv12+p6*mv13+p10*mv14+p14*mv15;
|
|
338
|
+
out[15]=p3*mv12+p7*mv13+p11*mv14+p15*mv15;
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
343
|
+
// Space transforms — mapLocation / mapDirection
|
|
344
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
345
|
+
//
|
|
346
|
+
// FLAT DISPATCH: every from→to pair is a self-contained leaf.
|
|
347
|
+
// No path calls back into mapLocation/mapDirection (no reentrancy).
|
|
348
|
+
// All intermediates are stack locals (zero shared state).
|
|
349
|
+
//
|
|
350
|
+
|
|
351
|
+
// ── Location Transform ───────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Relative transform for locations (points).
|
|
355
|
+
*
|
|
356
|
+
* out = inv(to) · from
|
|
357
|
+
*
|
|
358
|
+
* Maps a point from the `from` frame into the `to` frame:
|
|
359
|
+
*
|
|
360
|
+
* p_to = out · p_from
|
|
361
|
+
*
|
|
362
|
+
* @param {ArrayLike<number>} out Destination 4×4 matrix (length 16).
|
|
363
|
+
* @param {ArrayLike<number>} from Source frame transform.
|
|
364
|
+
* @param {ArrayLike<number>} to Destination frame transform.
|
|
365
|
+
* @returns {ArrayLike<number>|null} `out`, or `null` if `to` is singular.
|
|
366
|
+
*/
|
|
367
|
+
function mat4Location(out, from, to) {
|
|
368
|
+
return mat4Invert(out, to) && mat4Mul(out, out, from);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Direction Transform ──────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Relative transform for directions (vectors).
|
|
375
|
+
*
|
|
376
|
+
* Uses only the upper-left 3×3 blocks, ignoring translation.
|
|
377
|
+
*
|
|
378
|
+
* Corresponds to:
|
|
379
|
+
*
|
|
380
|
+
* out = to₃ · inv(from₃)
|
|
381
|
+
*
|
|
382
|
+
* and maps directions as:
|
|
383
|
+
*
|
|
384
|
+
* d_to = out · d_from
|
|
385
|
+
*
|
|
386
|
+
* Note: the final write is transposed so the result matches this module's
|
|
387
|
+
* matrix layout and multiplication convention.
|
|
388
|
+
*
|
|
389
|
+
* @param {ArrayLike<number>} out Destination 3×3 matrix (length 9).
|
|
390
|
+
* @param {ArrayLike<number>} from Source frame transform.
|
|
391
|
+
* @param {ArrayLike<number>} to Destination frame transform.
|
|
392
|
+
* @returns {ArrayLike<number>|null} `out`, or `null` if `from` is singular.
|
|
393
|
+
*/
|
|
394
|
+
function mat3Direction(out, from, to) {
|
|
395
|
+
const a00=from[0], a01=from[1], a02=from[2],
|
|
396
|
+
a10=from[4], a11=from[5], a12=from[6],
|
|
397
|
+
a20=from[8], a21=from[9], a22=from[10];
|
|
398
|
+
const b01=a22*a11-a12*a21,
|
|
399
|
+
b11=a12*a20-a22*a10,
|
|
400
|
+
b21=a21*a10-a11*a20;
|
|
401
|
+
let det=a00*b01+a01*b11+a02*b21;
|
|
402
|
+
if (Math.abs(det) < 1e-12) return null;
|
|
403
|
+
det=1/det;
|
|
404
|
+
const i00=b01*det;
|
|
405
|
+
const i01=(a02*a21-a22*a01)*det;
|
|
406
|
+
const i02=(a12*a01-a02*a11)*det;
|
|
407
|
+
const i10=b11*det;
|
|
408
|
+
const i11=(a22*a00-a02*a20)*det;
|
|
409
|
+
const i12=(a02*a10-a12*a00)*det;
|
|
410
|
+
const i20=b21*det;
|
|
411
|
+
const i21=(a01*a20-a21*a00)*det;
|
|
412
|
+
const i22=(a11*a00-a01*a10)*det;
|
|
413
|
+
const t00=to[0], t01=to[1], t02=to[2],
|
|
414
|
+
t10=to[4], t11=to[5], t12=to[6],
|
|
415
|
+
t20=to[8], t21=to[9], t22=to[10];
|
|
416
|
+
const m00=t00*i00+t10*i01+t20*i02;
|
|
417
|
+
const m01=t01*i00+t11*i01+t21*i02;
|
|
418
|
+
const m02=t02*i00+t12*i01+t22*i02;
|
|
419
|
+
const m10=t00*i10+t10*i11+t20*i12;
|
|
420
|
+
const m11=t01*i10+t11*i11+t21*i12;
|
|
421
|
+
const m12=t02*i10+t12*i11+t22*i12;
|
|
422
|
+
const m20=t00*i20+t10*i21+t20*i22;
|
|
423
|
+
const m21=t01*i20+t11*i21+t21*i22;
|
|
424
|
+
const m22=t02*i20+t12*i21+t22*i22;
|
|
425
|
+
out[0]=m00; out[1]=m10; out[2]=m20;
|
|
426
|
+
out[3]=m01; out[4]=m11; out[5]=m21;
|
|
427
|
+
out[6]=m02; out[7]=m12; out[8]=m22;
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Location leaf helpers ────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
function _worldToScreen(out, px, py, pz, pv, vp, ndcZMin) {
|
|
434
|
+
const x = pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
|
|
435
|
+
const y = pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
436
|
+
const z = pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
437
|
+
const w = pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
438
|
+
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
439
|
+
const nx=x/w, ny=y/w, nz=z/w;
|
|
440
|
+
const ndcZRange = 1 - ndcZMin;
|
|
441
|
+
out[0] = (nx*0.5+0.5)*vp[2]+vp[0];
|
|
442
|
+
out[1] = (ny*0.5+0.5)*vp[3]+vp[1];
|
|
443
|
+
out[2] = (nz - ndcZMin) / ndcZRange;
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function _screenToWorld(out, px, py, pz, ipv, vp, ndcZMin) {
|
|
448
|
+
const sx=(px-vp[0])/vp[2], sy=(py-vp[1])/vp[3];
|
|
449
|
+
const nx=sx*2-1, ny=sy*2-1;
|
|
450
|
+
const ndcZRange = 1 - ndcZMin;
|
|
451
|
+
const nz = pz * ndcZRange + ndcZMin;
|
|
452
|
+
const x=ipv[0]*nx+ipv[4]*ny+ipv[8]*nz+ipv[12];
|
|
453
|
+
const y=ipv[1]*nx+ipv[5]*ny+ipv[9]*nz+ipv[13];
|
|
454
|
+
const z=ipv[2]*nx+ipv[6]*ny+ipv[10]*nz+ipv[14];
|
|
455
|
+
const w=ipv[3]*nx+ipv[7]*ny+ipv[11]*nz+ipv[15];
|
|
456
|
+
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
457
|
+
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function _worldToNDC(out, px, py, pz, pv) {
|
|
462
|
+
const x=pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
|
|
463
|
+
const y=pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
464
|
+
const z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
465
|
+
const w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
466
|
+
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
467
|
+
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function _ndcToWorld(out, px, py, pz, ipv) {
|
|
472
|
+
const x=ipv[0]*px+ipv[4]*py+ipv[8]*pz+ipv[12];
|
|
473
|
+
const y=ipv[1]*px+ipv[5]*py+ipv[9]*pz+ipv[13];
|
|
474
|
+
const z=ipv[2]*px+ipv[6]*py+ipv[10]*pz+ipv[14];
|
|
475
|
+
const w=ipv[3]*px+ipv[7]*py+ipv[11]*pz+ipv[15];
|
|
476
|
+
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
477
|
+
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function _screenToNDC(out, px, py, pz, vp, ndcZMin) {
|
|
482
|
+
const ndcZRange = 1 - ndcZMin;
|
|
483
|
+
out[0] = ((px-vp[0])/vp[2])*2-1;
|
|
484
|
+
out[1] = ((py-vp[1])/vp[3])*2-1;
|
|
485
|
+
out[2] = pz * ndcZRange + ndcZMin;
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function _ndcToScreen(out, px, py, pz, vp, ndcZMin) {
|
|
490
|
+
const ndcZRange = 1 - ndcZMin;
|
|
491
|
+
out[0] = (px*0.5+0.5)*vp[2]+vp[0];
|
|
492
|
+
out[1] = (py*0.5+0.5)*vp[3]+vp[1];
|
|
493
|
+
out[2] = (pz - ndcZMin) / ndcZRange;
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── Inline PV and IPV helpers (stack-local, for paths that need them) ────
|
|
498
|
+
|
|
499
|
+
function _ensurePV(m) {
|
|
500
|
+
if (m.pv) return m.pv;
|
|
501
|
+
// Inline P · V (standard GL: clip = P · V · world_point)
|
|
502
|
+
const p = m.proj, v = m.view;
|
|
503
|
+
return [
|
|
504
|
+
p[0]*v[0]+p[4]*v[1]+p[8]*v[2]+p[12]*v[3],
|
|
505
|
+
p[1]*v[0]+p[5]*v[1]+p[9]*v[2]+p[13]*v[3],
|
|
506
|
+
p[2]*v[0]+p[6]*v[1]+p[10]*v[2]+p[14]*v[3],
|
|
507
|
+
p[3]*v[0]+p[7]*v[1]+p[11]*v[2]+p[15]*v[3],
|
|
508
|
+
p[0]*v[4]+p[4]*v[5]+p[8]*v[6]+p[12]*v[7],
|
|
509
|
+
p[1]*v[4]+p[5]*v[5]+p[9]*v[6]+p[13]*v[7],
|
|
510
|
+
p[2]*v[4]+p[6]*v[5]+p[10]*v[6]+p[14]*v[7],
|
|
511
|
+
p[3]*v[4]+p[7]*v[5]+p[11]*v[6]+p[15]*v[7],
|
|
512
|
+
p[0]*v[8]+p[4]*v[9]+p[8]*v[10]+p[12]*v[11],
|
|
513
|
+
p[1]*v[8]+p[5]*v[9]+p[9]*v[10]+p[13]*v[11],
|
|
514
|
+
p[2]*v[8]+p[6]*v[9]+p[10]*v[10]+p[14]*v[11],
|
|
515
|
+
p[3]*v[8]+p[7]*v[9]+p[11]*v[10]+p[15]*v[11],
|
|
516
|
+
p[0]*v[12]+p[4]*v[13]+p[8]*v[14]+p[12]*v[15],
|
|
517
|
+
p[1]*v[12]+p[5]*v[13]+p[9]*v[14]+p[13]*v[15],
|
|
518
|
+
p[2]*v[12]+p[6]*v[13]+p[10]*v[14]+p[14]*v[15],
|
|
519
|
+
p[3]*v[12]+p[7]*v[13]+p[11]*v[14]+p[15]*v[15],
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Map a point between coordinate spaces.
|
|
525
|
+
*
|
|
526
|
+
* @param {Vec3} out Result written here.
|
|
527
|
+
* @param {number} px,py,pz Input point.
|
|
528
|
+
* @param {string} from Source space.
|
|
529
|
+
* @param {string} to Target space.
|
|
530
|
+
* @param {object} m Matrices bag { proj, view, eye?, pv?, ipv?, model?,
|
|
531
|
+
* fromFrame?, toFrameInv? }.
|
|
532
|
+
* @param {Vec4} vp Viewport [x, y, width, height].
|
|
533
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
|
|
534
|
+
*/
|
|
535
|
+
function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
|
|
536
|
+
// WORLD ↔ SCREEN
|
|
537
|
+
if (from === WORLD && to === SCREEN)
|
|
538
|
+
return _worldToScreen(out, px,py,pz, _ensurePV(m), vp, ndcZMin);
|
|
539
|
+
if (from === SCREEN && to === WORLD)
|
|
540
|
+
return _screenToWorld(out, px,py,pz, m.ipv, vp, ndcZMin);
|
|
541
|
+
|
|
542
|
+
// WORLD ↔ NDC
|
|
543
|
+
if (from === WORLD && to === NDC)
|
|
544
|
+
return _worldToNDC(out, px,py,pz, _ensurePV(m));
|
|
545
|
+
if (from === NDC && to === WORLD)
|
|
546
|
+
return _ndcToWorld(out, px,py,pz, m.ipv);
|
|
547
|
+
|
|
548
|
+
// SCREEN ↔ NDC
|
|
549
|
+
if (from === SCREEN && to === NDC)
|
|
550
|
+
return _screenToNDC(out, px,py,pz, vp, ndcZMin);
|
|
551
|
+
if (from === NDC && to === SCREEN)
|
|
552
|
+
return _ndcToScreen(out, px,py,pz, vp, ndcZMin);
|
|
553
|
+
|
|
554
|
+
// WORLD ↔ EYE
|
|
555
|
+
if (from === WORLD && to === EYE)
|
|
556
|
+
return mat4MulPoint(out, m.view, px,py,pz);
|
|
557
|
+
if (from === EYE && to === WORLD)
|
|
558
|
+
return mat4MulPoint(out, m.eye, px,py,pz);
|
|
559
|
+
|
|
560
|
+
// EYE ↔ SCREEN (inline: eye→world→screen / screen→world→eye)
|
|
561
|
+
if (from === EYE && to === SCREEN) {
|
|
562
|
+
const ex=m.eye[0]*px+m.eye[4]*py+m.eye[8]*pz+m.eye[12],
|
|
563
|
+
ey=m.eye[1]*px+m.eye[5]*py+m.eye[9]*pz+m.eye[13],
|
|
564
|
+
ez=m.eye[2]*px+m.eye[6]*py+m.eye[10]*pz+m.eye[14];
|
|
565
|
+
return _worldToScreen(out, ex,ey,ez, _ensurePV(m), vp, ndcZMin);
|
|
566
|
+
}
|
|
567
|
+
if (from === SCREEN && to === EYE) {
|
|
568
|
+
_screenToWorld(out, px,py,pz, m.ipv, vp, ndcZMin);
|
|
569
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
570
|
+
return mat4MulPoint(out, m.view, wx,wy,wz);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// EYE ↔ NDC (inline: eye→world→ndc / ndc→world→eye)
|
|
574
|
+
if (from === EYE && to === NDC) {
|
|
575
|
+
const ex=m.eye[0]*px+m.eye[4]*py+m.eye[8]*pz+m.eye[12],
|
|
576
|
+
ey=m.eye[1]*px+m.eye[5]*py+m.eye[9]*pz+m.eye[13],
|
|
577
|
+
ez=m.eye[2]*px+m.eye[6]*py+m.eye[10]*pz+m.eye[14];
|
|
578
|
+
return _worldToNDC(out, ex,ey,ez, _ensurePV(m));
|
|
579
|
+
}
|
|
580
|
+
if (from === NDC && to === EYE) {
|
|
581
|
+
_ndcToWorld(out, px,py,pz, m.ipv);
|
|
582
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
583
|
+
return mat4MulPoint(out, m.view, wx,wy,wz);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// MATRIX (custom frame) ↔ WORLD
|
|
587
|
+
if (from === MATRIX && to === WORLD)
|
|
588
|
+
return mat4MulPoint(out, m.fromFrame, px,py,pz);
|
|
589
|
+
if (from === WORLD && to === MATRIX)
|
|
590
|
+
return mat4MulPoint(out, m.toFrameInv, px,py,pz);
|
|
591
|
+
|
|
592
|
+
// MATRIX ↔ EYE
|
|
593
|
+
if (from === MATRIX && to === EYE) {
|
|
594
|
+
const fx=m.fromFrame[0]*px+m.fromFrame[4]*py+m.fromFrame[8]*pz+m.fromFrame[12],
|
|
595
|
+
fy=m.fromFrame[1]*px+m.fromFrame[5]*py+m.fromFrame[9]*pz+m.fromFrame[13],
|
|
596
|
+
fz=m.fromFrame[2]*px+m.fromFrame[6]*py+m.fromFrame[10]*pz+m.fromFrame[14];
|
|
597
|
+
return mat4MulPoint(out, m.view, fx,fy,fz);
|
|
598
|
+
}
|
|
599
|
+
if (from === EYE && to === MATRIX) {
|
|
600
|
+
const ex=m.eye[0]*px+m.eye[4]*py+m.eye[8]*pz+m.eye[12],
|
|
601
|
+
ey=m.eye[1]*px+m.eye[5]*py+m.eye[9]*pz+m.eye[13],
|
|
602
|
+
ez=m.eye[2]*px+m.eye[6]*py+m.eye[10]*pz+m.eye[14];
|
|
603
|
+
return mat4MulPoint(out, m.toFrameInv, ex,ey,ez);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// MATRIX ↔ SCREEN
|
|
607
|
+
if (from === MATRIX && to === SCREEN) {
|
|
608
|
+
const fx=m.fromFrame[0]*px+m.fromFrame[4]*py+m.fromFrame[8]*pz+m.fromFrame[12],
|
|
609
|
+
fy=m.fromFrame[1]*px+m.fromFrame[5]*py+m.fromFrame[9]*pz+m.fromFrame[13],
|
|
610
|
+
fz=m.fromFrame[2]*px+m.fromFrame[6]*py+m.fromFrame[10]*pz+m.fromFrame[14];
|
|
611
|
+
return _worldToScreen(out, fx,fy,fz, _ensurePV(m), vp, ndcZMin);
|
|
612
|
+
}
|
|
613
|
+
if (from === SCREEN && to === MATRIX) {
|
|
614
|
+
_screenToWorld(out, px,py,pz, m.ipv, vp, ndcZMin);
|
|
615
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
616
|
+
return mat4MulPoint(out, m.toFrameInv, wx,wy,wz);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// MATRIX ↔ NDC
|
|
620
|
+
if (from === MATRIX && to === NDC) {
|
|
621
|
+
const fx=m.fromFrame[0]*px+m.fromFrame[4]*py+m.fromFrame[8]*pz+m.fromFrame[12],
|
|
622
|
+
fy=m.fromFrame[1]*px+m.fromFrame[5]*py+m.fromFrame[9]*pz+m.fromFrame[13],
|
|
623
|
+
fz=m.fromFrame[2]*px+m.fromFrame[6]*py+m.fromFrame[10]*pz+m.fromFrame[14];
|
|
624
|
+
return _worldToNDC(out, fx,fy,fz, _ensurePV(m));
|
|
625
|
+
}
|
|
626
|
+
if (from === NDC && to === MATRIX) {
|
|
627
|
+
_ndcToWorld(out, px,py,pz, m.ipv);
|
|
628
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
629
|
+
return mat4MulPoint(out, m.toFrameInv, wx,wy,wz);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// MATRIX ↔ MATRIX
|
|
633
|
+
if (from === MATRIX && to === MATRIX) {
|
|
634
|
+
const fx=m.fromFrame[0]*px+m.fromFrame[4]*py+m.fromFrame[8]*pz+m.fromFrame[12],
|
|
635
|
+
fy=m.fromFrame[1]*px+m.fromFrame[5]*py+m.fromFrame[9]*pz+m.fromFrame[13],
|
|
636
|
+
fz=m.fromFrame[2]*px+m.fromFrame[6]*py+m.fromFrame[10]*pz+m.fromFrame[14];
|
|
637
|
+
return mat4MulPoint(out, m.toFrameInv, fx,fy,fz);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Fallback
|
|
641
|
+
out[0]=px; out[1]=py; out[2]=pz;
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Direction helpers ────────────────────────────────────────────────────
|
|
646
|
+
|
|
647
|
+
/** Apply the 3×3 linear part of a mat4 (rotation/scale, no translation) */
|
|
648
|
+
function _applyDir(out, mat, dx, dy, dz) {
|
|
649
|
+
out[0]=mat[0]*dx+mat[4]*dy+mat[8]*dz;
|
|
650
|
+
out[1]=mat[1]*dx+mat[5]*dy+mat[9]*dz;
|
|
651
|
+
out[2]=mat[2]*dx+mat[6]*dy+mat[10]*dz;
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* World→Screen direction. Self-contained: reads proj scalars + view mat.
|
|
657
|
+
* The existing p5.tree code nested _direction and _location calls here;
|
|
658
|
+
* this version inlines all math with stack locals.
|
|
659
|
+
*/
|
|
660
|
+
function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
|
|
661
|
+
// 1. World → Eye direction: R · d (standard column-major mat × vec)
|
|
662
|
+
const edx = view[0]*dx + view[4]*dy + view[8]*dz;
|
|
663
|
+
const edy = view[1]*dx + view[5]*dy + view[9]*dz;
|
|
664
|
+
const edz = view[2]*dx + view[6]*dy + view[10]*dz;
|
|
665
|
+
|
|
666
|
+
const isPersp = proj[15] === 0;
|
|
667
|
+
let sdx = edx, sdy = edy;
|
|
668
|
+
|
|
669
|
+
if (isPersp) {
|
|
670
|
+
// Camera-eye Z of world origin (inline WORLD→EYE for [0,0,0]):
|
|
671
|
+
// view * [0,0,0,1] = column 3 of view
|
|
672
|
+
const zEye = view[8]*0 + view[9]*0 + view[10]*0 + view[14]; // = view[14]
|
|
673
|
+
const halfTan = Math.tan(projFov(proj) / 2);
|
|
674
|
+
const k = Math.abs(zEye * halfTan);
|
|
675
|
+
const pixPerUnit = vpH / (2 * k);
|
|
676
|
+
sdx *= pixPerUnit;
|
|
677
|
+
sdy *= pixPerUnit;
|
|
678
|
+
} else {
|
|
679
|
+
// Ortho: pixels per world unit along X/Y
|
|
680
|
+
const orthoW = Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin));
|
|
681
|
+
sdx *= vpW / orthoW;
|
|
682
|
+
sdy *= vpH / Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Z: map eye-space dz to screen-space dz
|
|
686
|
+
const near = projNear(proj, ndcZMin), far = projFar(proj);
|
|
687
|
+
const depthRange = near - far;
|
|
688
|
+
let sdz;
|
|
689
|
+
if (isPersp) {
|
|
690
|
+
sdz = edz / (depthRange / Math.tan(projFov(proj) / 2));
|
|
691
|
+
} else {
|
|
692
|
+
sdz = edz / (depthRange / (Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin)) / vpW));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
out[0] = sdx; out[1] = sdy; out[2] = sdz;
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function _screenToWorldDir(out, dx, dy, dz, proj, view, eye, vpW, vpH, ndcZMin) {
|
|
700
|
+
const isPersp = proj[15] === 0;
|
|
701
|
+
let edx = dx, edy = dy;
|
|
702
|
+
|
|
703
|
+
if (isPersp) {
|
|
704
|
+
const zEye = view[14];
|
|
705
|
+
const halfTan = Math.tan(projFov(proj) / 2);
|
|
706
|
+
const k = Math.abs(zEye * halfTan);
|
|
707
|
+
edx *= 2 * k / vpH;
|
|
708
|
+
edy *= 2 * k / vpH;
|
|
709
|
+
} else {
|
|
710
|
+
const orthoW = Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin));
|
|
711
|
+
edx *= orthoW / vpW;
|
|
712
|
+
edy *= Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin)) / vpH;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const near = projNear(proj, ndcZMin), far = projFar(proj);
|
|
716
|
+
const depthRange = near - far;
|
|
717
|
+
let edz;
|
|
718
|
+
if (isPersp) {
|
|
719
|
+
edz = dz * (depthRange / Math.tan(projFov(proj) / 2));
|
|
720
|
+
} else {
|
|
721
|
+
edz = dz * (depthRange / (Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin)) / vpW));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Eye → World direction (dMatrix = upper-left 3×3 of eye = inv(view))
|
|
725
|
+
_applyDir(out, eye, edx, edy, edz);
|
|
726
|
+
return out;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function _screenToNDCDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
|
|
730
|
+
const ndcZRange = 1 - ndcZMin;
|
|
731
|
+
out[0] = 2 * dx / vpW;
|
|
732
|
+
out[1] = 2 * dy / vpH;
|
|
733
|
+
out[2] = dz * ndcZRange;
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function _ndcToScreenDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
|
|
738
|
+
const ndcZRange = 1 - ndcZMin;
|
|
739
|
+
out[0] = vpW * dx / 2;
|
|
740
|
+
out[1] = vpH * dy / 2;
|
|
741
|
+
out[2] = dz / ndcZRange;
|
|
742
|
+
return out;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Map a direction between coordinate spaces.
|
|
747
|
+
* Same flat-dispatch as mapLocation.
|
|
748
|
+
*/
|
|
749
|
+
function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
750
|
+
const vpW = Math.abs(vp[2]), vpH = Math.abs(vp[3]);
|
|
751
|
+
|
|
752
|
+
// EYE ↔ WORLD (most common: dMatrix operation)
|
|
753
|
+
if (from === EYE && to === WORLD) return _applyDir(out, m.eye, dx, dy, dz);
|
|
754
|
+
if (from === WORLD && to === EYE) return _applyDir(out, m.view, dx, dy, dz);
|
|
755
|
+
|
|
756
|
+
// WORLD ↔ SCREEN
|
|
757
|
+
if (from === WORLD && to === SCREEN)
|
|
758
|
+
return _worldToScreenDir(out, dx,dy,dz, m.proj, m.view, vpW, vpH, ndcZMin);
|
|
759
|
+
if (from === SCREEN && to === WORLD)
|
|
760
|
+
return _screenToWorldDir(out, dx,dy,dz, m.proj, m.view, m.eye, vpW, vpH, ndcZMin);
|
|
761
|
+
|
|
762
|
+
// SCREEN ↔ NDC
|
|
763
|
+
if (from === SCREEN && to === NDC)
|
|
764
|
+
return _screenToNDCDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
765
|
+
if (from === NDC && to === SCREEN)
|
|
766
|
+
return _ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
767
|
+
|
|
768
|
+
// WORLD ↔ NDC (chain: world→screen→ndc / ndc→screen→world)
|
|
769
|
+
if (from === WORLD && to === NDC) {
|
|
770
|
+
_worldToScreenDir(out, dx,dy,dz, m.proj, m.view, vpW, vpH, ndcZMin);
|
|
771
|
+
const sx=out[0],sy=out[1],sz=out[2];
|
|
772
|
+
return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
|
|
773
|
+
}
|
|
774
|
+
if (from === NDC && to === WORLD) {
|
|
775
|
+
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
776
|
+
const sx=out[0],sy=out[1],sz=out[2];
|
|
777
|
+
return _screenToWorldDir(out, sx,sy,sz, m.proj, m.view, m.eye, vpW, vpH, ndcZMin);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// EYE ↔ SCREEN
|
|
781
|
+
if (from === EYE && to === SCREEN) {
|
|
782
|
+
// eye→world→screen
|
|
783
|
+
_applyDir(out, m.eye, dx,dy,dz);
|
|
784
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
785
|
+
return _worldToScreenDir(out, wx,wy,wz, m.proj, m.view, vpW, vpH, ndcZMin);
|
|
786
|
+
}
|
|
787
|
+
if (from === SCREEN && to === EYE) {
|
|
788
|
+
_screenToWorldDir(out, dx,dy,dz, m.proj, m.view, m.eye, vpW, vpH, ndcZMin);
|
|
789
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
790
|
+
return _applyDir(out, m.view, wx,wy,wz);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// EYE ↔ NDC
|
|
794
|
+
if (from === EYE && to === NDC) {
|
|
795
|
+
_applyDir(out, m.eye, dx,dy,dz);
|
|
796
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
797
|
+
_worldToScreenDir(out, wx,wy,wz, m.proj, m.view, vpW, vpH, ndcZMin);
|
|
798
|
+
const sx=out[0],sy=out[1],sz=out[2];
|
|
799
|
+
return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
|
|
800
|
+
}
|
|
801
|
+
if (from === NDC && to === EYE) {
|
|
802
|
+
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
803
|
+
const sx=out[0],sy=out[1],sz=out[2];
|
|
804
|
+
_screenToWorldDir(out, sx,sy,sz, m.proj, m.view, m.eye, vpW, vpH, ndcZMin);
|
|
805
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
806
|
+
return _applyDir(out, m.view, wx,wy,wz);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// MATRIX ↔ WORLD
|
|
810
|
+
if (from === MATRIX && to === WORLD) return _applyDir(out, m.fromFrame, dx,dy,dz);
|
|
811
|
+
if (from === WORLD && to === MATRIX) return _applyDir(out, m.toFrameInv, dx,dy,dz);
|
|
812
|
+
|
|
813
|
+
// MATRIX ↔ EYE
|
|
814
|
+
if (from === MATRIX && to === EYE) {
|
|
815
|
+
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
816
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
817
|
+
return _applyDir(out, m.view, wx,wy,wz);
|
|
818
|
+
}
|
|
819
|
+
if (from === EYE && to === MATRIX) {
|
|
820
|
+
_applyDir(out, m.eye, dx,dy,dz);
|
|
821
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
822
|
+
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// MATRIX ↔ SCREEN
|
|
826
|
+
if (from === MATRIX && to === SCREEN) {
|
|
827
|
+
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
828
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
829
|
+
return _worldToScreenDir(out, wx,wy,wz, m.proj, m.view, vpW, vpH, ndcZMin);
|
|
830
|
+
}
|
|
831
|
+
if (from === SCREEN && to === MATRIX) {
|
|
832
|
+
_screenToWorldDir(out, dx,dy,dz, m.proj, m.view, m.eye, vpW, vpH, ndcZMin);
|
|
833
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
834
|
+
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// MATRIX ↔ NDC
|
|
838
|
+
if (from === MATRIX && to === NDC) {
|
|
839
|
+
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
840
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
841
|
+
_worldToScreenDir(out, wx,wy,wz, m.proj, m.view, vpW, vpH, ndcZMin);
|
|
842
|
+
const sx=out[0],sy=out[1],sz=out[2];
|
|
843
|
+
return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
|
|
844
|
+
}
|
|
845
|
+
if (from === NDC && to === MATRIX) {
|
|
846
|
+
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
847
|
+
const sx=out[0],sy=out[1],sz=out[2];
|
|
848
|
+
_screenToWorldDir(out, sx,sy,sz, m.proj, m.view, m.eye, vpW, vpH, ndcZMin);
|
|
849
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
850
|
+
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// MATRIX ↔ MATRIX
|
|
854
|
+
if (from === MATRIX && to === MATRIX) {
|
|
855
|
+
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
856
|
+
const wx=out[0],wy=out[1],wz=out[2];
|
|
857
|
+
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Fallback
|
|
861
|
+
out[0]=dx; out[1]=dy; out[2]=dz;
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
866
|
+
// pixelRatio
|
|
867
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* World-units-per-pixel at a given eye-space Z depth.
|
|
871
|
+
* @param {ArrayLike<number>} proj Projection Mat4.
|
|
872
|
+
* @param {number} vpH Viewport height (pixels).
|
|
873
|
+
* @param {number} eyeZ Eye-space Z of the point (negative for in-front-of camera).
|
|
874
|
+
* @param {number} ndcZMin WEBGL or WEBGPU.
|
|
875
|
+
*/
|
|
876
|
+
function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
|
|
877
|
+
if (projIsOrtho(proj)) {
|
|
878
|
+
return Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin)) / vpH;
|
|
879
|
+
}
|
|
880
|
+
return 2 * Math.abs(eyeZ) * Math.tan(projFov(proj) / 2) / vpH;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* @file Pure quaternion/spline math + PoseTrack state machine.
|
|
885
|
+
* @module tree/track
|
|
886
|
+
* @license GPL-3.0-only
|
|
887
|
+
*
|
|
888
|
+
* Zero dependencies. No p5, DOM, WebGL, or WebGPU usage.
|
|
889
|
+
* All quaternion operations use flat [x,y,z,w] arrays (w-last = glTF layout).
|
|
890
|
+
* PoseTrack is a pure state machine that stores {pos,rot,scl} keyframes
|
|
891
|
+
* and advances a cursor via tick().
|
|
892
|
+
*
|
|
893
|
+
* ── Exports ──────────────────────────────────────────────────────────────────
|
|
894
|
+
* Quaternion helpers
|
|
895
|
+
* qSet qCopy qDot qNormalize qNegate qMul qSlerp
|
|
896
|
+
* qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
897
|
+
* quatToAxisAngle
|
|
898
|
+
* Spline / vector helpers
|
|
899
|
+
* catmullRomVec3 lerpVec3
|
|
900
|
+
* Transform / mat4 helpers
|
|
901
|
+
* transformToMat4 mat4ToTransform
|
|
902
|
+
* Track
|
|
903
|
+
* PoseTrack
|
|
904
|
+
*
|
|
905
|
+
* ── Hook architecture ─────────────────────────────────────────────────────────
|
|
906
|
+
* _onActivate / _onDeactivate — lib-space (underscore, set by host layer)
|
|
907
|
+
* Fire exactly on playing transitions: false→true / true→false.
|
|
908
|
+
* Used by the addon to register/unregister from the draw-loop tick set.
|
|
909
|
+
*
|
|
910
|
+
* onPlay / onEnd — user-space (public, set by user)
|
|
911
|
+
* onPlay : fires in play() when playback actually starts (transition).
|
|
912
|
+
* onEnd : fires in tick() when cursor reaches a natural boundary.
|
|
913
|
+
* onEnd does NOT fire on explicit stop() / reset().
|
|
914
|
+
*
|
|
915
|
+
* Firing order:
|
|
916
|
+
* play() → user onPlay → lib _onActivate
|
|
917
|
+
* tick() → user onEnd → lib _onDeactivate
|
|
918
|
+
* stop() → lib _onDeactivate (no user hook)
|
|
919
|
+
* reset() → lib _onDeactivate (no user hook)
|
|
920
|
+
*
|
|
921
|
+
* ── Playback semantics (rate) ─────────────────────────────────────────────────
|
|
922
|
+
* rate > 0 forward playback
|
|
923
|
+
* rate < 0 backward playback
|
|
924
|
+
* rate === 0 frozen: tick() is a no-op; the playing flag is NOT changed.
|
|
925
|
+
*
|
|
926
|
+
* play() is the sole method that sets playing = true.
|
|
927
|
+
* stop() is the sole method that sets playing = false.
|
|
928
|
+
* Assigning rate never implicitly starts or stops playback.
|
|
929
|
+
*
|
|
930
|
+
* ── One-keyframe behaviour ────────────────────────────────────────────────────
|
|
931
|
+
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
932
|
+
* setting playing = true and without animating.
|
|
933
|
+
*/
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
// =========================================================================
|
|
937
|
+
// S1 Quaternion helpers (flat [x, y, z, w], w-last)
|
|
938
|
+
// =========================================================================
|
|
939
|
+
|
|
940
|
+
/** Set all four components. @returns {number[]} out */
|
|
941
|
+
const qSet = (out, x, y, z, w) => {
|
|
942
|
+
out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out;
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
/** Copy quaternion a into out. @returns {number[]} out */
|
|
946
|
+
const qCopy = (out, a) => {
|
|
947
|
+
out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out;
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
/** Dot product of two quaternions. */
|
|
951
|
+
const qDot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
952
|
+
|
|
953
|
+
/** Normalise in-place. @returns {number[]} out */
|
|
954
|
+
const qNormalize = (out) => {
|
|
955
|
+
const len = Math.sqrt(qDot(out, out)) || 1;
|
|
956
|
+
out[0] /= len; out[1] /= len; out[2] /= len; out[3] /= len;
|
|
957
|
+
return out;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
/** Negate all components in-place. @returns {number[]} out */
|
|
961
|
+
const qNegate = (out) => {
|
|
962
|
+
out[0] = -out[0]; out[1] = -out[1]; out[2] = -out[2]; out[3] = -out[3];
|
|
963
|
+
return out;
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
/** out = a * b (Hamilton product). @returns {number[]} out */
|
|
967
|
+
const qMul = (out, a, b) => {
|
|
968
|
+
const ax = a[0], ay = a[1], az = a[2], aw = a[3];
|
|
969
|
+
const bx = b[0], by = b[1], bz = b[2], bw = b[3];
|
|
970
|
+
out[0] = aw*bx + ax*bw + ay*bz - az*by;
|
|
971
|
+
out[1] = aw*by - ax*bz + ay*bw + az*bx;
|
|
972
|
+
out[2] = aw*bz + ax*by - ay*bx + az*bw;
|
|
973
|
+
out[3] = aw*bw - ax*bx - ay*by - az*bz;
|
|
974
|
+
return out;
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* SLERP between quaternions a and b at parameter t.
|
|
979
|
+
* Shortest-arc: negates b when dot < 0.
|
|
980
|
+
* Near-equal fallback: nlerp when dot ~= 1.
|
|
981
|
+
* @param {number[]} out 4-element result array.
|
|
982
|
+
* @param {number[]} a Start quaternion [x,y,z,w].
|
|
983
|
+
* @param {number[]} b End quaternion [x,y,z,w].
|
|
984
|
+
* @param {number} t Blend [0, 1].
|
|
985
|
+
* @returns {number[]} out
|
|
986
|
+
*/
|
|
987
|
+
const qSlerp = (out, a, b, t) => {
|
|
988
|
+
let d = qDot(a, b);
|
|
989
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3];
|
|
990
|
+
if (d < 0) { d = -d; bx = -bx; by = -by; bz = -bz; bw = -bw; }
|
|
991
|
+
if (d > 0.9995) {
|
|
992
|
+
out[0] = a[0] + t*(bx - a[0]);
|
|
993
|
+
out[1] = a[1] + t*(by - a[1]);
|
|
994
|
+
out[2] = a[2] + t*(bz - a[2]);
|
|
995
|
+
out[3] = a[3] + t*(bw - a[3]);
|
|
996
|
+
return qNormalize(out);
|
|
997
|
+
}
|
|
998
|
+
const theta = Math.acos(d), sinT = Math.sin(theta);
|
|
999
|
+
const s0 = Math.sin((1 - t) * theta) / sinT;
|
|
1000
|
+
const s1 = Math.sin(t * theta) / sinT;
|
|
1001
|
+
out[0] = s0*a[0] + s1*bx;
|
|
1002
|
+
out[1] = s0*a[1] + s1*by;
|
|
1003
|
+
out[2] = s0*a[2] + s1*bz;
|
|
1004
|
+
out[3] = s0*a[3] + s1*bw;
|
|
1005
|
+
return out;
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Build a quaternion from an axis-angle rotation.
|
|
1010
|
+
* The axis need not be normalised.
|
|
1011
|
+
* @returns {number[]} out
|
|
1012
|
+
*/
|
|
1013
|
+
const qFromAxisAngle = (out, ax, ay, az, angle) => {
|
|
1014
|
+
const half = angle * 0.5;
|
|
1015
|
+
const s = Math.sin(half);
|
|
1016
|
+
const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
|
|
1017
|
+
out[0] = s * ax / len;
|
|
1018
|
+
out[1] = s * ay / len;
|
|
1019
|
+
out[2] = s * az / len;
|
|
1020
|
+
out[3] = Math.cos(half);
|
|
1021
|
+
return out;
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Build a quaternion from a look direction (negative-Z forward convention)
|
|
1026
|
+
* and an optional up vector (defaults to +Y).
|
|
1027
|
+
* @param {number[]} out
|
|
1028
|
+
* @param {number[]} dir Forward direction [x,y,z].
|
|
1029
|
+
* @param {number[]} [up] Up vector [x,y,z].
|
|
1030
|
+
* @returns {number[]} out
|
|
1031
|
+
*/
|
|
1032
|
+
const qFromLookDir = (out, dir, up) => {
|
|
1033
|
+
let fx = dir[0], fy = dir[1], fz = dir[2];
|
|
1034
|
+
const fLen = Math.sqrt(fx*fx + fy*fy + fz*fz) || 1;
|
|
1035
|
+
fx /= fLen; fy /= fLen; fz /= fLen;
|
|
1036
|
+
let ux = up ? up[0] : 0, uy = up ? up[1] : 1, uz = up ? up[2] : 0;
|
|
1037
|
+
let rx = uy*fz - uz*fy, ry = uz*fx - ux*fz, rz = ux*fy - uy*fx;
|
|
1038
|
+
const rLen = Math.sqrt(rx*rx + ry*ry + rz*rz) || 1;
|
|
1039
|
+
rx /= rLen; ry /= rLen; rz /= rLen;
|
|
1040
|
+
ux = fy*rz - fz*ry; uy = fz*rx - fx*rz; uz = fx*ry - fy*rx;
|
|
1041
|
+
return qFromRotMat3x3(out, rx, ry, rz, ux, uy, uz, -fx, -fy, -fz);
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Build a quaternion from a 3x3 rotation matrix supplied as 9 row-major scalars.
|
|
1046
|
+
* @returns {number[]} out (normalised)
|
|
1047
|
+
*/
|
|
1048
|
+
const qFromRotMat3x3 = (out, m00, m01, m02, m10, m11, m12, m20, m21, m22) => {
|
|
1049
|
+
const tr = m00 + m11 + m22;
|
|
1050
|
+
if (tr > 0) {
|
|
1051
|
+
const s = 0.5 / Math.sqrt(tr + 1);
|
|
1052
|
+
out[3] = 0.25 / s;
|
|
1053
|
+
out[0] = (m21 - m12) * s;
|
|
1054
|
+
out[1] = (m02 - m20) * s;
|
|
1055
|
+
out[2] = (m10 - m01) * s;
|
|
1056
|
+
} else if (m00 > m11 && m00 > m22) {
|
|
1057
|
+
const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
|
|
1058
|
+
out[3] = (m21 - m12) / s;
|
|
1059
|
+
out[0] = 0.25 * s;
|
|
1060
|
+
out[1] = (m01 + m10) / s;
|
|
1061
|
+
out[2] = (m02 + m20) / s;
|
|
1062
|
+
} else if (m11 > m22) {
|
|
1063
|
+
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
1064
|
+
out[3] = (m02 - m20) / s;
|
|
1065
|
+
out[0] = (m01 + m10) / s;
|
|
1066
|
+
out[1] = 0.25 * s;
|
|
1067
|
+
out[2] = (m12 + m21) / s;
|
|
1068
|
+
} else {
|
|
1069
|
+
const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
|
|
1070
|
+
out[3] = (m10 - m01) / s;
|
|
1071
|
+
out[0] = (m02 + m20) / s;
|
|
1072
|
+
out[1] = (m12 + m21) / s;
|
|
1073
|
+
out[2] = 0.25 * s;
|
|
1074
|
+
}
|
|
1075
|
+
return qNormalize(out);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Extract a unit quaternion from the upper-left 3x3 of a column-major mat4.
|
|
1080
|
+
* @param {number[]} out
|
|
1081
|
+
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1082
|
+
* @returns {number[]} out
|
|
1083
|
+
*/
|
|
1084
|
+
const qFromMat4 = (out, m) =>
|
|
1085
|
+
qFromRotMat3x3(out, m[0], m[4], m[8], m[1], m[5], m[9], m[2], m[6], m[10]);
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Write a quaternion into a column-major mat4 (rotation block only;
|
|
1089
|
+
* translation and perspective rows/cols are set to identity values).
|
|
1090
|
+
* @param {Float32Array|number[]} out 16-element array.
|
|
1091
|
+
* @param {number[]} q [x,y,z,w].
|
|
1092
|
+
* @returns {Float32Array|number[]} out
|
|
1093
|
+
*/
|
|
1094
|
+
const qToMat4 = (out, q) => {
|
|
1095
|
+
const x = q[0], y = q[1], z = q[2], w = q[3];
|
|
1096
|
+
const x2 = x+x, y2 = y+y, z2 = z+z;
|
|
1097
|
+
const xx = x*x2, xy = x*y2, xz = x*z2;
|
|
1098
|
+
const yy = y*y2, yz = y*z2, zz = z*z2;
|
|
1099
|
+
const wx = w*x2, wy = w*y2, wz = w*z2;
|
|
1100
|
+
out[0] = 1-(yy+zz); out[1] = xy+wz; out[2] = xz-wy; out[3] = 0;
|
|
1101
|
+
out[4] = xy-wz; out[5] = 1-(xx+zz); out[6] = yz+wx; out[7] = 0;
|
|
1102
|
+
out[8] = xz+wy; out[9] = yz-wx; out[10] = 1-(xx+yy); out[11] = 0;
|
|
1103
|
+
out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1;
|
|
1104
|
+
return out;
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Decompose a unit quaternion into { axis:[x,y,z], angle } (radians).
|
|
1109
|
+
* out is optional; a new object is returned if omitted.
|
|
1110
|
+
* @param {number[]} q [x,y,z,w].
|
|
1111
|
+
* @param {Object} [out]
|
|
1112
|
+
* @returns {{ axis: number[], angle: number }}
|
|
1113
|
+
*/
|
|
1114
|
+
const quatToAxisAngle = (q, out) => {
|
|
1115
|
+
out = out || {};
|
|
1116
|
+
const x = q[0], y = q[1], z = q[2], w = q[3];
|
|
1117
|
+
const sinHalf = Math.sqrt(x*x + y*y + z*z);
|
|
1118
|
+
if (sinHalf < 1e-8) { out.axis = [0, 1, 0]; out.angle = 0; return out; }
|
|
1119
|
+
out.angle = 2 * Math.atan2(sinHalf, w);
|
|
1120
|
+
out.axis = [x / sinHalf, y / sinHalf, z / sinHalf];
|
|
1121
|
+
return out;
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// =========================================================================
|
|
1125
|
+
// S2 Spline / vector helpers
|
|
1126
|
+
// =========================================================================
|
|
1127
|
+
|
|
1128
|
+
function _dist3(a, b) {
|
|
1129
|
+
const dx = a[0]-b[0], dy = a[1]-b[1], dz = a[2]-b[2];
|
|
1130
|
+
return Math.sqrt(dx*dx + dy*dy + dz*dz);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Centripetal Catmull-Rom interpolation (alpha = 0.5, Barry-Goldman).
|
|
1135
|
+
* out = interp(p0, p1, p2, p3, t) where t in [0,1] maps p1 -> p2.
|
|
1136
|
+
* When p0 === p1 or p2 === p3 (boundary), the chord is reused, giving
|
|
1137
|
+
* zero-tension clamped end tangents.
|
|
1138
|
+
* @param {number[]} out 3-element result.
|
|
1139
|
+
* @param {number[]} p0 Control point before p1.
|
|
1140
|
+
* @param {number[]} p1 Start of this segment.
|
|
1141
|
+
* @param {number[]} p2 End of this segment.
|
|
1142
|
+
* @param {number[]} p3 Control point after p2.
|
|
1143
|
+
* @param {number} t Blend [0, 1].
|
|
1144
|
+
* @returns {number[]} out
|
|
1145
|
+
*/
|
|
1146
|
+
const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
1147
|
+
const alpha = 0.5;
|
|
1148
|
+
const dt0 = Math.pow(_dist3(p0, p1), alpha) || 1;
|
|
1149
|
+
const dt1 = Math.pow(_dist3(p1, p2), alpha) || 1;
|
|
1150
|
+
const dt2 = Math.pow(_dist3(p2, p3), alpha) || 1;
|
|
1151
|
+
for (let i = 0; i < 3; i++) {
|
|
1152
|
+
const t1_0 = (p1[i]-p0[i])/dt0 - (p2[i]-p0[i])/(dt0+dt1) + (p2[i]-p1[i])/dt1;
|
|
1153
|
+
const t2_0 = (p2[i]-p1[i])/dt1 - (p3[i]-p1[i])/(dt1+dt2) + (p3[i]-p2[i])/dt2;
|
|
1154
|
+
const m1 = t1_0 * dt1;
|
|
1155
|
+
const m2 = t2_0 * dt1;
|
|
1156
|
+
const a = 2*p1[i] - 2*p2[i] + m1 + m2;
|
|
1157
|
+
const b = -3*p1[i] + 3*p2[i] - 2*m1 - m2;
|
|
1158
|
+
out[i] = a*t*t*t + b*t*t + m1*t + p1[i];
|
|
1159
|
+
}
|
|
1160
|
+
return out;
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Linear interpolation between two vec3s.
|
|
1165
|
+
* @param {number[]} out
|
|
1166
|
+
* @param {number[]} a
|
|
1167
|
+
* @param {number[]} b
|
|
1168
|
+
* @param {number} t Blend [0, 1].
|
|
1169
|
+
* @returns {number[]} out
|
|
1170
|
+
*/
|
|
1171
|
+
const lerpVec3 = (out, a, b, t) => {
|
|
1172
|
+
out[0] = a[0] + t*(b[0]-a[0]);
|
|
1173
|
+
out[1] = a[1] + t*(b[1]-a[1]);
|
|
1174
|
+
out[2] = a[2] + t*(b[2]-a[2]);
|
|
1175
|
+
return out;
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
// =========================================================================
|
|
1179
|
+
// S3 Transform <-> Mat4
|
|
1180
|
+
// =========================================================================
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Write a TRS transform into a column-major mat4.
|
|
1184
|
+
* Rotation is encoded as a quaternion; scale is baked into rotation columns.
|
|
1185
|
+
* @param {Float32Array|number[]} out 16-element column-major mat4.
|
|
1186
|
+
* @param {{ pos:number[], rot:number[], scl:number[] }} xform
|
|
1187
|
+
* @returns {Float32Array|number[]} out
|
|
1188
|
+
*/
|
|
1189
|
+
const transformToMat4 = (out, xform) => {
|
|
1190
|
+
qToMat4(out, xform.rot);
|
|
1191
|
+
const sx = xform.scl[0], sy = xform.scl[1], sz = xform.scl[2];
|
|
1192
|
+
out[0] *= sx; out[1] *= sx; out[2] *= sx;
|
|
1193
|
+
out[4] *= sy; out[5] *= sy; out[6] *= sy;
|
|
1194
|
+
out[8] *= sz; out[9] *= sz; out[10] *= sz;
|
|
1195
|
+
out[12] = xform.pos[0];
|
|
1196
|
+
out[13] = xform.pos[1];
|
|
1197
|
+
out[14] = xform.pos[2];
|
|
1198
|
+
return out;
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Decompose a column-major mat4 into a TRS transform.
|
|
1203
|
+
* Assumes no shear. Scale is extracted from column lengths.
|
|
1204
|
+
* @param {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1205
|
+
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1206
|
+
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1207
|
+
*/
|
|
1208
|
+
const mat4ToTransform = (out, m) => {
|
|
1209
|
+
out.pos[0] = m[12]; out.pos[1] = m[13]; out.pos[2] = m[14];
|
|
1210
|
+
const sx = Math.sqrt(m[0]*m[0] + m[1]*m[1] + m[2]*m[2]);
|
|
1211
|
+
const sy = Math.sqrt(m[4]*m[4] + m[5]*m[5] + m[6]*m[6]);
|
|
1212
|
+
const sz = Math.sqrt(m[8]*m[8] + m[9]*m[9] + m[10]*m[10]);
|
|
1213
|
+
out.scl[0] = sx; out.scl[1] = sy; out.scl[2] = sz;
|
|
1214
|
+
qFromRotMat3x3(out.rot,
|
|
1215
|
+
m[0]/sx, m[4]/sy, m[8]/sz,
|
|
1216
|
+
m[1]/sx, m[5]/sy, m[9]/sz,
|
|
1217
|
+
m[2]/sx, m[6]/sy, m[10]/sz);
|
|
1218
|
+
return out;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// =========================================================================
|
|
1222
|
+
// S4 Spec parser (keyframe input normalisation)
|
|
1223
|
+
// =========================================================================
|
|
1224
|
+
|
|
1225
|
+
const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
1226
|
+
const _clamp01 = (x) => x < 0 ? 0 : (x > 1 ? 1 : x);
|
|
1227
|
+
const _clampS = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
|
|
1228
|
+
|
|
1229
|
+
function _parseVec3(v) {
|
|
1230
|
+
if (!v) return null;
|
|
1231
|
+
if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0], v[1], v[2]];
|
|
1232
|
+
if (typeof v === 'object' && 'x' in v) return [v.x || 0, v.y || 0, v.z || 0];
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function _parseQuat(v) {
|
|
1237
|
+
if (!v) return null;
|
|
1238
|
+
if (Array.isArray(v) && v.length === 4 && v.every(n => typeof n === 'number')) return [v[0], v[1], v[2], v[3]];
|
|
1239
|
+
if (v.axis && typeof v.angle === 'number') {
|
|
1240
|
+
const a = Array.isArray(v.axis) ? v.axis : [v.axis.x || 0, v.axis.y || 0, v.axis.z || 0];
|
|
1241
|
+
return qFromAxisAngle([0, 0, 0, 1], a[0], a[1], a[2], v.angle);
|
|
1242
|
+
}
|
|
1243
|
+
if (v.dir) {
|
|
1244
|
+
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x || 0, v.dir.y || 0, v.dir.z || 0];
|
|
1245
|
+
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x || 0, v.up.y || 0, v.up.z || 0]) : null;
|
|
1246
|
+
return qFromLookDir([0, 0, 0, 1], d, u);
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function _parseSpec(spec) {
|
|
1252
|
+
if (!spec || typeof spec !== 'object') return null;
|
|
1253
|
+
const pos = _parseVec3(spec.pos) || [0, 0, 0];
|
|
1254
|
+
const rot = _parseQuat(spec.rot) || [0, 0, 0, 1];
|
|
1255
|
+
const scl = _parseVec3(spec.scl) || [1, 1, 1];
|
|
1256
|
+
return { pos, rot, scl };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function _sameTransform(a, b) {
|
|
1260
|
+
for (let i = 0; i < 3; i++) if (a.pos[i] !== b.pos[i] || a.scl[i] !== b.scl[i]) return false;
|
|
1261
|
+
for (let i = 0; i < 4; i++) if (a.rot[i] !== b.rot[i]) return false;
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// =========================================================================
|
|
1266
|
+
// S5 PoseTrack
|
|
1267
|
+
// =========================================================================
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Renderer-agnostic keyframe animation track.
|
|
1271
|
+
*
|
|
1272
|
+
* Keyframes are TRS pose objects: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z] }.
|
|
1273
|
+
* The track maintains a scalar cursor (seg, f) that advances each tick().
|
|
1274
|
+
*
|
|
1275
|
+
* Position uses centripetal Catmull-Rom spline by default (posInterp = 'catmullrom');
|
|
1276
|
+
* set posInterp = 'linear' to switch to lerp. Rotation uses SLERP. Scale uses LERP.
|
|
1277
|
+
*
|
|
1278
|
+
* Rate semantics:
|
|
1279
|
+
* rate > 0 forward
|
|
1280
|
+
* rate < 0 backward
|
|
1281
|
+
* rate === 0 frozen: tick() is a no-op; playing is NOT changed
|
|
1282
|
+
*
|
|
1283
|
+
* Assigning rate never starts or stops playback.
|
|
1284
|
+
* Only play() sets playing = true. Only stop() / reset() set it to false.
|
|
1285
|
+
*
|
|
1286
|
+
* One-keyframe behaviour:
|
|
1287
|
+
* play() with exactly one keyframe snaps eval() to that keyframe
|
|
1288
|
+
* without setting playing = true and without firing hooks.
|
|
1289
|
+
*/
|
|
1290
|
+
class PoseTrack {
|
|
1291
|
+
constructor() {
|
|
1292
|
+
/** @type {Array<{pos:number[],rot:number[],scl:number[]}>} */
|
|
1293
|
+
this.keyframes = [];
|
|
1294
|
+
/** Whether playback is active. @type {boolean} */
|
|
1295
|
+
this.playing = false;
|
|
1296
|
+
/** Loop flag (overridden by pingPong). @type {boolean} */
|
|
1297
|
+
this.loop = false;
|
|
1298
|
+
/** Ping-pong bounce mode (takes precedence over loop). @type {boolean} */
|
|
1299
|
+
this.pingPong = false;
|
|
1300
|
+
/** Frames per segment (>=1). @type {number} */
|
|
1301
|
+
this.duration = 30;
|
|
1302
|
+
/** Current segment index. @type {number} */
|
|
1303
|
+
this.seg = 0;
|
|
1304
|
+
/** Frame offset within current segment (can be fractional). @type {number} */
|
|
1305
|
+
this.f = 0;
|
|
1306
|
+
/**
|
|
1307
|
+
* Position interpolation mode.
|
|
1308
|
+
* @type {'catmullrom'|'linear'}
|
|
1309
|
+
*/
|
|
1310
|
+
this.posInterp = 'catmullrom';
|
|
1311
|
+
|
|
1312
|
+
// Scratch arrays reused by eval() / toMatrix() — avoids hot-path allocations
|
|
1313
|
+
this._pos = [0, 0, 0];
|
|
1314
|
+
this._rot = [0, 0, 0, 1];
|
|
1315
|
+
this._scl = [1, 1, 1];
|
|
1316
|
+
|
|
1317
|
+
// Internal rate — assigning never touches playing
|
|
1318
|
+
this._rate = 1;
|
|
1319
|
+
|
|
1320
|
+
// User-space hooks
|
|
1321
|
+
/** Called once when play() starts a false->true transition. @type {Function|null} */
|
|
1322
|
+
this.onPlay = null;
|
|
1323
|
+
/** Called once by tick() when cursor reaches a natural boundary (once mode). @type {Function|null} */
|
|
1324
|
+
this.onEnd = null;
|
|
1325
|
+
|
|
1326
|
+
// Lib-space hooks (set by host layer — e.g. p5 bridge)
|
|
1327
|
+
/** @type {Function|null} */
|
|
1328
|
+
this._onActivate = null;
|
|
1329
|
+
/** @type {Function|null} */
|
|
1330
|
+
this._onDeactivate = null;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ── rate ────────────────────────────────────────────────────────────────
|
|
1334
|
+
// Getter/setter so future consumers get the right value from track.rate,
|
|
1335
|
+
// while the setter intentionally has NO side effects on playing.
|
|
1336
|
+
|
|
1337
|
+
/** Playback rate. 0 = frozen (playing flag unchanged). @type {number} */
|
|
1338
|
+
get rate() { return this._rate; }
|
|
1339
|
+
set rate(v) {
|
|
1340
|
+
this._rate = (typeof v === 'number' && Number.isFinite(v)) ? v : 1;
|
|
1341
|
+
// Intentionally does NOT start or stop playback.
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** Number of interpolatable segments (keyframes.length - 1, min 0). @type {number} */
|
|
1345
|
+
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1346
|
+
|
|
1347
|
+
// ── Keyframe management ──────────────────────────────────────────────────
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Append a keyframe. Adjacent duplicates are skipped by default.
|
|
1351
|
+
* @param {{ pos?, rot?, scl? }} spec pos/rot/scl arrays, {x,y,z}, axis-angle, look-dir.
|
|
1352
|
+
* @param {{ deduplicate?: boolean }} [opts]
|
|
1353
|
+
*/
|
|
1354
|
+
add(spec, opts) {
|
|
1355
|
+
const kf = _parseSpec(spec);
|
|
1356
|
+
if (!kf) return;
|
|
1357
|
+
const dedup = !opts || opts.deduplicate !== false;
|
|
1358
|
+
if (dedup && this.keyframes.length > 0) {
|
|
1359
|
+
if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1360
|
+
}
|
|
1361
|
+
this.keyframes.push(kf);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Replace (or append at end) the keyframe at index.
|
|
1366
|
+
* @param {number} index Existing index or keyframes.length to append.
|
|
1367
|
+
* @param {{ pos?, rot?, scl? }} spec
|
|
1368
|
+
* @returns {boolean}
|
|
1369
|
+
*/
|
|
1370
|
+
set(index, spec) {
|
|
1371
|
+
if (!_isNum(index)) return false;
|
|
1372
|
+
const i = index | 0;
|
|
1373
|
+
const kf = _parseSpec(spec);
|
|
1374
|
+
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1375
|
+
if (i === this.keyframes.length) { this.keyframes.push(kf); }
|
|
1376
|
+
else { this.keyframes[i] = kf; }
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Remove the keyframe at index. Adjusts cursor if needed.
|
|
1382
|
+
* @param {number} index
|
|
1383
|
+
* @returns {boolean}
|
|
1384
|
+
*/
|
|
1385
|
+
remove(index) {
|
|
1386
|
+
if (!_isNum(index)) return false;
|
|
1387
|
+
const i = index | 0;
|
|
1388
|
+
if (i < 0 || i >= this.keyframes.length) return false;
|
|
1389
|
+
this.keyframes.splice(i, 1);
|
|
1390
|
+
const nSeg = this.segments;
|
|
1391
|
+
if (nSeg === 0) { this.seg = 0; this.f = 0; }
|
|
1392
|
+
else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// ── Transport ────────────────────────────────────────────────────────────
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Start or update playback.
|
|
1400
|
+
* Accepts a numeric rate or an options object: { rate, duration, loop, pingPong, onPlay, onEnd }.
|
|
1401
|
+
*
|
|
1402
|
+
* Zero keyframes: no-op.
|
|
1403
|
+
* One keyframe: snaps cursor (seg=0, f=0); no playing=true, no hooks.
|
|
1404
|
+
* Already playing: updates params in place; hooks are not re-fired.
|
|
1405
|
+
* rate=0 is valid: track will be playing but frozen until rate changes.
|
|
1406
|
+
*
|
|
1407
|
+
* @param {number|Object} [rateOrOpts]
|
|
1408
|
+
* @returns {PoseTrack} this
|
|
1409
|
+
*/
|
|
1410
|
+
play(rateOrOpts) {
|
|
1411
|
+
if (this.keyframes.length === 0) return this;
|
|
1412
|
+
|
|
1413
|
+
// One keyframe: snap only, no animation, no hooks
|
|
1414
|
+
if (this.keyframes.length === 1) {
|
|
1415
|
+
this.seg = 0; this.f = 0;
|
|
1416
|
+
return this;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (typeof rateOrOpts === 'number' && Number.isFinite(rateOrOpts)) {
|
|
1420
|
+
this._rate = rateOrOpts;
|
|
1421
|
+
} else if (rateOrOpts && typeof rateOrOpts === 'object') {
|
|
1422
|
+
const o = rateOrOpts;
|
|
1423
|
+
if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
|
|
1424
|
+
if ('loop' in o) this.loop = !!o.loop;
|
|
1425
|
+
if ('pingPong' in o) this.pingPong = !!o.pingPong;
|
|
1426
|
+
if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
|
|
1427
|
+
if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
|
|
1428
|
+
if (_isNum(o.rate)) this._rate = o.rate;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Clamp cursor into valid range
|
|
1432
|
+
const nSeg = this.segments;
|
|
1433
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1434
|
+
if (this.seg < 0) this.seg = 0;
|
|
1435
|
+
if (this.seg >= nSeg) this.seg = nSeg - 1;
|
|
1436
|
+
if (this.f < 0) this.f = 0;
|
|
1437
|
+
if (this.f > dur) this.f = dur;
|
|
1438
|
+
|
|
1439
|
+
const wasPlaying = this.playing;
|
|
1440
|
+
this.playing = true;
|
|
1441
|
+
|
|
1442
|
+
if (!wasPlaying) {
|
|
1443
|
+
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1444
|
+
this._onActivate?.();
|
|
1445
|
+
}
|
|
1446
|
+
return this;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Stop playback. Does NOT reset time unless reset is true.
|
|
1451
|
+
* @param {boolean} [reset=false] If true, seek to start or end based on rate direction.
|
|
1452
|
+
* @returns {PoseTrack} this
|
|
1453
|
+
*/
|
|
1454
|
+
stop(reset) {
|
|
1455
|
+
const wasPlaying = this.playing;
|
|
1456
|
+
this.playing = false;
|
|
1457
|
+
if (wasPlaying) this._onDeactivate?.();
|
|
1458
|
+
if (!reset || this.keyframes.length <= 1) return this;
|
|
1459
|
+
this.seek(this._rate < 0 ? 1 : 0);
|
|
1460
|
+
return this;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Clear all keyframes and stop. Fires _onDeactivate if was playing.
|
|
1465
|
+
* @returns {PoseTrack} this
|
|
1466
|
+
*/
|
|
1467
|
+
reset() {
|
|
1468
|
+
const wasPlaying = this.playing;
|
|
1469
|
+
this.playing = false;
|
|
1470
|
+
if (wasPlaying) this._onDeactivate?.();
|
|
1471
|
+
this.keyframes.length = 0;
|
|
1472
|
+
this.seg = 0; this.f = 0;
|
|
1473
|
+
return this;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Seek to a normalised position [0,1] across the full path.
|
|
1478
|
+
* Can optionally target a specific segment (t is then local to that segment).
|
|
1479
|
+
* Does not change the playing flag.
|
|
1480
|
+
* @param {number} t Normalised time [0, 1].
|
|
1481
|
+
* @param {number} [segIndex] Optional segment override.
|
|
1482
|
+
* @returns {PoseTrack} this
|
|
1483
|
+
*/
|
|
1484
|
+
seek(t, segIndex) {
|
|
1485
|
+
const nSeg = this.segments;
|
|
1486
|
+
if (nSeg === 0) { this.seg = 0; this.f = 0; return this; }
|
|
1487
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1488
|
+
if (_isNum(segIndex)) {
|
|
1489
|
+
this.seg = _clampS(segIndex | 0, 0, nSeg - 1);
|
|
1490
|
+
this.f = _clamp01(t) * dur;
|
|
1491
|
+
} else {
|
|
1492
|
+
this._setCursorFromScalar(_clamp01(t) * nSeg * dur);
|
|
1493
|
+
}
|
|
1494
|
+
return this;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Normalised playback time across the full path [0, 1].
|
|
1499
|
+
* Returns 0 when fewer than 2 keyframes exist.
|
|
1500
|
+
* @returns {number}
|
|
1501
|
+
*/
|
|
1502
|
+
time() {
|
|
1503
|
+
const nSeg = this.segments;
|
|
1504
|
+
if (nSeg === 0) return 0;
|
|
1505
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1506
|
+
return _clamp01((this.seg * dur + this.f) / (nSeg * dur));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Snapshot of the current transport state.
|
|
1511
|
+
* @returns {{ keyframes:number, segments:number, seg:number, f:number,
|
|
1512
|
+
* time:number, playing:boolean, loop:boolean, pingPong:boolean,
|
|
1513
|
+
* rate:number, duration:number }}
|
|
1514
|
+
*/
|
|
1515
|
+
info() {
|
|
1516
|
+
return {
|
|
1517
|
+
keyframes: this.keyframes.length,
|
|
1518
|
+
segments: this.segments,
|
|
1519
|
+
seg: this.seg,
|
|
1520
|
+
f: this.f,
|
|
1521
|
+
playing: this.playing,
|
|
1522
|
+
loop: this.loop,
|
|
1523
|
+
pingPong: this.pingPong,
|
|
1524
|
+
rate: this._rate,
|
|
1525
|
+
duration: this.duration,
|
|
1526
|
+
time: this.segments > 0 ? this.time() : 0
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Advance the cursor by rate frames.
|
|
1532
|
+
*
|
|
1533
|
+
* rate === 0: frozen — returns this.playing without moving (no-op).
|
|
1534
|
+
* Returns false and fires onEnd/_onDeactivate when a once-mode boundary is hit.
|
|
1535
|
+
* Returns true while playing and continuing.
|
|
1536
|
+
*
|
|
1537
|
+
* @returns {boolean}
|
|
1538
|
+
*/
|
|
1539
|
+
tick() {
|
|
1540
|
+
if (!this.playing) return false;
|
|
1541
|
+
const nSeg = this.segments;
|
|
1542
|
+
if (nSeg === 0) {
|
|
1543
|
+
this.playing = false; this._onDeactivate?.(); return false;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// Frozen: position does not advance, playing stays true
|
|
1547
|
+
if (this._rate === 0) return true;
|
|
1548
|
+
|
|
1549
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1550
|
+
const total = nSeg * dur;
|
|
1551
|
+
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1552
|
+
const next = s + this._rate;
|
|
1553
|
+
|
|
1554
|
+
// ── pingPong ──
|
|
1555
|
+
if (this.pingPong) {
|
|
1556
|
+
let pos = next, flips = 0;
|
|
1557
|
+
while (pos < 0 || pos > total) {
|
|
1558
|
+
if (pos < 0) { pos = -pos; flips++; }
|
|
1559
|
+
else { pos = 2 * total - pos; flips++; }
|
|
1560
|
+
}
|
|
1561
|
+
if (flips & 1) this._rate = -this._rate;
|
|
1562
|
+
this._setCursorFromScalar(pos);
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// ── loop ──
|
|
1567
|
+
if (this.loop) {
|
|
1568
|
+
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// ── once — boundary check ──
|
|
1573
|
+
if (next <= 0) {
|
|
1574
|
+
this._setCursorFromScalar(0);
|
|
1575
|
+
this.playing = false;
|
|
1576
|
+
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1577
|
+
this._onDeactivate?.();
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
if (next >= total) {
|
|
1581
|
+
this._setCursorFromScalar(total);
|
|
1582
|
+
this.playing = false;
|
|
1583
|
+
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1584
|
+
this._onDeactivate?.();
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
this._setCursorFromScalar(next);
|
|
1589
|
+
return true;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Evaluate the interpolated pose at the current cursor into out.
|
|
1594
|
+
* If out is omitted a new object is allocated (avoid in hot paths).
|
|
1595
|
+
* Uses centripetal Catmull-Rom for position (posInterp === 'catmullrom') or lerp.
|
|
1596
|
+
* @param {{ pos:number[], rot:number[], scl:number[] }} [out]
|
|
1597
|
+
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1598
|
+
*/
|
|
1599
|
+
eval(out) {
|
|
1600
|
+
out = out || { pos: [0, 0, 0], rot: [0, 0, 0, 1], scl: [1, 1, 1] };
|
|
1601
|
+
const n = this.keyframes.length;
|
|
1602
|
+
if (n === 0) return out;
|
|
1603
|
+
|
|
1604
|
+
if (n === 1) {
|
|
1605
|
+
const k = this.keyframes[0];
|
|
1606
|
+
out.pos[0]=k.pos[0]; out.pos[1]=k.pos[1]; out.pos[2]=k.pos[2];
|
|
1607
|
+
out.rot[0]=k.rot[0]; out.rot[1]=k.rot[1]; out.rot[2]=k.rot[2]; out.rot[3]=k.rot[3];
|
|
1608
|
+
out.scl[0]=k.scl[0]; out.scl[1]=k.scl[1]; out.scl[2]=k.scl[2];
|
|
1609
|
+
return out;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
const nSeg = n - 1;
|
|
1613
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1614
|
+
const seg = _clampS(this.seg, 0, nSeg - 1);
|
|
1615
|
+
const t = _clamp01(this.f / dur);
|
|
1616
|
+
const k0 = this.keyframes[seg];
|
|
1617
|
+
const k1 = this.keyframes[seg + 1];
|
|
1618
|
+
|
|
1619
|
+
// Position
|
|
1620
|
+
if (this.posInterp === 'catmullrom') {
|
|
1621
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1622
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1623
|
+
catmullRomVec3(out.pos, p0, k0.pos, k1.pos, p3, t);
|
|
1624
|
+
} else {
|
|
1625
|
+
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Rotation — SLERP
|
|
1629
|
+
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
1630
|
+
|
|
1631
|
+
// Scale — LERP
|
|
1632
|
+
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
1633
|
+
|
|
1634
|
+
return out;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Evaluate the current cursor into an existing column-major mat4.
|
|
1639
|
+
* Reuses internal scratch arrays — no allocation per call.
|
|
1640
|
+
* @param {Float32Array|number[]} outMat4 16-element array.
|
|
1641
|
+
* @returns {Float32Array|number[]} outMat4
|
|
1642
|
+
*/
|
|
1643
|
+
toMatrix(outMat4) {
|
|
1644
|
+
const xf = this.eval({ pos: this._pos, rot: this._rot, scl: this._scl });
|
|
1645
|
+
return transformToMat4(outMat4, xf);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// ── Private ──────────────────────────────────────────────────────────────
|
|
1649
|
+
|
|
1650
|
+
/** @private */
|
|
1651
|
+
_setCursorFromScalar(s) {
|
|
1652
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1653
|
+
const nSeg = this.segments;
|
|
1654
|
+
this.seg = Math.floor(s / dur);
|
|
1655
|
+
this.f = s - this.seg * dur;
|
|
1656
|
+
if (this.seg >= nSeg) { this.seg = nSeg - 1; this.f = dur; }
|
|
1657
|
+
if (this.seg < 0) { this.seg = 0; this.f = 0; }
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* @file Frustum planes and visibility tests — zero allocations.
|
|
1663
|
+
* @module tree/visibility
|
|
1664
|
+
* @license GPL-3.0-only
|
|
1665
|
+
*
|
|
1666
|
+
* Planes are a flat Float64Array(24): 6 planes × 4 floats [a, b, c, d].
|
|
1667
|
+
* All inputs are scalars. All outputs are INVISIBLE | VISIBLE | SEMIVISIBLE.
|
|
1668
|
+
*/
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
// Plane indices
|
|
1672
|
+
const PLANE_LEFT = 0, PLANE_RIGHT = 1, PLANE_NEAR = 2,
|
|
1673
|
+
PLANE_FAR = 3, PLANE_TOP = 4, PLANE_BOTTOM = 5;
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Compute 6 frustum planes from camera basis (world space) + projection params.
|
|
1677
|
+
* All inputs are scalars — the addon extracts them from the inverse view matrix
|
|
1678
|
+
* and projection queries before calling.
|
|
1679
|
+
*
|
|
1680
|
+
* @param {Float64Array} out 24-float output.
|
|
1681
|
+
* @param {number} posX,posY,posZ Camera world position.
|
|
1682
|
+
* @param {number} vdX,vdY,vdZ View direction (−Z in eye space, world).
|
|
1683
|
+
* @param {number} upX,upY,upZ Camera up.
|
|
1684
|
+
* @param {number} rtX,rtY,rtZ Camera right.
|
|
1685
|
+
* @param {boolean} ortho true if orthographic.
|
|
1686
|
+
* @param {number} near,far,left,right,top,bottom Projection plane values.
|
|
1687
|
+
*/
|
|
1688
|
+
function frustumPlanes(
|
|
1689
|
+
out,
|
|
1690
|
+
posX, posY, posZ,
|
|
1691
|
+
vdX, vdY, vdZ,
|
|
1692
|
+
upX, upY, upZ,
|
|
1693
|
+
rtX, rtY, rtZ,
|
|
1694
|
+
ortho,
|
|
1695
|
+
near, far, left, right, top, bottom
|
|
1696
|
+
) {
|
|
1697
|
+
const posViewDir = posX*vdX + posY*vdY + posZ*vdZ;
|
|
1698
|
+
const posRight = posX*rtX + posY*rtY + posZ*rtZ;
|
|
1699
|
+
const posUp = posX*upX + posY*upY + posZ*upZ;
|
|
1700
|
+
|
|
1701
|
+
if (ortho) {
|
|
1702
|
+
// Left: normal = −right
|
|
1703
|
+
out[0] = -rtX; out[1] = -rtY; out[2] = -rtZ;
|
|
1704
|
+
out[3] = -(posRight - left) * -1; // dot(pos - right*left, -right) ... simplified:
|
|
1705
|
+
// Actually: d = dot(pointOnPlane, normal)
|
|
1706
|
+
// pointOnPlane = pos + right*left (left is negative for standard ortho)
|
|
1707
|
+
// normal = -right
|
|
1708
|
+
// d = dot(pos + right*left, -right) = -posRight - left
|
|
1709
|
+
out[3] = -posRight - left;
|
|
1710
|
+
|
|
1711
|
+
// Right: normal = right
|
|
1712
|
+
out[4] = rtX; out[5] = rtY; out[6] = rtZ;
|
|
1713
|
+
out[7] = posRight + right;
|
|
1714
|
+
|
|
1715
|
+
// Top: normal = up
|
|
1716
|
+
out[16] = upX; out[17] = upY; out[18] = upZ;
|
|
1717
|
+
out[19] = posUp - bottom; // note: p5 top/bottom are swapped in sign convention
|
|
1718
|
+
|
|
1719
|
+
// Bottom: normal = -up
|
|
1720
|
+
out[20] = -upX; out[21] = -upY; out[22] = -upZ;
|
|
1721
|
+
out[23] = -posUp + top;
|
|
1722
|
+
} else {
|
|
1723
|
+
// Left
|
|
1724
|
+
const hfovl = Math.atan2(left, near);
|
|
1725
|
+
const shfovl = Math.sin(hfovl), chfovl = Math.cos(hfovl);
|
|
1726
|
+
out[0] = vdX*shfovl - rtX*chfovl;
|
|
1727
|
+
out[1] = vdY*shfovl - rtY*chfovl;
|
|
1728
|
+
out[2] = vdZ*shfovl - rtZ*chfovl;
|
|
1729
|
+
out[3] = shfovl*posViewDir - chfovl*posRight;
|
|
1730
|
+
|
|
1731
|
+
// Right
|
|
1732
|
+
const hfovr = Math.atan2(right, near);
|
|
1733
|
+
const shfovr = Math.sin(hfovr), chfovr = Math.cos(hfovr);
|
|
1734
|
+
out[4] = -vdX*shfovr + rtX*chfovr;
|
|
1735
|
+
out[5] = -vdY*shfovr + rtY*chfovr;
|
|
1736
|
+
out[6] = -vdZ*shfovr + rtZ*chfovr;
|
|
1737
|
+
out[7] = -shfovr*posViewDir + chfovr*posRight;
|
|
1738
|
+
|
|
1739
|
+
// Top
|
|
1740
|
+
const fovt = Math.atan2(top, near);
|
|
1741
|
+
const sfovt = Math.sin(fovt), cfovt = Math.cos(fovt);
|
|
1742
|
+
out[16] = -vdX*sfovt + upX*cfovt;
|
|
1743
|
+
out[17] = -vdY*sfovt + upY*cfovt;
|
|
1744
|
+
out[18] = -vdZ*sfovt + upZ*cfovt;
|
|
1745
|
+
out[19] = -sfovt*posViewDir + cfovt*posUp;
|
|
1746
|
+
|
|
1747
|
+
// Bottom
|
|
1748
|
+
const fovb = Math.atan2(bottom, near);
|
|
1749
|
+
const sfovb = Math.sin(fovb), cfovb = Math.cos(fovb);
|
|
1750
|
+
out[20] = vdX*sfovb - upX*cfovb;
|
|
1751
|
+
out[21] = vdY*sfovb - upY*cfovb;
|
|
1752
|
+
out[22] = vdZ*sfovb - upZ*cfovb;
|
|
1753
|
+
out[23] = sfovb*posViewDir - cfovb*posUp;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Near plane: normal = −viewDir
|
|
1757
|
+
out[8] = -vdX; out[9] = -vdY; out[10] = -vdZ;
|
|
1758
|
+
out[11] = -posViewDir - near;
|
|
1759
|
+
|
|
1760
|
+
// Far plane: normal = viewDir
|
|
1761
|
+
out[12] = vdX; out[13] = vdY; out[14] = vdZ;
|
|
1762
|
+
out[15] = posViewDir + far;
|
|
1763
|
+
|
|
1764
|
+
return out;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Signed distance from point to one frustum plane.
|
|
1769
|
+
* @param {Float64Array} planes 24-float planes buffer.
|
|
1770
|
+
* @param {number} planeIdx 0–5 (LEFT, RIGHT, NEAR, FAR, TOP, BOTTOM).
|
|
1771
|
+
* @param {number} px,py,pz Point coordinates.
|
|
1772
|
+
* @returns {number}
|
|
1773
|
+
*/
|
|
1774
|
+
function distanceToPlane(planes, planeIdx, px, py, pz) {
|
|
1775
|
+
const b = planeIdx * 4;
|
|
1776
|
+
return planes[b]*px + planes[b+1]*py + planes[b+2]*pz - planes[b+3];
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/** @returns {number} INVISIBLE | VISIBLE | SEMIVISIBLE */
|
|
1780
|
+
function pointVisibility(planes, px, py, pz) {
|
|
1781
|
+
for (let i = 0; i < 6; i++) {
|
|
1782
|
+
if (distanceToPlane(planes, i, px, py, pz) > 0) return INVISIBLE;
|
|
1783
|
+
}
|
|
1784
|
+
return VISIBLE;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/** @returns {number} INVISIBLE | VISIBLE | SEMIVISIBLE */
|
|
1788
|
+
function sphereVisibility(planes, cx, cy, cz, radius) {
|
|
1789
|
+
let allIn = true;
|
|
1790
|
+
for (let i = 0; i < 6; i++) {
|
|
1791
|
+
const d = distanceToPlane(planes, i, cx, cy, cz);
|
|
1792
|
+
if (d > radius) return INVISIBLE;
|
|
1793
|
+
if (d > 0 || -d < radius) allIn = false;
|
|
1794
|
+
}
|
|
1795
|
+
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/** @returns {number} INVISIBLE | VISIBLE | SEMIVISIBLE */
|
|
1799
|
+
function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
1800
|
+
let allIn = true;
|
|
1801
|
+
for (let i = 0; i < 6; i++) {
|
|
1802
|
+
const b = i * 4;
|
|
1803
|
+
const a = planes[b], bv = planes[b+1], c = planes[b+2], d = planes[b+3];
|
|
1804
|
+
let allOut = true;
|
|
1805
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
1806
|
+
const cx = (corner & 4) ? x0 : x1;
|
|
1807
|
+
const cy = (corner & 2) ? y0 : y1;
|
|
1808
|
+
const cz = (corner & 1) ? z0 : z1;
|
|
1809
|
+
const dist = a*cx + bv*cy + c*cz - d;
|
|
1810
|
+
if (dist > 0) { allIn = false; }
|
|
1811
|
+
else { allOut = false; }
|
|
1812
|
+
}
|
|
1813
|
+
if (allOut) return INVISIBLE;
|
|
1814
|
+
}
|
|
1815
|
+
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
export { EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, boxVisibility, catmullRomVec3, distanceToPlane, frustumPlanes, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3FromMat4T, mat3MulVec3, mat3NormalFromMat4, mat4Identity, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4PMV, mat4PV, mat4ToTransform, mat4Transpose, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
|
|
1819
|
+
//# sourceMappingURL=index.js.map
|