@vib3code/sdk 2.0.3-canary.b52c293 → 2.0.3-canary.c544441

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.b52c293",
3
+ "version": "2.0.3-canary.c544441",
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
  }
@@ -16,13 +16,14 @@ import { Vec4 } from '../../math/Vec4.js';
16
16
  * @param {number} radius - Hypersphere radius
17
17
  * @returns {Vec4} Point on hypersphere
18
18
  */
19
- export function projectToHypersphere(point, radius = 1) {
19
+ export function projectToHypersphere(point, radius = 1, target = null) {
20
20
  const len = point.length();
21
21
  if (len < 0.0001) {
22
22
  // Handle origin - project to north pole
23
+ if (target) return target.set(0, 0, 0, radius);
23
24
  return new Vec4(0, 0, 0, radius);
24
25
  }
25
- return point.scale(radius / len);
26
+ return point.scale(radius / len, target);
26
27
  }
27
28
 
28
29
  /**
@@ -30,9 +31,10 @@ export function projectToHypersphere(point, radius = 1) {
30
31
  * Maps all of 3D space onto the 4D hypersphere
31
32
  * @param {Vec4} point - Input point (uses x, y, z)
32
33
  * @param {number} radius - Hypersphere radius
34
+ * @param {Vec4} [target=null] - Optional target vector
33
35
  * @returns {Vec4} Point on hypersphere
34
36
  */
35
- export function stereographicToHypersphere(point, radius = 1) {
37
+ export function stereographicToHypersphere(point, radius = 1, target = null) {
36
38
  const x = point.x;
37
39
  const y = point.y;
38
40
  const z = point.z;
@@ -40,6 +42,15 @@ export function stereographicToHypersphere(point, radius = 1) {
40
42
  const sumSq = x * x + y * y + z * z;
41
43
  const denom = sumSq + 1;
42
44
 
45
+ if (target) {
46
+ return target.set(
47
+ (2 * x) / denom * radius,
48
+ (2 * y) / denom * radius,
49
+ (2 * z) / denom * radius,
50
+ (sumSq - 1) / denom * radius
51
+ );
52
+ }
53
+
43
54
  return new Vec4(
44
55
  (2 * x) / denom * radius,
45
56
  (2 * y) / denom * radius,
@@ -56,12 +67,22 @@ export function stereographicToHypersphere(point, radius = 1) {
56
67
  * @param {number} phi - Azimuth on base S² (0 to 2π)
57
68
  * @param {number} psi - Fiber angle (0 to 2π)
58
69
  * @param {number} radius - Hypersphere radius
70
+ * @param {Vec4} [target=null] - Optional target vector
59
71
  * @returns {Vec4} Point on hypersphere
60
72
  */
61
- export function hopfFibration(theta, phi, psi, radius = 1) {
73
+ export function hopfFibration(theta, phi, psi, radius = 1, target = null) {
62
74
  const cosTheta2 = Math.cos(theta / 2);
63
75
  const sinTheta2 = Math.sin(theta / 2);
64
76
 
77
+ if (target) {
78
+ return target.set(
79
+ cosTheta2 * Math.cos((phi + psi) / 2) * radius,
80
+ cosTheta2 * Math.sin((phi + psi) / 2) * radius,
81
+ sinTheta2 * Math.cos((phi - psi) / 2) * radius,
82
+ sinTheta2 * Math.sin((phi - psi) / 2) * radius
83
+ );
84
+ }
85
+
65
86
  return new Vec4(
66
87
  cosTheta2 * Math.cos((phi + psi) / 2) * radius,
67
88
  cosTheta2 * Math.sin((phi + psi) / 2) * radius,
@@ -78,8 +99,9 @@ export function hopfFibration(theta, phi, psi, radius = 1) {
78
99
  * @returns {Vec4[]} Warped vertices
79
100
  */
80
101
  export function warpRadial(vertices, radius = 1, blendFactor = 1) {
102
+ const onSphere = new Vec4();
81
103
  return vertices.map(v => {
82
- const onSphere = projectToHypersphere(v, radius);
104
+ projectToHypersphere(v, radius, onSphere);
83
105
  return v.lerp(onSphere, blendFactor);
84
106
  });
85
107
  }
@@ -93,8 +115,9 @@ export function warpRadial(vertices, radius = 1, blendFactor = 1) {
93
115
  * @returns {Vec4[]} Warped vertices
94
116
  */
95
117
  export function warpStereographic(vertices, radius = 1, scale = 1) {
118
+ const scaled = new Vec4();
96
119
  return vertices.map(v => {
97
- const scaled = v.scale(scale);
120
+ v.scale(scale, scaled);
98
121
  return stereographicToHypersphere(scaled, radius);
99
122
  });
100
123
  }
@@ -146,25 +169,31 @@ export function warpHypersphereCore(geometry, options = {}) {
146
169
  twist = 1
147
170
  } = options;
148
171
 
149
- let warpedVertices;
150
-
151
- // Pre-scale vertices
152
- const scaledVertices = geometry.vertices.map(v => v.scale(scale));
153
-
154
- switch (method) {
155
- case 'stereographic':
156
- warpedVertices = warpStereographic(scaledVertices, radius, 1);
157
- break;
158
-
159
- case 'hopf':
160
- warpedVertices = warpHopf(scaledVertices, radius, twist);
161
- break;
172
+ const temp = new Vec4();
173
+ const warpedVertices = geometry.vertices.map(v => {
174
+ // Combined scaling and warping to minimize allocations
175
+ const result = v.scale(scale);
176
+
177
+ if (method === 'stereographic') {
178
+ stereographicToHypersphere(result, radius, result);
179
+ } else if (method === 'hopf') {
180
+ const r = result.length();
181
+ if (r < 0.0001) {
182
+ result.set(0, 0, 0, radius);
183
+ } else {
184
+ const theta = Math.acos(result.z / r);
185
+ const phi = Math.atan2(result.y, result.x);
186
+ const psi = result.w * twist + phi * 0.5;
187
+ hopfFibration(theta, phi, psi, radius, result);
188
+ }
189
+ } else {
190
+ // Radial (default)
191
+ projectToHypersphere(result, radius, temp);
192
+ result.lerp(temp, blend, result);
193
+ }
162
194
 
163
- case 'radial':
164
- default:
165
- warpedVertices = warpRadial(scaledVertices, radius, blend);
166
- break;
167
- }
195
+ return result;
196
+ });
168
197
 
169
198
  return {
170
199
  ...geometry,
@@ -18,6 +18,12 @@
18
18
  import { Vec4 } from './Vec4.js';
19
19
 
20
20
  export class Mat4x4 {
21
+ /**
22
+ * Internal token to skip initialization during construction
23
+ * @private
24
+ */
25
+ static UNINITIALIZED = {};
26
+
21
27
  /**
22
28
  * Create a new 4x4 matrix
23
29
  * Default is identity matrix
@@ -26,6 +32,8 @@ export class Mat4x4 {
26
32
  constructor(elements = null) {
27
33
  this.data = new Float32Array(16);
28
34
 
35
+ if (elements === Mat4x4.UNINITIALIZED) return;
36
+
29
37
  if (elements) {
30
38
  if (elements.length !== 16) {
31
39
  throw new Error('Mat4x4 requires exactly 16 elements');
@@ -45,12 +53,7 @@ export class Mat4x4 {
45
53
  * @returns {Mat4x4}
46
54
  */
47
55
  static identity() {
48
- return new Mat4x4([
49
- 1, 0, 0, 0,
50
- 0, 1, 0, 0,
51
- 0, 0, 1, 0,
52
- 0, 0, 0, 1
53
- ]);
56
+ return new Mat4x4();
54
57
  }
55
58
 
56
59
  /**
@@ -58,7 +61,7 @@ export class Mat4x4 {
58
61
  * @returns {Mat4x4}
59
62
  */
60
63
  static zero() {
61
- return new Mat4x4(new Float32Array(16));
64
+ return new Mat4x4(Mat4x4.UNINITIALIZED);
62
65
  }
63
66
 
64
67
  /**
@@ -161,7 +164,7 @@ export class Mat4x4 {
161
164
  * @returns {Mat4x4} New matrix = this * m
162
165
  */
163
166
  multiply(m, target = null) {
164
- const out = target || new Mat4x4();
167
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
165
168
  const r = out.data;
166
169
  const a = this.data;
167
170
  const b = m.data;
@@ -321,7 +324,7 @@ export class Mat4x4 {
321
324
  * @returns {Mat4x4} New matrix
322
325
  */
323
326
  add(m, target = null) {
324
- const out = target || new Mat4x4();
327
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
325
328
  const r = out.data;
326
329
  const a = this.data;
327
330
  const b = m.data;
@@ -339,7 +342,7 @@ export class Mat4x4 {
339
342
  * @returns {Mat4x4} New matrix
340
343
  */
341
344
  scale(s, target = null) {
342
- const out = target || new Mat4x4();
345
+ const out = target || new Mat4x4(Mat4x4.UNINITIALIZED);
343
346
  const r = out.data;
344
347
  const a = this.data;
345
348
 
@@ -355,12 +358,13 @@ export class Mat4x4 {
355
358
  */
356
359
  transpose() {
357
360
  const m = this.data;
358
- return new Mat4x4([
359
- m[0], m[4], m[8], m[12],
360
- m[1], m[5], m[9], m[13],
361
- m[2], m[6], m[10], m[14],
362
- m[3], m[7], m[11], m[15]
363
- ]);
361
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
362
+ const r = out.data;
363
+ r[0] = m[0]; r[4] = m[1]; r[8] = m[2]; r[12] = m[3];
364
+ r[1] = m[4]; r[5] = m[5]; r[9] = m[6]; r[13] = m[7];
365
+ r[2] = m[8]; r[6] = m[9]; r[10] = m[10]; r[14] = m[11];
366
+ r[3] = m[12]; r[7] = m[13]; r[11] = m[14]; r[15] = m[15];
367
+ return out;
364
368
  }
365
369
 
366
370
  /**
@@ -415,7 +419,8 @@ export class Mat4x4 {
415
419
  */
416
420
  inverse() {
417
421
  const m = this.data;
418
- const inv = new Float32Array(16);
422
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
423
+ const inv = out.data;
419
424
 
420
425
  inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] +
421
426
  m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
@@ -476,7 +481,7 @@ export class Mat4x4 {
476
481
  inv[i] *= invDet;
477
482
  }
478
483
 
479
- return new Mat4x4(inv);
484
+ return out;
480
485
  }
481
486
 
482
487
  /**
@@ -689,12 +694,13 @@ export class Mat4x4 {
689
694
  static rotationXY(angle) {
690
695
  const c = Math.cos(angle);
691
696
  const s = Math.sin(angle);
692
- return new Mat4x4([
693
- c, s, 0, 0,
694
- -s, c, 0, 0,
695
- 0, 0, 1, 0,
696
- 0, 0, 0, 1
697
- ]);
697
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
698
+ 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;
703
+ return out;
698
704
  }
699
705
 
700
706
  /**
@@ -705,12 +711,13 @@ export class Mat4x4 {
705
711
  static rotationXZ(angle) {
706
712
  const c = Math.cos(angle);
707
713
  const s = Math.sin(angle);
708
- return new Mat4x4([
709
- c, 0, -s, 0,
710
- 0, 1, 0, 0,
711
- s, 0, c, 0,
712
- 0, 0, 0, 1
713
- ]);
714
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
715
+ 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;
720
+ return out;
714
721
  }
715
722
 
716
723
  /**
@@ -721,12 +728,13 @@ export class Mat4x4 {
721
728
  static rotationYZ(angle) {
722
729
  const c = Math.cos(angle);
723
730
  const s = Math.sin(angle);
724
- return new Mat4x4([
725
- 1, 0, 0, 0,
726
- 0, c, s, 0,
727
- 0, -s, c, 0,
728
- 0, 0, 0, 1
729
- ]);
731
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
732
+ 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;
737
+ return out;
730
738
  }
731
739
 
732
740
  /**
@@ -738,12 +746,13 @@ export class Mat4x4 {
738
746
  static rotationXW(angle) {
739
747
  const c = Math.cos(angle);
740
748
  const s = Math.sin(angle);
741
- return new Mat4x4([
742
- c, 0, 0, s,
743
- 0, 1, 0, 0,
744
- 0, 0, 1, 0,
745
- -s, 0, 0, c
746
- ]);
749
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
750
+ 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;
755
+ return out;
747
756
  }
748
757
 
749
758
  /**
@@ -754,12 +763,13 @@ export class Mat4x4 {
754
763
  static rotationYW(angle) {
755
764
  const c = Math.cos(angle);
756
765
  const s = Math.sin(angle);
757
- return new Mat4x4([
758
- 1, 0, 0, 0,
759
- 0, c, 0, s,
760
- 0, 0, 1, 0,
761
- 0, -s, 0, c
762
- ]);
766
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
767
+ 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;
772
+ return out;
763
773
  }
764
774
 
765
775
  /**
@@ -770,12 +780,13 @@ export class Mat4x4 {
770
780
  static rotationZW(angle) {
771
781
  const c = Math.cos(angle);
772
782
  const s = Math.sin(angle);
773
- return new Mat4x4([
774
- 1, 0, 0, 0,
775
- 0, 1, 0, 0,
776
- 0, 0, c, s,
777
- 0, 0, -s, c
778
- ]);
783
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
784
+ 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;
789
+ return out;
779
790
  }
780
791
 
781
792
  /**
@@ -847,12 +858,13 @@ export class Mat4x4 {
847
858
  * @returns {Mat4x4}
848
859
  */
849
860
  static uniformScale(s) {
850
- return new Mat4x4([
851
- s, 0, 0, 0,
852
- 0, s, 0, 0,
853
- 0, 0, s, 0,
854
- 0, 0, 0, s
855
- ]);
861
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
862
+ const r = out.data;
863
+ r[0] = s;
864
+ r[5] = s;
865
+ r[10] = s;
866
+ r[15] = s;
867
+ return out;
856
868
  }
857
869
 
858
870
  /**
@@ -864,12 +876,13 @@ export class Mat4x4 {
864
876
  * @returns {Mat4x4}
865
877
  */
866
878
  static scale(sx, sy, sz, sw = 1) {
867
- return new Mat4x4([
868
- sx, 0, 0, 0,
869
- 0, sy, 0, 0,
870
- 0, 0, sz, 0,
871
- 0, 0, 0, sw
872
- ]);
879
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
880
+ const r = out.data;
881
+ r[0] = sx;
882
+ r[5] = sy;
883
+ r[10] = sz;
884
+ r[15] = sw;
885
+ return out;
873
886
  }
874
887
 
875
888
  /**
@@ -885,12 +898,13 @@ export class Mat4x4 {
885
898
  static translation(tx, ty, tz, tw = 0) {
886
899
  // For true 4D translation, you need 5D homogeneous coordinates
887
900
  // This is a placeholder that adds the translation to the W column
888
- return new Mat4x4([
889
- 1, 0, 0, 0,
890
- 0, 1, 0, 0,
891
- 0, 0, 1, 0,
892
- tx, ty, tz, 1 + tw
893
- ]);
901
+ const out = new Mat4x4(Mat4x4.UNINITIALIZED);
902
+ const r = out.data;
903
+ r[0] = 1;
904
+ r[5] = 1;
905
+ r[10] = 1;
906
+ r[12] = tx; r[13] = ty; r[14] = tz; r[15] = 1 + tw;
907
+ return out;
894
908
  }
895
909
  }
896
910
 
@@ -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
 
@@ -126,10 +138,33 @@ export class Projection {
126
138
  * Project array of Vec4s using perspective projection
127
139
  * @param {Vec4[]} vectors
128
140
  * @param {number} d
141
+ * @param {object} [options]
142
+ * @param {Vec4[]} [target] - Optional target array to write results to
129
143
  * @returns {Vec4[]}
130
144
  */
131
- static perspectiveArray(vectors, d = 2, options = {}) {
132
- return vectors.map(v => Projection.perspective(v, d, options));
145
+ static perspectiveArray(vectors, d = 2, options = {}, target = null) {
146
+ // Handle options overload for 'd'
147
+ if (typeof d === 'object') {
148
+ options = d;
149
+ d = options.d ?? 2;
150
+ }
151
+
152
+ if (!target) {
153
+ return vectors.map(v => Projection.perspective(v, d, options));
154
+ }
155
+
156
+ const count = vectors.length;
157
+ // Iterate and reuse
158
+ for (let i = 0; i < count; i++) {
159
+ const out = target[i];
160
+ if (out) {
161
+ Projection.perspective(vectors[i], d, options, out);
162
+ } else {
163
+ target[i] = Projection.perspective(vectors[i], d, options);
164
+ }
165
+ }
166
+
167
+ return target;
133
168
  }
134
169
 
135
170
  /**
@@ -429,9 +429,10 @@ export class Rotor4D {
429
429
 
430
430
  /**
431
431
  * Convert rotor to 4x4 rotation matrix (column-major for WebGL)
432
+ * @param {Float32Array|Array} [target] - Optional target array to write into
432
433
  * @returns {Float32Array} 16-element array in column-major order
433
434
  */
434
- toMatrix() {
435
+ toMatrix(target = null) {
435
436
  // Normalize first for numerical stability
436
437
  const n = this.norm();
437
438
  const invN = n > 1e-10 ? 1 / n : 1;
@@ -495,6 +496,35 @@ export class Rotor4D {
495
496
  // Formula derived from sandwich product R v R†
496
497
  // Diagonal: s² minus bivectors containing that axis, plus others
497
498
  // Off-diagonal: 2*s*bivector terms for single-plane contributions
499
+
500
+ if (target) {
501
+ // Column 0 (transformed X axis)
502
+ target[0] = s2 - xy2 - xz2 + yz2 - xw2 + yw2 + zw2 - xyzw2;
503
+ target[1] = sxy + xzyz + xwyw - zwxyzw;
504
+ target[2] = sxz - xyyz + xwzw + ywxyzw;
505
+ target[3] = sxw - xyyw - xzzw - yzxyzw;
506
+
507
+ // Column 1 (transformed Y axis)
508
+ target[4] = -sxy + xzyz + xwyw + zwxyzw;
509
+ target[5] = s2 - xy2 + xz2 - yz2 + xw2 - yw2 + zw2 - xyzw2;
510
+ target[6] = syz + xyxz + ywzw - xwxyzw;
511
+ target[7] = syw + xyxw - yzzw + xzxyzw;
512
+
513
+ // Column 2 (transformed Z axis)
514
+ target[8] = -sxz - xyyz + xwzw - ywxyzw;
515
+ target[9] = -syz + xyxz + ywzw + xwxyzw;
516
+ target[10] = s2 + xy2 - xz2 - yz2 + xw2 + yw2 - zw2 - xyzw2;
517
+ target[11] = szw + xzxw + yzyw - xyxyzw;
518
+
519
+ // Column 3 (transformed W axis)
520
+ target[12] = -sxw - xyyw - xzzw + yzxyzw;
521
+ target[13] = -syw + xyxw - yzzw - xzxyzw;
522
+ target[14] = -szw + xzxw + yzyw + xyxyzw;
523
+ target[15] = s2 + xy2 + xz2 + yz2 - xw2 - yw2 - zw2 - xyzw2;
524
+
525
+ return target;
526
+ }
527
+
498
528
  return new Float32Array([
499
529
  // Column 0 (transformed X axis)
500
530
  s2 - xy2 - xz2 + yz2 - xw2 + yw2 + zw2 - xyzw2,
@@ -500,29 +500,74 @@ export class Node4D {
500
500
  * @private
501
501
  */
502
502
  _updateLocalMatrix() {
503
- // Start with identity
504
- this._localMatrix = Mat4x4.identity();
503
+ // Ensure matrix exists
504
+ if (!this._localMatrix) {
505
+ this._localMatrix = new Mat4x4();
506
+ }
507
+
508
+ const m = this._localMatrix.data;
509
+ const s = this._scale;
510
+ const p = this._position;
511
+
512
+ // 1. Write rotation directly to local matrix (No allocation)
513
+ this._rotation.toMatrix(m);
514
+
515
+ // 2. Apply scale (Diagonal matrix multiplication on the right)
516
+ // M = M * S
517
+ // Columns of M are scaled by s.x, s.y, s.z, s.w
518
+
519
+ // Col 0
520
+ m[0] *= s.x; m[1] *= s.x; m[2] *= s.x; m[3] *= s.x;
521
+ // Col 1
522
+ m[4] *= s.y; m[5] *= s.y; m[6] *= s.y; m[7] *= s.y;
523
+ // Col 2
524
+ m[8] *= s.z; m[9] *= s.z; m[10] *= s.z; m[11] *= s.z;
525
+ // Col 3
526
+ m[12] *= s.w; m[13] *= s.w; m[14] *= s.w; m[15] *= s.w;
527
+
528
+ // 3. Apply translation (Matrix multiplication on the left)
529
+ // M = T * M
530
+ // T is standard 3D translation:
531
+ // [ 1 0 0 px ]
532
+ // [ 0 1 0 py ]
533
+ // [ 0 0 1 pz ]
534
+ // [ 0 0 0 1 ]
535
+ //
536
+ // Row 0 += px * Row 3
537
+ // Row 1 += py * Row 3
538
+ // Row 2 += pz * Row 3
539
+
540
+ const px = p.x;
541
+ const py = p.y;
542
+ const pz = p.z;
543
+
544
+ // Row 3 elements of M (which are used in the calculation)
545
+ const m3 = m[3];
546
+ const m7 = m[7];
547
+ const m11 = m[11];
548
+ const m15 = m[15];
549
+
550
+ if (px !== 0) {
551
+ m[0] += px * m3;
552
+ m[4] += px * m7;
553
+ m[8] += px * m11;
554
+ m[12] += px * m15;
555
+ }
556
+
557
+ if (py !== 0) {
558
+ m[1] += py * m3;
559
+ m[5] += py * m7;
560
+ m[9] += py * m11;
561
+ m[13] += py * m15;
562
+ }
563
+
564
+ if (pz !== 0) {
565
+ m[2] += pz * m3;
566
+ m[6] += pz * m7;
567
+ m[10] += pz * m11;
568
+ m[14] += pz * m15;
569
+ }
505
570
 
506
- // Apply scale
507
- const scaleMatrix = Mat4x4.identity();
508
- scaleMatrix.set(0, 0, this._scale.x);
509
- scaleMatrix.set(1, 1, this._scale.y);
510
- scaleMatrix.set(2, 2, this._scale.z);
511
- scaleMatrix.set(3, 3, this._scale.w);
512
-
513
- // Apply rotation (toMatrix returns Float32Array, wrap in Mat4x4)
514
- const rotationMatrix = new Mat4x4(this._rotation.toMatrix());
515
-
516
- // Apply translation (in 4D, translation is stored in last column, keep [3,3]=1)
517
- const translationMatrix = Mat4x4.identity();
518
- translationMatrix.set(0, 3, this._position.x);
519
- translationMatrix.set(1, 3, this._position.y);
520
- translationMatrix.set(2, 3, this._position.z);
521
- // Note: position.w is the 4th spatial coordinate, handled separately
522
- // Matrix[3,3] must remain 1 for proper transformation
523
-
524
- // Compose: T * R * S
525
- this._localMatrix = translationMatrix.multiply(rotationMatrix).multiply(scaleMatrix);
526
571
  this._localDirty = false;
527
572
  }
528
573
 
@@ -535,10 +580,15 @@ export class Node4D {
535
580
  this._updateLocalMatrix();
536
581
  }
537
582
 
583
+ // Ensure matrix exists
584
+ if (!this._worldMatrix) {
585
+ this._worldMatrix = new Mat4x4();
586
+ }
587
+
538
588
  if (this._parent) {
539
- this._worldMatrix = this._parent.worldMatrix.multiply(this._localMatrix);
589
+ this._parent.worldMatrix.multiply(this._localMatrix, this._worldMatrix);
540
590
  } else {
541
- this._worldMatrix = this._localMatrix.clone();
591
+ this._worldMatrix.copy(this._localMatrix);
542
592
  }
543
593
 
544
594
  this._worldDirty = false;
@@ -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
+ });
@@ -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")