@vib3code/sdk 2.0.1
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/CHANGELOG.md +118 -0
- package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +34 -0
- package/DOCS/CI_TESTING.md +38 -0
- package/DOCS/CLI_ONBOARDING.md +75 -0
- package/DOCS/CONTROL_REFERENCE.md +64 -0
- package/DOCS/DEV_TRACK_ANALYSIS.md +77 -0
- package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +42 -0
- package/DOCS/DEV_TRACK_SESSION_2026-01-31.md +220 -0
- package/DOCS/ENV_SETUP.md +189 -0
- package/DOCS/EXPORT_FORMATS.md +417 -0
- package/DOCS/GPU_DISPOSAL_GUIDE.md +21 -0
- package/DOCS/LICENSING_TIERS.md +275 -0
- package/DOCS/MASTER_PLAN_2026-01-31.md +570 -0
- package/DOCS/OBS_SETUP_GUIDE.md +98 -0
- package/DOCS/PROJECT_SETUP.md +66 -0
- package/DOCS/RENDERER_LIFECYCLE.md +40 -0
- package/DOCS/REPO_MANIFEST.md +121 -0
- package/DOCS/SESSION_014_PLAN.md +195 -0
- package/DOCS/SESSION_LOG_2026-01-07.md +56 -0
- package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +72 -0
- package/DOCS/SYSTEM_AUDIT_2026-01-30.md +738 -0
- package/DOCS/SYSTEM_INVENTORY.md +520 -0
- package/DOCS/TELEMETRY_EXPORTS.md +34 -0
- package/DOCS/WEBGPU_STATUS.md +38 -0
- package/DOCS/XR_BENCHMARKS.md +608 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/docs/.nojekyll +0 -0
- package/docs/01-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/02-hyperspatial_ego_death.html +346 -0
- package/docs/03-post_cartesian_sublime.html +346 -0
- package/docs/04-crystalline_void_meditation.html +346 -0
- package/docs/05-quantum_decoherence_ballet.html +346 -0
- package/docs/06-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/07-hyperspatial_ego_death.html +346 -0
- package/docs/08-post_cartesian_sublime.html +346 -0
- package/docs/09-crystalline_void_meditation.html +346 -0
- package/docs/10-quantum_decoherence_ballet.html +346 -0
- package/docs/11-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/12-hyperspatial_ego_death.html +346 -0
- package/docs/13-post_cartesian_sublime.html +346 -0
- package/docs/index.html +794 -0
- package/docs/test-hub.html +441 -0
- package/docs/url-state.js +102 -0
- package/docs/vib3-exports/01-quantum-quantum-tetrahedron-lattice.html +489 -0
- package/docs/vib3-exports/02-quantum-quantum-hypersphere-matrix.html +489 -0
- package/docs/vib3-exports/03-quantum-quantum-hypertetra-fractal.html +489 -0
- package/docs/vib3-exports/04-faceted-faceted-crystal-structure.html +407 -0
- package/docs/vib3-exports/05-faceted-faceted-klein-bottle.html +407 -0
- package/docs/vib3-exports/06-faceted-faceted-hypertetra-torus.html +407 -0
- package/docs/vib3-exports/07-holographic-holographic-wave-field.html +457 -0
- package/docs/vib3-exports/08-holographic-holographic-hypersphere-sphere.html +457 -0
- package/docs/vib3-exports/09-holographic-holographic-hypertetra-crystal.html +457 -0
- package/docs/vib3-exports/index.html +238 -0
- package/docs/webgpu-live.html +702 -0
- package/package.json +367 -0
- package/src/advanced/AIPresetGenerator.js +777 -0
- package/src/advanced/MIDIController.js +703 -0
- package/src/advanced/OffscreenWorker.js +1051 -0
- package/src/advanced/WebGPUCompute.js +1051 -0
- package/src/advanced/WebXRRenderer.js +680 -0
- package/src/agent/cli/AgentCLI.js +615 -0
- package/src/agent/cli/index.js +14 -0
- package/src/agent/index.js +73 -0
- package/src/agent/mcp/MCPServer.js +950 -0
- package/src/agent/mcp/index.js +9 -0
- package/src/agent/mcp/tools.js +548 -0
- package/src/agent/telemetry/EventStream.js +669 -0
- package/src/agent/telemetry/Instrumentation.js +618 -0
- package/src/agent/telemetry/TelemetryExporters.js +427 -0
- package/src/agent/telemetry/TelemetryService.js +464 -0
- package/src/agent/telemetry/index.js +52 -0
- package/src/benchmarks/BenchmarkRunner.js +381 -0
- package/src/benchmarks/MetricsCollector.js +299 -0
- package/src/benchmarks/index.js +9 -0
- package/src/benchmarks/scenes.js +259 -0
- package/src/cli/index.js +675 -0
- package/src/config/ApiConfig.js +88 -0
- package/src/core/CanvasManager.js +217 -0
- package/src/core/ErrorReporter.js +117 -0
- package/src/core/ParameterMapper.js +333 -0
- package/src/core/Parameters.js +396 -0
- package/src/core/RendererContracts.js +200 -0
- package/src/core/UnifiedResourceManager.js +370 -0
- package/src/core/VIB3Engine.js +636 -0
- package/src/core/renderers/FacetedRendererAdapter.js +32 -0
- package/src/core/renderers/HolographicRendererAdapter.js +29 -0
- package/src/core/renderers/QuantumRendererAdapter.js +29 -0
- package/src/core/renderers/RendererLifecycleManager.js +63 -0
- package/src/creative/ColorPresetsSystem.js +980 -0
- package/src/creative/ParameterTimeline.js +1061 -0
- package/src/creative/PostProcessingPipeline.js +1113 -0
- package/src/creative/TransitionAnimator.js +683 -0
- package/src/export/CSSExporter.js +226 -0
- package/src/export/CardGeneratorBase.js +279 -0
- package/src/export/ExportManager.js +580 -0
- package/src/export/FacetedCardGenerator.js +279 -0
- package/src/export/HolographicCardGenerator.js +543 -0
- package/src/export/LottieExporter.js +552 -0
- package/src/export/QuantumCardGenerator.js +315 -0
- package/src/export/SVGExporter.js +519 -0
- package/src/export/ShaderExporter.js +903 -0
- package/src/export/TradingCardGenerator.js +3055 -0
- package/src/export/TradingCardManager.js +181 -0
- package/src/export/VIB3PackageExporter.js +559 -0
- package/src/export/index.js +14 -0
- package/src/export/systems/TradingCardSystemFaceted.js +494 -0
- package/src/export/systems/TradingCardSystemHolographic.js +452 -0
- package/src/export/systems/TradingCardSystemQuantum.js +411 -0
- package/src/faceted/FacetedSystem.js +963 -0
- package/src/features/CollectionManager.js +433 -0
- package/src/gallery/CollectionManager.js +240 -0
- package/src/gallery/GallerySystem.js +485 -0
- package/src/geometry/GeometryFactory.js +314 -0
- package/src/geometry/GeometryLibrary.js +72 -0
- package/src/geometry/buffers/BufferBuilder.js +338 -0
- package/src/geometry/buffers/index.js +18 -0
- package/src/geometry/generators/Crystal.js +420 -0
- package/src/geometry/generators/Fractal.js +298 -0
- package/src/geometry/generators/KleinBottle.js +197 -0
- package/src/geometry/generators/Sphere.js +192 -0
- package/src/geometry/generators/Tesseract.js +160 -0
- package/src/geometry/generators/Tetrahedron.js +225 -0
- package/src/geometry/generators/Torus.js +304 -0
- package/src/geometry/generators/Wave.js +341 -0
- package/src/geometry/index.js +142 -0
- package/src/geometry/warp/HypersphereCore.js +211 -0
- package/src/geometry/warp/HypertetraCore.js +386 -0
- package/src/geometry/warp/index.js +57 -0
- package/src/holograms/HolographicVisualizer.js +1073 -0
- package/src/holograms/RealHolographicSystem.js +966 -0
- package/src/holograms/variantRegistry.js +69 -0
- package/src/integrations/FigmaPlugin.js +854 -0
- package/src/integrations/OBSMode.js +754 -0
- package/src/integrations/ThreeJsPackage.js +660 -0
- package/src/integrations/TouchDesignerExport.js +552 -0
- package/src/integrations/frameworks/Vib3React.js +591 -0
- package/src/integrations/frameworks/Vib3Svelte.js +654 -0
- package/src/integrations/frameworks/Vib3Vue.js +628 -0
- package/src/llm/LLMParameterInterface.js +240 -0
- package/src/llm/LLMParameterUI.js +577 -0
- package/src/math/Mat4x4.js +708 -0
- package/src/math/Projection.js +341 -0
- package/src/math/Rotor4D.js +637 -0
- package/src/math/Vec4.js +476 -0
- package/src/math/constants.js +164 -0
- package/src/math/index.js +68 -0
- package/src/math/projections.js +54 -0
- package/src/math/rotations.js +196 -0
- package/src/quantum/QuantumEngine.js +906 -0
- package/src/quantum/QuantumVisualizer.js +1103 -0
- package/src/reactivity/ReactivityConfig.js +499 -0
- package/src/reactivity/ReactivityManager.js +586 -0
- package/src/reactivity/SpatialInputSystem.js +1783 -0
- package/src/reactivity/index.js +93 -0
- package/src/render/CommandBuffer.js +465 -0
- package/src/render/MultiCanvasBridge.js +340 -0
- package/src/render/RenderCommand.js +514 -0
- package/src/render/RenderResourceRegistry.js +523 -0
- package/src/render/RenderState.js +552 -0
- package/src/render/RenderTarget.js +512 -0
- package/src/render/ShaderLoader.js +253 -0
- package/src/render/ShaderProgram.js +599 -0
- package/src/render/UnifiedRenderBridge.js +496 -0
- package/src/render/backends/WebGLBackend.js +1108 -0
- package/src/render/backends/WebGPUBackend.js +1409 -0
- package/src/render/commands/CommandBufferExecutor.js +607 -0
- package/src/render/commands/RenderCommandBuffer.js +661 -0
- package/src/render/commands/index.js +17 -0
- package/src/render/index.js +367 -0
- package/src/scene/Disposable.js +498 -0
- package/src/scene/MemoryPool.js +618 -0
- package/src/scene/Node4D.js +697 -0
- package/src/scene/ResourceManager.js +599 -0
- package/src/scene/Scene4D.js +540 -0
- package/src/scene/index.js +98 -0
- package/src/schemas/error.schema.json +84 -0
- package/src/schemas/extension.schema.json +88 -0
- package/src/schemas/index.js +214 -0
- package/src/schemas/parameters.schema.json +142 -0
- package/src/schemas/tool-pack.schema.json +44 -0
- package/src/schemas/tool-response.schema.json +127 -0
- package/src/shaders/common/fullscreen.vert.glsl +5 -0
- package/src/shaders/common/fullscreen.vert.wgsl +17 -0
- package/src/shaders/common/geometry24.glsl +65 -0
- package/src/shaders/common/geometry24.wgsl +54 -0
- package/src/shaders/common/rotation4d.glsl +85 -0
- package/src/shaders/common/rotation4d.wgsl +86 -0
- package/src/shaders/common/uniforms.glsl +44 -0
- package/src/shaders/common/uniforms.wgsl +48 -0
- package/src/shaders/faceted/faceted.frag.glsl +129 -0
- package/src/shaders/faceted/faceted.frag.wgsl +164 -0
- package/src/shaders/holographic/holographic.frag.glsl +406 -0
- package/src/shaders/holographic/holographic.frag.wgsl +185 -0
- package/src/shaders/quantum/quantum.frag.glsl +513 -0
- package/src/shaders/quantum/quantum.frag.wgsl +361 -0
- package/src/testing/ParallelTestFramework.js +519 -0
- package/src/testing/__snapshots__/exportFormats.test.js.snap +24 -0
- package/src/testing/exportFormats.test.js +8 -0
- package/src/testing/projections.test.js +14 -0
- package/src/testing/rotations.test.js +37 -0
- package/src/ui/InteractivityMenu.js +516 -0
- package/src/ui/StatusManager.js +96 -0
- package/src/ui/adaptive/renderers/webgpu/BufferLayout.ts +252 -0
- package/src/ui/adaptive/renderers/webgpu/PolytopeInstanceBuffer.ts +144 -0
- package/src/ui/adaptive/renderers/webgpu/TripleBufferedUniform.ts +170 -0
- package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +735 -0
- package/src/ui/adaptive/renderers/webgpu/index.ts +112 -0
- package/src/variations/VariationManager.js +431 -0
- package/src/viewer/AudioReactivity.js +505 -0
- package/src/viewer/CardBending.js +481 -0
- package/src/viewer/GalleryUI.js +832 -0
- package/src/viewer/ReactivityManager.js +590 -0
- package/src/viewer/TradingCardExporter.js +600 -0
- package/src/viewer/ViewerPortal.js +374 -0
- package/src/viewer/index.js +12 -0
- package/src/wasm/WasmLoader.js +296 -0
- package/src/wasm/index.js +132 -0
- package/tools/agentic/mcpTools.js +88 -0
- package/tools/cli/agent-cli.js +92 -0
- package/tools/export/formats.js +24 -0
- package/tools/math/rotation-baseline.mjs +64 -0
- package/tools/shader-sync-verify.js +937 -0
- package/tools/telemetry/manifestPipeline.js +141 -0
- package/tools/telemetry/telemetryEvents.js +35 -0
- package/types/adaptive-sdk.d.ts +185 -0
- package/types/advanced/AIPresetGenerator.d.ts +81 -0
- package/types/advanced/MIDIController.d.ts +100 -0
- package/types/advanced/OffscreenWorker.d.ts +82 -0
- package/types/advanced/WebGPUCompute.d.ts +52 -0
- package/types/advanced/WebXRRenderer.d.ts +77 -0
- package/types/advanced/index.d.ts +46 -0
- package/types/core/ErrorReporter.d.ts +50 -0
- package/types/core/VIB3Engine.d.ts +204 -0
- package/types/creative/ColorPresetsSystem.d.ts +91 -0
- package/types/creative/ParameterTimeline.d.ts +74 -0
- package/types/creative/PostProcessingPipeline.d.ts +109 -0
- package/types/creative/TransitionAnimator.d.ts +71 -0
- package/types/creative/index.d.ts +35 -0
- package/types/integrations/FigmaPlugin.d.ts +46 -0
- package/types/integrations/OBSMode.d.ts +74 -0
- package/types/integrations/ThreeJsPackage.d.ts +62 -0
- package/types/integrations/TouchDesignerExport.d.ts +36 -0
- package/types/integrations/Vib3React.d.ts +74 -0
- package/types/integrations/Vib3Svelte.d.ts +63 -0
- package/types/integrations/Vib3Vue.d.ts +55 -0
- package/types/integrations/index.d.ts +52 -0
- package/types/reactivity/SpatialInputSystem.d.ts +173 -0
- package/types/reactivity/index.d.ts +394 -0
- package/types/render/CommandBuffer.d.ts +169 -0
- package/types/render/RenderCommand.d.ts +312 -0
- package/types/render/RenderState.d.ts +279 -0
- package/types/render/RenderTarget.d.ts +254 -0
- package/types/render/ShaderProgram.d.ts +277 -0
- package/types/render/UnifiedRenderBridge.d.ts +143 -0
- package/types/render/WebGLBackend.d.ts +168 -0
- package/types/render/WebGPUBackend.d.ts +186 -0
- package/types/render/index.d.ts +141 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Manager - Auto-discovery system for JSON collections
|
|
3
|
+
* Scans collections/ folder for JSON files and loads them automatically
|
|
4
|
+
*/
|
|
5
|
+
export class CollectionManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.collections = new Map();
|
|
8
|
+
this.baseCollectionPath = './collections/';
|
|
9
|
+
this.loadingPromises = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Auto-discover and load all JSON collections from collections/ folder AND localStorage
|
|
14
|
+
* FIXED: Now checks both file-based and localStorage saved variations
|
|
15
|
+
*/
|
|
16
|
+
async autoDiscoverCollections() {
|
|
17
|
+
console.log('🔍 Auto-discovering collections in collections/ folder AND localStorage...');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Method 1: Load known files first
|
|
21
|
+
const knownFiles = ['base-variations.json'];
|
|
22
|
+
const loadPromises = [];
|
|
23
|
+
|
|
24
|
+
for (const filename of knownFiles) {
|
|
25
|
+
loadPromises.push(
|
|
26
|
+
this.loadCollection(filename).catch(() => null)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Wait for known files to load
|
|
31
|
+
await Promise.all(loadPromises);
|
|
32
|
+
|
|
33
|
+
// Method 2: CRITICAL FIX - Load user-saved variations from localStorage
|
|
34
|
+
await this.loadUserSavedVariations();
|
|
35
|
+
|
|
36
|
+
console.log(`✅ Auto-discovery complete: ${this.collections.size} collections loaded`);
|
|
37
|
+
console.log('📁 Includes both file-based and localStorage user variations');
|
|
38
|
+
|
|
39
|
+
return Array.from(this.collections.values());
|
|
40
|
+
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('❌ Collections auto-discovery error:', error);
|
|
43
|
+
// Still try to load localStorage variations even if file loading fails
|
|
44
|
+
await this.loadUserSavedVariations();
|
|
45
|
+
return Array.from(this.collections.values());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load a specific collection from the collections/ folder
|
|
51
|
+
*/
|
|
52
|
+
async loadCollection(filename) {
|
|
53
|
+
const fullPath = this.baseCollectionPath + filename;
|
|
54
|
+
|
|
55
|
+
// Avoid duplicate loading
|
|
56
|
+
if (this.loadingPromises.has(filename)) {
|
|
57
|
+
return this.loadingPromises.get(filename);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const loadPromise = this.fetchCollectionFile(fullPath, filename);
|
|
61
|
+
this.loadingPromises.set(filename, loadPromise);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const collection = await loadPromise;
|
|
65
|
+
this.collections.set(filename, collection);
|
|
66
|
+
console.log(`📋 Loaded collection: ${collection.name} (${collection.variations.length} variations)`);
|
|
67
|
+
return collection;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.log(`📁 Collection ${filename} not available (this is normal for user files)`);
|
|
70
|
+
this.loadingPromises.delete(filename);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch and parse a collection file
|
|
77
|
+
*/
|
|
78
|
+
async fetchCollectionFile(fullPath, filename) {
|
|
79
|
+
const response = await fetch(fullPath);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
|
|
86
|
+
// Validate collection format
|
|
87
|
+
if (!data.type || data.type !== 'holographic-collection') {
|
|
88
|
+
throw new Error('Invalid collection format: missing type');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!data.variations || !Array.isArray(data.variations)) {
|
|
92
|
+
throw new Error('Invalid collection format: missing variations array');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add metadata
|
|
96
|
+
data.filename = filename;
|
|
97
|
+
data.loadedAt = new Date().toISOString();
|
|
98
|
+
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all loaded collections
|
|
104
|
+
*/
|
|
105
|
+
getAllCollections() {
|
|
106
|
+
return Array.from(this.collections.values());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a specific collection by filename
|
|
111
|
+
*/
|
|
112
|
+
getCollection(filename) {
|
|
113
|
+
return this.collections.get(filename);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get all variations from all collections (flattened)
|
|
118
|
+
*/
|
|
119
|
+
getAllVariations() {
|
|
120
|
+
const allVariations = [];
|
|
121
|
+
let currentId = 0;
|
|
122
|
+
|
|
123
|
+
for (const collection of this.collections.values()) {
|
|
124
|
+
for (const variation of collection.variations) {
|
|
125
|
+
allVariations.push({
|
|
126
|
+
...variation,
|
|
127
|
+
id: currentId++,
|
|
128
|
+
collectionName: collection.name,
|
|
129
|
+
collectionFilename: collection.filename,
|
|
130
|
+
isFromCollection: true
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return allVariations;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Save a new collection or append to existing user collection
|
|
140
|
+
*/
|
|
141
|
+
async saveCollection(collection, filename) {
|
|
142
|
+
// Validate filename
|
|
143
|
+
if (!filename.endsWith('.json')) {
|
|
144
|
+
filename += '.json';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let formattedCollection;
|
|
148
|
+
|
|
149
|
+
// Check if we're appending to an existing user collection
|
|
150
|
+
const existingCollection = this.collections.get(filename);
|
|
151
|
+
if (existingCollection && filename.includes('user-custom-')) {
|
|
152
|
+
// Append to existing collection
|
|
153
|
+
const newVariations = [...existingCollection.variations, ...collection.variations];
|
|
154
|
+
|
|
155
|
+
// Update IDs to be sequential
|
|
156
|
+
newVariations.forEach((variation, index) => {
|
|
157
|
+
variation.id = index;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
formattedCollection = {
|
|
161
|
+
...existingCollection,
|
|
162
|
+
totalVariations: newVariations.length,
|
|
163
|
+
variations: newVariations,
|
|
164
|
+
updated: new Date().toISOString()
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
console.log(`📝 Appending to existing collection: ${existingCollection.name}`);
|
|
168
|
+
} else {
|
|
169
|
+
// Create new collection
|
|
170
|
+
formattedCollection = {
|
|
171
|
+
name: collection.name || 'Unnamed Collection',
|
|
172
|
+
description: collection.description || '',
|
|
173
|
+
version: '1.0',
|
|
174
|
+
type: 'holographic-collection',
|
|
175
|
+
profileName: collection.profileName || 'VIB34D System',
|
|
176
|
+
totalVariations: collection.variations.length,
|
|
177
|
+
created: new Date().toISOString(),
|
|
178
|
+
variations: collection.variations
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Convert to JSON
|
|
183
|
+
const jsonData = JSON.stringify(formattedCollection, null, 2);
|
|
184
|
+
|
|
185
|
+
// Create download (since we can't write directly to collections/)
|
|
186
|
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
|
187
|
+
const url = URL.createObjectURL(blob);
|
|
188
|
+
const link = document.createElement('a');
|
|
189
|
+
link.href = url;
|
|
190
|
+
link.download = filename;
|
|
191
|
+
document.body.appendChild(link);
|
|
192
|
+
link.click();
|
|
193
|
+
document.body.removeChild(link);
|
|
194
|
+
URL.revokeObjectURL(url);
|
|
195
|
+
|
|
196
|
+
console.log(`💾 Collection saved: ${filename}`);
|
|
197
|
+
console.log(`📁 To use: Move ${filename} to collections/ folder and refresh gallery`);
|
|
198
|
+
|
|
199
|
+
// Update our internal collection if it exists
|
|
200
|
+
if (existingCollection) {
|
|
201
|
+
this.collections.set(filename, formattedCollection);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return formattedCollection;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create a collection from custom variations
|
|
209
|
+
*/
|
|
210
|
+
createCustomCollection(customVariations, name) {
|
|
211
|
+
const collection = {
|
|
212
|
+
name: name || `Custom Collection ${new Date().toLocaleDateString()}`,
|
|
213
|
+
description: 'User-created custom holographic variations',
|
|
214
|
+
version: '1.0',
|
|
215
|
+
type: 'holographic-collection',
|
|
216
|
+
profileName: 'Active Holographic Systems',
|
|
217
|
+
totalVariations: customVariations.length,
|
|
218
|
+
created: new Date().toISOString(),
|
|
219
|
+
variations: customVariations.map((cv, index) => ({
|
|
220
|
+
id: index,
|
|
221
|
+
name: cv.name || `Custom Variation ${index + 1}`,
|
|
222
|
+
isCustom: true,
|
|
223
|
+
parameters: {
|
|
224
|
+
geometryType: cv.params.geometry,
|
|
225
|
+
density: cv.params.density,
|
|
226
|
+
speed: cv.params.speed,
|
|
227
|
+
chaos: cv.params.chaos,
|
|
228
|
+
morph: cv.params.morph,
|
|
229
|
+
hue: cv.params.hue,
|
|
230
|
+
saturation: cv.params.saturation,
|
|
231
|
+
intensity: cv.params.intensity
|
|
232
|
+
}
|
|
233
|
+
}))
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return collection;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* CRITICAL FIX: Load user-saved variations from localStorage (UnifiedSaveManager storage)
|
|
241
|
+
* This bridges the gap between save system and gallery system
|
|
242
|
+
*/
|
|
243
|
+
async loadUserSavedVariations() {
|
|
244
|
+
try {
|
|
245
|
+
// Check UnifiedSaveManager storage keys
|
|
246
|
+
const unifiedVariationsKey = 'vib34d-unified-variations';
|
|
247
|
+
const unifiedCollectionsKey = 'vib34d-unified-collections';
|
|
248
|
+
|
|
249
|
+
console.log('🔍 DIAGNOSTIC: Checking localStorage for user variations...');
|
|
250
|
+
console.log('🔍 Looking for keys:', unifiedVariationsKey, unifiedCollectionsKey);
|
|
251
|
+
|
|
252
|
+
// DIAGNOSTIC: Check what's actually in localStorage
|
|
253
|
+
const allKeys = Object.keys(localStorage).filter(k => k.includes('vib34d'));
|
|
254
|
+
console.log('🔍 All VIB34D localStorage keys found:', allKeys);
|
|
255
|
+
|
|
256
|
+
// Load unified variations
|
|
257
|
+
const storedVariations = localStorage.getItem(unifiedVariationsKey);
|
|
258
|
+
console.log('🔍 Raw variations data length:', storedVariations ? storedVariations.length : 'NULL');
|
|
259
|
+
|
|
260
|
+
if (storedVariations) {
|
|
261
|
+
const variations = JSON.parse(storedVariations);
|
|
262
|
+
console.log(`🔵 Found ${variations.length} user-saved variations in localStorage`);
|
|
263
|
+
console.log('🔍 First variation sample:', variations[0]);
|
|
264
|
+
|
|
265
|
+
if (variations.length > 0) {
|
|
266
|
+
// Group variations by date for tabs
|
|
267
|
+
const variationsByDate = {};
|
|
268
|
+
variations.forEach(variation => {
|
|
269
|
+
const date = new Date(variation.timestamp || variation.created || Date.now());
|
|
270
|
+
const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
271
|
+
const displayDate = date.toLocaleDateString('en-US', {
|
|
272
|
+
weekday: 'short',
|
|
273
|
+
year: 'numeric',
|
|
274
|
+
month: 'short',
|
|
275
|
+
day: 'numeric'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (!variationsByDate[dateKey]) {
|
|
279
|
+
variationsByDate[dateKey] = {
|
|
280
|
+
displayDate,
|
|
281
|
+
variations: []
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
variationsByDate[dateKey].variations.push(variation);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Create a unified collection with date-based organization
|
|
288
|
+
const userCollection = {
|
|
289
|
+
name: `User Saved Variations (${variations.length})`,
|
|
290
|
+
description: `Custom variations saved by user - organized by date`,
|
|
291
|
+
version: '1.0',
|
|
292
|
+
type: 'holographic-collection',
|
|
293
|
+
profileName: 'VIB34D User',
|
|
294
|
+
totalVariations: variations.length,
|
|
295
|
+
variationsByDate: variationsByDate, // Add date organization
|
|
296
|
+
created: new Date().toISOString(),
|
|
297
|
+
filename: 'user-saved-localStorage.json',
|
|
298
|
+
loadedAt: new Date().toISOString(),
|
|
299
|
+
variations: variations.map((variation, index) => ({
|
|
300
|
+
id: index + 100, // Start user variations at ID 100+
|
|
301
|
+
name: variation.name || `User Variation ${index + 1}`,
|
|
302
|
+
isCustom: true,
|
|
303
|
+
globalId: variation.id,
|
|
304
|
+
system: variation.system || 'faceted',
|
|
305
|
+
parameters: this.normalizeParameters(variation.parameters || {})
|
|
306
|
+
}))
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Add to collections
|
|
310
|
+
this.collections.set('user-saved-localStorage.json', userCollection);
|
|
311
|
+
console.log(`✅ Added user collection: ${userCollection.name}`);
|
|
312
|
+
} else {
|
|
313
|
+
console.log('⚠️ No variations found in parsed data - creating empty collection');
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
console.log('⚠️ No stored variations found in localStorage - this is normal for first-time users');
|
|
317
|
+
// Create a demo collection to prevent "No Collections Found" error
|
|
318
|
+
const demoCollection = {
|
|
319
|
+
name: 'Welcome to VIB34D Gallery (Demo)',
|
|
320
|
+
description: 'Save your first variation using the 💾 Save to Gallery button!',
|
|
321
|
+
version: '1.0',
|
|
322
|
+
type: 'holographic-collection',
|
|
323
|
+
profileName: 'VIB34D System',
|
|
324
|
+
totalVariations: 1,
|
|
325
|
+
created: new Date().toISOString(),
|
|
326
|
+
filename: 'demo-welcome.json',
|
|
327
|
+
loadedAt: new Date().toISOString(),
|
|
328
|
+
variations: [{
|
|
329
|
+
id: 0,
|
|
330
|
+
name: 'Demo - Save Your First!',
|
|
331
|
+
isDemo: true,
|
|
332
|
+
system: 'faceted',
|
|
333
|
+
parameters: {
|
|
334
|
+
geometryType: 1,
|
|
335
|
+
density: 25,
|
|
336
|
+
speed: 1.0,
|
|
337
|
+
chaos: 0.2,
|
|
338
|
+
morph: 0.5,
|
|
339
|
+
hue: 240,
|
|
340
|
+
saturation: 0.8,
|
|
341
|
+
intensity: 0.6,
|
|
342
|
+
rot4dXW: 0,
|
|
343
|
+
rot4dYW: 0,
|
|
344
|
+
rot4dZW: 0,
|
|
345
|
+
dimension: 3.8
|
|
346
|
+
}
|
|
347
|
+
}]
|
|
348
|
+
};
|
|
349
|
+
this.collections.set('demo-welcome.json', demoCollection);
|
|
350
|
+
console.log('✅ Added demo welcome collection');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Load unified collections
|
|
354
|
+
const storedCollections = localStorage.getItem(unifiedCollectionsKey);
|
|
355
|
+
if (storedCollections) {
|
|
356
|
+
try {
|
|
357
|
+
const collectionsArray = JSON.parse(storedCollections);
|
|
358
|
+
console.log(`🔵 Found ${collectionsArray.length} user collections in localStorage`);
|
|
359
|
+
|
|
360
|
+
collectionsArray.forEach(([filename, collection]) => {
|
|
361
|
+
if (collection && collection.variations) {
|
|
362
|
+
// Ensure proper formatting
|
|
363
|
+
collection.filename = filename;
|
|
364
|
+
collection.loadedAt = new Date().toISOString();
|
|
365
|
+
|
|
366
|
+
// Normalize variation IDs to avoid conflicts
|
|
367
|
+
collection.variations.forEach((variation, index) => {
|
|
368
|
+
if (!variation.globalId) {
|
|
369
|
+
variation.globalId = `USER-${Date.now()}-${index}`;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
this.collections.set(filename, collection);
|
|
374
|
+
console.log(`✅ Added localStorage collection: ${collection.name}`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
} catch (collectionsError) {
|
|
378
|
+
console.warn('⚠️ Error parsing stored collections:', collectionsError);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`📊 Total collections after localStorage load: ${this.collections.size}`);
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('❌ Error loading user-saved variations from localStorage:', error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Normalize parameters to match expected gallery format
|
|
391
|
+
*/
|
|
392
|
+
normalizeParameters(params) {
|
|
393
|
+
// Convert between different parameter formats
|
|
394
|
+
const normalized = {
|
|
395
|
+
geometryType: params.geometry || params.geometryType || 0,
|
|
396
|
+
density: params.gridDensity || params.density || 10,
|
|
397
|
+
speed: params.speed || 1.0,
|
|
398
|
+
chaos: params.chaos || 0,
|
|
399
|
+
morph: params.morphFactor || params.morph || 0,
|
|
400
|
+
hue: params.hue || 200,
|
|
401
|
+
saturation: params.saturation || 0.8,
|
|
402
|
+
intensity: params.intensity || 0.5,
|
|
403
|
+
// 4D rotation parameters
|
|
404
|
+
rot4dXW: params.rot4dXW || 0,
|
|
405
|
+
rot4dYW: params.rot4dYW || 0,
|
|
406
|
+
rot4dZW: params.rot4dZW || 0,
|
|
407
|
+
dimension: params.dimension || 3.8
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return normalized;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get collection statistics
|
|
415
|
+
*/
|
|
416
|
+
getStatistics() {
|
|
417
|
+
const collections = Array.from(this.collections.values());
|
|
418
|
+
const stats = {
|
|
419
|
+
totalCollections: collections.length,
|
|
420
|
+
totalVariations: collections.reduce((sum, c) => sum + c.variations.length, 0),
|
|
421
|
+
customCollections: collections.filter(c => c.name.includes('User') || c.name.includes('Custom')).length,
|
|
422
|
+
baseCollections: collections.filter(c => c.name.includes('Base')).length,
|
|
423
|
+
collections: collections.map(c => ({
|
|
424
|
+
name: c.name,
|
|
425
|
+
filename: c.filename,
|
|
426
|
+
variationCount: c.variations.length,
|
|
427
|
+
created: c.created
|
|
428
|
+
}))
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return stats;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Manager - Auto-discovery system for JSON collections
|
|
3
|
+
* Scans collections/ folder for JSON files and loads them automatically
|
|
4
|
+
*/
|
|
5
|
+
export class CollectionManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.collections = new Map();
|
|
8
|
+
this.baseCollectionPath = './collections/';
|
|
9
|
+
this.loadingPromises = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Auto-discover and load all JSON collections from collections/ folder
|
|
14
|
+
*/
|
|
15
|
+
async autoDiscoverCollections() {
|
|
16
|
+
console.log('🔍 Auto-discovering collections...');
|
|
17
|
+
|
|
18
|
+
// List of known collection files to try loading
|
|
19
|
+
const knownCollections = [
|
|
20
|
+
'base-variations.json',
|
|
21
|
+
'community-favorites.json',
|
|
22
|
+
'dual-geometry-experiments.json',
|
|
23
|
+
'holographic-gemstones.json',
|
|
24
|
+
'special-variations.json',
|
|
25
|
+
'geometric-dreams.json',
|
|
26
|
+
'experimental-forms.json',
|
|
27
|
+
'paul-custom-pack.json',
|
|
28
|
+
'custom-variations.json'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const loadPromises = knownCollections.map(filename =>
|
|
32
|
+
this.loadCollection(filename).catch(err => {
|
|
33
|
+
// Silently fail for missing files
|
|
34
|
+
console.log(`📁 Collection not found: ${filename}`);
|
|
35
|
+
return null;
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Also try to load any user-custom files with date pattern
|
|
40
|
+
const datePattern = /user-custom-\d{4}-\d{2}-\d{2}\.json$/;
|
|
41
|
+
for (let i = 0; i < 10; i++) {
|
|
42
|
+
const date = new Date();
|
|
43
|
+
date.setDate(date.getDate() - i);
|
|
44
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
45
|
+
const filename = `user-custom-${dateStr}.json`;
|
|
46
|
+
loadPromises.push(
|
|
47
|
+
this.loadCollection(filename).catch(() => null)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const results = await Promise.allSettled(loadPromises);
|
|
52
|
+
const loadedCount = results.filter(r => r.status === 'fulfilled' && r.value).length;
|
|
53
|
+
|
|
54
|
+
console.log(`✅ Auto-discovery complete: ${loadedCount} collections loaded`);
|
|
55
|
+
return Array.from(this.collections.values());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load a specific collection from the collections/ folder
|
|
60
|
+
*/
|
|
61
|
+
async loadCollection(filename) {
|
|
62
|
+
const fullPath = this.baseCollectionPath + filename;
|
|
63
|
+
|
|
64
|
+
// Avoid duplicate loading
|
|
65
|
+
if (this.loadingPromises.has(filename)) {
|
|
66
|
+
return this.loadingPromises.get(filename);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const loadPromise = this.fetchCollectionFile(fullPath, filename);
|
|
70
|
+
this.loadingPromises.set(filename, loadPromise);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const collection = await loadPromise;
|
|
74
|
+
this.collections.set(filename, collection);
|
|
75
|
+
console.log(`📋 Loaded collection: ${collection.name} (${collection.variations.length} variations)`);
|
|
76
|
+
return collection;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.warn(`❌ Failed to load collection ${filename}:`, error.message);
|
|
79
|
+
this.loadingPromises.delete(filename);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch and parse a collection file
|
|
86
|
+
*/
|
|
87
|
+
async fetchCollectionFile(fullPath, filename) {
|
|
88
|
+
const response = await fetch(fullPath);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
|
|
95
|
+
// Validate collection format
|
|
96
|
+
if (!data.type || data.type !== 'holographic-collection') {
|
|
97
|
+
throw new Error('Invalid collection format: missing type');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!data.variations || !Array.isArray(data.variations)) {
|
|
101
|
+
throw new Error('Invalid collection format: missing variations array');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add metadata
|
|
105
|
+
data.filename = filename;
|
|
106
|
+
data.loadedAt = new Date().toISOString();
|
|
107
|
+
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get all loaded collections
|
|
113
|
+
*/
|
|
114
|
+
getAllCollections() {
|
|
115
|
+
return Array.from(this.collections.values());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a specific collection by filename
|
|
120
|
+
*/
|
|
121
|
+
getCollection(filename) {
|
|
122
|
+
return this.collections.get(filename);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get all variations from all collections (flattened)
|
|
127
|
+
*/
|
|
128
|
+
getAllVariations() {
|
|
129
|
+
const allVariations = [];
|
|
130
|
+
let currentId = 0;
|
|
131
|
+
|
|
132
|
+
for (const collection of this.collections.values()) {
|
|
133
|
+
for (const variation of collection.variations) {
|
|
134
|
+
allVariations.push({
|
|
135
|
+
...variation,
|
|
136
|
+
id: currentId++,
|
|
137
|
+
collectionName: collection.name,
|
|
138
|
+
collectionFilename: collection.filename,
|
|
139
|
+
isFromCollection: true
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return allVariations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Save a new collection to the collections/ folder
|
|
149
|
+
*/
|
|
150
|
+
async saveCollection(collection, filename) {
|
|
151
|
+
// Validate filename
|
|
152
|
+
if (!filename.endsWith('.json')) {
|
|
153
|
+
filename += '.json';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Ensure proper collection format
|
|
157
|
+
const formattedCollection = {
|
|
158
|
+
name: collection.name || 'Unnamed Collection',
|
|
159
|
+
description: collection.description || '',
|
|
160
|
+
version: '1.0',
|
|
161
|
+
type: 'holographic-collection',
|
|
162
|
+
profileName: collection.profileName || 'Active Holographic Systems',
|
|
163
|
+
totalVariations: collection.variations.length,
|
|
164
|
+
created: new Date().toISOString(),
|
|
165
|
+
variations: collection.variations
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Convert to JSON
|
|
169
|
+
const jsonData = JSON.stringify(formattedCollection, null, 2);
|
|
170
|
+
|
|
171
|
+
// Create download (since we can't write directly to collections/)
|
|
172
|
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
|
173
|
+
const url = URL.createObjectURL(blob);
|
|
174
|
+
const link = document.createElement('a');
|
|
175
|
+
link.href = url;
|
|
176
|
+
link.download = filename;
|
|
177
|
+
document.body.appendChild(link);
|
|
178
|
+
link.click();
|
|
179
|
+
document.body.removeChild(link);
|
|
180
|
+
URL.revokeObjectURL(url);
|
|
181
|
+
|
|
182
|
+
console.log(`💾 Collection saved: ${filename}`);
|
|
183
|
+
console.log(`📁 To use: Move ${filename} to collections/ folder and refresh`);
|
|
184
|
+
|
|
185
|
+
return formattedCollection;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a collection from custom variations
|
|
190
|
+
*/
|
|
191
|
+
createCustomCollection(customVariations, name) {
|
|
192
|
+
const collection = {
|
|
193
|
+
name: name || `Custom Collection ${new Date().toLocaleDateString()}`,
|
|
194
|
+
description: 'User-created custom holographic variations',
|
|
195
|
+
version: '1.0',
|
|
196
|
+
type: 'holographic-collection',
|
|
197
|
+
profileName: 'Active Holographic Systems',
|
|
198
|
+
totalVariations: customVariations.length,
|
|
199
|
+
created: new Date().toISOString(),
|
|
200
|
+
variations: customVariations.map((cv, index) => ({
|
|
201
|
+
id: index,
|
|
202
|
+
name: cv.name || `Custom Variation ${index + 1}`,
|
|
203
|
+
isCustom: true,
|
|
204
|
+
parameters: {
|
|
205
|
+
geometryType: cv.params.geometry,
|
|
206
|
+
density: cv.params.density,
|
|
207
|
+
speed: cv.params.speed,
|
|
208
|
+
chaos: cv.params.chaos,
|
|
209
|
+
morph: cv.params.morph,
|
|
210
|
+
hue: cv.params.hue,
|
|
211
|
+
saturation: cv.params.saturation,
|
|
212
|
+
intensity: cv.params.intensity
|
|
213
|
+
}
|
|
214
|
+
}))
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return collection;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get collection statistics
|
|
222
|
+
*/
|
|
223
|
+
getStatistics() {
|
|
224
|
+
const collections = Array.from(this.collections.values());
|
|
225
|
+
const stats = {
|
|
226
|
+
totalCollections: collections.length,
|
|
227
|
+
totalVariations: collections.reduce((sum, c) => sum + c.variations.length, 0),
|
|
228
|
+
customCollections: collections.filter(c => c.name.includes('Custom')).length,
|
|
229
|
+
baseCollections: collections.filter(c => c.name.includes('Base')).length,
|
|
230
|
+
collections: collections.map(c => ({
|
|
231
|
+
name: c.name,
|
|
232
|
+
filename: c.filename,
|
|
233
|
+
variationCount: c.variations.length,
|
|
234
|
+
created: c.created
|
|
235
|
+
}))
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return stats;
|
|
239
|
+
}
|
|
240
|
+
}
|