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

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.bd4d4a5",
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",
@@ -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
 
@@ -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;