@vib3code/sdk 2.0.3-canary.19bcbe1 → 2.0.3-canary.1f10a9c

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vib3code/sdk",
3
- "version": "2.0.3-canary.19bcbe1",
3
+ "version": "2.0.3-canary.1f10a9c",
4
4
  "description": "VIB3+ 4D Visualization SDK - Unified engine with 6D rotation, MCP agentic integration, and cross-platform support",
5
5
  "type": "module",
6
6
  "main": "src/core/VIB3Engine.js",
@@ -9,9 +9,7 @@
9
9
  "vib3": "./src/cli/index.js",
10
10
  "vib3-mcp": "./src/agent/mcp/stdio-server.js"
11
11
  },
12
- "sideEffects": [
13
- "./src/geometry/builders/*.js"
14
- ],
12
+ "sideEffects": false,
15
13
  "exports": {
16
14
  ".": {
17
15
  "types": "./types/adaptive-sdk.d.ts",
package/src/cli/index.js CHANGED
@@ -5,9 +5,34 @@
5
5
  */
6
6
 
7
7
  import { performance } from 'node:perf_hooks';
8
+ import path from 'node:path';
8
9
  import { mcpServer, toolDefinitions } from '../agent/index.js';
9
10
  import { schemaRegistry } from '../schemas/index.js';
10
11
 
12
+ /**
13
+ * Resolves a file path and ensures it is within the current working directory.
14
+ * @param {string} filePath The path to validate
15
+ * @returns {string} The resolved absolute path
16
+ * @throws {Error} If the path is outside the allowed directory
17
+ */
18
+ function getSafePath(filePath) {
19
+ if (!filePath) return filePath;
20
+
21
+ const cwd = process.cwd();
22
+ const resolvedPath = path.resolve(cwd, filePath);
23
+ const relative = path.relative(cwd, resolvedPath);
24
+
25
+ const isOutside = relative.startsWith('..') || path.isAbsolute(relative);
26
+
27
+ if (isOutside) {
28
+ const error = new Error('Security Error: Path traversal detected. Access denied.');
29
+ error.code = 'EACCES';
30
+ throw error;
31
+ }
32
+
33
+ return resolvedPath;
34
+ }
35
+
11
36
  /**
12
37
  * CLI Configuration
13
38
  */
@@ -446,7 +471,7 @@ async function handleTools(parsed, startTime) {
446
471
  */
447
472
  async function handleValidate(parsed, startTime) {
448
473
  const subcommand = parsed.subcommand || 'pack';
449
- const filePath = parsed.options.file || parsed.options.f || parsed.positional[0];
474
+ let filePath = parsed.options.file || parsed.options.f || parsed.positional[0];
450
475
 
451
476
  if (!filePath && subcommand !== 'schema') {
452
477
  return wrapResponse('validate', {
@@ -460,6 +485,10 @@ async function handleValidate(parsed, startTime) {
460
485
  }
461
486
 
462
487
  try {
488
+ if (filePath) {
489
+ filePath = getSafePath(filePath);
490
+ }
491
+
463
492
  switch (subcommand) {
464
493
  case 'pack': {
465
494
  // Validate a .vib3 scene pack file
@@ -533,6 +562,17 @@ async function handleValidate(parsed, startTime) {
533
562
  }, false, performance.now() - startTime);
534
563
  }
535
564
  } catch (error) {
565
+ if (error.code === 'EACCES') {
566
+ return wrapResponse('validate', {
567
+ error: {
568
+ type: 'SecurityError',
569
+ code: 'ACCESS_DENIED',
570
+ message: error.message,
571
+ suggestion: 'Ensure the file path is within the project directory'
572
+ }
573
+ }, false, performance.now() - startTime);
574
+ }
575
+
536
576
  if (error.code === 'ENOENT') {
537
577
  return wrapResponse('validate', {
538
578
  error: {
@@ -659,7 +699,6 @@ async function main() {
659
699
  */
660
700
  async function handleInit(parsed, startTime) {
661
701
  const { writeFileSync, mkdirSync, existsSync } = await import('node:fs');
662
- const { join } = await import('node:path');
663
702
 
664
703
  const projectName = parsed.subcommand || parsed.positional[0] || 'my-vib3-app';
665
704
  const template = parsed.options.template || parsed.options.t || 'vanilla';
@@ -677,7 +716,22 @@ async function handleInit(parsed, startTime) {
677
716
  }, false, performance.now() - startTime);
678
717
  }
679
718
 
680
- const projectDir = join(process.cwd(), projectName);
719
+ let projectDir;
720
+ try {
721
+ projectDir = getSafePath(projectName);
722
+ } catch (error) {
723
+ if (error.code === 'EACCES') {
724
+ return wrapResponse('init', {
725
+ error: {
726
+ type: 'SecurityError',
727
+ code: 'ACCESS_DENIED',
728
+ message: error.message,
729
+ suggestion: 'Choose a project name that results in a valid subdirectory'
730
+ }
731
+ }, false, performance.now() - startTime);
732
+ }
733
+ throw error;
734
+ }
681
735
 
682
736
  if (existsSync(projectDir)) {
683
737
  return wrapResponse('init', {
@@ -695,8 +749,8 @@ async function handleInit(parsed, startTime) {
695
749
  const files = getTemplateFiles(template, projectName);
696
750
 
697
751
  for (const [filename, content] of Object.entries(files)) {
698
- const filepath = join(projectDir, filename);
699
- const dir = join(projectDir, filename.split('/').slice(0, -1).join('/'));
752
+ const filepath = path.join(projectDir, filename);
753
+ const dir = path.join(projectDir, filename.split('/').slice(0, -1).join('/'));
700
754
  if (dir !== projectDir) {
701
755
  mkdirSync(dir, { recursive: true });
702
756
  }
@@ -355,17 +355,21 @@ function projectPoints(points, rotor, dimension, width, height) {
355
355
  const centerY = height / 2;
356
356
  const scale = Math.min(width, height) * 0.4;
357
357
 
358
+ // Reuse vectors to minimize allocation
359
+ const rotatedBuffer = new Vec4();
360
+ const projectedBuffer = new Vec4();
361
+
358
362
  for (const point of points) {
359
363
  // Apply 4D rotation
360
- const rotated = rotor.rotate(point);
364
+ rotor.rotate(point, rotatedBuffer);
361
365
 
362
366
  // Project to 3D (perspective from W)
363
- const proj3d = rotated.projectPerspective(dimension);
367
+ rotatedBuffer.projectPerspective(dimension, projectedBuffer);
364
368
 
365
369
  // Project to 2D (simple orthographic for clean SVG)
366
- const x = centerX + proj3d.x * scale;
367
- const y = centerY - proj3d.y * scale; // Flip Y for SVG coordinates
368
- const depth = proj3d.z; // Keep depth for styling
370
+ const x = centerX + projectedBuffer.x * scale;
371
+ const y = centerY - projectedBuffer.y * scale; // Flip Y for SVG coordinates
372
+ const depth = projectedBuffer.z; // Keep depth for styling
369
373
 
370
374
  projected.push({ x, y, depth, original: point });
371
375
  }
@@ -48,6 +48,19 @@ export class Mat4x4 {
48
48
  }
49
49
  }
50
50
 
51
+ /**
52
+ * Reset to identity matrix
53
+ * @returns {Mat4x4} this
54
+ */
55
+ identity() {
56
+ const d = this.data;
57
+ d[0] = 1; d[1] = 0; d[2] = 0; d[3] = 0;
58
+ d[4] = 0; d[5] = 1; d[6] = 0; d[7] = 0;
59
+ d[8] = 0; d[9] = 0; d[10] = 1; d[11] = 0;
60
+ d[12] = 0; d[13] = 0; d[14] = 0; d[15] = 1;
61
+ return this;
62
+ }
63
+
51
64
  /**
52
65
  * Create identity matrix
53
66
  * @returns {Mat4x4}
@@ -354,12 +367,18 @@ export class Mat4x4 {
354
367
 
355
368
  /**
356
369
  * Transpose matrix
370
+ * @param {Mat4x4} [target=null] - Optional target matrix
357
371
  * @returns {Mat4x4} New transposed matrix
358
372
  */
359
- transpose() {
373
+ transpose(target = null) {
360
374
  const m = this.data;
361
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
375
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
362
376
  const r = out.data;
377
+ // If target is same as source, use intermediate or careful swap
378
+ if (target === this) {
379
+ return this.transposeInPlace();
380
+ }
381
+
363
382
  r[0] = m[0]; r[4] = m[1]; r[8] = m[2]; r[12] = m[3];
364
383
  r[1] = m[4]; r[5] = m[5]; r[9] = m[6]; r[13] = m[7];
365
384
  r[2] = m[8]; r[6] = m[9]; r[10] = m[10]; r[14] = m[11];
@@ -415,62 +434,85 @@ export class Mat4x4 {
415
434
 
416
435
  /**
417
436
  * Calculate inverse matrix
437
+ * @param {Mat4x4} [target=null] - Optional target matrix
418
438
  * @returns {Mat4x4|null} Inverse matrix or null if singular
419
439
  */
420
- inverse() {
440
+ inverse(target = null) {
421
441
  const m = this.data;
422
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
442
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
423
443
  const inv = out.data;
424
444
 
425
- inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] +
426
- m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
445
+ // Note: For in-place inversion (target === this), we need to be careful.
446
+ // The standard algorithm uses input values for every output cell.
447
+ // We can check for aliasing or use local variables if we wanted full safety,
448
+ // but simplest is to compute to temp if aliased, or just computing to the array directly works
449
+ // IF we cache everything first. But here we are writing to `inv` index by index.
450
+ // If inv === m, writing inv[0] destroys m[0] which is needed for inv[5] etc.
451
+ // So aliasing is NOT safe with this direct write approach.
452
+
453
+ // Handle aliasing by cloning first if needed, or using temp array.
454
+ // Since we want performance, let's detect aliasing.
455
+ let sourceData = m;
456
+ if (target === this) {
457
+ // Copy source data to temp array so we can write to 'this.data' safely
458
+ // We can't avoid allocation entirely in this specific edge case easily without unrolling everything into locals,
459
+ // which is huge for 4x4 inverse.
460
+ // Using a static temp buffer would be unsafe for threading/recursion (not an issue in JS single thread usually but still).
461
+ // Let's just clone the source data for the calculation.
462
+ sourceData = new Float32Array(m);
463
+ }
464
+
465
+ const s = sourceData;
466
+
467
+ inv[0] = s[5] * s[10] * s[15] - s[5] * s[11] * s[14] - s[9] * s[6] * s[15] +
468
+ s[9] * s[7] * s[14] + s[13] * s[6] * s[11] - s[13] * s[7] * s[10];
427
469
 
428
- inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] -
429
- m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
470
+ inv[4] = -s[4] * s[10] * s[15] + s[4] * s[11] * s[14] + s[8] * s[6] * s[15] -
471
+ s[8] * s[7] * s[14] - s[12] * s[6] * s[11] + s[12] * s[7] * s[10];
430
472
 
431
- inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] +
432
- m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
473
+ inv[8] = s[4] * s[9] * s[15] - s[4] * s[11] * s[13] - s[8] * s[5] * s[15] +
474
+ s[8] * s[7] * s[13] + s[12] * s[5] * s[11] - s[12] * s[7] * s[9];
433
475
 
434
- inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] -
435
- m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
476
+ inv[12] = -s[4] * s[9] * s[14] + s[4] * s[10] * s[13] + s[8] * s[5] * s[14] -
477
+ s[8] * s[6] * s[13] - s[12] * s[5] * s[10] + s[12] * s[6] * s[9];
436
478
 
437
- inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] -
438
- m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
479
+ inv[1] = -s[1] * s[10] * s[15] + s[1] * s[11] * s[14] + s[9] * s[2] * s[15] -
480
+ s[9] * s[3] * s[14] - s[13] * s[2] * s[11] + s[13] * s[3] * s[10];
439
481
 
440
- inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] +
441
- m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
482
+ inv[5] = s[0] * s[10] * s[15] - s[0] * s[11] * s[14] - s[8] * s[2] * s[15] +
483
+ s[8] * s[3] * s[14] + s[12] * s[2] * s[11] - s[12] * s[3] * s[10];
442
484
 
443
- inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] -
444
- m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
485
+ inv[9] = -s[0] * s[9] * s[15] + s[0] * s[11] * s[13] + s[8] * s[1] * s[15] -
486
+ s[8] * s[3] * s[13] - s[12] * s[1] * s[11] + s[12] * s[3] * s[9];
445
487
 
446
- inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] +
447
- m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
488
+ inv[13] = s[0] * s[9] * s[14] - s[0] * s[10] * s[13] - s[8] * s[1] * s[14] +
489
+ s[8] * s[2] * s[13] + s[12] * s[1] * s[10] - s[12] * s[2] * s[9];
448
490
 
449
- inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] +
450
- m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
491
+ inv[2] = s[1] * s[6] * s[15] - s[1] * s[7] * s[14] - s[5] * s[2] * s[15] +
492
+ s[5] * s[3] * s[14] + s[13] * s[2] * s[7] - s[13] * s[3] * s[6];
451
493
 
452
- inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] -
453
- m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
494
+ inv[6] = -s[0] * s[6] * s[15] + s[0] * s[7] * s[14] + s[4] * s[2] * s[15] -
495
+ s[4] * s[3] * s[14] - s[12] * s[2] * s[7] + s[12] * s[3] * s[6];
454
496
 
455
- inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] +
456
- m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
497
+ inv[10] = s[0] * s[5] * s[15] - s[0] * s[7] * s[13] - s[4] * s[1] * s[15] +
498
+ s[4] * s[3] * s[13] + s[12] * s[1] * s[7] - s[12] * s[3] * s[5];
457
499
 
458
- inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] -
459
- m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
500
+ inv[14] = -s[0] * s[5] * s[14] + s[0] * s[6] * s[13] + s[4] * s[1] * s[14] -
501
+ s[4] * s[2] * s[13] - s[12] * s[1] * s[6] + s[12] * s[2] * s[5];
460
502
 
461
- inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] -
462
- m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
503
+ inv[3] = -s[1] * s[6] * s[11] + s[1] * s[7] * s[10] + s[5] * s[2] * s[11] -
504
+ s[5] * s[3] * s[10] - s[9] * s[2] * s[7] + s[9] * s[3] * s[6];
463
505
 
464
- inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] +
465
- m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
506
+ inv[7] = s[0] * s[6] * s[11] - s[0] * s[7] * s[10] - s[4] * s[2] * s[11] +
507
+ s[4] * s[3] * s[10] + s[8] * s[2] * s[7] - s[8] * s[3] * s[6];
466
508
 
467
- inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] -
468
- m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
509
+ inv[11] = -s[0] * s[5] * s[11] + s[0] * s[7] * s[9] + s[4] * s[1] * s[11] -
510
+ s[4] * s[3] * s[9] - s[8] * s[1] * s[7] + s[8] * s[3] * s[5];
469
511
 
470
- inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] +
471
- m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
512
+ inv[15] = s[0] * s[5] * s[10] - s[0] * s[6] * s[9] - s[4] * s[1] * s[10] +
513
+ s[4] * s[2] * s[9] + s[8] * s[1] * s[6] - s[8] * s[2] * s[5];
472
514
 
473
- const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
515
+ const det = s[0] * inv[0] + s[1] * inv[4] + s[2] * inv[8] + s[3] * inv[12];
474
516
 
475
517
  if (Math.abs(det) < 1e-10) {
476
518
  return null; // Singular matrix
@@ -689,51 +731,78 @@ export class Mat4x4 {
689
731
  /**
690
732
  * Create XY plane rotation matrix (standard Z-axis rotation in 3D)
691
733
  * @param {number} angle - Rotation angle in radians
734
+ * @param {Mat4x4} [target=null] - Optional target matrix
692
735
  * @returns {Mat4x4}
693
736
  */
694
- static rotationXY(angle) {
737
+ static rotationXY(angle, target = null) {
695
738
  const c = Math.cos(angle);
696
739
  const s = Math.sin(angle);
697
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
740
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
698
741
  const r = out.data;
699
- r[0] = c; r[1] = s;
700
- r[4] = -s; r[5] = c;
701
- r[10] = 1;
702
- r[15] = 1;
742
+
743
+ if (target) {
744
+ r[0] = c; r[1] = s; r[2] = 0; r[3] = 0;
745
+ r[4] = -s; r[5] = c; r[6] = 0; r[7] = 0;
746
+ r[8] = 0; r[9] = 0; r[10] = 1; r[11] = 0;
747
+ r[12] = 0; r[13] = 0; r[14] = 0; r[15] = 1;
748
+ } else {
749
+ r[0] = c; r[1] = s;
750
+ r[4] = -s; r[5] = c;
751
+ r[10] = 1;
752
+ r[15] = 1;
753
+ }
703
754
  return out;
704
755
  }
705
756
 
706
757
  /**
707
758
  * Create XZ plane rotation matrix (standard Y-axis rotation in 3D)
708
759
  * @param {number} angle - Rotation angle in radians
760
+ * @param {Mat4x4} [target=null] - Optional target matrix
709
761
  * @returns {Mat4x4}
710
762
  */
711
- static rotationXZ(angle) {
763
+ static rotationXZ(angle, target = null) {
712
764
  const c = Math.cos(angle);
713
765
  const s = Math.sin(angle);
714
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
766
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
715
767
  const r = out.data;
716
- r[0] = c; r[2] = -s;
717
- r[5] = 1;
718
- r[8] = s; r[10] = c;
719
- r[15] = 1;
768
+
769
+ if (target) {
770
+ r[0] = c; r[1] = 0; r[2] = -s; r[3] = 0;
771
+ r[4] = 0; r[5] = 1; r[6] = 0; r[7] = 0;
772
+ r[8] = s; r[9] = 0; r[10] = c; r[11] = 0;
773
+ r[12] = 0; r[13] = 0; r[14] = 0; r[15] = 1;
774
+ } else {
775
+ r[0] = c; r[2] = -s;
776
+ r[5] = 1;
777
+ r[8] = s; r[10] = c;
778
+ r[15] = 1;
779
+ }
720
780
  return out;
721
781
  }
722
782
 
723
783
  /**
724
784
  * Create YZ plane rotation matrix (standard X-axis rotation in 3D)
725
785
  * @param {number} angle - Rotation angle in radians
786
+ * @param {Mat4x4} [target=null] - Optional target matrix
726
787
  * @returns {Mat4x4}
727
788
  */
728
- static rotationYZ(angle) {
789
+ static rotationYZ(angle, target = null) {
729
790
  const c = Math.cos(angle);
730
791
  const s = Math.sin(angle);
731
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
792
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
732
793
  const r = out.data;
733
- r[0] = 1;
734
- r[5] = c; r[6] = s;
735
- r[9] = -s; r[10] = c;
736
- r[15] = 1;
794
+
795
+ if (target) {
796
+ r[0] = 1; r[1] = 0; r[2] = 0; r[3] = 0;
797
+ r[4] = 0; r[5] = c; r[6] = s; r[7] = 0;
798
+ r[8] = 0; r[9] = -s; r[10] = c; r[11] = 0;
799
+ r[12] = 0; r[13] = 0; r[14] = 0; r[15] = 1;
800
+ } else {
801
+ r[0] = 1;
802
+ r[5] = c; r[6] = s;
803
+ r[9] = -s; r[10] = c;
804
+ r[15] = 1;
805
+ }
737
806
  return out;
738
807
  }
739
808
 
@@ -741,51 +810,78 @@ export class Mat4x4 {
741
810
  * Create XW plane rotation matrix (4D hyperspace rotation)
742
811
  * Creates "inside-out" effect when w approaches viewer
743
812
  * @param {number} angle - Rotation angle in radians
813
+ * @param {Mat4x4} [target=null] - Optional target matrix
744
814
  * @returns {Mat4x4}
745
815
  */
746
- static rotationXW(angle) {
816
+ static rotationXW(angle, target = null) {
747
817
  const c = Math.cos(angle);
748
818
  const s = Math.sin(angle);
749
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
819
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
750
820
  const r = out.data;
751
- r[0] = c; r[3] = s;
752
- r[5] = 1;
753
- r[10] = 1;
754
- r[12] = -s; r[15] = c;
821
+
822
+ if (target) {
823
+ r[0] = c; r[1] = 0; r[2] = 0; r[3] = s;
824
+ r[4] = 0; r[5] = 1; r[6] = 0; r[7] = 0;
825
+ r[8] = 0; r[9] = 0; r[10] = 1; r[11] = 0;
826
+ r[12] = -s; r[13] = 0; r[14] = 0; r[15] = c;
827
+ } else {
828
+ r[0] = c; r[3] = s;
829
+ r[5] = 1;
830
+ r[10] = 1;
831
+ r[12] = -s; r[15] = c;
832
+ }
755
833
  return out;
756
834
  }
757
835
 
758
836
  /**
759
837
  * Create YW plane rotation matrix (4D hyperspace rotation)
760
838
  * @param {number} angle - Rotation angle in radians
839
+ * @param {Mat4x4} [target=null] - Optional target matrix
761
840
  * @returns {Mat4x4}
762
841
  */
763
- static rotationYW(angle) {
842
+ static rotationYW(angle, target = null) {
764
843
  const c = Math.cos(angle);
765
844
  const s = Math.sin(angle);
766
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
845
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
767
846
  const r = out.data;
768
- r[0] = 1;
769
- r[5] = c; r[7] = s;
770
- r[10] = 1;
771
- r[13] = -s; r[15] = c;
847
+
848
+ if (target) {
849
+ r[0] = 1; r[1] = 0; r[2] = 0; r[3] = 0;
850
+ r[4] = 0; r[5] = c; r[6] = 0; r[7] = s;
851
+ r[8] = 0; r[9] = 0; r[10] = 1; r[11] = 0;
852
+ r[12] = 0; r[13] = -s; r[14] = 0; r[15] = c;
853
+ } else {
854
+ r[0] = 1;
855
+ r[5] = c; r[7] = s;
856
+ r[10] = 1;
857
+ r[13] = -s; r[15] = c;
858
+ }
772
859
  return out;
773
860
  }
774
861
 
775
862
  /**
776
863
  * Create ZW plane rotation matrix (4D hyperspace rotation)
777
864
  * @param {number} angle - Rotation angle in radians
865
+ * @param {Mat4x4} [target=null] - Optional target matrix
778
866
  * @returns {Mat4x4}
779
867
  */
780
- static rotationZW(angle) {
868
+ static rotationZW(angle, target = null) {
781
869
  const c = Math.cos(angle);
782
870
  const s = Math.sin(angle);
783
- const out = new Mat4x4(Mat4x4.UNINITIALIZED);
871
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
784
872
  const r = out.data;
785
- r[0] = 1;
786
- r[5] = 1;
787
- r[10] = c; r[11] = s;
788
- r[14] = -s; r[15] = c;
873
+
874
+ if (target) {
875
+ r[0] = 1; r[1] = 0; r[2] = 0; r[3] = 0;
876
+ r[4] = 0; r[5] = 1; r[6] = 0; r[7] = 0;
877
+ r[8] = 0; r[9] = 0; r[10] = c; r[11] = s;
878
+ r[12] = 0; r[13] = 0; r[14] = -s; r[15] = c;
879
+ } else {
880
+ r[0] = 1;
881
+ r[5] = 1;
882
+ r[10] = c; r[11] = s;
883
+ r[14] = -s; r[15] = c;
884
+ }
789
885
  return out;
790
886
  }
791
887
 
@@ -812,24 +908,58 @@ export class Mat4x4 {
812
908
  * Create combined rotation matrix from all 6 angles
813
909
  * Order: XY, XZ, YZ, XW, YW, ZW
814
910
  *
815
- * @param {object} angles - Rotation angles
816
- * @param {number} [angles.xy=0] - XY plane rotation
817
- * @param {number} [angles.xz=0] - XZ plane rotation
818
- * @param {number} [angles.yz=0] - YZ plane rotation
819
- * @param {number} [angles.xw=0] - XW plane rotation
820
- * @param {number} [angles.yw=0] - YW plane rotation
821
- * @param {number} [angles.zw=0] - ZW plane rotation
911
+ * Supports two signatures:
912
+ * 1. rotationFromAngles(angles, target?)
913
+ * 2. rotationFromAngles(xy, xz, yz, xw, yw, zw, target?)
914
+ *
915
+ * @param {object|number} anglesOrXY - Rotation angles object OR XY angle
916
+ * @param {number|Mat4x4} [xzOrTarget] - XZ angle OR target matrix
917
+ * @param {number} [yz=0] - YZ angle
918
+ * @param {number} [xw=0] - XW angle
919
+ * @param {number} [yw=0] - YW angle
920
+ * @param {number} [zw=0] - ZW angle
921
+ * @param {Mat4x4} [target=null] - Target matrix (if using 6-arg signature)
822
922
  * @returns {Mat4x4}
823
923
  */
824
- static rotationFromAngles(angles) {
825
- let result = Mat4x4.identity();
924
+ static rotationFromAngles(anglesOrXY, xzOrTarget, yz, xw, yw, zw, target) {
925
+ let xy = 0, xz = 0;
926
+ let _yz = 0, _xw = 0, _yw = 0, _zw = 0;
927
+ let out = null;
928
+
929
+ if (typeof anglesOrXY === 'number') {
930
+ // Signature: (xy, xz, yz, xw, yw, zw, target)
931
+ xy = anglesOrXY;
932
+ xz = typeof xzOrTarget === 'number' ? xzOrTarget : 0;
933
+ _yz = yz || 0;
934
+ _xw = xw || 0;
935
+ _yw = yw || 0;
936
+ _zw = zw || 0;
937
+ out = target || null;
938
+ } else {
939
+ // Signature: (angles, target)
940
+ const angles = anglesOrXY || {};
941
+ xy = angles.xy || 0;
942
+ xz = angles.xz || 0;
943
+ _yz = angles.yz || 0;
944
+ _xw = angles.xw || 0;
945
+ _yw = angles.yw || 0;
946
+ _zw = angles.zw || 0;
947
+ // The second argument is the target in this case
948
+ // Use duck typing or check constructor name to avoid circular dependency issues if any
949
+ if (xzOrTarget && typeof xzOrTarget === 'object' && xzOrTarget.data) {
950
+ out = xzOrTarget;
951
+ }
952
+ }
953
+
954
+ const result = out || new Mat4x4(); // Default constructor is identity
955
+ if (out) result.identity(); // Reset if reused
826
956
 
827
- if (angles.xy) result.rotateXY(angles.xy);
828
- if (angles.xz) result.rotateXZ(angles.xz);
829
- if (angles.yz) result.rotateYZ(angles.yz);
830
- if (angles.xw) result.rotateXW(angles.xw);
831
- if (angles.yw) result.rotateYW(angles.yw);
832
- if (angles.zw) result.rotateZW(angles.zw);
957
+ if (xy) result.rotateXY(xy);
958
+ if (xz) result.rotateXZ(xz);
959
+ if (_yz) result.rotateYZ(_yz);
960
+ if (_xw) result.rotateXW(_xw);
961
+ if (_yw) result.rotateYW(_yw);
962
+ if (_zw) result.rotateZW(_zw);
833
963
 
834
964
  return result;
835
965
  }
@@ -36,16 +36,28 @@ export class Projection {
36
36
  *
37
37
  * @param {Vec4} v - 4D point
38
38
  * @param {number} d - Distance parameter (typically 1.5-5)
39
+ * @param {object} [options] - Projection options
40
+ * @param {Vec4} [target] - Optional target vector to write result to
39
41
  * @returns {Vec4} Projected point (w=0)
40
42
  */
41
- static perspective(v, d = 2, options = {}) {
43
+ static perspective(v, d = 2, options = {}, target = null) {
42
44
  if (typeof d === 'object') {
43
45
  options = d;
44
46
  d = options.d ?? 2;
45
47
  }
46
- const epsilon = options.epsilon ?? DEFAULT_EPSILON;
48
+
49
+ // Handle options overload or direct target argument
50
+ if (!target && options && options.target) {
51
+ target = options.target;
52
+ }
53
+
54
+ const epsilon = (options && options.epsilon) ?? DEFAULT_EPSILON;
47
55
  const denom = clampDenominator(d - v.w, epsilon);
48
56
  const scale = 1 / denom;
57
+
58
+ if (target) {
59
+ return target.set(v.x * scale, v.y * scale, v.z * scale, 0);
60
+ }
49
61
  return new Vec4(v.x * scale, v.y * scale, v.z * scale, 0);
50
62
  }
51
63
 
@@ -60,12 +72,23 @@ export class Projection {
60
72
  * The projection point is at (0, 0, 0, 1) - the "north pole"
61
73
  *
62
74
  * @param {Vec4} v - 4D point (ideally on unit hypersphere)
75
+ * @param {object|Vec4} [options] - Projection options or target vector
76
+ * @param {Vec4} [target] - Optional target vector to write result to
63
77
  * @returns {Vec4} Projected point (w=0)
64
78
  */
65
- static stereographic(v, options = {}) {
66
- const epsilon = options.epsilon ?? DEFAULT_EPSILON;
79
+ static stereographic(v, options = {}, target = null) {
80
+ if (options instanceof Vec4) {
81
+ target = options;
82
+ options = {};
83
+ }
84
+
85
+ const epsilon = (options && options.epsilon) ?? DEFAULT_EPSILON;
67
86
  const denom = clampDenominator(1 - v.w, epsilon);
68
87
  const scale = 1 / denom;
88
+
89
+ if (target) {
90
+ return target.set(v.x * scale, v.y * scale, v.z * scale, 0);
91
+ }
69
92
  return new Vec4(v.x * scale, v.y * scale, v.z * scale, 0);
70
93
  }
71
94
 
@@ -95,9 +118,13 @@ export class Projection {
95
118
  * Parallel projection - no perspective distortion.
96
119
  *
97
120
  * @param {Vec4} v - 4D point
121
+ * @param {Vec4} [target] - Optional target vector to write result to
98
122
  * @returns {Vec4} Projected point (w=0)
99
123
  */
100
- static orthographic(v) {
124
+ static orthographic(v, target = null) {
125
+ if (target) {
126
+ return target.set(v.x, v.y, v.z, 0);
127
+ }
101
128
  return new Vec4(v.x, v.y, v.z, 0);
102
129
  }
103
130
 
@@ -126,10 +153,33 @@ export class Projection {
126
153
  * Project array of Vec4s using perspective projection
127
154
  * @param {Vec4[]} vectors
128
155
  * @param {number} d
156
+ * @param {object} [options]
157
+ * @param {Vec4[]} [target] - Optional target array to write results to
129
158
  * @returns {Vec4[]}
130
159
  */
131
- static perspectiveArray(vectors, d = 2, options = {}) {
132
- return vectors.map(v => Projection.perspective(v, d, options));
160
+ static perspectiveArray(vectors, d = 2, options = {}, target = null) {
161
+ // Handle options overload for 'd'
162
+ if (typeof d === 'object') {
163
+ options = d;
164
+ d = options.d ?? 2;
165
+ }
166
+
167
+ if (!target) {
168
+ return vectors.map(v => Projection.perspective(v, d, options));
169
+ }
170
+
171
+ const count = vectors.length;
172
+ // Iterate and reuse
173
+ for (let i = 0; i < count; i++) {
174
+ const out = target[i];
175
+ if (out) {
176
+ Projection.perspective(vectors[i], d, options, out);
177
+ } else {
178
+ target[i] = Projection.perspective(vectors[i], d, options);
179
+ }
180
+ }
181
+
182
+ return target;
133
183
  }
134
184
 
135
185
  /**
@@ -276,48 +276,54 @@ export class Rotor4D {
276
276
  * The result applies this rotation, then r's rotation
277
277
  *
278
278
  * @param {Rotor4D} r - Right operand
279
+ * @param {Rotor4D} [target=null] - Optional target rotor to write result into
279
280
  * @returns {Rotor4D} Composed rotor
280
281
  */
281
- multiply(r) {
282
+ multiply(r, target = null) {
282
283
  // Full geometric product of two rotors in 4D
283
284
  // This is derived from the geometric algebra product rules
284
285
 
285
286
  const a = this;
286
287
  const b = r;
287
288
 
288
- return new Rotor4D(
289
- // Scalar component
290
- a.s * b.s - a.xy * b.xy - a.xz * b.xz - a.yz * b.yz -
291
- a.xw * b.xw - a.yw * b.yw - a.zw * b.zw - a.xyzw * b.xyzw,
289
+ // Compute all components first to ensure safety if target aliases a or b
290
+ const s = a.s * b.s - a.xy * b.xy - a.xz * b.xz - a.yz * b.yz -
291
+ a.xw * b.xw - a.yw * b.yw - a.zw * b.zw - a.xyzw * b.xyzw;
292
292
 
293
- // XY bivector
294
- a.s * b.xy + a.xy * b.s + a.xz * b.yz - a.yz * b.xz +
295
- a.xw * b.yw - a.yw * b.xw - a.zw * b.xyzw - a.xyzw * b.zw,
293
+ const xy = a.s * b.xy + a.xy * b.s + a.xz * b.yz - a.yz * b.xz +
294
+ a.xw * b.yw - a.yw * b.xw - a.zw * b.xyzw - a.xyzw * b.zw;
296
295
 
297
- // XZ bivector
298
- a.s * b.xz + a.xz * b.s - a.xy * b.yz + a.yz * b.xy +
299
- a.xw * b.zw + a.yw * b.xyzw - a.zw * b.xw + a.xyzw * b.yw,
296
+ const xz = a.s * b.xz + a.xz * b.s - a.xy * b.yz + a.yz * b.xy +
297
+ a.xw * b.zw + a.yw * b.xyzw - a.zw * b.xw + a.xyzw * b.yw;
300
298
 
301
- // YZ bivector
302
- a.s * b.yz + a.yz * b.s + a.xy * b.xz - a.xz * b.xy -
303
- a.xw * b.xyzw + a.yw * b.zw - a.zw * b.yw - a.xyzw * b.xw,
299
+ const yz = a.s * b.yz + a.yz * b.s + a.xy * b.xz - a.xz * b.xy -
300
+ a.xw * b.xyzw + a.yw * b.zw - a.zw * b.yw - a.xyzw * b.xw;
304
301
 
305
- // XW bivector
306
- a.s * b.xw + a.xw * b.s - a.xy * b.yw + a.xz * b.zw +
307
- a.yz * b.xyzw + a.yw * b.xy - a.zw * b.xz + a.xyzw * b.yz,
302
+ const xw = a.s * b.xw + a.xw * b.s - a.xy * b.yw + a.xz * b.zw +
303
+ a.yz * b.xyzw + a.yw * b.xy - a.zw * b.xz + a.xyzw * b.yz;
308
304
 
309
- // YW bivector
310
- a.s * b.yw + a.yw * b.s + a.xy * b.xw - a.xz * b.xyzw -
311
- a.yz * b.zw - a.xw * b.xy + a.zw * b.yz - a.xyzw * b.xz,
305
+ const yw = a.s * b.yw + a.yw * b.s + a.xy * b.xw - a.xz * b.xyzw -
306
+ a.yz * b.zw - a.xw * b.xy + a.zw * b.yz - a.xyzw * b.xz;
312
307
 
313
- // ZW bivector
314
- a.s * b.zw + a.zw * b.s + a.xy * b.xyzw + a.xz * b.xw +
315
- a.yz * b.yw - a.xw * b.xz - a.yw * b.yz + a.xyzw * b.xy,
308
+ const zw = a.s * b.zw + a.zw * b.s + a.xy * b.xyzw + a.xz * b.xw +
309
+ a.yz * b.yw - a.xw * b.xz - a.yw * b.yz + a.xyzw * b.xy;
316
310
 
317
- // Pseudoscalar XYZW
318
- a.s * b.xyzw + a.xyzw * b.s + a.xy * b.zw - a.xz * b.yw +
319
- a.yz * b.xw + a.xw * b.yz - a.yw * b.xz + a.zw * b.xy
320
- );
311
+ const xyzw = a.s * b.xyzw + a.xyzw * b.s + a.xy * b.zw - a.xz * b.yw +
312
+ a.yz * b.xw + a.xw * b.yz - a.yw * b.xz + a.zw * b.xy;
313
+
314
+ if (target) {
315
+ target.s = s;
316
+ target.xy = xy;
317
+ target.xz = xz;
318
+ target.yz = yz;
319
+ target.xw = xw;
320
+ target.yw = yw;
321
+ target.zw = zw;
322
+ target.xyzw = xyzw;
323
+ return target;
324
+ }
325
+
326
+ return new Rotor4D(s, xy, xz, yz, xw, yw, zw, xyzw);
321
327
  }
322
328
 
323
329
  /**
package/src/math/Vec4.js CHANGED
@@ -313,7 +313,11 @@ export class Vec4 {
313
313
  * @returns {number}
314
314
  */
315
315
  distanceTo(v) {
316
- return this.sub(v).length();
316
+ const dx = this._x - v._x;
317
+ const dy = this._y - v._y;
318
+ const dz = this._z - v._z;
319
+ const dw = this._w - v._w;
320
+ return Math.sqrt(dx * dx + dy * dy + dz * dz + dw * dw);
317
321
  }
318
322
 
319
323
  /**
@@ -322,7 +326,11 @@ export class Vec4 {
322
326
  * @returns {number}
323
327
  */
324
328
  distanceToSquared(v) {
325
- return this.sub(v).lengthSquared();
329
+ const dx = this._x - v._x;
330
+ const dy = this._y - v._y;
331
+ const dz = this._z - v._z;
332
+ const dw = this._w - v._w;
333
+ return dx * dx + dy * dy + dz * dz + dw * dw;
326
334
  }
327
335
 
328
336
  /**
@@ -408,42 +416,91 @@ export class Vec4 {
408
416
  /**
409
417
  * Project 4D point to 3D using perspective projection
410
418
  * Projects from 4D to 3D by dividing by (d - w)
411
- * @param {number} d - Distance parameter (usually 2-5)
412
- * @param {object} [options] - Projection options (epsilon, distance)
419
+ * @param {number|object} d - Distance parameter (usually 2-5) or options object
420
+ * @param {object|Vec4} [options] - Projection options or target vector
421
+ * @param {Vec4} [target] - Target vector to store result
413
422
  * @returns {Vec4} Projected point (w component is 0)
414
423
  */
415
- projectPerspective(d = 2, options = {}) {
424
+ projectPerspective(d = 2, options = {}, target = null) {
416
425
  if (typeof d === 'object') {
426
+ // usage: projectPerspective({ distance: 2, ... }, target?)
427
+ if (options instanceof Vec4) {
428
+ target = options;
429
+ }
417
430
  options = d;
418
431
  d = options.distance ?? options.d ?? 2;
432
+ } else {
433
+ // usage: projectPerspective(d, options?, target?)
434
+ // usage: projectPerspective(d, target?)
435
+ if (options instanceof Vec4) {
436
+ target = options;
437
+ options = {};
438
+ }
419
439
  }
440
+
441
+ options = options || {};
442
+
420
443
  const epsilon = options.epsilon ?? 1e-5;
421
444
  const denom = d - this._w;
422
445
  const clamped = Math.abs(denom) < epsilon ? (denom >= 0 ? epsilon : -epsilon) : denom;
423
446
  const scale = 1 / clamped;
447
+
448
+ if (target) {
449
+ target._x = this._x * scale;
450
+ target._y = this._y * scale;
451
+ target._z = this._z * scale;
452
+ target._w = 0;
453
+ return target;
454
+ }
455
+
424
456
  return new Vec4(this._x * scale, this._y * scale, this._z * scale, 0);
425
457
  }
426
458
 
427
459
  /**
428
460
  * Project 4D point to 3D using stereographic projection
429
461
  * Maps 4D hypersphere to 3D space
430
- * @param {object} [options] - Projection options (epsilon)
462
+ * @param {object|Vec4} [options] - Projection options or target vector
463
+ * @param {Vec4} [target] - Target vector to store result
431
464
  * @returns {Vec4} Projected point (w component is 0)
432
465
  */
433
- projectStereographic(options = {}) {
466
+ projectStereographic(options = {}, target = null) {
467
+ if (options instanceof Vec4) {
468
+ target = options;
469
+ options = {};
470
+ }
471
+
472
+ options = options || {};
473
+
434
474
  const epsilon = options.epsilon ?? 1e-5;
435
475
  const denom = 1 - this._w;
436
476
  const clamped = Math.abs(denom) < epsilon ? (denom >= 0 ? epsilon : -epsilon) : denom;
437
477
  const scale = 1 / clamped;
478
+
479
+ if (target) {
480
+ target._x = this._x * scale;
481
+ target._y = this._y * scale;
482
+ target._z = this._z * scale;
483
+ target._w = 0;
484
+ return target;
485
+ }
486
+
438
487
  return new Vec4(this._x * scale, this._y * scale, this._z * scale, 0);
439
488
  }
440
489
 
441
490
  /**
442
491
  * Project 4D point to 3D using orthographic projection
443
492
  * Simply drops the W component
493
+ * @param {Vec4} [target=null] - Optional target vector
444
494
  * @returns {Vec4} Projected point (w component is 0)
445
495
  */
446
- projectOrthographic() {
496
+ projectOrthographic(target = null) {
497
+ if (target) {
498
+ target._x = this._x;
499
+ target._y = this._y;
500
+ target._z = this._z;
501
+ target._w = 0;
502
+ return target;
503
+ }
447
504
  return new Vec4(this._x, this._y, this._z, 0);
448
505
  }
449
506
 
@@ -787,6 +787,11 @@ void main() {
787
787
  }`;
788
788
 
789
789
  this.program = this.createProgram(vertexShaderSource, fragmentShaderSource);
790
+
791
+ if (!this.program) {
792
+ return;
793
+ }
794
+
790
795
  this.uniforms = {
791
796
  resolution: this.gl.getUniformLocation(this.program, 'u_resolution'),
792
797
  time: this.gl.getUniformLocation(this.program, 'u_time'),
@@ -817,6 +822,18 @@ void main() {
817
822
  * Create WebGL program from shaders
818
823
  */
819
824
  createProgram(vertexSource, fragmentSource) {
825
+ // CRITICAL FIX: Check WebGL context state before shader operations
826
+ if (!this.gl) {
827
+ console.error('❌ Cannot create program: WebGL context is null');
828
+ return null;
829
+ }
830
+
831
+ if (this.gl.isContextLost()) {
832
+ console.error('❌ Cannot create program: WebGL context is lost');
833
+ this._contextLost = true;
834
+ return null;
835
+ }
836
+
820
837
  const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
821
838
  const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
822
839
 
@@ -860,6 +877,7 @@ void main() {
860
877
 
861
878
  if (this.gl.isContextLost()) {
862
879
  console.error('❌ Cannot create shader: WebGL context is lost');
880
+ this._contextLost = true;
863
881
  if (window.mobileDebug) {
864
882
  window.mobileDebug.log(`❌ ${this.canvas?.id}: Cannot create shader - WebGL context is lost`);
865
883
  }
@@ -924,6 +942,11 @@ void main() {
924
942
  * Initialize vertex buffers
925
943
  */
926
944
  initBuffers() {
945
+ // CRITICAL FIX: Check WebGL context state before buffer operations
946
+ if (!this.gl || this.gl.isContextLost()) {
947
+ return;
948
+ }
949
+
927
950
  const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
928
951
 
929
952
  this.buffer = this.gl.createBuffer();
@@ -939,6 +962,11 @@ void main() {
939
962
  * Resize canvas and viewport
940
963
  */
941
964
  resize() {
965
+ // CRITICAL FIX: Check WebGL context state before viewport operations
966
+ if (!this.gl || this.gl.isContextLost()) {
967
+ return;
968
+ }
969
+
942
970
  // Mobile-optimized canvas sizing
943
971
  const dpr = Math.min(window.devicePixelRatio || 1, 2); // Cap at 2x for mobile performance
944
972
  const width = this.canvas.clientWidth;
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import Projection from '../math/Projection.js';
3
+ import Vec4 from '../math/Vec4.js';
4
+
5
+ describe('Projection Class', () => {
6
+ it('should project using perspective', () => {
7
+ const v = new Vec4(1, 1, 1, 0);
8
+ const p = Projection.perspective(v, 2);
9
+ expect(p.x).toBeCloseTo(0.5);
10
+ expect(p.y).toBeCloseTo(0.5);
11
+ expect(p.z).toBeCloseTo(0.5);
12
+ expect(p.w).toBe(0);
13
+ });
14
+
15
+ it('should support target vector in perspective', () => {
16
+ const v = new Vec4(1, 1, 1, 0);
17
+ const target = new Vec4();
18
+ const result = Projection.perspective(v, 2, {}, target);
19
+ expect(result).toBe(target);
20
+ expect(target.x).toBeCloseTo(0.5);
21
+ });
22
+
23
+ it('should project array using perspectiveArray', () => {
24
+ const vectors = [new Vec4(1, 1, 1, 0), new Vec4(2, 2, 2, 0)];
25
+ const result = Projection.perspectiveArray(vectors, 2);
26
+ expect(result.length).toBe(2);
27
+ expect(result[0].x).toBeCloseTo(0.5);
28
+ expect(result[1].x).toBeCloseTo(1.0);
29
+ });
30
+
31
+ it('should reuse target array in perspectiveArray', () => {
32
+ const vectors = [new Vec4(1, 1, 1, 0)];
33
+ const targetArray = [new Vec4()];
34
+ const result = Projection.perspectiveArray(vectors, 2, {}, targetArray);
35
+ expect(result).toBe(targetArray);
36
+ expect(targetArray[0].x).toBeCloseTo(0.5);
37
+ });
38
+ });
@@ -200,12 +200,17 @@ function createFallbackModule() {
200
200
  ),
201
201
 
202
202
  // Projections
203
- projectPerspective: JsProjection.perspectiveProject,
204
- projectStereographic: JsProjection.stereographicProject,
205
- projectOrthographic: JsProjection.orthographicProject,
206
- projectOblique: JsProjection.obliqueProject,
207
- projectSlice: JsProjection.sliceProject,
208
- projectToFloatArray: JsProjection.projectToFloatArray
203
+ projectPerspective: JsProjection.Projection.perspective,
204
+ projectStereographic: JsProjection.Projection.stereographic,
205
+ projectOrthographic: JsProjection.Projection.orthographic,
206
+ projectOblique: JsProjection.Projection.oblique,
207
+ projectSlice: (v, wPlane, tolerance) => {
208
+ if (JsProjection.SliceProjection.isInSlice(v, wPlane, tolerance)) {
209
+ return JsProjection.Projection.orthographic(v);
210
+ }
211
+ return null;
212
+ },
213
+ projectToFloatArray: JsProjection.Projection.perspectivePacked
209
214
  };
210
215
  }
211
216
 
@@ -0,0 +1,109 @@
1
+ import re
2
+
3
+ file_path = 'src/math/Projection.js'
4
+
5
+ with open(file_path, 'r') as f:
6
+ content = f.read()
7
+
8
+ # Replace perspective JSDoc and Implementation
9
+ perspective_old = r""" * @param {Vec4} v - 4D point
10
+ * @param {number} d - Distance parameter (typically 1.5-5)
11
+ * @returns {Vec4} Projected point (w=0)
12
+ */
13
+ static perspective(v, d = 2, options = {}) {
14
+ if (typeof d === 'object') {
15
+ options = d;
16
+ d = options.d ?? 2;
17
+ }
18
+ const epsilon = options.epsilon ?? DEFAULT_EPSILON;
19
+ const denom = clampDenominator(d - v.w, epsilon);
20
+ const scale = 1 / denom;
21
+ return new Vec4(v.x * scale, v.y * scale, v.z * scale, 0);
22
+ }"""
23
+
24
+ perspective_new = r""" * @param {Vec4} v - 4D point
25
+ * @param {number} d - Distance parameter (typically 1.5-5)
26
+ * @param {object} [options] - Projection options
27
+ * @param {Vec4} [target] - Optional target vector to write result to
28
+ * @returns {Vec4} Projected point (w=0)
29
+ */
30
+ static perspective(v, d = 2, options = {}, target = null) {
31
+ if (typeof d === 'object') {
32
+ options = d;
33
+ d = options.d ?? 2;
34
+ }
35
+
36
+ // Handle options overload or direct target argument
37
+ if (!target && options && options.target) {
38
+ target = options.target;
39
+ }
40
+
41
+ const epsilon = (options && options.epsilon) ?? DEFAULT_EPSILON;
42
+ const denom = clampDenominator(d - v.w, epsilon);
43
+ const scale = 1 / denom;
44
+
45
+ if (target) {
46
+ return target.set(v.x * scale, v.y * scale, v.z * scale, 0);
47
+ }
48
+ return new Vec4(v.x * scale, v.y * scale, v.z * scale, 0);
49
+ }"""
50
+
51
+ if perspective_old in content:
52
+ content = content.replace(perspective_old, perspective_new)
53
+ else:
54
+ print("Could not find perspective implementation to replace.")
55
+ # Attempt more robust search if exact match fails? No, simpler is safer for now.
56
+
57
+ # Replace perspectiveArray JSDoc and Implementation
58
+ perspective_array_old = r""" /**
59
+ * Project array of Vec4s using perspective projection
60
+ * @param {Vec4[]} vectors
61
+ * @param {number} d
62
+ * @returns {Vec4[]}
63
+ */
64
+ static perspectiveArray(vectors, d = 2, options = {}) {
65
+ return vectors.map(v => Projection.perspective(v, d, options));
66
+ }"""
67
+
68
+ perspective_array_new = r""" /**
69
+ * Project array of Vec4s using perspective projection
70
+ * @param {Vec4[]} vectors
71
+ * @param {number} d
72
+ * @param {object} [options]
73
+ * @param {Vec4[]} [target] - Optional target array to write results to
74
+ * @returns {Vec4[]}
75
+ */
76
+ static perspectiveArray(vectors, d = 2, options = {}, target = null) {
77
+ // Handle options overload for 'd'
78
+ if (typeof d === 'object') {
79
+ options = d;
80
+ d = options.d ?? 2;
81
+ }
82
+
83
+ if (!target) {
84
+ return vectors.map(v => Projection.perspective(v, d, options));
85
+ }
86
+
87
+ const count = vectors.length;
88
+ // Iterate and reuse
89
+ for (let i = 0; i < count; i++) {
90
+ const out = target[i];
91
+ if (out) {
92
+ Projection.perspective(vectors[i], d, options, out);
93
+ } else {
94
+ target[i] = Projection.perspective(vectors[i], d, options);
95
+ }
96
+ }
97
+
98
+ return target;
99
+ }"""
100
+
101
+ if perspective_array_old in content:
102
+ content = content.replace(perspective_array_old, perspective_array_new)
103
+ else:
104
+ print("Could not find perspectiveArray implementation to replace.")
105
+
106
+ with open(file_path, 'w') as f:
107
+ f.write(content)
108
+
109
+ print("Updated Projection.js")