@nakednous/tree 0.0.11 → 0.0.13

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
@@ -238,23 +238,27 @@ const quatToAxisAngle = (q, out) => {
238
238
  * from specs requires only scalar arithmetic and quaternion conversions.
239
239
  * Callers compose the resulting matrices using query.js (mat4Mul etc.).
240
240
  *
241
- * Lookat constructors live here because a camera is just a frame — the eye
242
- * matrix is the camera object's model matrix, not a camera-specific concept.
243
- * There is no camera module; mat4LookAt and mat4EyeMatrix are frame
244
- * constructions that happen to use lookat parameterisation.
245
- *
246
- * Projection constructors live here because they construct matrices from
247
- * geometric parameters. Projection scalar reads (projNear, projFov, etc.)
248
- * live in query.js they interrogate an existing projection matrix.
249
- *
250
- * Partial decomposers (mat4To___) are the inverse of construction — they
251
- * extract a single component from an existing matrix. Kept alongside
252
- * constructors because they are paired operations on the same components.
253
- *
254
- * Imports quat.js only. No dependency on query.js, visibility.js, or track.js.
241
+ * ── NDC Z convention ──────────────────────────────────────────────────────
242
+ * Controlled by `ndcZMin` in every projection constructor:
243
+ * WEBGL = −1 near NDC z = −1, far NDC z = +1
244
+ * WEBGPU = 0 near NDC z = 0, far NDC z = +1
245
+ *
246
+ * ── NDC Y convention ──────────────────────────────────────────────────────
247
+ * Controlled by `ndcYSign` in every projection constructor (default +1):
248
+ * +1 NDC y-up — standard: OpenGL / WebGL / WebGPU browser / Three.js / p5v2
249
+ * −1 NDC y-down — native Vulkan clip space
250
+ *
251
+ * Negating ndcYSign flips row 1 of the projection matrix (elements
252
+ * out[1], out[5], out[9], out[13]), reversing the y-axis in clip space.
253
+ * mat4View, mat4Eye, and all non-projection constructors are convention-
254
+ * agnostic they produce the same matrix regardless of the NDC y direction.
255
+ *
256
+ * ── Screen Y convention ───────────────────────────────────────────────────
257
+ * Screen-y direction (DOM y-down vs OpenGL y-up) is a separate concern from
258
+ * NDC-y direction and is handled in query.js via the signed viewport height.
259
+ * See the query.js module header for details.
255
260
  *
256
261
  * All functions follow the out-first, zero-allocation contract.
257
- * Returns null on degeneracy where applicable.
258
262
  */
259
263
 
260
264
 
@@ -264,8 +268,6 @@ const quatToAxisAngle = (q, out) => {
264
268
 
265
269
  /**
266
270
  * Rigid frame from orthonormal basis + translation.
267
- * The primitive that lookat constructors use internally.
268
- *
269
271
  * Column-major layout: col0=right, col1=up, col2=forward, col3=translation.
270
272
  *
271
273
  * @param {Float32Array|number[]} out 16-element destination.
@@ -273,7 +275,6 @@ const quatToAxisAngle = (q, out) => {
273
275
  * @param {number} ux,uy,uz Up vector (col 1).
274
276
  * @param {number} fx,fy,fz Forward vec (col 2).
275
277
  * @param {number} tx,ty,tz Translation (col 3).
276
- * @returns {Float32Array|number[]} out
277
278
  */
278
279
  function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
279
280
  out[0]=rx; out[1]=ry; out[2]=rz; out[3]=0;
@@ -285,28 +286,22 @@ function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
285
286
 
286
287
  /**
287
288
  * View matrix (world→eye) from lookat parameters.
289
+ * Camera looks along −Z in eye space; right = normalize(up × (−Z)).
288
290
  * Cheaper than building the eye matrix and inverting.
289
291
  *
290
- * Convention: −Z axis points toward center (camera looks along −Z in eye space).
291
- *
292
292
  * @param {Float32Array|number[]} out 16-element destination.
293
293
  * @param {number} ex,ey,ez Eye (camera) position.
294
- * @param {number} cx,cy,cz Center (look-at target).
294
+ * @param {number} cx,cy,cz Look-at target.
295
295
  * @param {number} ux,uy,uz World up hint (need not be unit).
296
- * @returns {Float32Array|number[]} out
297
296
  */
298
- function mat4LookAt(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
299
- // z = normalize(eye - center) (camera +Z away from target)
297
+ function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
300
298
  let zx=ex-cx, zy=ey-cy, zz=ez-cz;
301
299
  const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
302
300
  zx/=zl; zy/=zl; zz/=zl;
303
- // x = normalize(up × z) (right)
304
301
  let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
305
302
  const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
306
303
  xx/=xl; xy/=xl; xz/=xl;
307
- // y = z × x (up_ortho, guaranteed perpendicular)
308
304
  const yx=zy*xz-zz*xy, yy=zz*xx-zx*xz, yz=zx*xy-zy*xx;
309
- // View = [R | -R·t] (column-major)
310
305
  out[0]=xx; out[1]=yx; out[2]=zx; out[3]=0;
311
306
  out[4]=xy; out[5]=yy; out[6]=zy; out[7]=0;
312
307
  out[8]=xz; out[9]=yz; out[10]=zz; out[11]=0;
@@ -320,16 +315,14 @@ function mat4LookAt(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
320
315
  /**
321
316
  * Eye matrix (eye→world) from lookat parameters.
322
317
  * Transpose of the rotation block + direct translation column.
323
- * Same inputs as mat4LookAt.
318
+ * Same parameters as mat4View.
324
319
  *
325
320
  * @param {Float32Array|number[]} out 16-element destination.
326
- * @param {number} ex,ey,ez Eye (camera) position.
327
- * @param {number} cx,cy,cz Center (look-at target).
328
- * @param {number} ux,uy,uz World up hint (need not be unit).
329
- * @returns {Float32Array|number[]} out
321
+ * @param {number} ex,ey,ez Eye position.
322
+ * @param {number} cx,cy,cz Look-at target.
323
+ * @param {number} ux,uy,uz World up hint.
330
324
  */
331
- function mat4EyeMatrix(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
332
- // Same basis computation as mat4LookAt.
325
+ function mat4Eye(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
333
326
  let zx=ex-cx, zy=ey-cy, zz=ez-cz;
334
327
  const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
335
328
  zx/=zl; zy/=zl; zz/=zl;
@@ -337,7 +330,6 @@ function mat4EyeMatrix(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
337
330
  const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
338
331
  xx/=xl; xy/=xl; xz/=xl;
339
332
  const yx=zy*xz-zz*xy, yy=zz*xx-zx*xz, yz=zx*xy-zy*xx;
340
- // Eye matrix = [R^T | t] (rotation transposed, translation = eye position)
341
333
  out[0]=xx; out[1]=xy; out[2]=xz; out[3]=0;
342
334
  out[4]=yx; out[5]=yy; out[6]=yz; out[7]=0;
343
335
  out[8]=zx; out[9]=zy; out[10]=zz; out[11]=0;
@@ -350,22 +342,20 @@ function mat4EyeMatrix(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
350
342
  // =========================================================================
351
343
 
352
344
  /**
353
- * Column-major mat4 from flat TRS scalars.
354
- * No struct allocation — all components passed as plain numbers.
345
+ * Column-major mat4 from flat TRS scalars. No struct allocation.
355
346
  *
356
347
  * @param {Float32Array|number[]} out 16-element destination.
357
348
  * @param {number} tx,ty,tz Translation.
358
349
  * @param {number} qx,qy,qz,qw Rotation quaternion [x,y,z,w].
359
350
  * @param {number} sx,sy,sz Scale.
360
- * @returns {Float32Array|number[]} out
361
351
  */
362
352
  function mat4FromTRS(out, tx,ty,tz, qx,qy,qz,qw, sx,sy,sz) {
363
353
  const x2=qx+qx,y2=qy+qy,z2=qz+qz;
364
354
  const xx=qx*x2,xy=qx*y2,xz=qx*z2,yy=qy*y2,yz=qy*z2,zz=qz*z2;
365
355
  const wx=qw*x2,wy=qw*y2,wz=qw*z2;
366
- out[0]=(1-(yy+zz))*sx; out[1]=(xy+wz)*sx; out[2]=(xz-wy)*sx; out[3]=0;
367
- out[4]=(xy-wz)*sy; out[5]=(1-(xx+zz))*sy; out[6]=(yz+wx)*sy; out[7]=0;
368
- out[8]=(xz+wy)*sz; out[9]=(yz-wx)*sz; out[10]=(1-(xx+yy))*sz; out[11]=0;
356
+ out[0]=(1-(yy+zz))*sx; out[1]=(xy+wz)*sx; out[2]=(xz-wy)*sx; out[3]=0;
357
+ out[4]=(xy-wz)*sy; out[5]=(1-(xx+zz))*sy; out[6]=(yz+wx)*sy; out[7]=0;
358
+ out[8]=(xz+wy)*sz; out[9]=(yz-wx)*sz; out[10]=(1-(xx+yy))*sz; out[11]=0;
369
359
  out[12]=tx; out[13]=ty; out[14]=tz; out[15]=1;
370
360
  return out;
371
361
  }
@@ -374,12 +364,11 @@ function mat4FromTRS(out, tx,ty,tz, qx,qy,qz,qw, sx,sy,sz) {
374
364
  * Translation-only mat4.
375
365
  * @param {Float32Array|number[]} out 16-element destination.
376
366
  * @param {number} tx,ty,tz
377
- * @returns {Float32Array|number[]} out
378
367
  */
379
368
  function mat4FromTranslation(out, tx,ty,tz) {
380
- out[0]=1; out[1]=0; out[2]=0; out[3]=0;
381
- out[4]=0; out[5]=1; out[6]=0; out[7]=0;
382
- out[8]=0; out[9]=0; out[10]=1; out[11]=0;
369
+ out[0]=1; out[1]=0; out[2]=0; out[3]=0;
370
+ out[4]=0; out[5]=1; out[6]=0; out[7]=0;
371
+ out[8]=0; out[9]=0; out[10]=1; out[11]=0;
383
372
  out[12]=tx; out[13]=ty; out[14]=tz; out[15]=1;
384
373
  return out;
385
374
  }
@@ -388,11 +377,10 @@ function mat4FromTranslation(out, tx,ty,tz) {
388
377
  * Scale-only mat4.
389
378
  * @param {Float32Array|number[]} out 16-element destination.
390
379
  * @param {number} sx,sy,sz
391
- * @returns {Float32Array|number[]} out
392
380
  */
393
381
  function mat4FromScale(out, sx,sy,sz) {
394
- out[0]=sx; out[1]=0; out[2]=0; out[3]=0;
395
- out[4]=0; out[5]=sy; out[6]=0; out[7]=0;
382
+ out[0]=sx; out[1]=0; out[2]=0; out[3]=0;
383
+ out[4]=0; out[5]=sy; out[6]=0; out[7]=0;
396
384
  out[8]=0; out[9]=0; out[10]=sz; out[11]=0;
397
385
  out[12]=0; out[13]=0; out[14]=0; out[15]=1;
398
386
  return out;
@@ -405,22 +393,19 @@ function mat4FromScale(out, sx,sy,sz) {
405
393
  /**
406
394
  * Perspective projection matrix.
407
395
  *
408
- * NDC convention: ndcZMin = WEBGL (−1) or WEBGPU (0).
409
- * near maps to ndcZMin, far maps to +1.
410
- *
411
396
  * @param {Float32Array|number[]} out 16-element destination.
412
- * @param {number} fov Vertical field of view (radians).
413
- * @param {number} aspect Width / height.
414
- * @param {number} near Near plane distance (positive).
415
- * @param {number} far Far plane distance (positive, > near).
416
- * @param {number} ndcZMin -1 (WEBGL) or 0 (WEBGPU).
417
- * @returns {Float32Array|number[]} out
397
+ * @param {number} fov Vertical field of view (radians).
398
+ * @param {number} aspect Width / height.
399
+ * @param {number} near Near plane distance (positive).
400
+ * @param {number} far Far plane distance (positive, > near).
401
+ * @param {number} ndcZMin1 (WEBGL) or 0 (WEBGPU).
402
+ * @param {number} [ndcYSign=1] +1 = NDC y-up (default); −1 = NDC y-down (native Vulkan).
418
403
  */
419
- function mat4Perspective(out, fov, aspect, near, far, ndcZMin) {
404
+ function mat4Perspective(out, fov, aspect, near, far, ndcZMin, ndcYSign=1) {
420
405
  const f = 1 / Math.tan(fov * 0.5);
421
- out[0]=f/aspect; out[1]=0; out[2]=0; out[3]=0;
422
- out[4]=0; out[5]=f; out[6]=0; out[7]=0;
423
- out[8]=0; out[9]=0;
406
+ out[0]=f/aspect; out[1]=0; out[2]=0; out[3]=0;
407
+ out[4]=0; out[5]=ndcYSign*f; out[6]=0; out[7]=0;
408
+ out[8]=0; out[9]=0;
424
409
  out[10]=(ndcZMin*near-far)/(far-near);
425
410
  out[11]=-1;
426
411
  out[12]=0; out[13]=0;
@@ -432,22 +417,18 @@ function mat4Perspective(out, fov, aspect, near, far, ndcZMin) {
432
417
  /**
433
418
  * Orthographic projection matrix.
434
419
  *
435
- * NDC convention: ndcZMin = WEBGL (−1) or WEBGPU (0).
436
- *
437
420
  * @param {Float32Array|number[]} out 16-element destination.
438
421
  * @param {number} left,right,bottom,top Frustum extents.
439
422
  * @param {number} near,far Clip plane distances (positive).
440
- * @param {number} ndcZMin -1 (WEBGL) or 0 (WEBGPU).
441
- * @returns {Float32Array|number[]} out
423
+ * @param {number} ndcZMin 1 (WEBGL) or 0 (WEBGPU).
424
+ * @param {number} [ndcYSign=1] +1 = NDC y-up (default); −1 = NDC y-down (native Vulkan).
442
425
  */
443
- function mat4Ortho(out, left, right, bottom, top, near, far, ndcZMin) {
426
+ function mat4Ortho(out, left, right, bottom, top, near, far, ndcZMin, ndcYSign=1) {
444
427
  const rl=1/(right-left), tb=1/(top-bottom), fn=1/(far-near);
445
- out[0]=2*rl; out[1]=0; out[2]=0; out[3]=0;
446
- out[4]=0; out[5]=2*tb; out[6]=0; out[7]=0;
447
- out[8]=0; out[9]=0;
448
- out[10]=(ndcZMin-1)*fn;
449
- out[11]=0;
450
- out[12]=-(right+left)*rl; out[13]=-(top+bottom)*tb;
428
+ out[0]=2*rl; out[1]=0; out[2]=0; out[3]=0;
429
+ out[4]=0; out[5]=ndcYSign*2*tb; out[6]=0; out[7]=0;
430
+ out[8]=0; out[9]=0; out[10]=(ndcZMin-1)*fn; out[11]=0;
431
+ out[12]=-(right+left)*rl; out[13]=ndcYSign*(-(top+bottom)*tb);
451
432
  out[14]=(ndcZMin*far-near)*fn;
452
433
  out[15]=1;
453
434
  return out;
@@ -456,19 +437,17 @@ function mat4Ortho(out, left, right, bottom, top, near, far, ndcZMin) {
456
437
  /**
457
438
  * Frustum (off-centre perspective) projection matrix.
458
439
  *
459
- * NDC convention: ndcZMin = WEBGL (−1) or WEBGPU (0).
460
- *
461
440
  * @param {Float32Array|number[]} out 16-element destination.
462
441
  * @param {number} left,right,bottom,top Near-plane extents.
463
442
  * @param {number} near,far Clip plane distances (positive).
464
- * @param {number} ndcZMin -1 (WEBGL) or 0 (WEBGPU).
465
- * @returns {Float32Array|number[]} out
443
+ * @param {number} ndcZMin 1 (WEBGL) or 0 (WEBGPU).
444
+ * @param {number} [ndcYSign=1] +1 = NDC y-up (default); −1 = NDC y-down (native Vulkan).
466
445
  */
467
- function mat4Frustum(out, left, right, bottom, top, near, far, ndcZMin) {
446
+ function mat4Frustum(out, left, right, bottom, top, near, far, ndcZMin, ndcYSign=1) {
468
447
  const rl=1/(right-left), tb=1/(top-bottom);
469
- out[0]=2*near*rl; out[1]=0; out[2]=0; out[3]=0;
470
- out[4]=0; out[5]=2*near*tb; out[6]=0; out[7]=0;
471
- out[8]=(right+left)*rl; out[9]=(top+bottom)*tb;
448
+ out[0]=2*near*rl; out[1]=0; out[2]=0; out[3]=0;
449
+ out[4]=0; out[5]=ndcYSign*2*near*tb; out[6]=0; out[7]=0;
450
+ out[8]=(right+left)*rl; out[9]=ndcYSign*(top+bottom)*tb;
472
451
  out[10]=(ndcZMin*near-far)/(far-near);
473
452
  out[11]=-1;
474
453
  out[12]=0; out[13]=0;
@@ -482,29 +461,25 @@ function mat4Frustum(out, left, right, bottom, top, near, far, ndcZMin) {
482
461
  // =========================================================================
483
462
 
484
463
  /**
485
- * Bias matrix: remaps xyz from NDC to texture/UV space [0,1].
486
- * xy always remap from [−1,1]; z remaps from [ndcZMin,1].
487
- * Used to transform light-space NDC coordinates for shadow map sampling.
488
- *
489
- * Column-major (WebGL, ndcZMin=−1):
490
- * [ 0.5 0 0 0.5 ]
491
- * [ 0 0.5 0 0.5 ]
492
- * [ 0 0 0.5 0.5 ]
493
- * [ 0 0 0 1 ]
494
- *
495
- * Column-major (WebGPU, ndcZMin=0):
496
- * [ 0.5 0 0 0.5 ]
497
- * [ 0 0.5 0 0.5 ]
498
- * [ 0 0 1 0 ]
499
- * [ 0 0 0 1 ]
464
+ * Bias matrix: remaps xyz from NDC to texture/UV space [0, 1].
465
+ * xy remap from [−1, 1]; z remaps from [ndcZMin, 1].
466
+ * Used to convert light-space NDC coordinates to shadow map UV.
467
+ *
468
+ * Convention note: the standard bias maps NDC y = −1 → texture v = 0 and
469
+ * NDC y = +1 → texture v = 1. This is correct for both NDC y-up and y-down
470
+ * conventions because the shadow map was rendered with the same projection.
471
+ *
472
+ * Column-major (WEBGL, ndcZMin=−1): Column-major (WEBGPU, ndcZMin=0):
473
+ * [ 0.5 0 0 0.5 ] [ 0.5 0 0 0.5 ]
474
+ * [ 0 0.5 0 0.5 ] [ 0 0.5 0 0.5 ]
475
+ * [ 0 0 0.5 0.5 ] [ 0 0 1 0 ]
476
+ * [ 0 0 0 1 ] [ 0 0 0 1 ]
500
477
  *
501
478
  * @param {Float32Array|number[]} out 16-element destination.
502
479
  * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
503
- * @returns {Float32Array|number[]} out
504
480
  */
505
481
  function mat4Bias(out, ndcZMin) {
506
- const sz = 1 / (1 - ndcZMin);
507
- const tz = -ndcZMin / (1 - ndcZMin);
482
+ const sz=1/(1-ndcZMin), tz=-ndcZMin/(1-ndcZMin);
508
483
  out[0]=0.5; out[1]=0; out[2]=0; out[3]=0;
509
484
  out[4]=0; out[5]=0.5; out[6]=0; out[7]=0;
510
485
  out[8]=0; out[9]=0; out[10]=sz; out[11]=0;
@@ -519,13 +494,12 @@ function mat4Bias(out, ndcZMin) {
519
494
  * @param {Float32Array|number[]} out 16-element destination.
520
495
  * @param {number} nx,ny,nz Unit plane normal.
521
496
  * @param {number} d Plane offset (dot(point_on_plane, normal)).
522
- * @returns {Float32Array|number[]} out
523
497
  */
524
498
  function mat4Reflect(out, nx,ny,nz,d) {
525
- out[0]=1-2*nx*nx; out[1]=-2*ny*nx; out[2]=-2*nz*nx; out[3]=0;
526
- out[4]=-2*nx*ny; out[5]=1-2*ny*ny; out[6]=-2*nz*ny; out[7]=0;
527
- out[8]=-2*nx*nz; out[9]=-2*ny*nz; out[10]=1-2*nz*nz; out[11]=0;
528
- out[12]=2*d*nx; out[13]=2*d*ny; out[14]=2*d*nz; out[15]=1;
499
+ out[0]=1-2*nx*nx; out[1]=-2*ny*nx; out[2]=-2*nz*nx; out[3]=0;
500
+ out[4]=-2*nx*ny; out[5]=1-2*ny*ny; out[6]=-2*nz*ny; out[7]=0;
501
+ out[8]=-2*nx*nz; out[9]=-2*ny*nz; out[10]=1-2*nz*nz; out[11]=0;
502
+ out[12]=2*d*nx; out[13]=2*d*ny; out[14]=2*d*nz; out[15]=1;
529
503
  return out;
530
504
  }
531
505
 
@@ -537,7 +511,6 @@ function mat4Reflect(out, nx,ny,nz,d) {
537
511
  * Extract translation from a column-major mat4 (column 3).
538
512
  * @param {Float32Array|number[]} out3 3-element destination.
539
513
  * @param {Float32Array|number[]} m 16-element source.
540
- * @returns {Float32Array|number[]} out3
541
514
  */
542
515
  function mat4ToTranslation(out3, m) {
543
516
  out3[0]=m[12]; out3[1]=m[13]; out3[2]=m[14];
@@ -545,11 +518,10 @@ function mat4ToTranslation(out3, m) {
545
518
  }
546
519
 
547
520
  /**
548
- * Extract scale from a column-major mat4 (column lengths of rotation block).
521
+ * Extract scale from a column-major mat4 (column lengths of the rotation block).
549
522
  * Assumes no shear.
550
523
  * @param {Float32Array|number[]} out3 3-element destination.
551
524
  * @param {Float32Array|number[]} m 16-element source.
552
- * @returns {Float32Array|number[]} out3
553
525
  */
554
526
  function mat4ToScale(out3, m) {
555
527
  out3[0]=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2]);
@@ -560,11 +532,9 @@ function mat4ToScale(out3, m) {
560
532
 
561
533
  /**
562
534
  * Extract rotation as a unit quaternion from a column-major mat4.
563
- * Scale is factored out from each column before extraction.
564
- * Assumes no shear.
535
+ * Scale is factored out from each column. Assumes no shear.
565
536
  * @param {number[]} out4 4-element [x,y,z,w] destination.
566
537
  * @param {Float32Array|number[]} m 16-element source.
567
- * @returns {number[]} out4
568
538
  */
569
539
  function mat4ToRotation(out4, m) {
570
540
  const sx=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2])||1;
@@ -581,28 +551,43 @@ function mat4ToRotation(out4, m) {
581
551
  * @module tree/query
582
552
  * @license AGPL-3.0-only
583
553
  *
584
- * The operative layer — receives existing matrices and extracts information.
554
+ * The operative layer — receives matrices and extracts information.
585
555
  * Contrast with form.js which constructs matrices from specs.
586
556
  *
587
- * form.js — you have specs, you want a matrix
588
- * query.js you have a matrix, you want information
589
- *
590
- * No dependency on form.js. Operating on matrices requires no knowledge
591
- * of how they were constructed.
557
+ * form.js — specs matrix
558
+ * query.js — matrix information
592
559
  *
593
560
  * Storage: column-major Float32Array / ArrayLike<number>.
594
- * Element [col*4 + row] = M[row, col].
595
- *
596
561
  * Multiply: mat4Mul(out, A, B) = A · B (standard math order).
597
- *
598
562
  * Pipeline: clip = P · V · M · v
599
- * P = projection (eye → clip)
600
- * V = view (world → eye)
601
- * M = model (local → world)
602
563
  *
603
- * NDC convention parameter (ndcZMin):
604
- * WEBGL = -1 z [−1, 1]
605
- * WEBGPU = 0 z ∈ [0, 1]
564
+ * ── NDC Z convention ──────────────────────────────────────────────────────
565
+ * Passed as `ndcZMin` to every space-transform function:
566
+ * WEBGL = −1 z ∈ [−1, 1]
567
+ * WEBGPU = 0 z ∈ [ 0, 1]
568
+ *
569
+ * ── NDC Y convention ──────────────────────────────────────────────────────
570
+ * Standard (OpenGL / WebGL / WebGPU browser / Three.js / p5v2):
571
+ * NDC y-up — y = +1 at top, y = −1 at bottom.
572
+ * Native Vulkan: NDC y-down — projections constructed with ndcYSign = −1
573
+ * (see form.js). Query functions are convention-agnostic: they work on
574
+ * whatever matrices are passed in.
575
+ *
576
+ * ── Viewport convention ───────────────────────────────────────────────────
577
+ * vp = [x, y, w, h] — w and h are SIGNED.
578
+ *
579
+ * The sign of h encodes the relationship between NDC y and screen y:
580
+ * h < 0 (e.g. −canvasH): screen y-DOWN (DOM / p5 mouseX·mouseY / Vulkan surface)
581
+ * NDC y=+1 → screen y=0 (top)
582
+ * NDC y=−1 → screen y=H (bottom)
583
+ * h > 0 (e.g. +canvasH): screen y-UP (OpenGL desktop / WebGL gl_FragCoord)
584
+ * NDC y=−1 → screen y=0 (bottom)
585
+ * NDC y=+1 → screen y=H (top)
586
+ *
587
+ * Pass [0, canvasH, canvasW, −canvasH] for p5/DOM coordinates.
588
+ * Pass [0, 0, canvasW, canvasH] for WebGL gl_FragCoord / OpenGL bottom-left.
589
+ * All helpers use vp[2]/vp[3] signed — no Math.abs — so both conventions
590
+ * work automatically without any branching.
606
591
  *
607
592
  * All functions follow the out-first, zero-allocation contract.
608
593
  * Returns null on degeneracy (singular matrix, etc.).
@@ -610,10 +595,10 @@ function mat4ToRotation(out4, m) {
610
595
 
611
596
 
612
597
  // ═══════════════════════════════════════════════════════════════════════════
613
- // Mat4 math
598
+ // Mat4 arithmetic
614
599
  // ═══════════════════════════════════════════════════════════════════════════
615
600
 
616
- /** out = A · B (column-major, standard math order) */
601
+ /** out = A · B (column-major) */
617
602
  function mat4Mul(out, A, B) {
618
603
  const a0=A[0],a1=A[1],a2=A[2],a3=A[3],
619
604
  a4=A[4],a5=A[5],a6=A[6],a7=A[7],
@@ -642,229 +627,150 @@ function mat4Mul(out, A, B) {
642
627
  return out;
643
628
  }
644
629
 
645
- /** out = inverse(src). Returns null if singular. */
630
+ /** out = inverse(src). Returns null if singular (|det| < 1e-12). */
646
631
  function mat4Invert(out, src) {
647
- const s=src;
648
- const a00=s[0],a01=s[1],a02=s[2],a03=s[3],
649
- a10=s[4],a11=s[5],a12=s[6],a13=s[7],
650
- a20=s[8],a21=s[9],a22=s[10],a23=s[11],
651
- a30=s[12],a31=s[13],a32=s[14],a33=s[15];
652
- const b00=a00*a11-a01*a10,b01=a00*a12-a02*a10,
653
- b02=a00*a13-a03*a10,b03=a01*a12-a02*a11,
654
- b04=a01*a13-a03*a11,b05=a02*a13-a03*a12,
655
- b06=a20*a31-a21*a30,b07=a20*a32-a22*a30,
656
- b08=a20*a33-a23*a30,b09=a21*a32-a22*a31,
657
- b10=a21*a33-a23*a31,b11=a22*a33-a23*a32;
658
- let det=b00*b11-b01*b10+b02*b09+b03*b08-b04*b07+b05*b06;
632
+ const s0=src[0],s1=src[1],s2=src[2],s3=src[3],
633
+ s4=src[4],s5=src[5],s6=src[6],s7=src[7],
634
+ s8=src[8],s9=src[9],s10=src[10],s11=src[11],
635
+ s12=src[12],s13=src[13],s14=src[14],s15=src[15];
636
+ const b0=s0*s5-s1*s4, b1=s0*s6-s2*s4, b2=s0*s7-s3*s4,
637
+ b3=s1*s6-s2*s5, b4=s1*s7-s3*s5, b5=s2*s7-s3*s6,
638
+ b6=s8*s13-s9*s12, b7=s8*s14-s10*s12, b8=s8*s15-s11*s12,
639
+ b9=s9*s14-s10*s13, b10=s9*s15-s11*s13, b11=s10*s15-s11*s14;
640
+ let det=b0*b11-b1*b10+b2*b9+b3*b8-b4*b7+b5*b6;
659
641
  if (Math.abs(det) < 1e-12) return null;
660
- det=1/det;
661
- out[0]=(a11*b11-a12*b10+a13*b09)*det;
662
- out[1]=(a02*b10-a01*b11-a03*b09)*det;
663
- out[2]=(a31*b05-a32*b04+a33*b03)*det;
664
- out[3]=(a22*b04-a21*b05-a23*b03)*det;
665
- out[4]=(a12*b08-a10*b11-a13*b07)*det;
666
- out[5]=(a00*b11-a02*b08+a03*b07)*det;
667
- out[6]=(a32*b02-a30*b05-a33*b01)*det;
668
- out[7]=(a20*b05-a22*b02+a23*b01)*det;
669
- out[8]=(a10*b10-a11*b08+a13*b06)*det;
670
- out[9]=(a01*b08-a00*b10-a03*b06)*det;
671
- out[10]=(a30*b04-a31*b02+a33*b00)*det;
672
- out[11]=(a21*b02-a20*b04-a23*b00)*det;
673
- out[12]=(a11*b07-a10*b09-a12*b06)*det;
674
- out[13]=(a00*b09-a01*b07+a02*b06)*det;
675
- out[14]=(a31*b01-a30*b03-a32*b00)*det;
676
- out[15]=(a20*b03-a21*b01+a22*b00)*det;
677
- return out;
678
- }
679
-
680
- /** out = transpose(src) */
681
- function mat4Transpose(out, src) {
682
- if (out === src) {
683
- let t;
684
- t=src[1];out[1]=src[4];out[4]=t;
685
- t=src[2];out[2]=src[8];out[8]=t;
686
- t=src[3];out[3]=src[12];out[12]=t;
687
- t=src[6];out[6]=src[9];out[9]=t;
688
- t=src[7];out[7]=src[13];out[13]=t;
689
- t=src[11];out[11]=src[14];out[14]=t;
690
- } else {
691
- out[0]=src[0];out[1]=src[4];out[2]=src[8];out[3]=src[12];
692
- out[4]=src[1];out[5]=src[5];out[6]=src[9];out[7]=src[13];
693
- out[8]=src[2];out[9]=src[6];out[10]=src[10];out[11]=src[14];
694
- out[12]=src[3];out[13]=src[7];out[14]=src[11];out[15]=src[15];
695
- }
642
+ det = 1/det;
643
+ out[0]=(s5*b11-s6*b10+s7*b9)*det;
644
+ out[1]=(s2*b10-s1*b11-s3*b9)*det;
645
+ out[2]=(s13*b5-s14*b4+s15*b3)*det;
646
+ out[3]=(s10*b4-s9*b5-s11*b3)*det;
647
+ out[4]=(s6*b8-s4*b11-s7*b7)*det;
648
+ out[5]=(s0*b11-s2*b8+s3*b7)*det;
649
+ out[6]=(s14*b2-s12*b5-s15*b1)*det;
650
+ out[7]=(s8*b5-s10*b2+s11*b1)*det;
651
+ out[8]=(s4*b10-s5*b8+s7*b6)*det;
652
+ out[9]=(s1*b8-s0*b10-s3*b6)*det;
653
+ out[10]=(s12*b4-s13*b2+s15*b0)*det;
654
+ out[11]=(s9*b2-s8*b4-s11*b0)*det;
655
+ out[12]=(s5*b7-s4*b9-s6*b6)*det;
656
+ out[13]=(s0*b9-s1*b7+s2*b6)*det;
657
+ out[14]=(s13*b1-s12*b3-s14*b0)*det;
658
+ out[15]=(s8*b3-s9*b1+s10*b0)*det;
696
659
  return out;
697
660
  }
698
661
 
699
- /** out[0..8] = inverseTranspose(upper3×3(src)) (normal matrix) */
662
+ /**
663
+ * Normal matrix: inverseTranspose(upper-left 3×3 of src).
664
+ * On degeneracy writes zeros and returns out.
665
+ * @param {Float32Array|number[]} out 9-element destination.
666
+ * @param {Float32Array|number[]} src 16-element mat4.
667
+ */
700
668
  function mat3NormalFromMat4(out, src) {
701
669
  const a00=src[0],a01=src[1],a02=src[2],
702
670
  a10=src[4],a11=src[5],a12=src[6],
703
671
  a20=src[8],a21=src[9],a22=src[10];
704
- const b01=a22*a11-a12*a21,
705
- b11=-a22*a01+a02*a21,
706
- b21=a12*a01-a02*a11;
672
+ const b01=a22*a11-a12*a21, b11=-a22*a01+a02*a21, b21=a12*a01-a02*a11;
707
673
  let det=a00*b01+a10*b11+a20*b21;
708
674
  if (Math.abs(det) < 1e-12) { for(let i=0;i<9;i++)out[i]=0; return out; }
709
675
  det=1/det;
710
- out[0]=b01*det;
711
- out[1]=(-a22*a10+a12*a20)*det;
712
- out[2]=(a21*a10-a11*a20)*det;
713
- out[3]=b11*det;
714
- out[4]=(a22*a00-a02*a20)*det;
715
- out[5]=(-a21*a00+a01*a20)*det;
716
- out[6]=b21*det;
717
- out[7]=(-a12*a00+a02*a10)*det;
718
- out[8]=(a11*a00-a01*a10)*det;
676
+ out[0]=b01*det; out[1]=(-a22*a10+a12*a20)*det; out[2]=(a21*a10-a11*a20)*det;
677
+ out[3]=b11*det; out[4]=(a22*a00-a02*a20)*det; out[5]=(-a21*a00+a01*a20)*det;
678
+ out[6]=b21*det; out[7]=(-a12*a00+a02*a10)*det; out[8]=(a11*a00-a01*a10)*det;
719
679
  return out;
720
680
  }
721
681
 
722
- /** out = mat4 * [x,y,z,1], perspective-divides, writes xyz */
682
+ /** out = mat4 * [x,y,z,1], perspective-divides, writes xyz. */
723
683
  function mat4MulPoint(out, m, x, y, z) {
724
- const rx = m[0]*x + m[4]*y + m[8]*z + m[12];
725
- const ry = m[1]*x + m[5]*y + m[9]*z + m[13];
726
- const rz = m[2]*x + m[6]*y + m[10]*z + m[14];
727
- const rw = m[3]*x + m[7]*y + m[11]*z + m[15];
728
- if (rw !== 0 && rw !== 1) {
729
- out[0] = rx/rw; out[1] = ry/rw; out[2] = rz/rw;
730
- } else {
731
- out[0] = rx; out[1] = ry; out[2] = rz;
732
- }
684
+ 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],
685
+ rz=m[2]*x+m[6]*y+m[10]*z+m[14], rw=m[3]*x+m[7]*y+m[11]*z+m[15];
686
+ if (rw!==0&&rw!==1) { out[0]=rx/rw; out[1]=ry/rw; out[2]=rz/rw; }
687
+ else { out[0]=rx; out[1]=ry; out[2]=rz; }
733
688
  return out;
734
689
  }
735
690
 
736
691
  /**
737
- * Apply only the 3×3 linear block of a mat4 to a direction vector.
738
- * No translation, no perspective divide. Suitable for directions and normals
739
- * when the matrix is known to be orthogonal (use mat3NormalFromMat4 for normals
740
- * under non-uniform scale).
741
- *
742
- * @param {Float32Array|number[]} out 3-element destination.
743
- * @param {Float32Array|number[]} m 16-element mat4.
744
- * @param {number} dx,dy,dz Input direction.
745
- * @returns {Float32Array|number[]} out
692
+ * Apply only the 3×3 linear block of a mat4 to a direction (no translation,
693
+ * no perspective divide). Use mat3NormalFromMat4 for normals under non-uniform scale.
746
694
  */
747
695
  function mat4MulDir(out, m, dx, dy, dz) {
748
- out[0] = m[0]*dx + m[4]*dy + m[8]*dz;
749
- out[1] = m[1]*dx + m[5]*dy + m[9]*dz;
750
- out[2] = m[2]*dx + m[6]*dy + m[10]*dz;
696
+ out[0]=m[0]*dx+m[4]*dy+m[8]*dz;
697
+ out[1]=m[1]*dx+m[5]*dy+m[9]*dz;
698
+ out[2]=m[2]*dx+m[6]*dy+m[10]*dz;
751
699
  return out;
752
700
  }
753
701
 
754
702
  // ═══════════════════════════════════════════════════════════════════════════
755
- // Projection queries (read scalars from a projection mat4)
703
+ // Projection queries
756
704
  // ═══════════════════════════════════════════════════════════════════════════
757
705
 
758
- /** @returns {boolean} true if orthographic */
706
+ /** @returns {boolean} true if orthographic. */
759
707
  function projIsOrtho(p) { return p[15] !== 0; }
760
708
 
761
709
  /**
762
710
  * Near plane distance.
763
- * @param {ArrayLike<number>} p Projection Mat4.
764
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
711
+ * @param {ArrayLike<number>} p Projection mat4.
712
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
765
713
  */
766
714
  function projNear(p, ndcZMin) {
767
- return p[15] === 0
768
- ? p[14] / (p[10] + ndcZMin)
769
- : (p[14] - ndcZMin) / p[10];
715
+ return p[15]===0 ? p[14]/(p[10]+ndcZMin) : (p[14]-ndcZMin)/p[10];
770
716
  }
771
717
 
772
- /** Far plane distance (convention-independent: far always maps to NDC z=1). */
718
+ /** Far plane distance (far always maps to NDC z = 1, convention-independent). */
773
719
  function projFar(p) {
774
- return p[15] === 0
775
- ? p[14] / (1 + p[10])
776
- : (p[14] - 1) / p[10];
777
- }
778
-
779
- function projLeft(p, ndcZMin) {
780
- return p[15] === 1
781
- ? -(1 + p[12]) / p[0]
782
- : projNear(p, ndcZMin) * (p[8] - 1) / p[0];
720
+ return p[15]===0 ? p[14]/(1+p[10]) : (p[14]-1)/p[10];
783
721
  }
784
722
 
785
- function projRight(p, ndcZMin) {
786
- return p[15] === 1
787
- ? (1 - p[12]) / p[0]
788
- : projNear(p, ndcZMin) * (1 + p[8]) / p[0];
789
- }
723
+ function projLeft (p, ndcZMin) { return p[15]===1 ? -(1+p[12])/p[0] : projNear(p,ndcZMin)*(p[8]-1)/p[0]; }
724
+ function projRight (p, ndcZMin) { return p[15]===1 ? (1-p[12])/p[0] : projNear(p,ndcZMin)*(1+p[8])/p[0]; }
725
+ function projTop (p, ndcZMin) { return p[15]===1 ? (p[13]-1)/p[5] : projNear(p,ndcZMin)*(p[9]-1)/p[5]; }
726
+ function projBottom(p, ndcZMin) { return p[15]===1 ? (1+p[13])/p[5] : projNear(p,ndcZMin)*(1+p[9])/p[5]; }
790
727
 
791
- function projTop(p, ndcZMin) {
792
- return p[15] === 1
793
- ? (p[13] - 1) / p[5]
794
- : projNear(p, ndcZMin) * (p[9] - 1) / p[5];
795
- }
796
-
797
- function projBottom(p, ndcZMin) {
798
- return p[15] === 1
799
- ? (1 + p[13]) / p[5]
800
- : projNear(p, ndcZMin) * (1 + p[9]) / p[5];
801
- }
802
-
803
- /** Vertical fov (radians, perspective only). */
804
- function projFov(p) {
805
- return Math.abs(2 * Math.atan(1 / p[5]));
806
- }
807
-
808
- /** Horizontal fov (radians, perspective only). */
809
- function projHfov(p) {
810
- return Math.abs(2 * Math.atan(1 / p[0]));
811
- }
728
+ /** Vertical field of view in radians (perspective only). */
729
+ function projFov (p) { return Math.abs(2*Math.atan(1/p[5])); }
730
+ /** Horizontal field of view in radians (perspective only). */
731
+ function projHfov(p) { return Math.abs(2*Math.atan(1/p[0])); }
812
732
 
813
733
  // ═══════════════════════════════════════════════════════════════════════════
814
- // Derived matrices (convenience)
734
+ // Derived matrices
815
735
  // ═══════════════════════════════════════════════════════════════════════════
816
736
 
817
737
  /** out = P · V */
818
- function mat4PV(out, proj, view) { return mat4Mul(out, proj, view); }
819
-
738
+ function mat4PV(out, proj, view) { return mat4Mul(out, proj, view); }
820
739
  /** out = V · M */
821
740
  function mat4MV(out, model, view) { return mat4Mul(out, view, model); }
822
741
 
823
742
  // ═══════════════════════════════════════════════════════════════════════════
824
- // Location / Direction transforms
743
+ // Frame-relative transforms
825
744
  // ═══════════════════════════════════════════════════════════════════════════
826
745
 
827
746
  /**
828
- * Relative transform for locations (points): out = inv(to) · from.
829
- * @param {ArrayLike<number>} out 16-element destination.
830
- * @param {ArrayLike<number>} from Source frame transform.
831
- * @param {ArrayLike<number>} to Destination frame transform.
832
- * @returns {ArrayLike<number>|null} out, or null if to is singular.
747
+ * Location transform between frames: out = inv(to) · from.
748
+ * @returns {ArrayLike<number>|null} out, or null if `to` is singular.
833
749
  */
834
750
  function mat4Location(out, from, to) {
835
751
  return mat4Invert(out, to) && mat4Mul(out, out, from);
836
752
  }
837
753
 
838
754
  /**
839
- * Relative transform for directions (vectors): out = to₃ · inv(from₃).
840
- * Uses only the upper-left 3×3 blocks, ignoring translation.
841
- * @param {ArrayLike<number>} out 9-element destination.
842
- * @param {ArrayLike<number>} from Source frame transform.
843
- * @param {ArrayLike<number>} to Destination frame transform.
844
- * @returns {ArrayLike<number>|null} out, or null if from is singular.
755
+ * Direction transform between frames: out = to₃ · inv(from₃).
756
+ * Uses only the upper-left 3×3 blocks (rotation/scale, no translation).
757
+ * @returns {ArrayLike<number>|null} out, or null if `from` is singular.
845
758
  */
846
759
  function mat3Direction(out, from, to) {
847
- const a00=from[0], a01=from[1], a02=from[2],
848
- a10=from[4], a11=from[5], a12=from[6],
849
- a20=from[8], a21=from[9], a22=from[10];
850
- const b01=a22*a11-a12*a21,
851
- b11=a12*a20-a22*a10,
852
- b21=a21*a10-a11*a20;
760
+ const a00=from[0],a01=from[1],a02=from[2],
761
+ a10=from[4],a11=from[5],a12=from[6],
762
+ a20=from[8],a21=from[9],a22=from[10];
763
+ const b01=a22*a11-a12*a21, b11=a12*a20-a22*a10, b21=a21*a10-a11*a20;
853
764
  let det=a00*b01+a01*b11+a02*b21;
854
765
  if (Math.abs(det) < 1e-12) return null;
855
766
  det=1/det;
856
- const i00=b01*det, i01=(a02*a21-a22*a01)*det, i02=(a12*a01-a02*a11)*det;
857
- const i10=b11*det, i11=(a22*a00-a02*a20)*det, i12=(a02*a10-a12*a00)*det;
858
- const i20=b21*det, i21=(a01*a20-a21*a00)*det, i22=(a11*a00-a01*a10)*det;
859
- const t00=to[0], t01=to[1], t02=to[2],
860
- t10=to[4], t11=to[5], t12=to[6],
861
- t20=to[8], t21=to[9], t22=to[10];
862
- const m00=t00*i00+t10*i01+t20*i02, m01=t01*i00+t11*i01+t21*i02, m02=t02*i00+t12*i01+t22*i02;
863
- const m10=t00*i10+t10*i11+t20*i12, m11=t01*i10+t11*i11+t21*i12, m12=t02*i10+t12*i11+t22*i12;
864
- const m20=t00*i20+t10*i21+t20*i22, m21=t01*i20+t11*i21+t21*i22, m22=t02*i20+t12*i21+t22*i22;
865
- out[0]=m00; out[1]=m10; out[2]=m20;
866
- out[3]=m01; out[4]=m11; out[5]=m21;
867
- out[6]=m02; out[7]=m12; out[8]=m22;
767
+ const i00=b01*det, i01=(a02*a21-a22*a01)*det, i02=(a12*a01-a02*a11)*det;
768
+ const i10=b11*det, i11=(a22*a00-a02*a20)*det, i12=(a02*a10-a12*a00)*det;
769
+ const i20=b21*det, i21=(a01*a20-a21*a00)*det, i22=(a11*a00-a01*a10)*det;
770
+ const t00=to[0],t01=to[1],t02=to[2], t10=to[4],t11=to[5],t12=to[6], t20=to[8],t21=to[9],t22=to[10];
771
+ out[0]=t00*i00+t10*i01+t20*i02; out[1]=t01*i00+t11*i01+t21*i02; out[2]=t02*i00+t12*i01+t22*i02;
772
+ out[3]=t00*i10+t10*i11+t20*i12; out[4]=t01*i10+t11*i11+t21*i12; out[5]=t02*i10+t12*i11+t22*i12;
773
+ out[6]=t00*i20+t10*i21+t20*i22; out[7]=t01*i20+t11*i21+t21*i22; out[8]=t02*i20+t12*i21+t22*i22;
868
774
  return out;
869
775
  }
870
776
 
@@ -872,215 +778,177 @@ function mat3Direction(out, from, to) {
872
778
  // Space transforms — mapLocation / mapDirection
873
779
  // ═══════════════════════════════════════════════════════════════════════════
874
780
  //
875
- // FLAT DISPATCH: every from→to pair is a self-contained leaf.
876
- // No path calls back into mapLocation/mapDirection (no reentrancy).
877
- // All intermediates are stack locals (zero shared state).
781
+ // Flat dispatch: every from→to pair is a self-contained leaf with only stack
782
+ // locals no reentrancy, no shared state between calls.
878
783
  //
879
- // Matrices bag m:
880
- // {
881
- // pMatrix: Float32Array(16) — projection (eyeclip)
882
- // vMatrix: Float32Array(16) — view (worldeye)
883
- // eMatrix?: Float32Array(16) eye (eye world, inv view); lazy
884
- // pvMatrix?: Float32Array(16) P · V; lazy
885
- // ipvMatrix?: Float32Array(16) inv(P · V); lazy
886
- // fromFrame?: Float32Array(16) MATRIX source frame (custom space)
887
- // toFrameInv?:Float32Array(16) — inv(MATRIX dest frame)
888
- // }
784
+ // Matrices bag `m`:
785
+ // mat4Proj Float32Array(16) projection (eye → clip)
786
+ // mat4View Float32Array(16) view (worldeye)
787
+ // mat4Eye? Float32Array(16) eye (eyeworld); caller fills before passing
788
+ // mat4PV? Float32Array(16) P · V; caller fills or _ensurePV allocates once
789
+ // mat4PVInv? Float32Array(16) inv(P · V); caller fills
790
+ // fromFrame? Float32Array(16) MATRIX source frame
791
+ // toFrameInv? Float32Array(16) inv(MATRIX dest frame)
792
+ //
793
+ // Viewport `vp` = [x, y, w, h]:
794
+ // Use SIGNED h to encode screen-y direction (see module header).
795
+ // Core formula: screen = (ndc*0.5+0.5)*vp[k] + vp[k-2] (k=2 for x, k=3 for y)
796
+ // Inverse: ndc = ((screen-vp[k-2])/vp[k])*2 - 1
797
+ // Negative vp[3] flips NDC y-up to screen y-down automatically.
889
798
  //
890
799
 
891
- // ── Location leaf helpers ────────────────────────────────────────────────
800
+ // ── Location helpers ─────────────────────────────────────────────────────
892
801
 
893
802
  function _worldToScreen(out, px, py, pz, pv, vp, ndcZMin) {
894
- const x = pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
895
- const y = pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
896
- const z = pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
897
- const w = pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
898
- const xi = (w !== 0 && w !== 1) ? 1/w : 1;
899
- const nx = x*xi, ny = y*xi, nz = z*xi;
900
- const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
901
- out[0] = vpX + vpW * (nx + 1) * 0.5;
902
- out[1] = vpY + vpH * (1 - (ny + 1) * 0.5);
903
- out[2] = (nz - ndcZMin) / (1 - ndcZMin);
803
+ 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],
804
+ z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14], w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
805
+ const xi=(w!==0&&w!==1)?1/w:1;
806
+ out[0]=(x*xi*0.5+0.5)*vp[2]+vp[0];
807
+ out[1]=(y*xi*0.5+0.5)*vp[3]+vp[1];
808
+ out[2]=(z*xi-ndcZMin)/(1-ndcZMin);
904
809
  return out;
905
810
  }
906
811
 
907
812
  function _screenToWorld(out, sx, sy, sz, ipv, vp, ndcZMin) {
908
- const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
909
- const nx = (sx - vpX) / vpW * 2 - 1;
910
- const ny = 1 - (sy - vpY) / vpH * 2;
911
- const nz = sz * (1 - ndcZMin) + ndcZMin;
912
- return mat4MulPoint(out, ipv, nx, ny, nz);
813
+ return mat4MulPoint(out, ipv,
814
+ ((sx-vp[0])/vp[2])*2-1,
815
+ ((sy-vp[1])/vp[3])*2-1,
816
+ sz*(1-ndcZMin)+ndcZMin);
913
817
  }
914
818
 
915
819
  function _worldToNDC(out, px, py, pz, pv) {
916
- const x=pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
917
- const y=pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
918
- const z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
919
- const w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
920
- const xi = (w !== 0 && w !== 1) ? 1/w : 1;
820
+ 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],
821
+ z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14], w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
822
+ const xi=(w!==0&&w!==1)?1/w:1;
921
823
  out[0]=x*xi; out[1]=y*xi; out[2]=z*xi;
922
824
  return out;
923
825
  }
924
826
 
925
- function _ndcToWorld(out, nx, ny, nz, ipv) {
926
- return mat4MulPoint(out, ipv, nx, ny, nz);
927
- }
827
+ function _ndcToWorld(out, nx, ny, nz, ipv) { return mat4MulPoint(out,ipv,nx,ny,nz); }
928
828
 
929
829
  function _screenToNDC(out, sx, sy, sz, vp, ndcZMin) {
930
- const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
931
- out[0] = (sx - vpX) / vpW * 2 - 1;
932
- out[1] = 1 - (sy - vpY) / vpH * 2;
933
- out[2] = sz * (1 - ndcZMin) + ndcZMin;
830
+ out[0]=((sx-vp[0])/vp[2])*2-1;
831
+ out[1]=((sy-vp[1])/vp[3])*2-1;
832
+ out[2]=sz*(1-ndcZMin)+ndcZMin;
934
833
  return out;
935
834
  }
936
835
 
937
836
  function _ndcToScreen(out, nx, ny, nz, vp, ndcZMin) {
938
- const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
939
- out[0] = vpX + vpW * (nx + 1) * 0.5;
940
- out[1] = vpY + vpH * (1 - (ny + 1) * 0.5);
941
- out[2] = (nz - ndcZMin) / (1 - ndcZMin);
837
+ out[0]=(nx*0.5+0.5)*vp[2]+vp[0];
838
+ out[1]=(ny*0.5+0.5)*vp[3]+vp[1];
839
+ out[2]=(nz-ndcZMin)/(1-ndcZMin);
942
840
  return out;
943
841
  }
944
842
 
945
843
  function _ensurePV(m) {
946
- if (m.pvMatrix) return m.pvMatrix;
947
- m.pvMatrix = new Float32Array(16);
948
- mat4Mul(m.pvMatrix, m.pMatrix, m.vMatrix);
949
- return m.pvMatrix;
844
+ if (m.mat4PV) return m.mat4PV;
845
+ m.mat4PV = new Float32Array(16);
846
+ mat4Mul(m.mat4PV, m.mat4Proj, m.mat4View);
847
+ return m.mat4PV;
950
848
  }
951
849
 
952
850
  /**
953
851
  * Map a point between named coordinate spaces.
954
852
  *
955
- * @param {Vec3} out Result written here.
956
- * @param {number} px,py,pz Input point.
957
- * @param {string} from Source space constant.
958
- * @param {string} to Target space constant.
959
- * @param {object} m Matrices bag:
960
- * { pMatrix, vMatrix, eMatrix?, pvMatrix?, ipvMatrix?, fromFrame?, toFrameInv? }
961
- * @param {Vec4} vp Viewport [x, y, width, height].
962
- * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
853
+ * @param {number[]} out 3-element destination — written and returned.
854
+ * @param {number} px,py,pz Input point.
855
+ * @param {string} from Source space (WORLD, EYE, SCREEN, NDC, MATRIX).
856
+ * @param {string} to Destination space.
857
+ * @param {object} m Matrices bag — see module header.
858
+ * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
859
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
860
+ * @returns {number[]} out
963
861
  */
964
862
  function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
965
- // WORLD SCREEN
966
- if (from === WORLD && to === SCREEN)
967
- return _worldToScreen(out, px,py,pz, _ensurePV(m), vp, ndcZMin);
968
- if (from === SCREEN && to === WORLD)
969
- return _screenToWorld(out, px,py,pz, m.ipvMatrix, vp, ndcZMin);
970
-
971
- // WORLD NDC
972
- if (from === WORLD && to === NDC)
973
- return _worldToNDC(out, px,py,pz, _ensurePV(m));
974
- if (from === NDC && to === WORLD)
975
- return _ndcToWorld(out, px,py,pz, m.ipvMatrix);
976
-
977
- // SCREEN ↔ NDC
978
- if (from === SCREEN && to === NDC)
979
- return _screenToNDC(out, px,py,pz, vp, ndcZMin);
980
- if (from === NDC && to === SCREEN)
981
- return _ndcToScreen(out, px,py,pz, vp, ndcZMin);
982
-
983
- // WORLD ↔ EYE
984
- if (from === WORLD && to === EYE)
985
- return mat4MulPoint(out, m.vMatrix, px,py,pz);
986
- if (from === EYE && to === WORLD)
987
- return mat4MulPoint(out, m.eMatrix, px,py,pz);
988
-
989
- // EYE ↔ SCREEN
990
- if (from === EYE && to === SCREEN) {
991
- const e = m.eMatrix;
992
- const ex=e[0]*px+e[4]*py+e[8]*pz+e[12],
993
- ey=e[1]*px+e[5]*py+e[9]*pz+e[13],
994
- ez=e[2]*px+e[6]*py+e[10]*pz+e[14];
995
- return _worldToScreen(out, ex,ey,ez, _ensurePV(m), vp, ndcZMin);
863
+ if (from===WORLD && to===SCREEN) return _worldToScreen(out,px,py,pz,_ensurePV(m),vp,ndcZMin);
864
+ if (from===SCREEN && to===WORLD) return _screenToWorld(out,px,py,pz,m.mat4PVInv,vp,ndcZMin);
865
+
866
+ if (from===WORLD && to===NDC) return _worldToNDC(out,px,py,pz,_ensurePV(m));
867
+ if (from===NDC && to===WORLD) return _ndcToWorld(out,px,py,pz,m.mat4PVInv);
868
+
869
+ if (from===SCREEN && to===NDC) return _screenToNDC(out,px,py,pz,vp,ndcZMin);
870
+ if (from===NDC && to===SCREEN) return _ndcToScreen(out,px,py,pz,vp,ndcZMin);
871
+
872
+ if (from===WORLD && to===EYE) return mat4MulPoint(out,m.mat4View,px,py,pz);
873
+ if (from===EYE && to===WORLD) return mat4MulPoint(out,m.mat4Eye,px,py,pz);
874
+
875
+ if (from===EYE && to===SCREEN) {
876
+ const e=m.mat4Eye;
877
+ return _worldToScreen(out,e[0]*px+e[4]*py+e[8]*pz+e[12],
878
+ e[1]*px+e[5]*py+e[9]*pz+e[13],
879
+ e[2]*px+e[6]*py+e[10]*pz+e[14],_ensurePV(m),vp,ndcZMin);
996
880
  }
997
- if (from === SCREEN && to === EYE) {
998
- _screenToWorld(out, px,py,pz, m.ipvMatrix, vp, ndcZMin);
999
- const wx=out[0],wy=out[1],wz=out[2];
1000
- return mat4MulPoint(out, m.vMatrix, wx,wy,wz);
881
+ if (from===SCREEN && to===EYE) {
882
+ _screenToWorld(out,px,py,pz,m.mat4PVInv,vp,ndcZMin);
883
+ return mat4MulPoint(out,m.mat4View,out[0],out[1],out[2]);
1001
884
  }
1002
885
 
1003
- // EYE NDC
1004
- if (from === EYE && to === NDC) {
1005
- const e = m.eMatrix;
1006
- const ex=e[0]*px+e[4]*py+e[8]*pz+e[12],
1007
- ey=e[1]*px+e[5]*py+e[9]*pz+e[13],
1008
- ez=e[2]*px+e[6]*py+e[10]*pz+e[14];
1009
- return _worldToNDC(out, ex,ey,ez, _ensurePV(m));
886
+ if (from===EYE && to===NDC) {
887
+ const e=m.mat4Eye;
888
+ return _worldToNDC(out,e[0]*px+e[4]*py+e[8]*pz+e[12],
889
+ e[1]*px+e[5]*py+e[9]*pz+e[13],
890
+ e[2]*px+e[6]*py+e[10]*pz+e[14],_ensurePV(m));
1010
891
  }
1011
- if (from === NDC && to === EYE) {
1012
- _ndcToWorld(out, px,py,pz, m.ipvMatrix);
1013
- const wx=out[0],wy=out[1],wz=out[2];
1014
- return mat4MulPoint(out, m.vMatrix, wx,wy,wz);
892
+ if (from===NDC && to===EYE) {
893
+ _ndcToWorld(out,px,py,pz,m.mat4PVInv);
894
+ return mat4MulPoint(out,m.mat4View,out[0],out[1],out[2]);
1015
895
  }
1016
896
 
1017
- // MATRIX (custom frame) WORLD
1018
- if (from === MATRIX && to === WORLD)
1019
- return mat4MulPoint(out, m.fromFrame, px,py,pz);
1020
- if (from === WORLD && to === MATRIX)
1021
- return mat4MulPoint(out, m.toFrameInv, px,py,pz);
1022
-
1023
- // MATRIX ↔ EYE
1024
- if (from === MATRIX && to === EYE) {
1025
- const f = m.fromFrame;
1026
- const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
1027
- fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
1028
- fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
1029
- return mat4MulPoint(out, m.vMatrix, fx,fy,fz);
897
+ if (from===MATRIX && to===WORLD) return mat4MulPoint(out,m.fromFrame,px,py,pz);
898
+ if (from===WORLD && to===MATRIX) return mat4MulPoint(out,m.toFrameInv,px,py,pz);
899
+
900
+ if (from===MATRIX && to===EYE) {
901
+ const f=m.fromFrame;
902
+ return mat4MulPoint(out,m.mat4View,f[0]*px+f[4]*py+f[8]*pz+f[12],
903
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
904
+ f[2]*px+f[6]*py+f[10]*pz+f[14]);
1030
905
  }
1031
- if (from === EYE && to === MATRIX) {
1032
- const e = m.eMatrix;
1033
- const ex=e[0]*px+e[4]*py+e[8]*pz+e[12],
1034
- ey=e[1]*px+e[5]*py+e[9]*pz+e[13],
1035
- ez=e[2]*px+e[6]*py+e[10]*pz+e[14];
1036
- return mat4MulPoint(out, m.toFrameInv, ex,ey,ez);
906
+ if (from===EYE && to===MATRIX) {
907
+ const e=m.mat4Eye;
908
+ return mat4MulPoint(out,m.toFrameInv,e[0]*px+e[4]*py+e[8]*pz+e[12],
909
+ e[1]*px+e[5]*py+e[9]*pz+e[13],
910
+ e[2]*px+e[6]*py+e[10]*pz+e[14]);
1037
911
  }
1038
912
 
1039
- // MATRIX SCREEN
1040
- if (from === MATRIX && to === SCREEN) {
1041
- const f = m.fromFrame;
1042
- const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
1043
- fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
1044
- fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
1045
- return _worldToScreen(out, fx,fy,fz, _ensurePV(m), vp, ndcZMin);
913
+ if (from===MATRIX && to===SCREEN) {
914
+ const f=m.fromFrame;
915
+ return _worldToScreen(out,f[0]*px+f[4]*py+f[8]*pz+f[12],
916
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
917
+ f[2]*px+f[6]*py+f[10]*pz+f[14],_ensurePV(m),vp,ndcZMin);
1046
918
  }
1047
- if (from === SCREEN && to === MATRIX) {
1048
- _screenToWorld(out, px,py,pz, m.ipvMatrix, vp, ndcZMin);
1049
- const wx=out[0],wy=out[1],wz=out[2];
1050
- return mat4MulPoint(out, m.toFrameInv, wx,wy,wz);
919
+ if (from===SCREEN && to===MATRIX) {
920
+ _screenToWorld(out,px,py,pz,m.mat4PVInv,vp,ndcZMin);
921
+ return mat4MulPoint(out,m.toFrameInv,out[0],out[1],out[2]);
1051
922
  }
1052
923
 
1053
- // MATRIX NDC
1054
- if (from === MATRIX && to === NDC) {
1055
- const f = m.fromFrame;
1056
- const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
1057
- fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
1058
- fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
1059
- return _worldToNDC(out, fx,fy,fz, _ensurePV(m));
924
+ if (from===MATRIX && to===NDC) {
925
+ const f=m.fromFrame;
926
+ return _worldToNDC(out,f[0]*px+f[4]*py+f[8]*pz+f[12],
927
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
928
+ f[2]*px+f[6]*py+f[10]*pz+f[14],_ensurePV(m));
1060
929
  }
1061
- if (from === NDC && to === MATRIX) {
1062
- _ndcToWorld(out, px,py,pz, m.ipvMatrix);
1063
- const wx=out[0],wy=out[1],wz=out[2];
1064
- return mat4MulPoint(out, m.toFrameInv, wx,wy,wz);
930
+ if (from===NDC && to===MATRIX) {
931
+ _ndcToWorld(out,px,py,pz,m.mat4PVInv);
932
+ return mat4MulPoint(out,m.toFrameInv,out[0],out[1],out[2]);
1065
933
  }
1066
934
 
1067
- // MATRIX MATRIX
1068
- if (from === MATRIX && to === MATRIX) {
1069
- const f = m.fromFrame;
1070
- const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
1071
- fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
1072
- fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
1073
- return mat4MulPoint(out, m.toFrameInv, fx,fy,fz);
935
+ if (from===MATRIX && to===MATRIX) {
936
+ const f=m.fromFrame;
937
+ return mat4MulPoint(out,m.toFrameInv,f[0]*px+f[4]*py+f[8]*pz+f[12],
938
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
939
+ f[2]*px+f[6]*py+f[10]*pz+f[14]);
1074
940
  }
1075
941
 
1076
- // Fallback
1077
942
  out[0]=px; out[1]=py; out[2]=pz;
1078
943
  return out;
1079
944
  }
1080
945
 
1081
- // ── Direction leaf helpers ───────────────────────────────────────────────
946
+ // ── Direction helpers ────────────────────────────────────────────────────
947
+ //
948
+ // Directions use only the linear 3×3 block — no translation, no w-divide.
949
+ // The signed vp[2]/vp[3] carries the y-convention automatically.
950
+ //
1082
951
 
1083
- /** Apply the 3×3 linear part of a mat4 (rotation/scale, no translation). */
1084
952
  function _applyDir(out, m, dx, dy, dz) {
1085
953
  out[0]=m[0]*dx+m[4]*dy+m[8]*dz;
1086
954
  out[1]=m[1]*dx+m[5]*dy+m[9]*dz;
@@ -1089,157 +957,123 @@ function _applyDir(out, m, dx, dy, dz) {
1089
957
  }
1090
958
 
1091
959
  function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
1092
- // Transform to clip space (no w divide for direction).
1093
- const vx=view[0]*dx+view[4]*dy+view[8]*dz;
1094
- const vy=view[1]*dx+view[5]*dy+view[9]*dz;
1095
- const vz=view[2]*dx+view[6]*dy+view[10]*dz;
1096
- const cx=proj[0]*vx+proj[4]*vy+proj[8]*vz;
1097
- const cy=proj[1]*vx+proj[5]*vy+proj[9]*vz;
1098
- const cz=proj[2]*vx+proj[6]*vy+proj[10]*vz;
1099
- // NDC→screen scale (direction, no offset).
1100
- out[0]=cx*vpW*0.5; out[1]=-cy*vpH*0.5;
1101
- out[2]=cz*(1-ndcZMin)*0.5;
960
+ const vx=view[0]*dx+view[4]*dy+view[8]*dz,
961
+ vy=view[1]*dx+view[5]*dy+view[9]*dz,
962
+ vz=view[2]*dx+view[6]*dy+view[10]*dz;
963
+ // vpH is signed — negative flips y component automatically.
964
+ out[0]=(proj[0]*vx+proj[4]*vy+proj[8]*vz)*vpW*0.5;
965
+ out[1]=(proj[1]*vx+proj[5]*vy+proj[9]*vz)*vpH*0.5;
966
+ out[2]=(proj[2]*vx+proj[6]*vy+proj[10]*vz)*(1-ndcZMin)*0.5;
1102
967
  return out;
1103
968
  }
1104
969
 
1105
- function _screenToWorldDir(out, dx, dy, dz, proj, eMatrix, vpW, vpH, ndcZMin) {
1106
- // Screen direction NDC direction.
1107
- const nx=dx/(vpW*0.5), ny=-dy/(vpH*0.5);
1108
- const nz=dz/((1-ndcZMin)*0.5);
1109
- // NDC direction → eye direction (inverse projection, linear only).
1110
- const ex=nx/proj[0], ey=ny/proj[5], ez=nz;
1111
- // Eye direction → world direction.
1112
- _applyDir(out, eMatrix, ex, ey, ez);
970
+ function _screenToWorldDir(out, dx, dy, dz, proj, eye, vpW, vpH, ndcZMin) {
971
+ // Inverse of _worldToScreenDir; signed vpW/vpH cancel the y-flip.
972
+ _applyDir(out, eye, dx/(vpW*0.5)/proj[0], dy/(vpH*0.5)/proj[5], dz/((1-ndcZMin)*0.5));
1113
973
  return out;
1114
974
  }
1115
975
 
1116
976
  function _screenToNDCDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
1117
- out[0]=dx/(vpW*0.5); out[1]=-dy/(vpH*0.5);
1118
- out[2]=dz/((1-ndcZMin)*0.5);
977
+ out[0]=dx/(vpW*0.5); out[1]=dy/(vpH*0.5); out[2]=dz/((1-ndcZMin)*0.5);
1119
978
  return out;
1120
979
  }
1121
980
 
1122
981
  function _ndcToScreenDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
1123
- out[0]=dx*vpW*0.5; out[1]=-dy*vpH*0.5;
1124
- out[2]=dz*(1-ndcZMin)*0.5;
982
+ out[0]=dx*vpW*0.5; out[1]=dy*vpH*0.5; out[2]=dz*(1-ndcZMin)*0.5;
1125
983
  return out;
1126
984
  }
1127
985
 
1128
986
  /**
1129
987
  * Map a direction between named coordinate spaces.
1130
- * Same bag contract as mapLocation.
988
+ * Same bag and viewport contract as mapLocation.
989
+ *
990
+ * @param {number[]} out 3-element destination.
991
+ * @param {number} dx,dy,dz Input direction.
992
+ * @param {string} from Source space.
993
+ * @param {string} to Destination space.
994
+ * @param {object} m Matrices bag — see module header.
995
+ * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
996
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
997
+ * @returns {number[]} out
1131
998
  */
1132
999
  function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
1133
- const vpW = Math.abs(vp[2]), vpH = Math.abs(vp[3]);
1134
-
1135
- // EYE WORLD (most common)
1136
- if (from === EYE && to === WORLD) return _applyDir(out, m.eMatrix, dx, dy, dz);
1137
- if (from === WORLD && to === EYE) return _applyDir(out, m.vMatrix, dx, dy, dz);
1138
-
1139
- // WORLD SCREEN
1140
- if (from === WORLD && to === SCREEN)
1141
- return _worldToScreenDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
1142
- if (from === SCREEN && to === WORLD)
1143
- return _screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
1144
-
1145
- // SCREEN ↔ NDC
1146
- if (from === SCREEN && to === NDC)
1147
- return _screenToNDCDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
1148
- if (from === NDC && to === SCREEN)
1149
- return _ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
1150
-
1151
- // WORLD ↔ NDC
1152
- if (from === WORLD && to === NDC) {
1153
- _worldToScreenDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
1154
- const sx=out[0],sy=out[1],sz=out[2];
1155
- return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
1000
+ const vpW=vp[2], vpH=vp[3]; // signed — carry y-convention through all helpers
1001
+
1002
+ if (from===EYE && to===WORLD) return _applyDir(out,m.mat4Eye, dx,dy,dz);
1003
+ if (from===WORLD && to===EYE) return _applyDir(out,m.mat4View,dx,dy,dz);
1004
+
1005
+ if (from===WORLD && to===SCREEN) return _worldToScreenDir(out,dx,dy,dz,m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1006
+ if (from===SCREEN && to===WORLD) return _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye, vpW,vpH,ndcZMin);
1007
+
1008
+ if (from===SCREEN && to===NDC) return _screenToNDCDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1009
+ if (from===NDC && to===SCREEN) return _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1010
+
1011
+ if (from===WORLD && to===NDC) {
1012
+ _worldToScreenDir(out,dx,dy,dz,m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1013
+ return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
1156
1014
  }
1157
- if (from === NDC && to === WORLD) {
1158
- _ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
1159
- const sx=out[0],sy=out[1],sz=out[2];
1160
- return _screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
1015
+ if (from===NDC && to===WORLD) {
1016
+ _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1017
+ return _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1161
1018
  }
1162
1019
 
1163
- // EYE SCREEN
1164
- if (from === EYE && to === SCREEN) {
1165
- _applyDir(out, m.eMatrix, dx,dy,dz);
1166
- const wx=out[0],wy=out[1],wz=out[2];
1167
- return _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
1020
+ if (from===EYE && to===SCREEN) {
1021
+ _applyDir(out,m.mat4Eye,dx,dy,dz);
1022
+ return _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1168
1023
  }
1169
- if (from === SCREEN && to === EYE) {
1170
- _screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
1171
- const wx=out[0],wy=out[1],wz=out[2];
1172
- return _applyDir(out, m.vMatrix, wx,wy,wz);
1024
+ if (from===SCREEN && to===EYE) {
1025
+ _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1026
+ return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
1173
1027
  }
1174
1028
 
1175
- // EYE NDC
1176
- if (from === EYE && to === NDC) {
1177
- _applyDir(out, m.eMatrix, dx,dy,dz);
1178
- const wx=out[0],wy=out[1],wz=out[2];
1179
- _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
1180
- const sx=out[0],sy=out[1],sz=out[2];
1181
- return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
1029
+ if (from===EYE && to===NDC) {
1030
+ _applyDir(out,m.mat4Eye,dx,dy,dz);
1031
+ _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1032
+ return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
1182
1033
  }
1183
- if (from === NDC && to === EYE) {
1184
- _ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
1185
- const sx=out[0],sy=out[1],sz=out[2];
1186
- _screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
1187
- const wx=out[0],wy=out[1],wz=out[2];
1188
- return _applyDir(out, m.vMatrix, wx,wy,wz);
1034
+ if (from===NDC && to===EYE) {
1035
+ _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1036
+ _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1037
+ return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
1189
1038
  }
1190
1039
 
1191
- // MATRIX WORLD
1192
- if (from === MATRIX && to === WORLD) return _applyDir(out, m.fromFrame, dx,dy,dz);
1193
- if (from === WORLD && to === MATRIX) return _applyDir(out, m.toFrameInv, dx,dy,dz);
1040
+ if (from===MATRIX && to===WORLD) return _applyDir(out,m.fromFrame, dx,dy,dz);
1041
+ if (from===WORLD && to===MATRIX) return _applyDir(out,m.toFrameInv,dx,dy,dz);
1194
1042
 
1195
- // MATRIX EYE
1196
- if (from === MATRIX && to === EYE) {
1197
- _applyDir(out, m.fromFrame, dx,dy,dz);
1198
- const wx=out[0],wy=out[1],wz=out[2];
1199
- return _applyDir(out, m.vMatrix, wx,wy,wz);
1043
+ if (from===MATRIX && to===EYE) {
1044
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1045
+ return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
1200
1046
  }
1201
- if (from === EYE && to === MATRIX) {
1202
- _applyDir(out, m.eMatrix, dx,dy,dz);
1203
- const wx=out[0],wy=out[1],wz=out[2];
1204
- return _applyDir(out, m.toFrameInv, wx,wy,wz);
1047
+ if (from===EYE && to===MATRIX) {
1048
+ _applyDir(out,m.mat4Eye,dx,dy,dz);
1049
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1205
1050
  }
1206
1051
 
1207
- // MATRIX SCREEN
1208
- if (from === MATRIX && to === SCREEN) {
1209
- _applyDir(out, m.fromFrame, dx,dy,dz);
1210
- const wx=out[0],wy=out[1],wz=out[2];
1211
- return _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
1052
+ if (from===MATRIX && to===SCREEN) {
1053
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1054
+ return _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1212
1055
  }
1213
- if (from === SCREEN && to === MATRIX) {
1214
- _screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
1215
- const wx=out[0],wy=out[1],wz=out[2];
1216
- return _applyDir(out, m.toFrameInv, wx,wy,wz);
1056
+ if (from===SCREEN && to===MATRIX) {
1057
+ _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1058
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1217
1059
  }
1218
1060
 
1219
- // MATRIX NDC
1220
- if (from === MATRIX && to === NDC) {
1221
- _applyDir(out, m.fromFrame, dx,dy,dz);
1222
- const wx=out[0],wy=out[1],wz=out[2];
1223
- _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
1224
- const sx=out[0],sy=out[1],sz=out[2];
1225
- return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
1061
+ if (from===MATRIX && to===NDC) {
1062
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1063
+ _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1064
+ return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
1226
1065
  }
1227
- if (from === NDC && to === MATRIX) {
1228
- _ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
1229
- const sx=out[0],sy=out[1],sz=out[2];
1230
- _screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
1231
- const wx=out[0],wy=out[1],wz=out[2];
1232
- return _applyDir(out, m.toFrameInv, wx,wy,wz);
1066
+ if (from===NDC && to===MATRIX) {
1067
+ _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1068
+ _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1069
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1233
1070
  }
1234
1071
 
1235
- // MATRIX MATRIX
1236
- if (from === MATRIX && to === MATRIX) {
1237
- _applyDir(out, m.fromFrame, dx,dy,dz);
1238
- const wx=out[0],wy=out[1],wz=out[2];
1239
- return _applyDir(out, m.toFrameInv, wx,wy,wz);
1072
+ if (from===MATRIX && to===MATRIX) {
1073
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1074
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1240
1075
  }
1241
1076
 
1242
- // Fallback
1243
1077
  out[0]=dx; out[1]=dy; out[2]=dz;
1244
1078
  return out;
1245
1079
  }
@@ -1250,16 +1084,15 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
1250
1084
 
1251
1085
  /**
1252
1086
  * World-units-per-pixel at a given eye-space Z depth.
1253
- * @param {ArrayLike<number>} proj Projection mat4.
1254
- * @param {number} vpH Viewport height (pixels).
1255
- * @param {number} eyeZ Eye-space Z (negative for in-front-of camera).
1256
- * @param {number} ndcZMin WEBGL or WEBGPU.
1087
+ * @param {ArrayLike<number>} proj Projection mat4.
1088
+ * @param {number} vpH Viewport height in pixels (positive).
1089
+ * @param {number} eyeZ Eye-space Z negative means in front of camera.
1090
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
1257
1091
  */
1258
1092
  function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
1259
- if (projIsOrtho(proj)) {
1260
- return Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin)) / vpH;
1261
- }
1262
- return 2 * Math.abs(eyeZ) * Math.tan(projFov(proj) / 2) / vpH;
1093
+ return projIsOrtho(proj)
1094
+ ? Math.abs(projTop(proj,ndcZMin)-projBottom(proj,ndcZMin)) / vpH
1095
+ : 2*Math.abs(eyeZ)*Math.tan(projFov(proj)/2) / vpH;
1263
1096
  }
1264
1097
 
1265
1098
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1267,40 +1100,35 @@ function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
1267
1100
  // ═══════════════════════════════════════════════════════════════════════════
1268
1101
 
1269
1102
  /**
1270
- * Apply the pick-matrix in-place: proj M_pick · proj
1103
+ * Mutate a projection matrix in-place so that the pixel at (px, py) maps to
1104
+ * the full NDC square — making a 1×1 FBO render contain exactly that pixel.
1105
+ *
1106
+ * Premultiplies by M_pick (column-major, rows 2 and 3 unchanged):
1271
1107
  *
1272
- * Zooms the frustum so that pixel (px, py) maps to the full NDC square,
1273
- * making a 1×1 framebuffer render contain exactly that pixel's content.
1274
- * Convention-independent correct for both perspective and orthographic.
1108
+ * ┌ sx 0 0 tx ┐ sx = |vp[2]|, sy = |vp[3]|
1109
+ * │ 0 sy 0 ty │ cx = ((px−vp[0])/vp[2])·2 1 (NDC x of pixel centre)
1110
+ * │ 0 0 1 0 │ cy = ((py−vp[1])/vp[3])·2 1 (NDC y, sign-aware)
1111
+ * └ 0 0 0 1 ┘ tx = −cx·sx, ty = −cy·sy
1275
1112
  *
1276
- * M_pick (column-major):
1277
- * [ sx 0 0 tx ] sx = W, sy = H
1278
- * [ 0 sy 0 ty ] cx = NDC X of pixel centre = 2*(px+0.5)/W − 1
1279
- * [ 0 0 1 0 ] cy = NDC Y of pixel centre = 1 − 2*(py+0.5)/H
1280
- * [ 0 0 0 1 ] tx = −cx·W, ty = −cy·H
1113
+ * Result: P_pick = M_pick · P_original.
1114
+ * The viewport sign convention (vp[3] < 0 for screen y-down) is preserved
1115
+ * automatically through cx/cy no separate flip needed.
1281
1116
  *
1282
1117
  * @param {Float32Array} proj Projection mat4 — mutated in place.
1283
- * @param {number} px Query X (CSS pixels).
1284
- * @param {number} py Query Y (CSS pixels).
1285
- * @param {number} W Canvas width (CSS pixels).
1286
- * @param {number} H Canvas height (CSS pixels).
1287
- * @returns {Float32Array} proj (same reference)
1118
+ * @param {number} px Query pixel X in screen coordinates.
1119
+ * @param {number} py Query pixel Y in screen coordinates.
1120
+ * @param {number[]} vp Viewport [x, y, w, h]; same signed convention as mapLocation.
1288
1121
  */
1289
- function mat4Pick(proj, px, py, W, H) {
1290
- const cx = 2 * (px + 0.5) / W - 1;
1291
- const cy = -2 * (py + 0.5) / H + 1;
1292
- const sx = W;
1293
- const sy = H;
1294
- const tx = -cx * W;
1295
- const ty = -cy * H;
1296
- for (let j = 0; j < 4; j++) {
1297
- const a = proj[j * 4];
1298
- const b = proj[j * 4 + 1];
1299
- const d = proj[j * 4 + 3];
1300
- proj[j * 4] = sx * a + tx * d;
1301
- proj[j * 4 + 1] = sy * b + ty * d;
1122
+ function mat4Pick(proj, px, py, vp) {
1123
+ const cx=((px-vp[0])/vp[2])*2-1;
1124
+ const cy=((py-vp[1])/vp[3])*2-1;
1125
+ const sx=Math.abs(vp[2]), sy=Math.abs(vp[3]);
1126
+ const tx=-cx*sx, ty=-cy*sy;
1127
+ for (let j=0; j<4; j++) {
1128
+ const a=proj[j*4], b=proj[j*4+1], d=proj[j*4+3];
1129
+ proj[j*4] = sx*a + tx*d;
1130
+ proj[j*4+1] = sy*b + ty*d;
1302
1131
  }
1303
- return proj;
1304
1132
  }
1305
1133
 
1306
1134
  /**
@@ -2445,5 +2273,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
2445
2273
  return allIn ? VISIBLE : SEMIVISIBLE;
2446
2274
  }
2447
2275
 
2448
- export { CameraTrack, 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, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4EyeMatrix, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Frustum, mat4Invert, mat4Location, mat4LookAt, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Perspective, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4Transpose, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNlerp, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
2276
+ export { CameraTrack, 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, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4Eye, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Frustum, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Perspective, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4View, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNlerp, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
2449
2277
  //# sourceMappingURL=index.js.map