@nakednous/tree 0.0.12 → 0.0.14

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; mat4View and mat4Eye 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
297
  function mat4View(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
299
- // z = normalize(eye - center) (camera +Z away from target)
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 mat4View(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 mat4View.
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
325
  function mat4Eye(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
332
- // Same basis computation as mat4View.
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 mat4Eye(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 mat4Eye(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,164 @@ 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;
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;
677
659
  return out;
678
660
  }
679
661
 
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
- }
696
- return out;
697
- }
698
-
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];
783
- }
784
-
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];
720
+ return p[15]===0 ? p[14]/(1+p[10]) : (p[14]-1)/p[10];
789
721
  }
790
722
 
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
- }
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]; }
796
725
 
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
- }
726
+ /**
727
+ * Top extent of the near plane in camera space (y_max).
728
+ * Positive for a standard y-up camera.
729
+ * @param {ArrayLike<number>} p Projection mat4.
730
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
731
+ */
732
+ function projTop (p, ndcZMin) { return p[15]===1 ? (1+p[13])/p[5] : projNear(p,ndcZMin)*(1+p[9])/p[5]; }
802
733
 
803
- /** Vertical fov (radians, perspective only). */
804
- function projFov(p) {
805
- return Math.abs(2 * Math.atan(1 / p[5]));
806
- }
734
+ /**
735
+ * Bottom extent of the near plane in camera space (y_min).
736
+ * Negative for a standard y-up camera.
737
+ * @param {ArrayLike<number>} p Projection mat4.
738
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
739
+ */
740
+ function projBottom(p, ndcZMin) { return p[15]===1 ? (p[13]-1)/p[5] : projNear(p,ndcZMin)*(p[9]-1)/p[5]; }
807
741
 
808
- /** Horizontal fov (radians, perspective only). */
809
- function projHfov(p) {
810
- return Math.abs(2 * Math.atan(1 / p[0]));
811
- }
742
+ /** Vertical field of view in radians (perspective only). */
743
+ function projFov (p) { return Math.abs(2*Math.atan(1/p[5])); }
744
+ /** Horizontal field of view in radians (perspective only). */
745
+ function projHfov(p) { return Math.abs(2*Math.atan(1/p[0])); }
812
746
 
813
747
  // ═══════════════════════════════════════════════════════════════════════════
814
- // Derived matrices (convenience)
748
+ // Derived matrices
815
749
  // ═══════════════════════════════════════════════════════════════════════════
816
750
 
817
751
  /** out = P · V */
818
- function mat4PV(out, proj, view) { return mat4Mul(out, proj, view); }
819
-
752
+ function mat4PV(out, proj, view) { return mat4Mul(out, proj, view); }
820
753
  /** out = V · M */
821
754
  function mat4MV(out, model, view) { return mat4Mul(out, view, model); }
822
755
 
823
756
  // ═══════════════════════════════════════════════════════════════════════════
824
- // Location / Direction transforms
757
+ // Frame-relative transforms
825
758
  // ═══════════════════════════════════════════════════════════════════════════
826
759
 
827
760
  /**
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.
761
+ * Location transform between frames: out = inv(to) · from.
762
+ * @returns {ArrayLike<number>|null} out, or null if `to` is singular.
833
763
  */
834
764
  function mat4Location(out, from, to) {
835
765
  return mat4Invert(out, to) && mat4Mul(out, out, from);
836
766
  }
837
767
 
838
768
  /**
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.
769
+ * Direction transform between frames: out = to₃ · inv(from₃).
770
+ * Uses only the upper-left 3×3 blocks (rotation/scale, no translation).
771
+ * @returns {ArrayLike<number>|null} out, or null if `from` is singular.
845
772
  */
846
773
  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;
774
+ const a00=from[0],a01=from[1],a02=from[2],
775
+ a10=from[4],a11=from[5],a12=from[6],
776
+ a20=from[8],a21=from[9],a22=from[10];
777
+ const b01=a22*a11-a12*a21, b11=a12*a20-a22*a10, b21=a21*a10-a11*a20;
853
778
  let det=a00*b01+a01*b11+a02*b21;
854
779
  if (Math.abs(det) < 1e-12) return null;
855
780
  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;
781
+ const i00=b01*det, i01=(a02*a21-a22*a01)*det, i02=(a12*a01-a02*a11)*det;
782
+ const i10=b11*det, i11=(a22*a00-a02*a20)*det, i12=(a02*a10-a12*a00)*det;
783
+ const i20=b21*det, i21=(a01*a20-a21*a00)*det, i22=(a11*a00-a01*a10)*det;
784
+ 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];
785
+ out[0]=t00*i00+t10*i01+t20*i02; out[1]=t01*i00+t11*i01+t21*i02; out[2]=t02*i00+t12*i01+t22*i02;
786
+ out[3]=t00*i10+t10*i11+t20*i12; out[4]=t01*i10+t11*i11+t21*i12; out[5]=t02*i10+t12*i11+t22*i12;
787
+ 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
788
  return out;
869
789
  }
870
790
 
@@ -872,215 +792,177 @@ function mat3Direction(out, from, to) {
872
792
  // Space transforms — mapLocation / mapDirection
873
793
  // ═══════════════════════════════════════════════════════════════════════════
874
794
  //
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).
795
+ // Flat dispatch: every from→to pair is a self-contained leaf with only stack
796
+ // locals no reentrancy, no shared state between calls.
797
+ //
798
+ // Matrices bag `m`:
799
+ // mat4Proj Float32Array(16) projection (eye → clip)
800
+ // mat4View Float32Array(16) view (world → eye)
801
+ // mat4Eye? Float32Array(16) eye (eye → world); caller fills before passing
802
+ // mat4PV? Float32Array(16) P · V; caller fills or _ensurePV allocates once
803
+ // mat4PVInv? Float32Array(16) inv(P · V); caller fills
804
+ // fromFrame? Float32Array(16) MATRIX source frame
805
+ // toFrameInv? Float32Array(16) inv(MATRIX dest frame)
878
806
  //
879
- // Matrices bag m:
880
- // {
881
- // pMatrix: Float32Array(16) projection (eye clip)
882
- // vMatrix: Float32Array(16) view (world → eye)
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
- // }
807
+ // Viewport `vp` = [x, y, w, h]:
808
+ // Use SIGNED h to encode screen-y direction (see module header).
809
+ // Core formula: screen = (ndc*0.5+0.5)*vp[k] + vp[k-2] (k=2 for x, k=3 for y)
810
+ // Inverse: ndc = ((screen-vp[k-2])/vp[k])*2 - 1
811
+ // Negative vp[3] flips NDC y-up to screen y-down automatically.
889
812
  //
890
813
 
891
- // ── Location leaf helpers ────────────────────────────────────────────────
814
+ // ── Location helpers ─────────────────────────────────────────────────────
892
815
 
893
816
  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);
817
+ 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],
818
+ z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14], w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
819
+ const xi=(w!==0&&w!==1)?1/w:1;
820
+ out[0]=(x*xi*0.5+0.5)*vp[2]+vp[0];
821
+ out[1]=(y*xi*0.5+0.5)*vp[3]+vp[1];
822
+ out[2]=(z*xi-ndcZMin)/(1-ndcZMin);
904
823
  return out;
905
824
  }
906
825
 
907
826
  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);
827
+ return mat4MulPoint(out, ipv,
828
+ ((sx-vp[0])/vp[2])*2-1,
829
+ ((sy-vp[1])/vp[3])*2-1,
830
+ sz*(1-ndcZMin)+ndcZMin);
913
831
  }
914
832
 
915
833
  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;
834
+ 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],
835
+ z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14], w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
836
+ const xi=(w!==0&&w!==1)?1/w:1;
921
837
  out[0]=x*xi; out[1]=y*xi; out[2]=z*xi;
922
838
  return out;
923
839
  }
924
840
 
925
- function _ndcToWorld(out, nx, ny, nz, ipv) {
926
- return mat4MulPoint(out, ipv, nx, ny, nz);
927
- }
841
+ function _ndcToWorld(out, nx, ny, nz, ipv) { return mat4MulPoint(out,ipv,nx,ny,nz); }
928
842
 
929
843
  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;
844
+ out[0]=((sx-vp[0])/vp[2])*2-1;
845
+ out[1]=((sy-vp[1])/vp[3])*2-1;
846
+ out[2]=sz*(1-ndcZMin)+ndcZMin;
934
847
  return out;
935
848
  }
936
849
 
937
850
  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);
851
+ out[0]=(nx*0.5+0.5)*vp[2]+vp[0];
852
+ out[1]=(ny*0.5+0.5)*vp[3]+vp[1];
853
+ out[2]=(nz-ndcZMin)/(1-ndcZMin);
942
854
  return out;
943
855
  }
944
856
 
945
857
  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;
858
+ if (m.mat4PV) return m.mat4PV;
859
+ m.mat4PV = new Float32Array(16);
860
+ mat4Mul(m.mat4PV, m.mat4Proj, m.mat4View);
861
+ return m.mat4PV;
950
862
  }
951
863
 
952
864
  /**
953
865
  * Map a point between named coordinate spaces.
954
866
  *
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).
867
+ * @param {number[]} out 3-element destination — written and returned.
868
+ * @param {number} px,py,pz Input point.
869
+ * @param {string} from Source space (WORLD, EYE, SCREEN, NDC, MATRIX).
870
+ * @param {string} to Destination space.
871
+ * @param {object} m Matrices bag — see module header.
872
+ * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
873
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
874
+ * @returns {number[]} out
963
875
  */
964
876
  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);
877
+ if (from===WORLD && to===SCREEN) return _worldToScreen(out,px,py,pz,_ensurePV(m),vp,ndcZMin);
878
+ if (from===SCREEN && to===WORLD) return _screenToWorld(out,px,py,pz,m.mat4PVInv,vp,ndcZMin);
879
+
880
+ if (from===WORLD && to===NDC) return _worldToNDC(out,px,py,pz,_ensurePV(m));
881
+ if (from===NDC && to===WORLD) return _ndcToWorld(out,px,py,pz,m.mat4PVInv);
882
+
883
+ if (from===SCREEN && to===NDC) return _screenToNDC(out,px,py,pz,vp,ndcZMin);
884
+ if (from===NDC && to===SCREEN) return _ndcToScreen(out,px,py,pz,vp,ndcZMin);
885
+
886
+ if (from===WORLD && to===EYE) return mat4MulPoint(out,m.mat4View,px,py,pz);
887
+ if (from===EYE && to===WORLD) return mat4MulPoint(out,m.mat4Eye,px,py,pz);
888
+
889
+ if (from===EYE && to===SCREEN) {
890
+ const e=m.mat4Eye;
891
+ return _worldToScreen(out,e[0]*px+e[4]*py+e[8]*pz+e[12],
892
+ e[1]*px+e[5]*py+e[9]*pz+e[13],
893
+ e[2]*px+e[6]*py+e[10]*pz+e[14],_ensurePV(m),vp,ndcZMin);
996
894
  }
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);
895
+ if (from===SCREEN && to===EYE) {
896
+ _screenToWorld(out,px,py,pz,m.mat4PVInv,vp,ndcZMin);
897
+ return mat4MulPoint(out,m.mat4View,out[0],out[1],out[2]);
1001
898
  }
1002
899
 
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));
900
+ if (from===EYE && to===NDC) {
901
+ const e=m.mat4Eye;
902
+ return _worldToNDC(out,e[0]*px+e[4]*py+e[8]*pz+e[12],
903
+ e[1]*px+e[5]*py+e[9]*pz+e[13],
904
+ e[2]*px+e[6]*py+e[10]*pz+e[14],_ensurePV(m));
1010
905
  }
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);
906
+ if (from===NDC && to===EYE) {
907
+ _ndcToWorld(out,px,py,pz,m.mat4PVInv);
908
+ return mat4MulPoint(out,m.mat4View,out[0],out[1],out[2]);
1015
909
  }
1016
910
 
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);
911
+ if (from===MATRIX && to===WORLD) return mat4MulPoint(out,m.fromFrame,px,py,pz);
912
+ if (from===WORLD && to===MATRIX) return mat4MulPoint(out,m.toFrameInv,px,py,pz);
913
+
914
+ if (from===MATRIX && to===EYE) {
915
+ const f=m.fromFrame;
916
+ return mat4MulPoint(out,m.mat4View,f[0]*px+f[4]*py+f[8]*pz+f[12],
917
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
918
+ f[2]*px+f[6]*py+f[10]*pz+f[14]);
1030
919
  }
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);
920
+ if (from===EYE && to===MATRIX) {
921
+ const e=m.mat4Eye;
922
+ return mat4MulPoint(out,m.toFrameInv,e[0]*px+e[4]*py+e[8]*pz+e[12],
923
+ e[1]*px+e[5]*py+e[9]*pz+e[13],
924
+ e[2]*px+e[6]*py+e[10]*pz+e[14]);
1037
925
  }
1038
926
 
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);
927
+ if (from===MATRIX && to===SCREEN) {
928
+ const f=m.fromFrame;
929
+ return _worldToScreen(out,f[0]*px+f[4]*py+f[8]*pz+f[12],
930
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
931
+ f[2]*px+f[6]*py+f[10]*pz+f[14],_ensurePV(m),vp,ndcZMin);
1046
932
  }
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);
933
+ if (from===SCREEN && to===MATRIX) {
934
+ _screenToWorld(out,px,py,pz,m.mat4PVInv,vp,ndcZMin);
935
+ return mat4MulPoint(out,m.toFrameInv,out[0],out[1],out[2]);
1051
936
  }
1052
937
 
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));
938
+ if (from===MATRIX && to===NDC) {
939
+ const f=m.fromFrame;
940
+ return _worldToNDC(out,f[0]*px+f[4]*py+f[8]*pz+f[12],
941
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
942
+ f[2]*px+f[6]*py+f[10]*pz+f[14],_ensurePV(m));
1060
943
  }
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);
944
+ if (from===NDC && to===MATRIX) {
945
+ _ndcToWorld(out,px,py,pz,m.mat4PVInv);
946
+ return mat4MulPoint(out,m.toFrameInv,out[0],out[1],out[2]);
1065
947
  }
1066
948
 
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);
949
+ if (from===MATRIX && to===MATRIX) {
950
+ const f=m.fromFrame;
951
+ return mat4MulPoint(out,m.toFrameInv,f[0]*px+f[4]*py+f[8]*pz+f[12],
952
+ f[1]*px+f[5]*py+f[9]*pz+f[13],
953
+ f[2]*px+f[6]*py+f[10]*pz+f[14]);
1074
954
  }
1075
955
 
1076
- // Fallback
1077
956
  out[0]=px; out[1]=py; out[2]=pz;
1078
957
  return out;
1079
958
  }
1080
959
 
1081
- // ── Direction leaf helpers ───────────────────────────────────────────────
960
+ // ── Direction helpers ────────────────────────────────────────────────────
961
+ //
962
+ // Directions use only the linear 3×3 block — no translation, no w-divide.
963
+ // The signed vp[2]/vp[3] carries the y-convention automatically.
964
+ //
1082
965
 
1083
- /** Apply the 3×3 linear part of a mat4 (rotation/scale, no translation). */
1084
966
  function _applyDir(out, m, dx, dy, dz) {
1085
967
  out[0]=m[0]*dx+m[4]*dy+m[8]*dz;
1086
968
  out[1]=m[1]*dx+m[5]*dy+m[9]*dz;
@@ -1089,157 +971,123 @@ function _applyDir(out, m, dx, dy, dz) {
1089
971
  }
1090
972
 
1091
973
  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;
974
+ const vx=view[0]*dx+view[4]*dy+view[8]*dz,
975
+ vy=view[1]*dx+view[5]*dy+view[9]*dz,
976
+ vz=view[2]*dx+view[6]*dy+view[10]*dz;
977
+ // vpH is signed — negative flips y component automatically.
978
+ out[0]=(proj[0]*vx+proj[4]*vy+proj[8]*vz)*vpW*0.5;
979
+ out[1]=(proj[1]*vx+proj[5]*vy+proj[9]*vz)*vpH*0.5;
980
+ out[2]=(proj[2]*vx+proj[6]*vy+proj[10]*vz)*(1-ndcZMin)*0.5;
1102
981
  return out;
1103
982
  }
1104
983
 
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);
984
+ function _screenToWorldDir(out, dx, dy, dz, proj, eye, vpW, vpH, ndcZMin) {
985
+ // Inverse of _worldToScreenDir; signed vpW/vpH cancel the y-flip.
986
+ _applyDir(out, eye, dx/(vpW*0.5)/proj[0], dy/(vpH*0.5)/proj[5], dz/((1-ndcZMin)*0.5));
1113
987
  return out;
1114
988
  }
1115
989
 
1116
990
  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);
991
+ out[0]=dx/(vpW*0.5); out[1]=dy/(vpH*0.5); out[2]=dz/((1-ndcZMin)*0.5);
1119
992
  return out;
1120
993
  }
1121
994
 
1122
995
  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;
996
+ out[0]=dx*vpW*0.5; out[1]=dy*vpH*0.5; out[2]=dz*(1-ndcZMin)*0.5;
1125
997
  return out;
1126
998
  }
1127
999
 
1128
1000
  /**
1129
1001
  * Map a direction between named coordinate spaces.
1130
- * Same bag contract as mapLocation.
1002
+ * Same bag and viewport contract as mapLocation.
1003
+ *
1004
+ * @param {number[]} out 3-element destination.
1005
+ * @param {number} dx,dy,dz Input direction.
1006
+ * @param {string} from Source space.
1007
+ * @param {string} to Destination space.
1008
+ * @param {object} m Matrices bag — see module header.
1009
+ * @param {number[]} vp Viewport [x, y, w, h]; sign of h encodes screen-y direction.
1010
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
1011
+ * @returns {number[]} out
1131
1012
  */
1132
1013
  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);
1014
+ const vpW=vp[2], vpH=vp[3]; // signed — carry y-convention through all helpers
1015
+
1016
+ if (from===EYE && to===WORLD) return _applyDir(out,m.mat4Eye, dx,dy,dz);
1017
+ if (from===WORLD && to===EYE) return _applyDir(out,m.mat4View,dx,dy,dz);
1018
+
1019
+ if (from===WORLD && to===SCREEN) return _worldToScreenDir(out,dx,dy,dz,m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1020
+ if (from===SCREEN && to===WORLD) return _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye, vpW,vpH,ndcZMin);
1021
+
1022
+ if (from===SCREEN && to===NDC) return _screenToNDCDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1023
+ if (from===NDC && to===SCREEN) return _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1024
+
1025
+ if (from===WORLD && to===NDC) {
1026
+ _worldToScreenDir(out,dx,dy,dz,m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1027
+ return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
1156
1028
  }
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);
1029
+ if (from===NDC && to===WORLD) {
1030
+ _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1031
+ return _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1161
1032
  }
1162
1033
 
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);
1034
+ if (from===EYE && to===SCREEN) {
1035
+ _applyDir(out,m.mat4Eye,dx,dy,dz);
1036
+ return _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1168
1037
  }
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);
1038
+ if (from===SCREEN && to===EYE) {
1039
+ _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1040
+ return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
1173
1041
  }
1174
1042
 
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);
1043
+ if (from===EYE && to===NDC) {
1044
+ _applyDir(out,m.mat4Eye,dx,dy,dz);
1045
+ _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1046
+ return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
1182
1047
  }
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);
1048
+ if (from===NDC && to===EYE) {
1049
+ _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1050
+ _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1051
+ return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
1189
1052
  }
1190
1053
 
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);
1054
+ if (from===MATRIX && to===WORLD) return _applyDir(out,m.fromFrame, dx,dy,dz);
1055
+ if (from===WORLD && to===MATRIX) return _applyDir(out,m.toFrameInv,dx,dy,dz);
1194
1056
 
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);
1057
+ if (from===MATRIX && to===EYE) {
1058
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1059
+ return _applyDir(out,m.mat4View,out[0],out[1],out[2]);
1200
1060
  }
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);
1061
+ if (from===EYE && to===MATRIX) {
1062
+ _applyDir(out,m.mat4Eye,dx,dy,dz);
1063
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1205
1064
  }
1206
1065
 
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);
1066
+ if (from===MATRIX && to===SCREEN) {
1067
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1068
+ return _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1212
1069
  }
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);
1070
+ if (from===SCREEN && to===MATRIX) {
1071
+ _screenToWorldDir(out,dx,dy,dz,m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1072
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1217
1073
  }
1218
1074
 
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);
1075
+ if (from===MATRIX && to===NDC) {
1076
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1077
+ _worldToScreenDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4View,vpW,vpH,ndcZMin);
1078
+ return _screenToNDCDir(out,out[0],out[1],out[2],vpW,vpH,ndcZMin);
1226
1079
  }
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);
1080
+ if (from===NDC && to===MATRIX) {
1081
+ _ndcToScreenDir(out,dx,dy,dz,vpW,vpH,ndcZMin);
1082
+ _screenToWorldDir(out,out[0],out[1],out[2],m.mat4Proj,m.mat4Eye,vpW,vpH,ndcZMin);
1083
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1233
1084
  }
1234
1085
 
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);
1086
+ if (from===MATRIX && to===MATRIX) {
1087
+ _applyDir(out,m.fromFrame,dx,dy,dz);
1088
+ return _applyDir(out,m.toFrameInv,out[0],out[1],out[2]);
1240
1089
  }
1241
1090
 
1242
- // Fallback
1243
1091
  out[0]=dx; out[1]=dy; out[2]=dz;
1244
1092
  return out;
1245
1093
  }
@@ -1250,16 +1098,15 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
1250
1098
 
1251
1099
  /**
1252
1100
  * 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.
1101
+ * @param {ArrayLike<number>} proj Projection mat4.
1102
+ * @param {number} vpH Viewport height in pixels (positive).
1103
+ * @param {number} eyeZ Eye-space Z negative means in front of camera.
1104
+ * @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
1257
1105
  */
1258
1106
  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;
1107
+ return projIsOrtho(proj)
1108
+ ? Math.abs(projTop(proj,ndcZMin)-projBottom(proj,ndcZMin)) / vpH
1109
+ : 2*Math.abs(eyeZ)*Math.tan(projFov(proj)/2) / vpH;
1263
1110
  }
1264
1111
 
1265
1112
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1267,40 +1114,35 @@ function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
1267
1114
  // ═══════════════════════════════════════════════════════════════════════════
1268
1115
 
1269
1116
  /**
1270
- * Apply the pick-matrix in-place: proj M_pick · proj
1117
+ * Mutate a projection matrix in-place so that the pixel at (px, py) maps to
1118
+ * the full NDC square — making a 1×1 FBO render contain exactly that pixel.
1119
+ *
1120
+ * Premultiplies by M_pick (column-major, rows 2 and 3 unchanged):
1271
1121
  *
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.
1122
+ * ┌ sx 0 0 tx ┐ sx = |vp[2]|, sy = |vp[3]|
1123
+ * │ 0 sy 0 ty │ cx = ((px−vp[0])/vp[2])·2 1 (NDC x of pixel centre)
1124
+ * │ 0 0 1 0 │ cy = ((py−vp[1])/vp[3])·2 1 (NDC y, sign-aware)
1125
+ * └ 0 0 0 1 ┘ tx = −cx·sx, ty = −cy·sy
1275
1126
  *
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
1127
+ * Result: P_pick = M_pick · P_original.
1128
+ * The viewport sign convention (vp[3] < 0 for screen y-down) is preserved
1129
+ * automatically through cx/cy no separate flip needed.
1281
1130
  *
1282
1131
  * @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)
1132
+ * @param {number} px Query pixel X in screen coordinates.
1133
+ * @param {number} py Query pixel Y in screen coordinates.
1134
+ * @param {number[]} vp Viewport [x, y, w, h]; same signed convention as mapLocation.
1288
1135
  */
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;
1136
+ function mat4Pick(proj, px, py, vp) {
1137
+ const cx=((px-vp[0])/vp[2])*2-1;
1138
+ const cy=((py-vp[1])/vp[3])*2-1;
1139
+ const sx=Math.abs(vp[2]), sy=Math.abs(vp[3]);
1140
+ const tx=-cx*sx, ty=-cy*sy;
1141
+ for (let j=0; j<4; j++) {
1142
+ const a=proj[j*4], b=proj[j*4+1], d=proj[j*4+3];
1143
+ proj[j*4] = sx*a + tx*d;
1144
+ proj[j*4+1] = sy*b + ty*d;
1302
1145
  }
1303
- return proj;
1304
1146
  }
1305
1147
 
1306
1148
  /**
@@ -2445,5 +2287,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
2445
2287
  return allIn ? VISIBLE : SEMIVISIBLE;
2446
2288
  }
2447
2289
 
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, mat4Eye, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Frustum, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Perspective, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, mat4Transpose, 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 };
2290
+ 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
2291
  //# sourceMappingURL=index.js.map