@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 +2 -4
- package/src/cli/index.js +59 -5
- package/src/export/SVGExporter.js +9 -5
- package/src/math/Mat4x4.js +218 -88
- package/src/math/Projection.js +57 -7
- package/src/math/Rotor4D.js +33 -27
- package/src/math/Vec4.js +65 -8
- package/src/quantum/QuantumVisualizer.js +28 -0
- package/src/testing/ProjectionClass.test.js +38 -0
- package/src/wasm/WasmLoader.js +11 -6
- package/tools/update_projection.py +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vib3code/sdk",
|
|
3
|
-
"version": "2.0.3-canary.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
+
rotor.rotate(point, rotatedBuffer);
|
|
361
365
|
|
|
362
366
|
// Project to 3D (perspective from W)
|
|
363
|
-
|
|
367
|
+
rotatedBuffer.projectPerspective(dimension, projectedBuffer);
|
|
364
368
|
|
|
365
369
|
// Project to 2D (simple orthographic for clean SVG)
|
|
366
|
-
const x = centerX +
|
|
367
|
-
const y = centerY -
|
|
368
|
-
const depth =
|
|
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
|
}
|
package/src/math/Mat4x4.js
CHANGED
|
@@ -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
|
-
|
|
426
|
-
|
|
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] = -
|
|
429
|
-
|
|
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] =
|
|
432
|
-
|
|
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] = -
|
|
435
|
-
|
|
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] = -
|
|
438
|
-
|
|
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] =
|
|
441
|
-
|
|
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] = -
|
|
444
|
-
|
|
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] =
|
|
447
|
-
|
|
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] =
|
|
450
|
-
|
|
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] = -
|
|
453
|
-
|
|
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] =
|
|
456
|
-
|
|
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] = -
|
|
459
|
-
|
|
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] = -
|
|
462
|
-
|
|
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] =
|
|
465
|
-
|
|
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] = -
|
|
468
|
-
|
|
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] =
|
|
471
|
-
|
|
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 =
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
* @param {number}
|
|
820
|
-
* @param {number} [
|
|
821
|
-
* @param {number} [
|
|
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(
|
|
825
|
-
let
|
|
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 (
|
|
828
|
-
if (
|
|
829
|
-
if (
|
|
830
|
-
if (
|
|
831
|
-
if (
|
|
832
|
-
if (
|
|
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
|
}
|
package/src/math/Projection.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/src/math/Rotor4D.js
CHANGED
|
@@ -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
|
-
|
|
289
|
-
|
|
290
|
-
a.
|
|
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
|
-
|
|
294
|
-
a.
|
|
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
|
-
|
|
298
|
-
a.
|
|
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
|
-
|
|
302
|
-
a.
|
|
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
|
-
|
|
306
|
-
a.
|
|
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
|
-
|
|
310
|
-
a.
|
|
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
|
-
|
|
314
|
-
a.
|
|
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
|
-
|
|
318
|
-
a.
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
});
|
package/src/wasm/WasmLoader.js
CHANGED
|
@@ -200,12 +200,17 @@ function createFallbackModule() {
|
|
|
200
200
|
),
|
|
201
201
|
|
|
202
202
|
// Projections
|
|
203
|
-
projectPerspective: JsProjection.
|
|
204
|
-
projectStereographic: JsProjection.
|
|
205
|
-
projectOrthographic: JsProjection.
|
|
206
|
-
projectOblique: JsProjection.
|
|
207
|
-
projectSlice:
|
|
208
|
-
|
|
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")
|