@vib3code/sdk 2.0.3-canary.91a95f3 → 2.0.3-canary.ef8d292

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 (74) hide show
  1. package/DOCS/MASTER_PLAN_2026-01-31.md +2 -2
  2. package/DOCS/SYSTEM_INVENTORY.md +2 -2
  3. package/DOCS/WEBGPU_STATUS.md +119 -38
  4. package/DOCS/archive/WEBGPU_STATUS_2026-02-15_STALE.md +38 -0
  5. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-13.md +13 -0
  6. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-15.md +142 -0
  7. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-16.md +108 -0
  8. package/docs/webgpu-live.html +1 -1
  9. package/package.json +10 -1
  10. package/src/agent/index.js +1 -3
  11. package/src/agent/mcp/MCPServer.js +347 -52
  12. package/src/agent/mcp/index.js +1 -1
  13. package/src/agent/mcp/tools.js +87 -0
  14. package/src/cli/index.js +374 -44
  15. package/src/core/VIB3Engine.js +55 -3
  16. package/src/core/index.js +18 -0
  17. package/src/core/renderers/FacetedRendererAdapter.js +10 -9
  18. package/src/core/renderers/HolographicRendererAdapter.js +11 -7
  19. package/src/core/renderers/QuantumRendererAdapter.js +11 -7
  20. package/src/creative/index.js +11 -0
  21. package/src/export/index.js +11 -1
  22. package/src/faceted/FacetedSystem.js +27 -10
  23. package/src/games/glyph-war/GlyphWarVisualizer.js +641 -0
  24. package/src/holograms/HolographicVisualizer.js +58 -89
  25. package/src/holograms/RealHolographicSystem.js +126 -31
  26. package/src/math/Mat4x4.js +70 -13
  27. package/src/math/Rotor4D.js +100 -39
  28. package/src/math/index.js +7 -7
  29. package/src/quantum/QuantumVisualizer.js +24 -20
  30. package/src/reactivity/index.js +3 -5
  31. package/src/render/LayerPresetManager.js +372 -0
  32. package/src/render/LayerReactivityBridge.js +344 -0
  33. package/src/render/LayerRelationshipGraph.js +610 -0
  34. package/src/render/MultiCanvasBridge.js +148 -25
  35. package/src/render/ShaderLoader.js +38 -0
  36. package/src/render/ShaderProgram.js +4 -4
  37. package/src/render/UnifiedRenderBridge.js +1 -1
  38. package/src/render/backends/WebGPUBackend.js +8 -4
  39. package/src/render/index.js +27 -2
  40. package/src/scene/index.js +4 -4
  41. package/src/shaders/common/geometry24.glsl +65 -0
  42. package/src/shaders/common/geometry24.wgsl +54 -0
  43. package/src/shaders/common/rotation4d.glsl +4 -4
  44. package/src/shaders/common/rotation4d.wgsl +2 -2
  45. package/src/shaders/common/uniforms.wgsl +15 -8
  46. package/src/shaders/faceted/faceted.frag.wgsl +19 -6
  47. package/src/shaders/holographic/holographic.frag.wgsl +7 -5
  48. package/src/shaders/quantum/quantum.frag.wgsl +7 -5
  49. package/src/testing/ParallelTestFramework.js +2 -2
  50. package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +2 -2
  51. package/src/viewer/GalleryUI.js +17 -0
  52. package/src/viewer/ViewerPortal.js +2 -2
  53. package/tools/shader-sync-verify.js +6 -4
  54. package/types/adaptive-sdk.d.ts +204 -5
  55. package/types/agent/cli.d.ts +78 -0
  56. package/types/agent/index.d.ts +18 -0
  57. package/types/agent/mcp.d.ts +87 -0
  58. package/types/agent/telemetry.d.ts +190 -0
  59. package/types/core/VIB3Engine.d.ts +26 -0
  60. package/types/core/index.d.ts +261 -0
  61. package/types/creative/AestheticMapper.d.ts +72 -0
  62. package/types/creative/ChoreographyPlayer.d.ts +96 -0
  63. package/types/creative/index.d.ts +17 -0
  64. package/types/export/index.d.ts +243 -0
  65. package/types/geometry/index.d.ts +164 -0
  66. package/types/math/index.d.ts +214 -0
  67. package/types/render/LayerPresetManager.d.ts +78 -0
  68. package/types/render/LayerReactivityBridge.d.ts +85 -0
  69. package/types/render/LayerRelationshipGraph.d.ts +174 -0
  70. package/types/render/index.d.ts +3 -0
  71. package/types/scene/index.d.ts +204 -0
  72. package/types/systems/index.d.ts +244 -0
  73. package/types/variations/index.d.ts +62 -0
  74. package/types/viewer/index.d.ts +225 -0
@@ -324,53 +324,114 @@ export class Rotor4D {
324
324
  * Rotate a 4D vector using sandwich product: v' = R v R†
325
325
  *
326
326
  * @param {Vec4} v - Vector to rotate
327
+ * @param {Vec4} [target] - Optional target vector to write result to
327
328
  * @returns {Vec4} Rotated vector
328
329
  */
329
- rotate(v) {
330
- // For efficiency, we expand the sandwich product directly
331
- // rather than doing two rotor multiplications
330
+ rotate(v, target) {
331
+ // Direct matrix multiplication without allocation
332
332
 
333
- const x = v.x, y = v.y, z = v.z, w = v.w;
334
-
335
- // Compute R v (rotor times vector)
336
- // Vector in GA is: x*e1 + y*e2 + z*e3 + w*e4
337
- // This produces a mixed multivector
338
-
339
- // Then multiply by R† (reverse of rotor)
340
- // Extract the vector part of the result
333
+ // Normalize components for stability (same as toMatrix)
334
+ const n = this.norm();
335
+ const invN = n > 1e-10 ? 1 / n : 1;
341
336
 
342
- // Pre-compute some common terms
343
- const s = this.s;
344
- const xy = this.xy, xz = this.xz, yz = this.yz;
345
- const xw = this.xw, yw = this.yw, zw = this.zw;
346
- const xyzw = this.xyzw;
337
+ const s = this.s * invN;
338
+ const xy = this.xy * invN;
339
+ const xz = this.xz * invN;
340
+ const yz = this.yz * invN;
341
+ const xw = this.xw * invN;
342
+ const yw = this.yw * invN;
343
+ const zw = this.zw * invN;
344
+ const xyzw = this.xyzw * invN;
347
345
 
348
- // Squared terms for the rotation formula
346
+ // Pre-compute products
349
347
  const s2 = s * s;
350
- const xy2 = xy * xy, xz2 = xz * xz, yz2 = yz * yz;
351
- const xw2 = xw * xw, yw2 = yw * yw, zw2 = zw * zw;
348
+ const xy2 = xy * xy;
349
+ const xz2 = xz * xz;
350
+ const yz2 = yz * yz;
351
+ const xw2 = xw * xw;
352
+ const yw2 = yw * yw;
353
+ const zw2 = zw * zw;
352
354
  const xyzw2 = xyzw * xyzw;
353
355
 
354
- // The full rotation formula derived from R v R†
355
- const newX =
356
- x * (s2 + xy2 + xz2 - yz2 + xw2 - yw2 - zw2 - xyzw2) +
357
- 2 * y * (s * xy + xz * yz + xw * yw - s * xyzw * zw + xy * s - xyzw * zw) +
358
- 2 * z * (s * xz - xy * yz + xw * zw + xyzw * yw) +
359
- 2 * w * (s * xw - xy * yw - xz * zw - xyzw * yz);
360
-
361
- // Simplified rotation using matrix form
362
- // This is equivalent but clearer
363
-
364
- // Actually, let's use the direct matrix multiplication approach
365
- // which is more numerically stable
366
-
367
- const m = this.toMatrix();
368
- return new Vec4(
369
- m[0] * x + m[4] * y + m[8] * z + m[12] * w,
370
- m[1] * x + m[5] * y + m[9] * z + m[13] * w,
371
- m[2] * x + m[6] * y + m[10] * z + m[14] * w,
372
- m[3] * x + m[7] * y + m[11] * z + m[15] * w
373
- );
356
+ // Cross terms
357
+ const sxy = 2 * s * xy;
358
+ const sxz = 2 * s * xz;
359
+ const syz = 2 * s * yz;
360
+ const sxw = 2 * s * xw;
361
+ const syw = 2 * s * yw;
362
+ const szw = 2 * s * zw;
363
+ // const sxyzw = 2 * s * xyzw; // Unused in rotation matrix
364
+
365
+ const xyxz = 2 * xy * xz;
366
+ const xyyz = 2 * xy * yz;
367
+ const xyxw = 2 * xy * xw;
368
+ const xyyw = 2 * xy * yw;
369
+ // const xyzw_c = 2 * xy * zw; // Unused in rotation matrix
370
+
371
+ const xzyz = 2 * xz * yz;
372
+ const xzxw = 2 * xz * xw;
373
+ const xzyw = 2 * xz * yw;
374
+ const xzzw = 2 * xz * zw;
375
+
376
+ const yzxw = 2 * yz * xw;
377
+ const yzyw = 2 * yz * yw;
378
+ const yzzw = 2 * yz * zw;
379
+
380
+ const xwyw = 2 * xw * yw;
381
+ const xwzw = 2 * xw * zw;
382
+ const ywzw = 2 * yw * zw;
383
+
384
+ const xyxyzw = 2 * xy * xyzw;
385
+ const xzxyzw = 2 * xz * xyzw;
386
+ const yzxyzw = 2 * yz * xyzw;
387
+ const xwxyzw = 2 * xw * xyzw;
388
+ const ywxyzw = 2 * yw * xyzw;
389
+ const zwxyzw = 2 * zw * xyzw;
390
+
391
+ // Matrix elements
392
+ // Col 0
393
+ const m00 = s2 - xy2 - xz2 + yz2 - xw2 + yw2 + zw2 - xyzw2;
394
+ const m01 = sxy + xzyz + xwyw - zwxyzw;
395
+ const m02 = sxz - xyyz + xwzw + ywxyzw;
396
+ const m03 = sxw - xyyw - xzzw - yzxyzw;
397
+
398
+ // Col 1
399
+ const m10 = -sxy + xzyz + xwyw + zwxyzw;
400
+ const m11 = s2 - xy2 + xz2 - yz2 + xw2 - yw2 + zw2 - xyzw2;
401
+ const m12 = syz + xyxz + ywzw - xwxyzw;
402
+ const m13 = syw + xyxw - yzzw + xzxyzw;
403
+
404
+ // Col 2
405
+ const m20 = -sxz - xyyz + xwzw - ywxyzw;
406
+ const m21 = -syz + xyxz + ywzw + xwxyzw;
407
+ const m22 = s2 + xy2 - xz2 - yz2 + xw2 + yw2 - zw2 - xyzw2;
408
+ const m23 = szw + xzxw + yzyw - xyxyzw;
409
+
410
+ // Col 3
411
+ const m30 = -sxw - xyyw - xzzw + yzxyzw;
412
+ const m31 = -syw + xyxw - yzzw - xzxyzw;
413
+ const m32 = -szw + xzxw + yzyw + xyxyzw;
414
+ const m33 = s2 + xy2 + xz2 + yz2 - xw2 - yw2 - zw2 - xyzw2;
415
+
416
+ const x = v.x;
417
+ const y = v.y;
418
+ const z = v.z;
419
+ const w = v.w;
420
+
421
+ const rx = m00 * x + m10 * y + m20 * z + m30 * w;
422
+ const ry = m01 * x + m11 * y + m21 * z + m31 * w;
423
+ const rz = m02 * x + m12 * y + m22 * z + m32 * w;
424
+ const rw = m03 * x + m13 * y + m23 * z + m33 * w;
425
+
426
+ if (target) {
427
+ target.x = rx;
428
+ target.y = ry;
429
+ target.z = rz;
430
+ target.w = rw;
431
+ return target;
432
+ }
433
+
434
+ return new Vec4(rx, ry, rz, rw);
374
435
  }
375
436
 
376
437
  /**
package/src/math/index.js CHANGED
@@ -59,10 +59,10 @@ export {
59
59
  clamp, lerp, smoothstep, smootherstep
60
60
  } from './constants.js';
61
61
 
62
- // Default export for convenience
63
- export default {
64
- Vec4: (await import('./Vec4.js')).Vec4,
65
- Rotor4D: (await import('./Rotor4D.js')).Rotor4D,
66
- Mat4x4: (await import('./Mat4x4.js')).Mat4x4,
67
- Projection: (await import('./Projection.js')).Projection
68
- };
62
+ // Default export uses the static imports already declared above
63
+ import { Vec4 as _Vec4 } from './Vec4.js';
64
+ import { Rotor4D as _Rotor4D } from './Rotor4D.js';
65
+ import { Mat4x4 as _Mat4x4 } from './Mat4x4.js';
66
+ import { Projection as _Projection } from './Projection.js';
67
+
68
+ export default { Vec4: _Vec4, Rotor4D: _Rotor4D, Mat4x4: _Mat4x4, Projection: _Projection };
@@ -6,6 +6,17 @@
6
6
 
7
7
  import { GeometryLibrary } from '../geometry/GeometryLibrary.js';
8
8
 
9
+ // Role-specific intensity values for 5-layer canvas architecture.
10
+ // IMPORTANT: Must stay in sync with shader epsilon comparisons in the fragment shader
11
+ // at the "LAYER-BY-LAYER COLOR SYSTEM" section (search for layerIndex).
12
+ const ROLE_INTENSITIES = {
13
+ 'background': 0.4,
14
+ 'shadow': 0.6,
15
+ 'content': 1.0,
16
+ 'highlight': 1.3,
17
+ 'accent': 1.6
18
+ };
19
+
9
20
  export class QuantumHolographicVisualizer {
10
21
  constructor(canvasIdOrElement, role, reactivity, variant) {
11
22
  this.canvas = (canvasIdOrElement instanceof HTMLCanvasElement)
@@ -14,6 +25,7 @@ export class QuantumHolographicVisualizer {
14
25
  this.role = role;
15
26
  this.reactivity = reactivity;
16
27
  this.variant = variant;
28
+ this._canvasLabel = typeof canvasIdOrElement === 'string' ? canvasIdOrElement : canvasIdOrElement?.id || 'unknown';
17
29
 
18
30
  // CRITICAL FIX: Define contextOptions as instance property to match SmartCanvasPool
19
31
  this.contextOptions = {
@@ -34,9 +46,9 @@ export class QuantumHolographicVisualizer {
34
46
  this.canvas.getContext('experimental-webgl', this.contextOptions);
35
47
 
36
48
  if (!this.gl) {
37
- console.error(`WebGL not supported for ${canvasId}`);
49
+ console.error(`WebGL not supported for ${this._canvasLabel}`);
38
50
  if (window.mobileDebug) {
39
- window.mobileDebug.log(`❌ ${canvasId}: WebGL context creation failed`);
51
+ window.mobileDebug.log(`❌ ${this._canvasLabel}: WebGL context creation failed`);
40
52
  }
41
53
  // Show user-friendly error instead of white screen
42
54
  this.showWebGLError();
@@ -44,7 +56,7 @@ export class QuantumHolographicVisualizer {
44
56
  } else {
45
57
  if (window.mobileDebug) {
46
58
  const version = this.gl.getParameter(this.gl.VERSION);
47
- window.mobileDebug.log(`✅ ${canvasId}: WebGL context created - ${version}`);
59
+ window.mobileDebug.log(`✅ ${this._canvasLabel}: WebGL context created - ${version}`);
48
60
  }
49
61
  }
50
62
 
@@ -59,15 +71,15 @@ export class QuantumHolographicVisualizer {
59
71
  this._onContextLost = (e) => {
60
72
  e.preventDefault();
61
73
  this._contextLost = true;
62
- console.warn(`WebGL context lost for ${canvasId}`);
74
+ console.warn(`WebGL context lost for ${this._canvasLabel}`);
63
75
  };
64
76
  this._onContextRestored = () => {
65
- console.log(`WebGL context restored for ${canvasId}`);
77
+ console.log(`WebGL context restored for ${this._canvasLabel}`);
66
78
  this._contextLost = false;
67
79
  try {
68
80
  this.init();
69
81
  } catch (err) {
70
- console.error(`Failed to reinit after context restore for ${canvasId}:`, err);
82
+ console.error(`Failed to reinit after context restore for ${this._canvasLabel}:`, err);
71
83
  }
72
84
  };
73
85
  this.canvas.addEventListener('webglcontextlost', this._onContextLost);
@@ -688,11 +700,12 @@ void main() {
688
700
 
689
701
  // LAYER-BY-LAYER COLOR SYSTEM with user hue/saturation/intensity controls
690
702
  // Determine canvas layer from role/variant (0=background, 1=shadow, 2=content, 3=highlight, 4=accent)
703
+ // Values must match ROLE_INTENSITIES in JS: bg=0.4, shadow=0.6, content=1.0, highlight=1.3, accent=1.6
691
704
  int layerIndex = 0;
692
- if (u_roleIntensity == 0.7) layerIndex = 1; // shadow layer
693
- else if (u_roleIntensity == 1.0) layerIndex = 2; // content layer
694
- else if (u_roleIntensity == 0.85) layerIndex = 3; // highlight layer
695
- else if (u_roleIntensity == 0.6) layerIndex = 4; // accent layer
705
+ if (abs(u_roleIntensity - 0.6) < 0.05) layerIndex = 1; // shadow layer
706
+ else if (abs(u_roleIntensity - 1.0) < 0.05) layerIndex = 2; // content layer
707
+ else if (abs(u_roleIntensity - 1.3) < 0.05) layerIndex = 3; // highlight layer
708
+ else if (abs(u_roleIntensity - 1.6) < 0.05) layerIndex = 4; // accent layer
696
709
 
697
710
  // Get layer-specific base color using user hue/saturation controls
698
711
  float colorTime = timeSpeed * 2.0 + value * 3.0;
@@ -1016,15 +1029,6 @@ void main() {
1016
1029
  this._renderParamsLogged = true;
1017
1030
  }
1018
1031
 
1019
- // Role-specific intensity for quantum effects
1020
- const roleIntensities = {
1021
- 'background': 0.4,
1022
- 'shadow': 0.6,
1023
- 'content': 1.0,
1024
- 'highlight': 1.3,
1025
- 'accent': 1.6
1026
- };
1027
-
1028
1032
  const time = Date.now() - this.startTime;
1029
1033
 
1030
1034
  // Set uniforms
@@ -1068,7 +1072,7 @@ void main() {
1068
1072
  this.gl.uniform1f(this.uniforms.rot4dZW, this.params.rot4dZW || 0.0);
1069
1073
  this.gl.uniform1f(this.uniforms.mouseIntensity, this.mouseIntensity);
1070
1074
  this.gl.uniform1f(this.uniforms.clickIntensity, this.clickIntensity);
1071
- this.gl.uniform1f(this.uniforms.roleIntensity, roleIntensities[this.role] || 1.0);
1075
+ this.gl.uniform1f(this.uniforms.roleIntensity, ROLE_INTENSITIES[this.role] || 1.0);
1072
1076
  this.gl.uniform1f(this.uniforms.breath, this.params.breath || 0.0);
1073
1077
 
1074
1078
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
@@ -23,10 +23,11 @@ export { ReactivityManager } from './ReactivityManager.js';
23
23
  /**
24
24
  * Create a pre-configured ReactivityManager with common settings
25
25
  */
26
- export function createReactivityManager(options = {}) {
26
+ export async function createReactivityManager(options = {}) {
27
27
  const { config, parameterUpdateFn } = options;
28
28
 
29
- const manager = new (await import('./ReactivityManager.js')).ReactivityManager(parameterUpdateFn);
29
+ const { ReactivityManager } = await import('./ReactivityManager.js');
30
+ const manager = new ReactivityManager(parameterUpdateFn);
30
31
 
31
32
  if (config) {
32
33
  manager.loadConfig(config);
@@ -39,7 +40,6 @@ export function createReactivityManager(options = {}) {
39
40
  * Create ReactivityConfig from a preset name
40
41
  */
41
42
  export function createPresetConfig(presetName) {
42
- const { ReactivityConfig } = require('./ReactivityConfig.js');
43
43
  const config = new ReactivityConfig();
44
44
 
45
45
  switch (presetName) {
@@ -89,5 +89,3 @@ export function createPresetConfig(presetName) {
89
89
 
90
90
  return config;
91
91
  }
92
-
93
- console.log('🎛️ VIB3+ Reactivity System loaded');
@@ -0,0 +1,372 @@
1
+ /**
2
+ * LayerPresetManager — Save, load, tune, and share layer relationship presets
3
+ *
4
+ * Works with LayerRelationshipGraph to persist custom layer configurations.
5
+ * Users and agents can:
6
+ * - Save the current graph state as a named preset
7
+ * - Load presets by name
8
+ * - Tune individual relationship parameters at runtime
9
+ * - Import/export preset libraries as JSON
10
+ * - List available presets (built-in + user)
11
+ *
12
+ * Storage: localStorage by default, configurable via constructor.
13
+ *
14
+ * Usage:
15
+ * const presets = new LayerPresetManager(graph);
16
+ * presets.save('my-look'); // save current graph
17
+ * presets.load('my-look'); // restore to graph
18
+ * presets.tune('shadow', { opacity: 0.6, gain: 3 }); // tweak a layer
19
+ * const lib = presets.exportLibrary(); // full JSON export
20
+ * presets.importLibrary(lib); // bulk import
21
+ */
22
+
23
+ import { LayerRelationshipGraph, PROFILES, PRESET_REGISTRY } from './LayerRelationshipGraph.js';
24
+
25
+ const STORAGE_KEY = 'vib3_layer_presets';
26
+
27
+ export class LayerPresetManager {
28
+ /**
29
+ * @param {LayerRelationshipGraph} graph - The live graph to save from / load into
30
+ * @param {object} [options]
31
+ * @param {Storage} [options.storage] - Storage backend (default: localStorage)
32
+ * @param {string} [options.storageKey] - Key prefix for storage
33
+ */
34
+ constructor(graph, options = {}) {
35
+ if (!(graph instanceof LayerRelationshipGraph)) {
36
+ throw new Error('LayerPresetManager requires a LayerRelationshipGraph instance');
37
+ }
38
+
39
+ /** @type {LayerRelationshipGraph} */
40
+ this._graph = graph;
41
+
42
+ /** @type {Storage|null} */
43
+ this._storage = options.storage !== undefined ? options.storage : this._getStorage();
44
+
45
+ /** @type {string} */
46
+ this._storageKey = options.storageKey || STORAGE_KEY;
47
+
48
+ /** @type {Map<string, object>} In-memory preset cache */
49
+ this._presets = new Map();
50
+
51
+ // Load persisted presets into memory
52
+ this._loadFromStorage();
53
+ }
54
+
55
+ /**
56
+ * Get localStorage safely (returns null in non-browser / restricted environments).
57
+ * @private
58
+ * @returns {Storage|null}
59
+ */
60
+ _getStorage() {
61
+ try {
62
+ if (typeof localStorage !== 'undefined') {
63
+ // Verify it works
64
+ localStorage.setItem('__vib3_test', '1');
65
+ localStorage.removeItem('__vib3_test');
66
+ return localStorage;
67
+ }
68
+ } catch (_e) { /* restricted */ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Load all presets from storage into the in-memory cache.
74
+ * @private
75
+ */
76
+ _loadFromStorage() {
77
+ if (!this._storage) return;
78
+
79
+ try {
80
+ const raw = this._storage.getItem(this._storageKey);
81
+ if (raw) {
82
+ const data = JSON.parse(raw);
83
+ if (data && typeof data === 'object') {
84
+ for (const [name, preset] of Object.entries(data)) {
85
+ this._presets.set(name, preset);
86
+ }
87
+ }
88
+ }
89
+ } catch (e) {
90
+ console.warn('LayerPresetManager: failed to load presets from storage:', e.message);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Persist the in-memory preset cache to storage.
96
+ * @private
97
+ */
98
+ _saveToStorage() {
99
+ if (!this._storage) return;
100
+
101
+ try {
102
+ const data = {};
103
+ for (const [name, preset] of this._presets) {
104
+ data[name] = preset;
105
+ }
106
+ this._storage.setItem(this._storageKey, JSON.stringify(data));
107
+ } catch (e) {
108
+ console.warn('LayerPresetManager: failed to save presets to storage:', e.message);
109
+ }
110
+ }
111
+
112
+ // ========================================================================
113
+ // Save / Load
114
+ // ========================================================================
115
+
116
+ /**
117
+ * Save the current graph state as a named preset.
118
+ *
119
+ * @param {string} name - Preset name (must not collide with built-in profile names)
120
+ * @param {object} [metadata] - Optional metadata (description, author, tags)
121
+ * @returns {object} The saved preset data
122
+ */
123
+ save(name, metadata = {}) {
124
+ if (!name || typeof name !== 'string') {
125
+ throw new Error('Preset name must be a non-empty string');
126
+ }
127
+
128
+ if (PROFILES[name]) {
129
+ throw new Error(`Cannot overwrite built-in profile "${name}". Choose a different name.`);
130
+ }
131
+
132
+ const config = this._graph.exportConfig();
133
+ const preset = {
134
+ name,
135
+ config,
136
+ metadata: {
137
+ ...metadata,
138
+ createdAt: new Date().toISOString(),
139
+ updatedAt: new Date().toISOString()
140
+ }
141
+ };
142
+
143
+ this._presets.set(name, preset);
144
+ this._saveToStorage();
145
+
146
+ return preset;
147
+ }
148
+
149
+ /**
150
+ * Load a preset by name into the live graph.
151
+ * Checks user presets first, then built-in profiles.
152
+ *
153
+ * @param {string} name - Preset or profile name
154
+ * @returns {boolean} True if loaded successfully
155
+ */
156
+ load(name) {
157
+ // User preset
158
+ const preset = this._presets.get(name);
159
+ if (preset && preset.config) {
160
+ this._graph.importConfig(preset.config);
161
+ return true;
162
+ }
163
+
164
+ // Built-in profile
165
+ if (PROFILES[name]) {
166
+ this._graph.loadProfile(name);
167
+ return true;
168
+ }
169
+
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Delete a user preset.
175
+ *
176
+ * @param {string} name
177
+ * @returns {boolean} True if deleted
178
+ */
179
+ delete(name) {
180
+ if (PROFILES[name]) {
181
+ throw new Error(`Cannot delete built-in profile "${name}".`);
182
+ }
183
+
184
+ const existed = this._presets.delete(name);
185
+ if (existed) {
186
+ this._saveToStorage();
187
+ }
188
+ return existed;
189
+ }
190
+
191
+ /**
192
+ * Check if a preset exists (user or built-in).
193
+ *
194
+ * @param {string} name
195
+ * @returns {boolean}
196
+ */
197
+ has(name) {
198
+ return this._presets.has(name) || !!PROFILES[name];
199
+ }
200
+
201
+ /**
202
+ * Get a preset's data without loading it.
203
+ *
204
+ * @param {string} name
205
+ * @returns {object|null}
206
+ */
207
+ get(name) {
208
+ return this._presets.get(name) || null;
209
+ }
210
+
211
+ // ========================================================================
212
+ // List / Browse
213
+ // ========================================================================
214
+
215
+ /**
216
+ * List all available presets (user + built-in).
217
+ *
218
+ * @returns {{ user: string[], builtIn: string[] }}
219
+ */
220
+ list() {
221
+ return {
222
+ user: Array.from(this._presets.keys()),
223
+ builtIn: Object.keys(PROFILES)
224
+ };
225
+ }
226
+
227
+ /**
228
+ * List all preset names (user + built-in) as a flat array.
229
+ *
230
+ * @returns {string[]}
231
+ */
232
+ listAll() {
233
+ return [
234
+ ...Object.keys(PROFILES),
235
+ ...Array.from(this._presets.keys())
236
+ ];
237
+ }
238
+
239
+ /**
240
+ * Get the count of user presets.
241
+ *
242
+ * @returns {number}
243
+ */
244
+ get count() {
245
+ return this._presets.size;
246
+ }
247
+
248
+ // ========================================================================
249
+ // Tune
250
+ // ========================================================================
251
+
252
+ /**
253
+ * Tune (hot-patch) a layer's relationship config without replacing the full graph.
254
+ * Re-instantiates the relationship function with updated config.
255
+ *
256
+ * @param {string} layerName - Layer to tune
257
+ * @param {object} configOverrides - Config values to merge (e.g., { opacity: 0.6, gain: 3 })
258
+ * @returns {boolean} True if tuned successfully
259
+ */
260
+ tune(layerName, configOverrides) {
261
+ if (!configOverrides || typeof configOverrides !== 'object') return false;
262
+
263
+ // Read current config for this layer from the graph
264
+ const graphConfig = this._graph.exportConfig();
265
+ const currentRel = graphConfig.relationships[layerName];
266
+
267
+ if (!currentRel || !currentRel.preset) {
268
+ // Can't tune a custom function or missing relationship
269
+ return false;
270
+ }
271
+
272
+ const factory = PRESET_REGISTRY[currentRel.preset];
273
+ if (!factory) return false;
274
+
275
+ // Merge overrides into existing config
276
+ const newConfig = { ...(currentRel.config || {}), ...configOverrides };
277
+
278
+ // Re-set the relationship with updated config
279
+ this._graph.setRelationship(layerName, {
280
+ preset: currentRel.preset,
281
+ config: newConfig
282
+ });
283
+
284
+ return true;
285
+ }
286
+
287
+ /**
288
+ * Get the current tunable config for a layer.
289
+ *
290
+ * @param {string} layerName
291
+ * @returns {object|null} The current { preset, config } or null
292
+ */
293
+ getLayerConfig(layerName) {
294
+ const graphConfig = this._graph.exportConfig();
295
+ return graphConfig.relationships[layerName] || null;
296
+ }
297
+
298
+ // ========================================================================
299
+ // Import / Export
300
+ // ========================================================================
301
+
302
+ /**
303
+ * Export all user presets as a JSON-serializable library.
304
+ *
305
+ * @returns {object} Library object with version and presets
306
+ */
307
+ exportLibrary() {
308
+ const presets = {};
309
+ for (const [name, preset] of this._presets) {
310
+ presets[name] = preset;
311
+ }
312
+
313
+ return {
314
+ version: '1.0',
315
+ type: 'vib3_layer_presets',
316
+ exportedAt: new Date().toISOString(),
317
+ count: this._presets.size,
318
+ presets
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Import presets from a library object.
324
+ * Does not overwrite existing presets unless `overwrite` is true.
325
+ *
326
+ * @param {object} library - Library object from exportLibrary()
327
+ * @param {object} [options]
328
+ * @param {boolean} [options.overwrite=false] - Overwrite existing presets
329
+ * @returns {{ imported: number, skipped: number }}
330
+ */
331
+ importLibrary(library, options = {}) {
332
+ const { overwrite = false } = options;
333
+
334
+ if (!library || !library.presets || typeof library.presets !== 'object') {
335
+ throw new Error('Invalid library format: missing presets object');
336
+ }
337
+
338
+ let imported = 0;
339
+ let skipped = 0;
340
+
341
+ for (const [name, preset] of Object.entries(library.presets)) {
342
+ if (PROFILES[name]) {
343
+ skipped++;
344
+ continue; // Never overwrite built-in profiles
345
+ }
346
+
347
+ if (!overwrite && this._presets.has(name)) {
348
+ skipped++;
349
+ continue;
350
+ }
351
+
352
+ this._presets.set(name, preset);
353
+ imported++;
354
+ }
355
+
356
+ if (imported > 0) {
357
+ this._saveToStorage();
358
+ }
359
+
360
+ return { imported, skipped };
361
+ }
362
+
363
+ /**
364
+ * Clear all user presets.
365
+ */
366
+ clear() {
367
+ this._presets.clear();
368
+ this._saveToStorage();
369
+ }
370
+ }
371
+
372
+ export default LayerPresetManager;