@vib3code/sdk 2.0.3-canary.7b9491f → 2.0.3-canary.8d2fdcd
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 +1 -1
- package/src/cli/index.js +59 -5
- package/src/export/SVGExporter.js +9 -5
- package/src/math/Projection.js +39 -4
- package/src/math/Vec4.js +46 -5
- package/src/testing/ProjectionClass.test.js +38 -0
- 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.8d2fdcd",
|
|
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",
|
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/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
|
|
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/math/Vec4.js
CHANGED
|
@@ -408,33 +408,74 @@ export class Vec4 {
|
|
|
408
408
|
/**
|
|
409
409
|
* Project 4D point to 3D using perspective projection
|
|
410
410
|
* 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
|
|
411
|
+
* @param {number|object} d - Distance parameter (usually 2-5) or options object
|
|
412
|
+
* @param {object|Vec4} [options] - Projection options or target vector
|
|
413
|
+
* @param {Vec4} [target] - Target vector to store result
|
|
413
414
|
* @returns {Vec4} Projected point (w component is 0)
|
|
414
415
|
*/
|
|
415
|
-
projectPerspective(d = 2, options = {}) {
|
|
416
|
+
projectPerspective(d = 2, options = {}, target = null) {
|
|
416
417
|
if (typeof d === 'object') {
|
|
418
|
+
// usage: projectPerspective({ distance: 2, ... }, target?)
|
|
419
|
+
if (options instanceof Vec4) {
|
|
420
|
+
target = options;
|
|
421
|
+
}
|
|
417
422
|
options = d;
|
|
418
423
|
d = options.distance ?? options.d ?? 2;
|
|
424
|
+
} else {
|
|
425
|
+
// usage: projectPerspective(d, options?, target?)
|
|
426
|
+
// usage: projectPerspective(d, target?)
|
|
427
|
+
if (options instanceof Vec4) {
|
|
428
|
+
target = options;
|
|
429
|
+
options = {};
|
|
430
|
+
}
|
|
419
431
|
}
|
|
432
|
+
|
|
433
|
+
options = options || {};
|
|
434
|
+
|
|
420
435
|
const epsilon = options.epsilon ?? 1e-5;
|
|
421
436
|
const denom = d - this._w;
|
|
422
437
|
const clamped = Math.abs(denom) < epsilon ? (denom >= 0 ? epsilon : -epsilon) : denom;
|
|
423
438
|
const scale = 1 / clamped;
|
|
439
|
+
|
|
440
|
+
if (target) {
|
|
441
|
+
target._x = this._x * scale;
|
|
442
|
+
target._y = this._y * scale;
|
|
443
|
+
target._z = this._z * scale;
|
|
444
|
+
target._w = 0;
|
|
445
|
+
return target;
|
|
446
|
+
}
|
|
447
|
+
|
|
424
448
|
return new Vec4(this._x * scale, this._y * scale, this._z * scale, 0);
|
|
425
449
|
}
|
|
426
450
|
|
|
427
451
|
/**
|
|
428
452
|
* Project 4D point to 3D using stereographic projection
|
|
429
453
|
* Maps 4D hypersphere to 3D space
|
|
430
|
-
* @param {object} [options] - Projection options
|
|
454
|
+
* @param {object|Vec4} [options] - Projection options or target vector
|
|
455
|
+
* @param {Vec4} [target] - Target vector to store result
|
|
431
456
|
* @returns {Vec4} Projected point (w component is 0)
|
|
432
457
|
*/
|
|
433
|
-
projectStereographic(options = {}) {
|
|
458
|
+
projectStereographic(options = {}, target = null) {
|
|
459
|
+
if (options instanceof Vec4) {
|
|
460
|
+
target = options;
|
|
461
|
+
options = {};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
options = options || {};
|
|
465
|
+
|
|
434
466
|
const epsilon = options.epsilon ?? 1e-5;
|
|
435
467
|
const denom = 1 - this._w;
|
|
436
468
|
const clamped = Math.abs(denom) < epsilon ? (denom >= 0 ? epsilon : -epsilon) : denom;
|
|
437
469
|
const scale = 1 / clamped;
|
|
470
|
+
|
|
471
|
+
if (target) {
|
|
472
|
+
target._x = this._x * scale;
|
|
473
|
+
target._y = this._y * scale;
|
|
474
|
+
target._z = this._z * scale;
|
|
475
|
+
target._w = 0;
|
|
476
|
+
return target;
|
|
477
|
+
}
|
|
478
|
+
|
|
438
479
|
return new Vec4(this._x * scale, this._y * scale, this._z * scale, 0);
|
|
439
480
|
}
|
|
440
481
|
|
|
@@ -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")
|