@vib3code/sdk 2.0.3-canary.3b3ca18 → 2.0.3-canary.4874bcf

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.
Files changed (69) hide show
  1. package/DOCS/AGENT_HARNESS_ARCHITECTURE.md +2 -0
  2. package/DOCS/ANDROID_DEPLOYMENT.md +59 -0
  3. package/DOCS/ARCHITECTURE.md +1 -0
  4. package/DOCS/CI_TESTING.md +2 -0
  5. package/DOCS/CLI_ONBOARDING.md +2 -0
  6. package/DOCS/CONTROL_REFERENCE.md +2 -0
  7. package/DOCS/CROSS_SITE_DESIGN_PATTERNS.md +2 -0
  8. package/DOCS/ENV_SETUP.md +2 -0
  9. package/DOCS/EPIC_SCROLL_EVENTS.md +2 -0
  10. package/DOCS/EXPANSION_DESIGN.md +2 -0
  11. package/DOCS/EXPANSION_DESIGN_ULTRA.md +2 -0
  12. package/DOCS/EXPORT_FORMATS.md +2 -0
  13. package/DOCS/GPU_DISPOSAL_GUIDE.md +2 -0
  14. package/DOCS/HANDOFF_LANDING_PAGE.md +2 -0
  15. package/DOCS/HANDOFF_SDK_DEVELOPMENT.md +2 -0
  16. package/DOCS/LICENSING_TIERS.md +2 -0
  17. package/DOCS/MASTER_PLAN_2026-01-31.md +2 -0
  18. package/DOCS/MULTIVIZ_CHOREOGRAPHY_PATTERNS.md +3 -1
  19. package/DOCS/OBS_SETUP_GUIDE.md +2 -0
  20. package/DOCS/OPTIMIZATION_PLAN_MATH.md +1 -0
  21. package/DOCS/PRODUCT_STRATEGY.md +2 -0
  22. package/DOCS/PROJECT_SETUP.md +2 -0
  23. package/DOCS/README.md +5 -3
  24. package/DOCS/REFERENCE_SCROLL_ANALYSIS.md +2 -0
  25. package/DOCS/RENDERER_LIFECYCLE.md +2 -0
  26. package/DOCS/REPO_MANIFEST.md +2 -0
  27. package/DOCS/ROADMAP.md +2 -0
  28. package/DOCS/SCROLL_TIMELINE_v3.md +2 -0
  29. package/DOCS/SITE_REFACTOR_PLAN.md +2 -0
  30. package/DOCS/STATUS.md +2 -0
  31. package/DOCS/SYSTEM_INVENTORY.md +2 -0
  32. package/DOCS/TELEMETRY_EXPORTS.md +2 -0
  33. package/DOCS/VISUAL_ANALYSIS_CLICKERSS.md +2 -0
  34. package/DOCS/VISUAL_ANALYSIS_FACETAD.md +2 -0
  35. package/DOCS/VISUAL_ANALYSIS_SIMONE.md +2 -0
  36. package/DOCS/VISUAL_ANALYSIS_TABLESIDE.md +2 -0
  37. package/DOCS/WEBGPU_STATUS.md +2 -0
  38. package/DOCS/XR_BENCHMARKS.md +2 -0
  39. package/DOCS/archive/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +1 -34
  40. package/DOCS/archive/DEV_TRACK_ANALYSIS.md +1 -80
  41. package/DOCS/archive/DEV_TRACK_PLAN_2026-01-07.md +1 -42
  42. package/DOCS/archive/SESSION_014_PLAN.md +1 -195
  43. package/DOCS/archive/SESSION_LOG_2026-01-07.md +1 -56
  44. package/DOCS/archive/STRATEGIC_BLUEPRINT_2026-01-07.md +1 -72
  45. package/DOCS/archive/SYSTEM_AUDIT_2026-01-30.md +1 -741
  46. package/DOCS/archive/WEBGPU_STATUS_2026-02-15_STALE.md +1 -38
  47. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-01-31.md +2 -0
  48. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-06.md +2 -0
  49. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-13.md +2 -0
  50. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-15.md +2 -0
  51. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-16.md +2 -0
  52. package/DOCS/dev-tracks/PERF_UPGRADE_2026-02-16.md +2 -0
  53. package/DOCS/dev-tracks/README.md +2 -0
  54. package/package.json +2 -4
  55. package/src/cli/index.js +59 -5
  56. package/src/export/SVGExporter.js +9 -5
  57. package/src/features/CollectionManager.js +27 -9
  58. package/src/gallery/CollectionManager.js +27 -9
  59. package/src/geometry/warp/HypersphereCore.js +53 -24
  60. package/src/math/Mat4x4.js +272 -128
  61. package/src/math/Projection.js +57 -7
  62. package/src/math/Rotor4D.js +64 -28
  63. package/src/math/Vec4.js +65 -8
  64. package/src/quantum/QuantumVisualizer.js +28 -0
  65. package/src/scene/Node4D.js +74 -24
  66. package/src/testing/ProjectionClass.test.js +38 -0
  67. package/src/variations/VariationManager.js +6 -1
  68. package/src/wasm/WasmLoader.js +11 -6
  69. package/tools/update_projection.py +109 -0
@@ -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
  /**
@@ -429,9 +435,10 @@ export class Rotor4D {
429
435
 
430
436
  /**
431
437
  * Convert rotor to 4x4 rotation matrix (column-major for WebGL)
438
+ * @param {Float32Array|Array} [target] - Optional target array to write into
432
439
  * @returns {Float32Array} 16-element array in column-major order
433
440
  */
434
- toMatrix() {
441
+ toMatrix(target = null) {
435
442
  // Normalize first for numerical stability
436
443
  const n = this.norm();
437
444
  const invN = n > 1e-10 ? 1 / n : 1;
@@ -495,6 +502,35 @@ export class Rotor4D {
495
502
  // Formula derived from sandwich product R v R†
496
503
  // Diagonal: s² minus bivectors containing that axis, plus others
497
504
  // Off-diagonal: 2*s*bivector terms for single-plane contributions
505
+
506
+ if (target) {
507
+ // Column 0 (transformed X axis)
508
+ target[0] = s2 - xy2 - xz2 + yz2 - xw2 + yw2 + zw2 - xyzw2;
509
+ target[1] = sxy + xzyz + xwyw - zwxyzw;
510
+ target[2] = sxz - xyyz + xwzw + ywxyzw;
511
+ target[3] = sxw - xyyw - xzzw - yzxyzw;
512
+
513
+ // Column 1 (transformed Y axis)
514
+ target[4] = -sxy + xzyz + xwyw + zwxyzw;
515
+ target[5] = s2 - xy2 + xz2 - yz2 + xw2 - yw2 + zw2 - xyzw2;
516
+ target[6] = syz + xyxz + ywzw - xwxyzw;
517
+ target[7] = syw + xyxw - yzzw + xzxyzw;
518
+
519
+ // Column 2 (transformed Z axis)
520
+ target[8] = -sxz - xyyz + xwzw - ywxyzw;
521
+ target[9] = -syz + xyxz + ywzw + xwxyzw;
522
+ target[10] = s2 + xy2 - xz2 - yz2 + xw2 + yw2 - zw2 - xyzw2;
523
+ target[11] = szw + xzxw + yzyw - xyxyzw;
524
+
525
+ // Column 3 (transformed W axis)
526
+ target[12] = -sxw - xyyw - xzzw + yzxyzw;
527
+ target[13] = -syw + xyxw - yzzw - xzxyzw;
528
+ target[14] = -szw + xzxw + yzyw + xyxyzw;
529
+ target[15] = s2 + xy2 + xz2 + yz2 - xw2 - yw2 - zw2 - xyzw2;
530
+
531
+ return target;
532
+ }
533
+
498
534
  return new Float32Array([
499
535
  // Column 0 (transformed X axis)
500
536
  s2 - xy2 - xz2 + yz2 - xw2 + yw2 + zw2 - xyzw2,
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;
@@ -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
+ });
@@ -417,7 +417,12 @@ export class VariationManager {
417
417
  * Get variation statistics
418
418
  */
419
419
  getStatistics() {
420
- const customCount = this.customVariations.filter(v => v !== null).length;
420
+ let customCount = 0;
421
+ for (let i = 0; i < this.customVariations.length; i++) {
422
+ if (this.customVariations[i] !== null) {
423
+ customCount++;
424
+ }
425
+ }
421
426
 
422
427
  return {
423
428
  totalVariations: this.totalVariations,
@@ -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