cyclecad 0.2.2 → 0.2.3
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/API-BUILD-MANIFEST.txt +339 -0
- package/API-SERVER.md +535 -0
- package/Architecture-Deck.pptx +0 -0
- package/CLAUDE.md +172 -11
- package/CLI-BUILD-SUMMARY.md +504 -0
- package/CLI-INDEX.md +356 -0
- package/CLI-README.md +466 -0
- package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
- package/CONNECTED_FABS_GUIDE.md +612 -0
- package/CONNECTED_FABS_README.md +310 -0
- package/DELIVERABLES.md +343 -0
- package/DFM-ANALYZER-INTEGRATION.md +368 -0
- package/DFM-QUICK-START.js +253 -0
- package/Dockerfile +69 -0
- package/IMPLEMENTATION.md +327 -0
- package/LICENSE +31 -0
- package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
- package/MCP-INDEX.md +264 -0
- package/QUICKSTART-API.md +388 -0
- package/QUICKSTART-CLI.md +211 -0
- package/QUICKSTART-MCP.md +196 -0
- package/README-MCP.md +208 -0
- package/TEST-TOKEN-ENGINE.md +319 -0
- package/TOKEN-ENGINE-SUMMARY.md +266 -0
- package/TOKENS-README.md +263 -0
- package/TOOLS-REFERENCE.md +254 -0
- package/app/index.html +168 -3
- package/app/js/TOKEN-INTEGRATION.md +391 -0
- package/app/js/agent-api.js +3 -3
- package/app/js/ai-copilot.js +1435 -0
- package/app/js/cam-pipeline.js +840 -0
- package/app/js/collaboration-ui.js +995 -0
- package/app/js/collaboration.js +1116 -0
- package/app/js/connected-fabs-example.js +404 -0
- package/app/js/connected-fabs.js +1449 -0
- package/app/js/dfm-analyzer.js +1760 -0
- package/app/js/marketplace.js +1994 -0
- package/app/js/material-library.js +2115 -0
- package/app/js/token-dashboard.js +563 -0
- package/app/js/token-engine.js +743 -0
- package/app/test-agent.html +1801 -0
- package/bin/cyclecad-cli.js +662 -0
- package/bin/cyclecad-mcp +2 -0
- package/bin/server.js +242 -0
- package/cycleCAD-Architecture.pptx +0 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/demo-mcp.sh +60 -0
- package/docs/API-SERVER-SUMMARY.md +375 -0
- package/docs/API-SERVER.md +667 -0
- package/docs/CAM-EXAMPLES.md +344 -0
- package/docs/CAM-INTEGRATION.md +612 -0
- package/docs/CAM-QUICK-REFERENCE.md +199 -0
- package/docs/CLI-INTEGRATION.md +510 -0
- package/docs/CLI.md +872 -0
- package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
- package/docs/MARKETPLACE-INTEGRATION.md +467 -0
- package/docs/MARKETPLACE-SETUP.html +439 -0
- package/docs/MCP-SERVER.md +403 -0
- package/examples/api-client-example.js +488 -0
- package/examples/api-client-example.py +359 -0
- package/examples/batch-manufacturing.txt +28 -0
- package/examples/batch-simple.txt +26 -0
- package/model-marketplace.html +1273 -0
- package/package.json +14 -3
- package/server/api-server.js +1120 -0
- package/server/mcp-server.js +1161 -0
- package/test-api-server.js +432 -0
- package/test-mcp.js +198 -0
- package/~$cycleCAD-Investor-Deck.pptx +0 -0
|
@@ -0,0 +1,1994 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marketplace.js — cycleCAD Model Marketplace Module
|
|
3
|
+
*
|
|
4
|
+
* Creators publish, discover, and purchase 3D models using $CYCLE tokens.
|
|
5
|
+
* Architecture:
|
|
6
|
+
* - Model publishing with parametric + access tiers
|
|
7
|
+
* - Full-text search + category browsing
|
|
8
|
+
* - Token-based purchase system with double-entry ledger
|
|
9
|
+
* - Creator dashboard with earnings analytics
|
|
10
|
+
* - Review system with caching discounts
|
|
11
|
+
* - Agent API integration
|
|
12
|
+
*
|
|
13
|
+
* Data storage: localStorage (demo) / IndexedDB (prod)
|
|
14
|
+
* License: All models stored with creator IP terms
|
|
15
|
+
*
|
|
16
|
+
* Version: 1.0.0
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Constants
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
const STORAGE_KEY = 'cyclecad_marketplace';
|
|
26
|
+
const MODELS_DB = 'cyclecad_models';
|
|
27
|
+
const TRANSACTIONS_DB = 'cyclecad_transactions';
|
|
28
|
+
const MAX_PREVIEW_SIZE = 512; // pixels
|
|
29
|
+
|
|
30
|
+
const MODEL_CATEGORIES = [
|
|
31
|
+
'Mechanical',
|
|
32
|
+
'Structural',
|
|
33
|
+
'Enclosure',
|
|
34
|
+
'Fastener',
|
|
35
|
+
'Custom',
|
|
36
|
+
'Template'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const ACCESS_TIERS = {
|
|
40
|
+
FREE_PREVIEW: {
|
|
41
|
+
id: 1,
|
|
42
|
+
name: 'Free Preview',
|
|
43
|
+
price: 0,
|
|
44
|
+
description: 'View only, no download'
|
|
45
|
+
},
|
|
46
|
+
MESH_DOWNLOAD: {
|
|
47
|
+
id: 2,
|
|
48
|
+
name: 'Mesh Download',
|
|
49
|
+
price: 50,
|
|
50
|
+
description: 'STL/OBJ export'
|
|
51
|
+
},
|
|
52
|
+
PARAMETRIC: {
|
|
53
|
+
id: 3,
|
|
54
|
+
name: 'Parametric',
|
|
55
|
+
price: 200,
|
|
56
|
+
description: 'Editable cycleCAD format'
|
|
57
|
+
},
|
|
58
|
+
FULL_IP: {
|
|
59
|
+
id: 4,
|
|
60
|
+
name: 'Full IP',
|
|
61
|
+
price: 1000,
|
|
62
|
+
description: 'STEP + source + history'
|
|
63
|
+
},
|
|
64
|
+
COMMERCIAL_USE: {
|
|
65
|
+
id: 5,
|
|
66
|
+
name: 'Commercial Use',
|
|
67
|
+
price: 2000,
|
|
68
|
+
description: 'License for resale'
|
|
69
|
+
},
|
|
70
|
+
DERIVATIVE: {
|
|
71
|
+
id: 6,
|
|
72
|
+
name: 'Derivative',
|
|
73
|
+
price: null, // 15% of parent model price
|
|
74
|
+
description: 'Fork and modify'
|
|
75
|
+
},
|
|
76
|
+
AGENT_ACCESS: {
|
|
77
|
+
id: 7,
|
|
78
|
+
name: 'Agent Access',
|
|
79
|
+
price: 5, // per-use
|
|
80
|
+
description: 'Micro-royalty per AI query'
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// State
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
let _currentUser = null;
|
|
89
|
+
let _userBalance = 0;
|
|
90
|
+
let _allModels = [];
|
|
91
|
+
let _purchaseHistory = [];
|
|
92
|
+
let _createdModels = [];
|
|
93
|
+
let _viewport = null;
|
|
94
|
+
let _tokenEngine = null;
|
|
95
|
+
let _eventListeners = {};
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Initialization
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Initialize the Marketplace module
|
|
103
|
+
*/
|
|
104
|
+
export function initMarketplace({ viewport, tokenEngine }) {
|
|
105
|
+
_viewport = viewport;
|
|
106
|
+
_tokenEngine = tokenEngine;
|
|
107
|
+
|
|
108
|
+
// Initialize current user from localStorage
|
|
109
|
+
const storedUser = localStorage.getItem('cyclecad_current_user');
|
|
110
|
+
if (storedUser) {
|
|
111
|
+
_currentUser = JSON.parse(storedUser);
|
|
112
|
+
} else {
|
|
113
|
+
_currentUser = {
|
|
114
|
+
id: crypto.randomUUID(),
|
|
115
|
+
name: 'Creator_' + Math.random().toString(36).substr(2, 9),
|
|
116
|
+
email: '',
|
|
117
|
+
avatar: null,
|
|
118
|
+
joinedDate: new Date().toISOString(),
|
|
119
|
+
bio: '',
|
|
120
|
+
website: ''
|
|
121
|
+
};
|
|
122
|
+
localStorage.setItem('cyclecad_current_user', JSON.stringify(_currentUser));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Load marketplace data
|
|
126
|
+
loadMarketplaceData();
|
|
127
|
+
|
|
128
|
+
// Create UI panel
|
|
129
|
+
createMarketplacePanel();
|
|
130
|
+
|
|
131
|
+
// Add toolbar button
|
|
132
|
+
addMarketplaceToolbarButton();
|
|
133
|
+
|
|
134
|
+
// Expose API globally
|
|
135
|
+
window.cycleCAD.marketplace = {
|
|
136
|
+
publish: publishModel,
|
|
137
|
+
search: searchModels,
|
|
138
|
+
browse: browseModels,
|
|
139
|
+
getDetails: getModelDetails,
|
|
140
|
+
purchase: purchaseModel,
|
|
141
|
+
download: downloadModel,
|
|
142
|
+
getPurchaseHistory,
|
|
143
|
+
getCreatorProfile,
|
|
144
|
+
getCreatorStats,
|
|
145
|
+
addReview,
|
|
146
|
+
getReviews,
|
|
147
|
+
withdrawEarnings,
|
|
148
|
+
listMyModels: listCreatorModels,
|
|
149
|
+
updateModel,
|
|
150
|
+
deleteModel,
|
|
151
|
+
on: addEventListener,
|
|
152
|
+
off: removeEventListener
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
console.log('[Marketplace] Initialized for user:', _currentUser.name);
|
|
156
|
+
|
|
157
|
+
return { userId: _currentUser.id };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Model Publishing
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Publish a model to the marketplace
|
|
166
|
+
*/
|
|
167
|
+
export function publishModel(modelData) {
|
|
168
|
+
const {
|
|
169
|
+
name,
|
|
170
|
+
description,
|
|
171
|
+
category,
|
|
172
|
+
tags = [],
|
|
173
|
+
tiers = [],
|
|
174
|
+
sourceGeometry = null,
|
|
175
|
+
parametricData = null,
|
|
176
|
+
metadata = {}
|
|
177
|
+
} = modelData;
|
|
178
|
+
|
|
179
|
+
// Validate
|
|
180
|
+
if (!name || !description || !category) {
|
|
181
|
+
return { ok: false, error: 'Missing required fields: name, description, category' };
|
|
182
|
+
}
|
|
183
|
+
if (!MODEL_CATEGORIES.includes(category)) {
|
|
184
|
+
return { ok: false, error: `Invalid category. Must be one of: ${MODEL_CATEGORIES.join(', ')}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const modelId = crypto.randomUUID();
|
|
188
|
+
const timestamp = new Date().toISOString();
|
|
189
|
+
|
|
190
|
+
// Generate preview thumbnail
|
|
191
|
+
let previewImage = null;
|
|
192
|
+
if (sourceGeometry) {
|
|
193
|
+
previewImage = generatePreviewThumbnail(sourceGeometry);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build model record
|
|
197
|
+
const model = {
|
|
198
|
+
id: modelId,
|
|
199
|
+
creatorId: _currentUser.id,
|
|
200
|
+
creatorName: _currentUser.name,
|
|
201
|
+
name,
|
|
202
|
+
description,
|
|
203
|
+
category,
|
|
204
|
+
tags: Array.isArray(tags) ? tags : [],
|
|
205
|
+
tiers: tiers.length > 0 ? tiers : [ACCESS_TIERS.FREE_PREVIEW],
|
|
206
|
+
previewImage,
|
|
207
|
+
sourceGeometry: sourceGeometry ? serializeGeometry(sourceGeometry) : null,
|
|
208
|
+
parametricData,
|
|
209
|
+
metadata: {
|
|
210
|
+
...metadata,
|
|
211
|
+
dimensions: extractDimensions(sourceGeometry),
|
|
212
|
+
polyCount: sourceGeometry ? countPolygons(sourceGeometry) : 0
|
|
213
|
+
},
|
|
214
|
+
stats: {
|
|
215
|
+
views: 0,
|
|
216
|
+
downloads: 0,
|
|
217
|
+
purchases: 0,
|
|
218
|
+
rating: 0,
|
|
219
|
+
reviewCount: 0
|
|
220
|
+
},
|
|
221
|
+
reviews: [],
|
|
222
|
+
publishedDate: timestamp,
|
|
223
|
+
updatedDate: timestamp,
|
|
224
|
+
derivedFromModelId: modelData.derivedFromModelId || null,
|
|
225
|
+
derivativeLicense: modelData.derivativeLicense || false
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
_allModels.push(model);
|
|
229
|
+
_createdModels.push(modelId);
|
|
230
|
+
|
|
231
|
+
saveMarketplaceData();
|
|
232
|
+
emitEvent('modelPublished', { modelId, modelName: name });
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
modelId,
|
|
237
|
+
model
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Update an existing model (creator only)
|
|
243
|
+
*/
|
|
244
|
+
export function updateModel(modelId, updates) {
|
|
245
|
+
const modelIndex = _allModels.findIndex(m => m.id === modelId);
|
|
246
|
+
if (modelIndex === -1) {
|
|
247
|
+
return { ok: false, error: 'Model not found' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const model = _allModels[modelIndex];
|
|
251
|
+
|
|
252
|
+
// Check ownership
|
|
253
|
+
if (model.creatorId !== _currentUser.id) {
|
|
254
|
+
return { ok: false, error: 'Permission denied: only creator can update model' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Merge updates
|
|
258
|
+
const updated = {
|
|
259
|
+
...model,
|
|
260
|
+
...updates,
|
|
261
|
+
id: model.id,
|
|
262
|
+
creatorId: model.creatorId,
|
|
263
|
+
publishedDate: model.publishedDate,
|
|
264
|
+
updatedDate: new Date().toISOString()
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
_allModels[modelIndex] = updated;
|
|
268
|
+
saveMarketplaceData();
|
|
269
|
+
emitEvent('modelUpdated', { modelId, updates });
|
|
270
|
+
|
|
271
|
+
return { ok: true, model: updated };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Delete a model (creator only)
|
|
276
|
+
*/
|
|
277
|
+
export function deleteModel(modelId) {
|
|
278
|
+
const modelIndex = _allModels.findIndex(m => m.id === modelId);
|
|
279
|
+
if (modelIndex === -1) {
|
|
280
|
+
return { ok: false, error: 'Model not found' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const model = _allModels[modelIndex];
|
|
284
|
+
if (model.creatorId !== _currentUser.id) {
|
|
285
|
+
return { ok: false, error: 'Permission denied' };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_allModels.splice(modelIndex, 1);
|
|
289
|
+
_createdModels = _createdModels.filter(id => id !== modelId);
|
|
290
|
+
saveMarketplaceData();
|
|
291
|
+
emitEvent('modelDeleted', { modelId });
|
|
292
|
+
|
|
293
|
+
return { ok: true };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Model Discovery
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Search models by query + filters
|
|
302
|
+
*/
|
|
303
|
+
export function searchModels(query, filters = {}) {
|
|
304
|
+
let results = _allModels;
|
|
305
|
+
|
|
306
|
+
// Text search
|
|
307
|
+
if (query && query.trim()) {
|
|
308
|
+
const q = query.toLowerCase();
|
|
309
|
+
results = results.filter(m =>
|
|
310
|
+
m.name.toLowerCase().includes(q) ||
|
|
311
|
+
m.description.toLowerCase().includes(q) ||
|
|
312
|
+
m.tags.some(t => t.toLowerCase().includes(q)) ||
|
|
313
|
+
m.creatorName.toLowerCase().includes(q)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Category filter
|
|
318
|
+
if (filters.category) {
|
|
319
|
+
results = results.filter(m => m.category === filters.category);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Price range filter
|
|
323
|
+
if (filters.priceMin !== undefined || filters.priceMax !== undefined) {
|
|
324
|
+
const minPrice = filters.priceMin || 0;
|
|
325
|
+
const maxPrice = filters.priceMax || Infinity;
|
|
326
|
+
results = results.filter(m => {
|
|
327
|
+
const price = m.tiers[0]?.price || 0;
|
|
328
|
+
return price >= minPrice && price <= maxPrice;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Rating filter
|
|
333
|
+
if (filters.minRating !== undefined) {
|
|
334
|
+
results = results.filter(m => m.stats.rating >= filters.minRating);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Pagination
|
|
338
|
+
const page = filters.page || 0;
|
|
339
|
+
const pageSize = filters.pageSize || 20;
|
|
340
|
+
const total = results.length;
|
|
341
|
+
const paged = results.slice(page * pageSize, (page + 1) * pageSize);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
results: paged,
|
|
346
|
+
total,
|
|
347
|
+
page,
|
|
348
|
+
pageSize,
|
|
349
|
+
hasMore: (page + 1) * pageSize < total
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Browse models by category with sorting
|
|
355
|
+
*/
|
|
356
|
+
export function browseModels(category, options = {}) {
|
|
357
|
+
const { sort = 'newest', page = 0, pageSize = 20 } = options;
|
|
358
|
+
|
|
359
|
+
let results = category ? _allModels.filter(m => m.category === category) : _allModels;
|
|
360
|
+
|
|
361
|
+
// Sort
|
|
362
|
+
switch (sort) {
|
|
363
|
+
case 'newest':
|
|
364
|
+
results.sort((a, b) => new Date(b.publishedDate) - new Date(a.publishedDate));
|
|
365
|
+
break;
|
|
366
|
+
case 'popular':
|
|
367
|
+
results.sort((a, b) => b.stats.downloads - a.stats.downloads);
|
|
368
|
+
break;
|
|
369
|
+
case 'price-low':
|
|
370
|
+
results.sort((a, b) => (a.tiers[0]?.price || 0) - (b.tiers[0]?.price || 0));
|
|
371
|
+
break;
|
|
372
|
+
case 'price-high':
|
|
373
|
+
results.sort((a, b) => (b.tiers[0]?.price || 0) - (a.tiers[0]?.price || 0));
|
|
374
|
+
break;
|
|
375
|
+
case 'rating':
|
|
376
|
+
results.sort((a, b) => b.stats.rating - a.stats.rating);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const total = results.length;
|
|
381
|
+
const paged = results.slice(page * pageSize, (page + 1) * pageSize);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
results: paged,
|
|
386
|
+
category,
|
|
387
|
+
sort,
|
|
388
|
+
total,
|
|
389
|
+
page,
|
|
390
|
+
pageSize,
|
|
391
|
+
hasMore: (page + 1) * pageSize < total
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get full model details + preview
|
|
397
|
+
*/
|
|
398
|
+
export function getModelDetails(modelId) {
|
|
399
|
+
const model = _allModels.find(m => m.id === modelId);
|
|
400
|
+
if (!model) {
|
|
401
|
+
return { ok: false, error: 'Model not found' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Increment view count
|
|
405
|
+
model.stats.views += 1;
|
|
406
|
+
saveMarketplaceData();
|
|
407
|
+
|
|
408
|
+
// Get creator profile
|
|
409
|
+
const creatorProfile = getCreatorProfile(model.creatorId);
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
model,
|
|
414
|
+
creator: creatorProfile.ok ? creatorProfile.profile : null,
|
|
415
|
+
canEdit: model.creatorId === _currentUser.id,
|
|
416
|
+
canDownload: isPurchased(modelId),
|
|
417
|
+
averageRating: model.stats.rating,
|
|
418
|
+
reviewCount: model.stats.reviewCount
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get creator profile + stats
|
|
424
|
+
*/
|
|
425
|
+
export function getCreatorProfile(creatorId) {
|
|
426
|
+
const models = _allModels.filter(m => m.creatorId === creatorId);
|
|
427
|
+
if (models.length === 0) {
|
|
428
|
+
return { ok: false, error: 'Creator not found' };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const totalDownloads = models.reduce((sum, m) => sum + m.stats.downloads, 0);
|
|
432
|
+
const totalViews = models.reduce((sum, m) => sum + m.stats.views, 0);
|
|
433
|
+
const avgRating = models.length > 0
|
|
434
|
+
? (models.reduce((sum, m) => sum + m.stats.rating, 0) / models.length).toFixed(2)
|
|
435
|
+
: 0;
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
profile: {
|
|
440
|
+
creatorId,
|
|
441
|
+
name: models[0].creatorName,
|
|
442
|
+
modelCount: models.length,
|
|
443
|
+
totalDownloads,
|
|
444
|
+
totalViews,
|
|
445
|
+
averageRating: parseFloat(avgRating),
|
|
446
|
+
earnings: calculateEarnings(creatorId),
|
|
447
|
+
topModels: models
|
|
448
|
+
.sort((a, b) => b.stats.downloads - a.stats.downloads)
|
|
449
|
+
.slice(0, 5)
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ============================================================================
|
|
455
|
+
// Purchase & Download
|
|
456
|
+
// ============================================================================
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Purchase access to a model at a specific tier
|
|
460
|
+
*/
|
|
461
|
+
export function purchaseModel(modelId, tierId) {
|
|
462
|
+
const model = _allModels.find(m => m.id === modelId);
|
|
463
|
+
if (!model) {
|
|
464
|
+
return { ok: false, error: 'Model not found' };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Find tier
|
|
468
|
+
const tierName = Object.keys(ACCESS_TIERS).find(key => ACCESS_TIERS[key].id === tierId);
|
|
469
|
+
if (!tierName) {
|
|
470
|
+
return { ok: false, error: 'Invalid tier ID' };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const tier = ACCESS_TIERS[tierName];
|
|
474
|
+
let price = tier.price;
|
|
475
|
+
|
|
476
|
+
// Handle derivative pricing
|
|
477
|
+
if (tierName === 'DERIVATIVE' && model.derivedFromModelId) {
|
|
478
|
+
const parentModel = _allModels.find(m => m.id === model.derivedFromModelId);
|
|
479
|
+
if (parentModel) {
|
|
480
|
+
price = Math.ceil(parentModel.tiers[0].price * 0.15);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Check balance (via token engine)
|
|
485
|
+
if (_tokenEngine && _userBalance < price) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
error: `Insufficient balance. Need ${price} tokens, have ${_userBalance}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const purchaseId = crypto.randomUUID();
|
|
493
|
+
const timestamp = new Date().toISOString();
|
|
494
|
+
|
|
495
|
+
// Record purchase
|
|
496
|
+
const purchase = {
|
|
497
|
+
id: purchaseId,
|
|
498
|
+
userId: _currentUser.id,
|
|
499
|
+
modelId,
|
|
500
|
+
tierId,
|
|
501
|
+
tierName,
|
|
502
|
+
price,
|
|
503
|
+
purchaseDate: timestamp,
|
|
504
|
+
downloadedTimes: 0,
|
|
505
|
+
expiryDate: addDays(new Date(), 365).toISOString() // 1 year license
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
_purchaseHistory.push(purchase);
|
|
509
|
+
|
|
510
|
+
// Deduct tokens (via token engine if available)
|
|
511
|
+
if (_tokenEngine) {
|
|
512
|
+
_userBalance -= price;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Increment model stats
|
|
516
|
+
model.stats.downloads += 1;
|
|
517
|
+
model.stats.purchases += 1;
|
|
518
|
+
|
|
519
|
+
// Log transaction (double-entry ledger)
|
|
520
|
+
logTransaction({
|
|
521
|
+
fromUserId: _currentUser.id,
|
|
522
|
+
toUserId: model.creatorId,
|
|
523
|
+
amount: price,
|
|
524
|
+
type: 'model_purchase',
|
|
525
|
+
relatedId: modelId,
|
|
526
|
+
timestamp
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
saveMarketplaceData();
|
|
530
|
+
emitEvent('modelPurchased', { modelId, tierId, price, purchaseId });
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
ok: true,
|
|
534
|
+
purchaseId,
|
|
535
|
+
accessUrl: `/download/${modelId}?token=${purchaseId}`,
|
|
536
|
+
expiryDate: purchase.expiryDate
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Download a purchased model
|
|
542
|
+
*/
|
|
543
|
+
export function downloadModel(modelId, format = 'stl') {
|
|
544
|
+
// Check if purchased
|
|
545
|
+
if (!isPurchased(modelId)) {
|
|
546
|
+
return { ok: false, error: 'Model not purchased. Please purchase first.' };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const model = _allModels.find(m => m.id === modelId);
|
|
550
|
+
if (!model) {
|
|
551
|
+
return { ok: false, error: 'Model not found' };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let downloadData = null;
|
|
555
|
+
let mimeType = 'application/octet-stream';
|
|
556
|
+
let filename = `${model.name.replace(/\s+/g, '_')}.${format}`;
|
|
557
|
+
|
|
558
|
+
// Format-specific export
|
|
559
|
+
switch (format.toLowerCase()) {
|
|
560
|
+
case 'stl':
|
|
561
|
+
case 'obj':
|
|
562
|
+
case 'gltf':
|
|
563
|
+
case 'json':
|
|
564
|
+
if (model.sourceGeometry) {
|
|
565
|
+
const geometry = deserializeGeometry(model.sourceGeometry);
|
|
566
|
+
downloadData = exportGeometry(geometry, format);
|
|
567
|
+
mimeType = format === 'json' ? 'application/json' : 'application/octet-stream';
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
case 'step':
|
|
571
|
+
if (model.parametricData) {
|
|
572
|
+
downloadData = model.parametricData;
|
|
573
|
+
mimeType = 'application/step';
|
|
574
|
+
filename = `${model.name.replace(/\s+/g, '_')}.step`;
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
default:
|
|
578
|
+
return { ok: false, error: `Unsupported format: ${format}` };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!downloadData) {
|
|
582
|
+
return { ok: false, error: `Model doesn't have ${format} export available` };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Record download
|
|
586
|
+
const purchase = _purchaseHistory.find(p => p.modelId === modelId);
|
|
587
|
+
if (purchase) {
|
|
588
|
+
purchase.downloadedTimes += 1;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
model.stats.downloads += 1;
|
|
592
|
+
saveMarketplaceData();
|
|
593
|
+
emitEvent('modelDownloaded', { modelId, format, filename });
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
ok: true,
|
|
597
|
+
data: downloadData,
|
|
598
|
+
filename,
|
|
599
|
+
mimeType
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// Purchase History & Favorites
|
|
605
|
+
// ============================================================================
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Get user's purchase history
|
|
609
|
+
*/
|
|
610
|
+
export function getPurchaseHistory() {
|
|
611
|
+
return _purchaseHistory.map(p => {
|
|
612
|
+
const model = _allModels.find(m => m.id === p.modelId);
|
|
613
|
+
return {
|
|
614
|
+
...p,
|
|
615
|
+
modelName: model?.name || 'Unknown',
|
|
616
|
+
modelCategory: model?.category || '',
|
|
617
|
+
creatorName: model?.creatorName || ''
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Check if user has purchased a model
|
|
624
|
+
*/
|
|
625
|
+
function isPurchased(modelId) {
|
|
626
|
+
return _purchaseHistory.some(p => p.modelId === modelId);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ============================================================================
|
|
630
|
+
// Creator Dashboard
|
|
631
|
+
// ============================================================================
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Get creator stats
|
|
635
|
+
*/
|
|
636
|
+
export function getCreatorStats(period = 'all') {
|
|
637
|
+
const createdModels = _allModels.filter(m => m.creatorId === _currentUser.id);
|
|
638
|
+
|
|
639
|
+
let filteredModels = createdModels;
|
|
640
|
+
if (period !== 'all') {
|
|
641
|
+
const now = new Date();
|
|
642
|
+
const startDate = new Date();
|
|
643
|
+
|
|
644
|
+
switch (period) {
|
|
645
|
+
case 'day':
|
|
646
|
+
startDate.setDate(now.getDate() - 1);
|
|
647
|
+
break;
|
|
648
|
+
case 'week':
|
|
649
|
+
startDate.setDate(now.getDate() - 7);
|
|
650
|
+
break;
|
|
651
|
+
case 'month':
|
|
652
|
+
startDate.setMonth(now.getMonth() - 1);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
filteredModels = createdModels.filter(m =>
|
|
657
|
+
new Date(m.publishedDate) >= startDate
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const stats = {
|
|
662
|
+
totalEarnings: calculateEarnings(_currentUser.id),
|
|
663
|
+
totalDownloads: filteredModels.reduce((sum, m) => sum + m.stats.downloads, 0),
|
|
664
|
+
totalViews: filteredModels.reduce((sum, m) => sum + m.stats.views, 0),
|
|
665
|
+
modelCount: createdModels.length,
|
|
666
|
+
averageRating: createdModels.length > 0
|
|
667
|
+
? (createdModels.reduce((sum, m) => sum + m.stats.rating, 0) / createdModels.length).toFixed(2)
|
|
668
|
+
: 0,
|
|
669
|
+
topModel: createdModels.sort((a, b) => b.stats.downloads - a.stats.downloads)[0] || null,
|
|
670
|
+
period
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
return { ok: true, stats };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* List models created by current user
|
|
678
|
+
*/
|
|
679
|
+
export function listCreatorModels() {
|
|
680
|
+
return _allModels.filter(m => m.creatorId === _currentUser.id);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Get earnings breakdown by period
|
|
685
|
+
*/
|
|
686
|
+
export function getEarningsBreakdown(period = 'daily') {
|
|
687
|
+
const createdModels = _allModels.filter(m => m.creatorId === _currentUser.id);
|
|
688
|
+
const breakdown = {};
|
|
689
|
+
|
|
690
|
+
createdModels.forEach(model => {
|
|
691
|
+
model.stats.downloads > 0 && model.tiers.forEach(tier => {
|
|
692
|
+
const date = new Date(model.publishedDate);
|
|
693
|
+
const key = getDateKey(date, period);
|
|
694
|
+
breakdown[key] = (breakdown[key] || 0) + (tier.price || 0);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
ok: true,
|
|
700
|
+
breakdown,
|
|
701
|
+
period,
|
|
702
|
+
total: Object.values(breakdown).reduce((a, b) => a + b, 0)
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Withdraw earnings (placeholder for Stripe/crypto)
|
|
708
|
+
*/
|
|
709
|
+
export function withdrawEarnings(amount, method = 'stripe') {
|
|
710
|
+
const stats = getCreatorStats();
|
|
711
|
+
if (!stats.ok || stats.stats.totalEarnings < amount) {
|
|
712
|
+
return { ok: false, error: 'Insufficient earnings to withdraw' };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const withdrawalId = crypto.randomUUID();
|
|
716
|
+
const withdrawal = {
|
|
717
|
+
id: withdrawalId,
|
|
718
|
+
amount,
|
|
719
|
+
method,
|
|
720
|
+
status: 'pending',
|
|
721
|
+
requestDate: new Date().toISOString(),
|
|
722
|
+
completedDate: null
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// In production: integrate with Stripe API / crypto payment gateway
|
|
726
|
+
console.log('[Marketplace] Withdrawal request:', withdrawal);
|
|
727
|
+
emitEvent('withdrawalRequested', withdrawal);
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
ok: true,
|
|
731
|
+
withdrawalId,
|
|
732
|
+
status: 'pending',
|
|
733
|
+
message: `Withdrawal of ${amount} tokens requested via ${method}. You'll receive it in 3-5 business days.`
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ============================================================================
|
|
738
|
+
// Review System
|
|
739
|
+
// ============================================================================
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Add review to a model
|
|
743
|
+
*/
|
|
744
|
+
export function addReview(modelId, rating, comment = '') {
|
|
745
|
+
if (!isPurchased(modelId)) {
|
|
746
|
+
return { ok: false, error: 'Must purchase model to leave review' };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const model = _allModels.find(m => m.id === modelId);
|
|
750
|
+
if (!model) {
|
|
751
|
+
return { ok: false, error: 'Model not found' };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const review = {
|
|
755
|
+
id: crypto.randomUUID(),
|
|
756
|
+
userId: _currentUser.id,
|
|
757
|
+
userName: _currentUser.name,
|
|
758
|
+
rating: Math.max(1, Math.min(5, Math.round(rating))),
|
|
759
|
+
comment,
|
|
760
|
+
date: new Date().toISOString(),
|
|
761
|
+
helpful: 0
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
model.reviews.push(review);
|
|
765
|
+
|
|
766
|
+
// Recalculate average rating
|
|
767
|
+
const avgRating = model.reviews.length > 0
|
|
768
|
+
? (model.reviews.reduce((sum, r) => sum + r.rating, 0) / model.reviews.length)
|
|
769
|
+
: 0;
|
|
770
|
+
|
|
771
|
+
model.stats.rating = parseFloat(avgRating.toFixed(1));
|
|
772
|
+
model.stats.reviewCount = model.reviews.length;
|
|
773
|
+
|
|
774
|
+
saveMarketplaceData();
|
|
775
|
+
emitEvent('reviewAdded', { modelId, rating, reviewId: review.id });
|
|
776
|
+
|
|
777
|
+
return { ok: true, review };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Get reviews for a model (paginated)
|
|
782
|
+
*/
|
|
783
|
+
export function getReviews(modelId, page = 0, pageSize = 10) {
|
|
784
|
+
const model = _allModels.find(m => m.id === modelId);
|
|
785
|
+
if (!model) {
|
|
786
|
+
return { ok: false, error: 'Model not found' };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const sorted = model.reviews.sort((a, b) =>
|
|
790
|
+
new Date(b.date) - new Date(a.date)
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const total = sorted.length;
|
|
794
|
+
const paged = sorted.slice(page * pageSize, (page + 1) * pageSize);
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
ok: true,
|
|
798
|
+
reviews: paged,
|
|
799
|
+
total,
|
|
800
|
+
page,
|
|
801
|
+
pageSize,
|
|
802
|
+
hasMore: (page + 1) * pageSize < total,
|
|
803
|
+
averageRating: model.stats.rating,
|
|
804
|
+
reviewCount: model.stats.reviewCount
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ============================================================================
|
|
809
|
+
// UI Panel
|
|
810
|
+
// ============================================================================
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Create marketplace panel HTML
|
|
814
|
+
*/
|
|
815
|
+
function createMarketplacePanel() {
|
|
816
|
+
const html = `
|
|
817
|
+
<div id="marketplace-panel" class="mp-panel">
|
|
818
|
+
<div class="mp-header">
|
|
819
|
+
<h2>Model Marketplace</h2>
|
|
820
|
+
<button class="mp-close-btn" data-close-panel="marketplace-panel">✕</button>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
<div class="mp-tabs">
|
|
824
|
+
<button class="mp-tab-btn active" data-tab="browse">Browse</button>
|
|
825
|
+
<button class="mp-tab-btn" data-tab="search">Search</button>
|
|
826
|
+
<button class="mp-tab-btn" data-tab="my-models">My Models</button>
|
|
827
|
+
<button class="mp-tab-btn" data-tab="purchases">Purchases</button>
|
|
828
|
+
<button class="mp-tab-btn" data-tab="publish">Publish</button>
|
|
829
|
+
<button class="mp-tab-btn" data-tab="earnings">Earnings</button>
|
|
830
|
+
</div>
|
|
831
|
+
|
|
832
|
+
<!-- Browse Tab -->
|
|
833
|
+
<div class="mp-tab-content active" data-tab="browse">
|
|
834
|
+
<div class="mp-category-filter">
|
|
835
|
+
<select id="mp-category-select" class="mp-select">
|
|
836
|
+
<option value="">All Categories</option>
|
|
837
|
+
${MODEL_CATEGORIES.map(cat => `<option value="${cat}">${cat}</option>`).join('')}
|
|
838
|
+
</select>
|
|
839
|
+
<select id="mp-sort-select" class="mp-select">
|
|
840
|
+
<option value="newest">Newest</option>
|
|
841
|
+
<option value="popular">Most Popular</option>
|
|
842
|
+
<option value="price-low">Price: Low to High</option>
|
|
843
|
+
<option value="price-high">Price: High to Low</option>
|
|
844
|
+
<option value="rating">Top Rated</option>
|
|
845
|
+
</select>
|
|
846
|
+
</div>
|
|
847
|
+
<div id="mp-browse-grid" class="mp-grid"></div>
|
|
848
|
+
<button id="mp-browse-more" class="mp-button">Load More</button>
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<!-- Search Tab -->
|
|
852
|
+
<div class="mp-tab-content" data-tab="search">
|
|
853
|
+
<input id="mp-search-input" type="text" class="mp-input" placeholder="Search models...">
|
|
854
|
+
<div class="mp-filters">
|
|
855
|
+
<label class="mp-label">
|
|
856
|
+
Price Range:
|
|
857
|
+
<input id="mp-price-min" type="number" class="mp-input-small" placeholder="Min" min="0">
|
|
858
|
+
<input id="mp-price-max" type="number" class="mp-input-small" placeholder="Max" min="0">
|
|
859
|
+
</label>
|
|
860
|
+
<label class="mp-label">
|
|
861
|
+
Min Rating:
|
|
862
|
+
<input id="mp-rating-min" type="number" class="mp-input-small" placeholder="0-5" min="0" max="5" step="0.5">
|
|
863
|
+
</label>
|
|
864
|
+
</div>
|
|
865
|
+
<div id="mp-search-results" class="mp-grid"></div>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<!-- My Models Tab -->
|
|
869
|
+
<div class="mp-tab-content" data-tab="my-models">
|
|
870
|
+
<button id="mp-refresh-models" class="mp-button">Refresh</button>
|
|
871
|
+
<div id="mp-my-models-list" class="mp-list"></div>
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<!-- Purchases Tab -->
|
|
875
|
+
<div class="mp-tab-content" data-tab="purchases">
|
|
876
|
+
<div id="mp-purchases-list" class="mp-list"></div>
|
|
877
|
+
</div>
|
|
878
|
+
|
|
879
|
+
<!-- Publish Tab -->
|
|
880
|
+
<div class="mp-tab-content" data-tab="publish">
|
|
881
|
+
<form id="mp-publish-form" class="mp-form">
|
|
882
|
+
<div class="mp-form-group">
|
|
883
|
+
<label class="mp-label">Model Name *</label>
|
|
884
|
+
<input id="mp-publish-name" type="text" class="mp-input" required>
|
|
885
|
+
</div>
|
|
886
|
+
<div class="mp-form-group">
|
|
887
|
+
<label class="mp-label">Description *</label>
|
|
888
|
+
<textarea id="mp-publish-desc" class="mp-input mp-textarea" required></textarea>
|
|
889
|
+
</div>
|
|
890
|
+
<div class="mp-form-group">
|
|
891
|
+
<label class="mp-label">Category *</label>
|
|
892
|
+
<select id="mp-publish-category" class="mp-select" required>
|
|
893
|
+
<option value="">Select category...</option>
|
|
894
|
+
${MODEL_CATEGORIES.map(cat => `<option value="${cat}">${cat}</option>`).join('')}
|
|
895
|
+
</select>
|
|
896
|
+
</div>
|
|
897
|
+
<div class="mp-form-group">
|
|
898
|
+
<label class="mp-label">Tags (comma-separated)</label>
|
|
899
|
+
<input id="mp-publish-tags" type="text" class="mp-input" placeholder="e.g. metal, cnc, precision">
|
|
900
|
+
</div>
|
|
901
|
+
<div class="mp-form-group">
|
|
902
|
+
<label class="mp-label">Access Tier *</label>
|
|
903
|
+
<select id="mp-publish-tier" class="mp-select" required>
|
|
904
|
+
<option value="1">Free Preview (0 tokens)</option>
|
|
905
|
+
<option value="2">Mesh Download (50 tokens)</option>
|
|
906
|
+
<option value="3">Parametric (200 tokens)</option>
|
|
907
|
+
<option value="4">Full IP (1,000 tokens)</option>
|
|
908
|
+
<option value="5">Commercial (2,000 tokens)</option>
|
|
909
|
+
</select>
|
|
910
|
+
</div>
|
|
911
|
+
<button type="submit" class="mp-button mp-button-primary">Publish Model</button>
|
|
912
|
+
</form>
|
|
913
|
+
</div>
|
|
914
|
+
|
|
915
|
+
<!-- Earnings Tab -->
|
|
916
|
+
<div class="mp-tab-content" data-tab="earnings">
|
|
917
|
+
<div id="mp-earnings-dashboard" class="mp-dashboard"></div>
|
|
918
|
+
<button id="mp-withdraw-btn" class="mp-button">Withdraw Earnings</button>
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
<!-- Model Detail Modal -->
|
|
923
|
+
<div id="marketplace-modal" class="mp-modal">
|
|
924
|
+
<div class="mp-modal-content">
|
|
925
|
+
<button class="mp-modal-close">✕</button>
|
|
926
|
+
<div class="mp-modal-body">
|
|
927
|
+
<div id="mp-modal-preview" class="mp-preview"></div>
|
|
928
|
+
<div id="mp-modal-info" class="mp-info"></div>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
`;
|
|
933
|
+
|
|
934
|
+
const panel = document.createElement('div');
|
|
935
|
+
panel.innerHTML = html;
|
|
936
|
+
document.body.appendChild(panel);
|
|
937
|
+
|
|
938
|
+
// Wire event handlers
|
|
939
|
+
wireMarketplacePanelEvents();
|
|
940
|
+
|
|
941
|
+
// Add styles
|
|
942
|
+
addMarketplaceStyles();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Wire marketplace panel events
|
|
947
|
+
*/
|
|
948
|
+
function wireMarketplacePanelEvents() {
|
|
949
|
+
// Tab switching
|
|
950
|
+
document.querySelectorAll('.mp-tab-btn').forEach(btn => {
|
|
951
|
+
btn.addEventListener('click', (e) => {
|
|
952
|
+
const tabName = e.target.dataset.tab;
|
|
953
|
+
document.querySelectorAll('.mp-tab-btn').forEach(b => b.classList.remove('active'));
|
|
954
|
+
document.querySelectorAll('.mp-tab-content').forEach(tc => tc.classList.remove('active'));
|
|
955
|
+
e.target.classList.add('active');
|
|
956
|
+
document.querySelector(`.mp-tab-content[data-tab="${tabName}"]`).classList.add('active');
|
|
957
|
+
|
|
958
|
+
// Load tab content
|
|
959
|
+
if (tabName === 'browse') loadBrowseTab();
|
|
960
|
+
if (tabName === 'my-models') loadMyModelsTab();
|
|
961
|
+
if (tabName === 'purchases') loadPurchasesTab();
|
|
962
|
+
if (tabName === 'earnings') loadEarningsTab();
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Search
|
|
967
|
+
document.getElementById('mp-search-input')?.addEventListener('input', (e) => {
|
|
968
|
+
const query = e.target.value;
|
|
969
|
+
const filters = {
|
|
970
|
+
priceMin: parseFloat(document.getElementById('mp-price-min')?.value || 0),
|
|
971
|
+
priceMax: parseFloat(document.getElementById('mp-price-max')?.value || Infinity),
|
|
972
|
+
minRating: parseFloat(document.getElementById('mp-rating-min')?.value || 0)
|
|
973
|
+
};
|
|
974
|
+
performSearch(query, filters);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
// Publish form
|
|
978
|
+
document.getElementById('mp-publish-form')?.addEventListener('submit', (e) => {
|
|
979
|
+
e.preventDefault();
|
|
980
|
+
const name = document.getElementById('mp-publish-name').value;
|
|
981
|
+
const description = document.getElementById('mp-publish-desc').value;
|
|
982
|
+
const category = document.getElementById('mp-publish-category').value;
|
|
983
|
+
const tags = document.getElementById('mp-publish-tags').value.split(',').map(t => t.trim());
|
|
984
|
+
const tierId = parseInt(document.getElementById('mp-publish-tier').value);
|
|
985
|
+
|
|
986
|
+
const result = publishModel({
|
|
987
|
+
name,
|
|
988
|
+
description,
|
|
989
|
+
category,
|
|
990
|
+
tags,
|
|
991
|
+
tiers: [Object.values(ACCESS_TIERS).find(t => t.id === tierId)]
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
if (result.ok) {
|
|
995
|
+
alert(`Model published! ID: ${result.modelId}`);
|
|
996
|
+
document.getElementById('mp-publish-form').reset();
|
|
997
|
+
loadMyModelsTab();
|
|
998
|
+
} else {
|
|
999
|
+
alert('Error: ' + result.error);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// Close modal
|
|
1004
|
+
document.querySelector('.mp-modal-close')?.addEventListener('click', () => {
|
|
1005
|
+
document.getElementById('marketplace-modal').classList.remove('show');
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
// Withdraw earnings
|
|
1009
|
+
document.getElementById('mp-withdraw-btn')?.addEventListener('click', () => {
|
|
1010
|
+
const amount = prompt('Amount to withdraw (tokens):');
|
|
1011
|
+
if (amount && !isNaN(amount)) {
|
|
1012
|
+
const result = withdrawEarnings(parseInt(amount), 'stripe');
|
|
1013
|
+
alert(result.message || 'Withdrawal processed');
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Load browse tab content
|
|
1020
|
+
*/
|
|
1021
|
+
function loadBrowseTab() {
|
|
1022
|
+
const category = document.getElementById('mp-category-select')?.value || '';
|
|
1023
|
+
const sort = document.getElementById('mp-sort-select')?.value || 'newest';
|
|
1024
|
+
|
|
1025
|
+
const result = browseModels(category || null, { sort, page: 0 });
|
|
1026
|
+
|
|
1027
|
+
const grid = document.getElementById('mp-browse-grid');
|
|
1028
|
+
if (grid) {
|
|
1029
|
+
grid.innerHTML = result.results
|
|
1030
|
+
.map(model => createModelCard(model))
|
|
1031
|
+
.join('');
|
|
1032
|
+
|
|
1033
|
+
grid.querySelectorAll('.mp-card').forEach(card => {
|
|
1034
|
+
card.addEventListener('click', () => {
|
|
1035
|
+
const modelId = card.dataset.modelId;
|
|
1036
|
+
showModelDetail(modelId);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Load my models tab
|
|
1044
|
+
*/
|
|
1045
|
+
function loadMyModelsTab() {
|
|
1046
|
+
const myModels = listCreatorModels();
|
|
1047
|
+
const list = document.getElementById('mp-my-models-list');
|
|
1048
|
+
|
|
1049
|
+
if (list) {
|
|
1050
|
+
list.innerHTML = myModels.length === 0
|
|
1051
|
+
? '<p class="mp-empty">No models published yet</p>'
|
|
1052
|
+
: myModels.map(m => `
|
|
1053
|
+
<div class="mp-item">
|
|
1054
|
+
<div class="mp-item-header">
|
|
1055
|
+
<strong>${m.name}</strong>
|
|
1056
|
+
<span class="mp-item-stats">${m.stats.downloads} downloads, ${m.stats.rating}/5★</span>
|
|
1057
|
+
</div>
|
|
1058
|
+
<p class="mp-item-desc">${m.description}</p>
|
|
1059
|
+
<div class="mp-item-actions">
|
|
1060
|
+
<button class="mp-button-sm" onclick="alert('Edit not yet implemented')">Edit</button>
|
|
1061
|
+
<button class="mp-button-sm" onclick="alert('Stats not yet implemented')">Analytics</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
`).join('');
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Load purchases tab
|
|
1070
|
+
*/
|
|
1071
|
+
function loadPurchasesTab() {
|
|
1072
|
+
const purchases = getPurchaseHistory();
|
|
1073
|
+
const list = document.getElementById('mp-purchases-list');
|
|
1074
|
+
|
|
1075
|
+
if (list) {
|
|
1076
|
+
list.innerHTML = purchases.length === 0
|
|
1077
|
+
? '<p class="mp-empty">No purchases yet</p>'
|
|
1078
|
+
: purchases.map(p => `
|
|
1079
|
+
<div class="mp-item">
|
|
1080
|
+
<div class="mp-item-header">
|
|
1081
|
+
<strong>${p.modelName}</strong>
|
|
1082
|
+
<span class="mp-item-stats">${p.price} tokens • ${p.tierName}</span>
|
|
1083
|
+
</div>
|
|
1084
|
+
<p class="mp-item-meta">Creator: ${p.creatorName} • ${new Date(p.purchaseDate).toLocaleDateString()}</p>
|
|
1085
|
+
<div class="mp-item-actions">
|
|
1086
|
+
<button class="mp-button-sm" onclick="downloadPurchasedModel('${p.modelId}', 'stl')">Download STL</button>
|
|
1087
|
+
<button class="mp-button-sm" onclick="downloadPurchasedModel('${p.modelId}', 'json')">Download JSON</button>
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
`).join('');
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Load earnings tab
|
|
1096
|
+
*/
|
|
1097
|
+
function loadEarningsTab() {
|
|
1098
|
+
const stats = getCreatorStats();
|
|
1099
|
+
const dashboard = document.getElementById('mp-earnings-dashboard');
|
|
1100
|
+
|
|
1101
|
+
if (dashboard && stats.ok) {
|
|
1102
|
+
dashboard.innerHTML = `
|
|
1103
|
+
<div class="mp-kpi-grid">
|
|
1104
|
+
<div class="mp-kpi">
|
|
1105
|
+
<div class="mp-kpi-value">${stats.stats.totalEarnings}</div>
|
|
1106
|
+
<div class="mp-kpi-label">Total Earnings (tokens)</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
<div class="mp-kpi">
|
|
1109
|
+
<div class="mp-kpi-value">${stats.stats.modelCount}</div>
|
|
1110
|
+
<div class="mp-kpi-label">Published Models</div>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div class="mp-kpi">
|
|
1113
|
+
<div class="mp-kpi-value">${stats.stats.totalDownloads}</div>
|
|
1114
|
+
<div class="mp-kpi-label">Total Downloads</div>
|
|
1115
|
+
</div>
|
|
1116
|
+
<div class="mp-kpi">
|
|
1117
|
+
<div class="mp-kpi-value">${stats.stats.averageRating}★</div>
|
|
1118
|
+
<div class="mp-kpi-label">Average Rating</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
`;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Create model card HTML
|
|
1127
|
+
*/
|
|
1128
|
+
function createModelCard(model) {
|
|
1129
|
+
const price = model.tiers[0]?.price || 'Free';
|
|
1130
|
+
return `
|
|
1131
|
+
<div class="mp-card" data-model-id="${model.id}">
|
|
1132
|
+
<div class="mp-card-preview">
|
|
1133
|
+
${model.previewImage ? `<img src="${model.previewImage}" alt="${model.name}">` : '<div class="mp-card-placeholder">📦</div>'}
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="mp-card-content">
|
|
1136
|
+
<h3 class="mp-card-title">${model.name}</h3>
|
|
1137
|
+
<p class="mp-card-creator">by ${model.creatorName}</p>
|
|
1138
|
+
<p class="mp-card-desc">${model.description.substring(0, 60)}...</p>
|
|
1139
|
+
<div class="mp-card-footer">
|
|
1140
|
+
<span class="mp-card-rating">${model.stats.rating.toFixed(1)}★ (${model.stats.reviewCount})</span>
|
|
1141
|
+
<span class="mp-card-price">${price} tokens</span>
|
|
1142
|
+
</div>
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
`;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Show model detail modal
|
|
1150
|
+
*/
|
|
1151
|
+
function showModelDetail(modelId) {
|
|
1152
|
+
const details = getModelDetails(modelId);
|
|
1153
|
+
if (!details.ok) {
|
|
1154
|
+
alert('Model not found');
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const { model, creator, canDownload } = details;
|
|
1159
|
+
const modal = document.getElementById('marketplace-modal');
|
|
1160
|
+
const preview = document.getElementById('mp-modal-preview');
|
|
1161
|
+
const info = document.getElementById('mp-modal-info');
|
|
1162
|
+
|
|
1163
|
+
preview.innerHTML = model.previewImage
|
|
1164
|
+
? `<img src="${model.previewImage}" style="width:100%; max-height:300px;">`
|
|
1165
|
+
: '<div style="height:300px; display:flex; align-items:center; justify-content:center;">No preview available</div>';
|
|
1166
|
+
|
|
1167
|
+
info.innerHTML = `
|
|
1168
|
+
<h2>${model.name}</h2>
|
|
1169
|
+
<p class="mp-creator-link">by ${model.creatorName}</p>
|
|
1170
|
+
<p>${model.description}</p>
|
|
1171
|
+
<div class="mp-metadata">
|
|
1172
|
+
<p><strong>Category:</strong> ${model.category}</p>
|
|
1173
|
+
<p><strong>Polycount:</strong> ${model.metadata.polyCount.toLocaleString()}</p>
|
|
1174
|
+
<p><strong>Rating:</strong> ${model.stats.rating.toFixed(1)}★ (${model.stats.reviewCount} reviews)</p>
|
|
1175
|
+
</div>
|
|
1176
|
+
<div class="mp-tiers">
|
|
1177
|
+
${model.tiers.map(tier => `
|
|
1178
|
+
<button class="mp-tier-btn" data-tier-id="${tier.id}">
|
|
1179
|
+
${tier.name} - ${tier.price || 'Custom'} tokens
|
|
1180
|
+
</button>
|
|
1181
|
+
`).join('')}
|
|
1182
|
+
</div>
|
|
1183
|
+
<div class="mp-reviews">
|
|
1184
|
+
<h4>Reviews</h4>
|
|
1185
|
+
${model.reviews.slice(0, 3).map(r => `
|
|
1186
|
+
<div class="mp-review">
|
|
1187
|
+
<strong>${r.userName}</strong> - ${r.rating}★<br/>
|
|
1188
|
+
${r.comment}
|
|
1189
|
+
</div>
|
|
1190
|
+
`).join('')}
|
|
1191
|
+
</div>
|
|
1192
|
+
`;
|
|
1193
|
+
|
|
1194
|
+
modal.classList.add('show');
|
|
1195
|
+
|
|
1196
|
+
// Wire tier buttons
|
|
1197
|
+
modal.querySelectorAll('.mp-tier-btn').forEach(btn => {
|
|
1198
|
+
btn.addEventListener('click', () => {
|
|
1199
|
+
const tierId = parseInt(btn.dataset.tierId);
|
|
1200
|
+
const result = purchaseModel(modelId, tierId);
|
|
1201
|
+
if (result.ok) {
|
|
1202
|
+
alert('Purchase successful!');
|
|
1203
|
+
modal.classList.remove('show');
|
|
1204
|
+
} else {
|
|
1205
|
+
alert('Error: ' + result.error);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Perform search
|
|
1213
|
+
*/
|
|
1214
|
+
function performSearch(query, filters) {
|
|
1215
|
+
const result = searchModels(query, filters);
|
|
1216
|
+
const grid = document.getElementById('mp-search-results');
|
|
1217
|
+
|
|
1218
|
+
if (grid) {
|
|
1219
|
+
grid.innerHTML = result.results.length === 0
|
|
1220
|
+
? '<p class="mp-empty">No results found</p>'
|
|
1221
|
+
: result.results.map(model => createModelCard(model)).join('');
|
|
1222
|
+
|
|
1223
|
+
grid.querySelectorAll('.mp-card').forEach(card => {
|
|
1224
|
+
card.addEventListener('click', () => {
|
|
1225
|
+
const modelId = card.dataset.modelId;
|
|
1226
|
+
showModelDetail(modelId);
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// ============================================================================
|
|
1233
|
+
// Helper Functions
|
|
1234
|
+
// ============================================================================
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Generate preview thumbnail from Three.js geometry
|
|
1238
|
+
*/
|
|
1239
|
+
function generatePreviewThumbnail(geometry) {
|
|
1240
|
+
// Create hidden canvas + Three.js scene
|
|
1241
|
+
const canvas = document.createElement('canvas');
|
|
1242
|
+
canvas.width = MAX_PREVIEW_SIZE;
|
|
1243
|
+
canvas.height = MAX_PREVIEW_SIZE;
|
|
1244
|
+
|
|
1245
|
+
try {
|
|
1246
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
1247
|
+
renderer.setClearColor(0x2d2d30, 1);
|
|
1248
|
+
|
|
1249
|
+
const scene = new THREE.Scene();
|
|
1250
|
+
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
|
|
1251
|
+
camera.position.set(100, 100, 100);
|
|
1252
|
+
camera.lookAt(0, 0, 0);
|
|
1253
|
+
|
|
1254
|
+
const material = new THREE.MeshPhongMaterial({ color: 0x58a6ff });
|
|
1255
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
1256
|
+
scene.add(mesh);
|
|
1257
|
+
|
|
1258
|
+
const light = new THREE.DirectionalLight(0xffffff, 1);
|
|
1259
|
+
light.position.set(100, 100, 100);
|
|
1260
|
+
scene.add(light);
|
|
1261
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
|
1262
|
+
|
|
1263
|
+
renderer.render(scene, camera);
|
|
1264
|
+
renderer.dispose();
|
|
1265
|
+
|
|
1266
|
+
return canvas.toDataURL('image/png');
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
console.warn('[Marketplace] Preview generation failed:', e);
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Serialize Three.js BufferGeometry for storage
|
|
1275
|
+
*/
|
|
1276
|
+
function serializeGeometry(geometry) {
|
|
1277
|
+
const positions = geometry.getAttribute('position');
|
|
1278
|
+
const normals = geometry.getAttribute('normal');
|
|
1279
|
+
const indices = geometry.getIndex();
|
|
1280
|
+
|
|
1281
|
+
return {
|
|
1282
|
+
positions: Array.from(positions.array),
|
|
1283
|
+
normals: normals ? Array.from(normals.array) : null,
|
|
1284
|
+
indices: indices ? Array.from(indices.array) : null
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Deserialize geometry from stored data
|
|
1290
|
+
*/
|
|
1291
|
+
function deserializeGeometry(data) {
|
|
1292
|
+
const geometry = new THREE.BufferGeometry();
|
|
1293
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(data.positions), 3));
|
|
1294
|
+
if (data.normals) {
|
|
1295
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(data.normals), 3));
|
|
1296
|
+
}
|
|
1297
|
+
if (data.indices) {
|
|
1298
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(data.indices), 1));
|
|
1299
|
+
}
|
|
1300
|
+
return geometry;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Export geometry in various formats
|
|
1305
|
+
*/
|
|
1306
|
+
function exportGeometry(geometry, format) {
|
|
1307
|
+
// Simplified export (full implementation would generate proper file formats)
|
|
1308
|
+
switch (format) {
|
|
1309
|
+
case 'json':
|
|
1310
|
+
return JSON.stringify(serializeGeometry(geometry));
|
|
1311
|
+
case 'stl':
|
|
1312
|
+
case 'obj':
|
|
1313
|
+
case 'gltf':
|
|
1314
|
+
// Would require full exporter implementation
|
|
1315
|
+
return `Exported ${format} format - full implementation pending`;
|
|
1316
|
+
default:
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Extract bounding box dimensions
|
|
1323
|
+
*/
|
|
1324
|
+
function extractDimensions(geometry) {
|
|
1325
|
+
if (!geometry) return { x: 0, y: 0, z: 0 };
|
|
1326
|
+
|
|
1327
|
+
geometry.computeBoundingBox();
|
|
1328
|
+
const bbox = geometry.boundingBox;
|
|
1329
|
+
return {
|
|
1330
|
+
x: (bbox.max.x - bbox.min.x).toFixed(2),
|
|
1331
|
+
y: (bbox.max.y - bbox.min.y).toFixed(2),
|
|
1332
|
+
z: (bbox.max.z - bbox.min.z).toFixed(2)
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Count polygons in geometry
|
|
1338
|
+
*/
|
|
1339
|
+
function countPolygons(geometry) {
|
|
1340
|
+
const indices = geometry.getIndex();
|
|
1341
|
+
return indices ? indices.count / 3 : geometry.getAttribute('position').count / 3;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Calculate earnings for a creator
|
|
1346
|
+
*/
|
|
1347
|
+
function calculateEarnings(creatorId) {
|
|
1348
|
+
const models = _allModels.filter(m => m.creatorId === creatorId);
|
|
1349
|
+
return models.reduce((sum, m) => sum + (m.stats.downloads * (m.tiers[0]?.price || 0)), 0);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Log transaction (double-entry ledger)
|
|
1354
|
+
*/
|
|
1355
|
+
function logTransaction(tx) {
|
|
1356
|
+
const transaction = {
|
|
1357
|
+
id: crypto.randomUUID(),
|
|
1358
|
+
...tx,
|
|
1359
|
+
recorded: new Date().toISOString()
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// In production: store in ledger database
|
|
1363
|
+
console.log('[Marketplace Ledger]', transaction);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Get date key for breakdown
|
|
1368
|
+
*/
|
|
1369
|
+
function getDateKey(date, period) {
|
|
1370
|
+
switch (period) {
|
|
1371
|
+
case 'daily':
|
|
1372
|
+
return date.toISOString().split('T')[0];
|
|
1373
|
+
case 'weekly':
|
|
1374
|
+
const week = Math.floor((date.getDate() - date.getDay()) / 7);
|
|
1375
|
+
return `${date.getFullYear()}-W${week}`;
|
|
1376
|
+
case 'monthly':
|
|
1377
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
1378
|
+
default:
|
|
1379
|
+
return date.getFullYear().toString();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Add days to date
|
|
1385
|
+
*/
|
|
1386
|
+
function addDays(date, days) {
|
|
1387
|
+
const result = new Date(date);
|
|
1388
|
+
result.setDate(result.getDate() + days);
|
|
1389
|
+
return result;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Add event listener
|
|
1394
|
+
*/
|
|
1395
|
+
function addEventListener(event, callback) {
|
|
1396
|
+
if (!_eventListeners[event]) {
|
|
1397
|
+
_eventListeners[event] = [];
|
|
1398
|
+
}
|
|
1399
|
+
_eventListeners[event].push(callback);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Remove event listener
|
|
1404
|
+
*/
|
|
1405
|
+
function removeEventListener(event, callback) {
|
|
1406
|
+
if (_eventListeners[event]) {
|
|
1407
|
+
_eventListeners[event] = _eventListeners[event].filter(cb => cb !== callback);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Emit event
|
|
1413
|
+
*/
|
|
1414
|
+
function emitEvent(event, data) {
|
|
1415
|
+
if (_eventListeners[event]) {
|
|
1416
|
+
_eventListeners[event].forEach(cb => cb(data));
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Add toolbar button
|
|
1422
|
+
*/
|
|
1423
|
+
function addMarketplaceToolbarButton() {
|
|
1424
|
+
const btn = document.createElement('button');
|
|
1425
|
+
btn.id = 'marketplace-btn';
|
|
1426
|
+
btn.className = 'toolbar-btn';
|
|
1427
|
+
btn.innerHTML = '🛍️ Marketplace';
|
|
1428
|
+
btn.addEventListener('click', () => {
|
|
1429
|
+
const panel = document.getElementById('marketplace-panel');
|
|
1430
|
+
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
const toolbar = document.getElementById('ce-buttons') || document.querySelector('.toolbar');
|
|
1434
|
+
if (toolbar) {
|
|
1435
|
+
toolbar.appendChild(btn);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Add marketplace styles
|
|
1441
|
+
*/
|
|
1442
|
+
function addMarketplaceStyles() {
|
|
1443
|
+
const styles = `
|
|
1444
|
+
#marketplace-panel {
|
|
1445
|
+
position: fixed;
|
|
1446
|
+
right: 20px;
|
|
1447
|
+
top: 100px;
|
|
1448
|
+
width: 600px;
|
|
1449
|
+
height: 700px;
|
|
1450
|
+
background: var(--bg-secondary);
|
|
1451
|
+
border: 1px solid var(--border-color);
|
|
1452
|
+
border-radius: 8px;
|
|
1453
|
+
box-shadow: var(--shadow-lg);
|
|
1454
|
+
display: flex;
|
|
1455
|
+
flex-direction: column;
|
|
1456
|
+
z-index: 1000;
|
|
1457
|
+
font-size: 13px;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.mp-header {
|
|
1461
|
+
padding: 16px;
|
|
1462
|
+
border-bottom: 1px solid var(--border-color);
|
|
1463
|
+
display: flex;
|
|
1464
|
+
justify-content: space-between;
|
|
1465
|
+
align-items: center;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.mp-header h2 {
|
|
1469
|
+
margin: 0;
|
|
1470
|
+
font-size: 16px;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.mp-close-btn {
|
|
1474
|
+
background: none;
|
|
1475
|
+
border: none;
|
|
1476
|
+
color: var(--text-secondary);
|
|
1477
|
+
cursor: pointer;
|
|
1478
|
+
font-size: 18px;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.mp-tabs {
|
|
1482
|
+
display: flex;
|
|
1483
|
+
border-bottom: 1px solid var(--border-color);
|
|
1484
|
+
background: var(--bg-tertiary);
|
|
1485
|
+
overflow-x: auto;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
.mp-tab-btn {
|
|
1489
|
+
flex: 1;
|
|
1490
|
+
padding: 8px 12px;
|
|
1491
|
+
border: none;
|
|
1492
|
+
background: none;
|
|
1493
|
+
color: var(--text-secondary);
|
|
1494
|
+
cursor: pointer;
|
|
1495
|
+
font-size: 12px;
|
|
1496
|
+
white-space: nowrap;
|
|
1497
|
+
border-bottom: 2px solid transparent;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.mp-tab-btn.active {
|
|
1501
|
+
color: var(--accent-blue);
|
|
1502
|
+
border-bottom-color: var(--accent-blue);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.mp-tab-content {
|
|
1506
|
+
flex: 1;
|
|
1507
|
+
overflow-y: auto;
|
|
1508
|
+
padding: 16px;
|
|
1509
|
+
display: none;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
.mp-tab-content.active {
|
|
1513
|
+
display: block;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.mp-grid {
|
|
1517
|
+
display: grid;
|
|
1518
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1519
|
+
gap: 12px;
|
|
1520
|
+
margin-bottom: 12px;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
.mp-card {
|
|
1524
|
+
background: var(--bg-tertiary);
|
|
1525
|
+
border: 1px solid var(--border-color);
|
|
1526
|
+
border-radius: 6px;
|
|
1527
|
+
overflow: hidden;
|
|
1528
|
+
cursor: pointer;
|
|
1529
|
+
transition: all var(--transition-base);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.mp-card:hover {
|
|
1533
|
+
border-color: var(--accent-blue);
|
|
1534
|
+
transform: translateY(-2px);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.mp-card-preview {
|
|
1538
|
+
width: 100%;
|
|
1539
|
+
height: 120px;
|
|
1540
|
+
background: var(--bg-primary);
|
|
1541
|
+
display: flex;
|
|
1542
|
+
align-items: center;
|
|
1543
|
+
justify-content: center;
|
|
1544
|
+
overflow: hidden;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.mp-card-preview img {
|
|
1548
|
+
width: 100%;
|
|
1549
|
+
height: 100%;
|
|
1550
|
+
object-fit: cover;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
.mp-card-placeholder {
|
|
1554
|
+
font-size: 48px;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.mp-card-content {
|
|
1558
|
+
padding: 8px;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
.mp-card-title {
|
|
1562
|
+
font-size: 12px;
|
|
1563
|
+
font-weight: bold;
|
|
1564
|
+
margin: 0 0 4px 0;
|
|
1565
|
+
white-space: nowrap;
|
|
1566
|
+
overflow: hidden;
|
|
1567
|
+
text-overflow: ellipsis;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
.mp-card-creator {
|
|
1571
|
+
font-size: 11px;
|
|
1572
|
+
color: var(--text-secondary);
|
|
1573
|
+
margin: 0 0 4px 0;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
.mp-card-desc {
|
|
1577
|
+
font-size: 11px;
|
|
1578
|
+
color: var(--text-muted);
|
|
1579
|
+
margin: 0 0 6px 0;
|
|
1580
|
+
line-height: 1.3;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
.mp-card-footer {
|
|
1584
|
+
display: flex;
|
|
1585
|
+
justify-content: space-between;
|
|
1586
|
+
font-size: 11px;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.mp-card-rating {
|
|
1590
|
+
color: var(--accent-yellow);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.mp-card-price {
|
|
1594
|
+
color: var(--accent-green);
|
|
1595
|
+
font-weight: bold;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
.mp-form {
|
|
1599
|
+
display: flex;
|
|
1600
|
+
flex-direction: column;
|
|
1601
|
+
gap: 12px;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
.mp-form-group {
|
|
1605
|
+
display: flex;
|
|
1606
|
+
flex-direction: column;
|
|
1607
|
+
gap: 4px;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.mp-label {
|
|
1611
|
+
font-size: 12px;
|
|
1612
|
+
font-weight: 500;
|
|
1613
|
+
color: var(--text-primary);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
.mp-input,
|
|
1617
|
+
.mp-select,
|
|
1618
|
+
.mp-textarea {
|
|
1619
|
+
padding: 6px 8px;
|
|
1620
|
+
background: var(--bg-tertiary);
|
|
1621
|
+
border: 1px solid var(--border-color);
|
|
1622
|
+
color: var(--text-primary);
|
|
1623
|
+
border-radius: 4px;
|
|
1624
|
+
font-size: 12px;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
.mp-input:focus,
|
|
1628
|
+
.mp-select:focus,
|
|
1629
|
+
.mp-textarea:focus {
|
|
1630
|
+
outline: none;
|
|
1631
|
+
border-color: var(--accent-blue);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
.mp-textarea {
|
|
1635
|
+
resize: vertical;
|
|
1636
|
+
min-height: 80px;
|
|
1637
|
+
font-family: monospace;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.mp-input-small {
|
|
1641
|
+
width: 80px;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.mp-button {
|
|
1645
|
+
padding: 8px 12px;
|
|
1646
|
+
background: var(--bg-tertiary);
|
|
1647
|
+
border: 1px solid var(--border-color);
|
|
1648
|
+
color: var(--text-primary);
|
|
1649
|
+
border-radius: 4px;
|
|
1650
|
+
cursor: pointer;
|
|
1651
|
+
font-size: 12px;
|
|
1652
|
+
transition: all var(--transition-fast);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
.mp-button:hover {
|
|
1656
|
+
background: var(--accent-blue-dark);
|
|
1657
|
+
border-color: var(--accent-blue);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.mp-button-primary {
|
|
1661
|
+
background: var(--accent-blue);
|
|
1662
|
+
color: white;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.mp-button-primary:hover {
|
|
1666
|
+
background: #4da3ff;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
.mp-button-sm {
|
|
1670
|
+
padding: 4px 8px;
|
|
1671
|
+
font-size: 11px;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
.mp-list {
|
|
1675
|
+
display: flex;
|
|
1676
|
+
flex-direction: column;
|
|
1677
|
+
gap: 12px;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
.mp-item {
|
|
1681
|
+
background: var(--bg-tertiary);
|
|
1682
|
+
border: 1px solid var(--border-color);
|
|
1683
|
+
border-radius: 4px;
|
|
1684
|
+
padding: 12px;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
.mp-item-header {
|
|
1688
|
+
display: flex;
|
|
1689
|
+
justify-content: space-between;
|
|
1690
|
+
margin-bottom: 8px;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.mp-item-stats {
|
|
1694
|
+
font-size: 11px;
|
|
1695
|
+
color: var(--text-secondary);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.mp-item-desc {
|
|
1699
|
+
font-size: 12px;
|
|
1700
|
+
color: var(--text-muted);
|
|
1701
|
+
margin: 0 0 8px 0;
|
|
1702
|
+
line-height: 1.4;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.mp-item-meta {
|
|
1706
|
+
font-size: 11px;
|
|
1707
|
+
color: var(--text-secondary);
|
|
1708
|
+
margin: 0 0 8px 0;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
.mp-item-actions {
|
|
1712
|
+
display: flex;
|
|
1713
|
+
gap: 8px;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
.mp-empty {
|
|
1717
|
+
text-align: center;
|
|
1718
|
+
color: var(--text-secondary);
|
|
1719
|
+
padding: 40px 20px;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
.mp-kpi-grid {
|
|
1723
|
+
display: grid;
|
|
1724
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1725
|
+
gap: 12px;
|
|
1726
|
+
margin-bottom: 20px;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.mp-kpi {
|
|
1730
|
+
background: var(--bg-tertiary);
|
|
1731
|
+
border: 1px solid var(--border-color);
|
|
1732
|
+
border-radius: 4px;
|
|
1733
|
+
padding: 12px;
|
|
1734
|
+
text-align: center;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
.mp-kpi-value {
|
|
1738
|
+
font-size: 24px;
|
|
1739
|
+
font-weight: bold;
|
|
1740
|
+
color: var(--accent-blue);
|
|
1741
|
+
margin-bottom: 4px;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.mp-kpi-label {
|
|
1745
|
+
font-size: 11px;
|
|
1746
|
+
color: var(--text-secondary);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.mp-modal {
|
|
1750
|
+
display: none;
|
|
1751
|
+
position: fixed;
|
|
1752
|
+
top: 0;
|
|
1753
|
+
left: 0;
|
|
1754
|
+
width: 100%;
|
|
1755
|
+
height: 100%;
|
|
1756
|
+
background: rgba(0, 0, 0, 0.7);
|
|
1757
|
+
z-index: 2000;
|
|
1758
|
+
align-items: center;
|
|
1759
|
+
justify-content: center;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.mp-modal.show {
|
|
1763
|
+
display: flex;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
.mp-modal-content {
|
|
1767
|
+
background: var(--bg-secondary);
|
|
1768
|
+
border: 1px solid var(--border-color);
|
|
1769
|
+
border-radius: 8px;
|
|
1770
|
+
width: 90%;
|
|
1771
|
+
max-width: 700px;
|
|
1772
|
+
max-height: 80vh;
|
|
1773
|
+
overflow-y: auto;
|
|
1774
|
+
position: relative;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.mp-modal-close {
|
|
1778
|
+
position: absolute;
|
|
1779
|
+
top: 12px;
|
|
1780
|
+
right: 12px;
|
|
1781
|
+
background: none;
|
|
1782
|
+
border: none;
|
|
1783
|
+
color: var(--text-secondary);
|
|
1784
|
+
font-size: 20px;
|
|
1785
|
+
cursor: pointer;
|
|
1786
|
+
z-index: 1;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
.mp-modal-body {
|
|
1790
|
+
padding: 20px;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
.mp-preview {
|
|
1794
|
+
margin-bottom: 16px;
|
|
1795
|
+
border-radius: 4px;
|
|
1796
|
+
overflow: hidden;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
.mp-info h2 {
|
|
1800
|
+
margin: 0 0 8px 0;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
.mp-creator-link {
|
|
1804
|
+
color: var(--accent-blue);
|
|
1805
|
+
font-size: 12px;
|
|
1806
|
+
margin: 0 0 12px 0;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
.mp-metadata {
|
|
1810
|
+
background: var(--bg-tertiary);
|
|
1811
|
+
border: 1px solid var(--border-color);
|
|
1812
|
+
border-radius: 4px;
|
|
1813
|
+
padding: 12px;
|
|
1814
|
+
margin: 12px 0;
|
|
1815
|
+
font-size: 12px;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
.mp-metadata p {
|
|
1819
|
+
margin: 6px 0;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
.mp-tiers {
|
|
1823
|
+
display: flex;
|
|
1824
|
+
flex-direction: column;
|
|
1825
|
+
gap: 8px;
|
|
1826
|
+
margin: 12px 0;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
.mp-tier-btn {
|
|
1830
|
+
padding: 8px;
|
|
1831
|
+
background: var(--accent-blue-dark);
|
|
1832
|
+
color: white;
|
|
1833
|
+
border: 1px solid var(--accent-blue);
|
|
1834
|
+
border-radius: 4px;
|
|
1835
|
+
cursor: pointer;
|
|
1836
|
+
font-size: 12px;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
.mp-tier-btn:hover {
|
|
1840
|
+
background: var(--accent-blue);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
.mp-reviews {
|
|
1844
|
+
margin-top: 16px;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.mp-reviews h4 {
|
|
1848
|
+
margin: 0 0 8px 0;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
.mp-review {
|
|
1852
|
+
background: var(--bg-tertiary);
|
|
1853
|
+
border-left: 3px solid var(--accent-blue);
|
|
1854
|
+
padding: 8px;
|
|
1855
|
+
margin-bottom: 8px;
|
|
1856
|
+
border-radius: 2px;
|
|
1857
|
+
font-size: 12px;
|
|
1858
|
+
line-height: 1.4;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
.mp-category-filter {
|
|
1862
|
+
display: flex;
|
|
1863
|
+
gap: 8px;
|
|
1864
|
+
margin-bottom: 12px;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
.mp-filters {
|
|
1868
|
+
display: flex;
|
|
1869
|
+
flex-direction: column;
|
|
1870
|
+
gap: 8px;
|
|
1871
|
+
margin-bottom: 12px;
|
|
1872
|
+
padding: 12px;
|
|
1873
|
+
background: var(--bg-tertiary);
|
|
1874
|
+
border-radius: 4px;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
.mp-filters .mp-label {
|
|
1878
|
+
display: flex;
|
|
1879
|
+
gap: 8px;
|
|
1880
|
+
align-items: center;
|
|
1881
|
+
font-size: 12px;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
@media (max-width: 1200px) {
|
|
1885
|
+
#marketplace-panel {
|
|
1886
|
+
width: 500px;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
@media (max-width: 768px) {
|
|
1891
|
+
#marketplace-panel {
|
|
1892
|
+
width: calc(100% - 40px);
|
|
1893
|
+
height: calc(100% - 120px);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
.mp-grid {
|
|
1897
|
+
grid-template-columns: 1fr;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
`;
|
|
1901
|
+
|
|
1902
|
+
const styleEl = document.createElement('style');
|
|
1903
|
+
styleEl.textContent = styles;
|
|
1904
|
+
document.head.appendChild(styleEl);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// ============================================================================
|
|
1908
|
+
// Data Persistence
|
|
1909
|
+
// ============================================================================
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* Save marketplace data to localStorage
|
|
1913
|
+
*/
|
|
1914
|
+
function saveMarketplaceData() {
|
|
1915
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
1916
|
+
models: _allModels,
|
|
1917
|
+
purchases: _purchaseHistory,
|
|
1918
|
+
createdModels: _createdModels,
|
|
1919
|
+
lastSaved: new Date().toISOString()
|
|
1920
|
+
}));
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Load marketplace data from localStorage
|
|
1925
|
+
*/
|
|
1926
|
+
function loadMarketplaceData() {
|
|
1927
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
1928
|
+
|
|
1929
|
+
if (stored) {
|
|
1930
|
+
try {
|
|
1931
|
+
const data = JSON.parse(stored);
|
|
1932
|
+
_allModels = data.models || [];
|
|
1933
|
+
_purchaseHistory = data.purchases || [];
|
|
1934
|
+
_createdModels = data.createdModels || [];
|
|
1935
|
+
} catch (e) {
|
|
1936
|
+
console.warn('[Marketplace] Failed to load data:', e);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Populate demo data if empty
|
|
1941
|
+
if (_allModels.length === 0) {
|
|
1942
|
+
populateDemoData();
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* Populate demo models
|
|
1948
|
+
*/
|
|
1949
|
+
function populateDemoData() {
|
|
1950
|
+
const demoModels = [
|
|
1951
|
+
{ name: 'M8 Hex Bolt', category: 'Fastener', desc: 'Standard M8 stainless steel hex bolt' },
|
|
1952
|
+
{ name: 'Bearing Housing', category: 'Mechanical', desc: 'Cast aluminum bearing housing with bore' },
|
|
1953
|
+
{ name: 'L-Bracket 80mm', category: 'Structural', desc: 'Welded steel L-bracket for structural support' },
|
|
1954
|
+
{ name: 'IP65 Box 120x80x40', category: 'Enclosure', desc: 'Weather-sealed enclosure for electronics' },
|
|
1955
|
+
{ name: 'Parametric Bracket', category: 'Template', desc: 'Configurable bracket with variable dimensions' },
|
|
1956
|
+
{ name: 'Shaft Coupler', category: 'Mechanical', desc: 'Flexible coupling for motor shaft' },
|
|
1957
|
+
{ name: 'DIN Rail Enclosure', category: 'Enclosure', desc: 'Standard DIN 46277 mounting cabinet' },
|
|
1958
|
+
{ name: 'cycleWASH Brush Holder', category: 'Custom', desc: 'Brush assembly holder for washing machine' }
|
|
1959
|
+
];
|
|
1960
|
+
|
|
1961
|
+
demoModels.forEach((m, i) => {
|
|
1962
|
+
const model = {
|
|
1963
|
+
id: crypto.randomUUID(),
|
|
1964
|
+
creatorId: 'demo_creator',
|
|
1965
|
+
creatorName: 'Demo Creator',
|
|
1966
|
+
name: m.name,
|
|
1967
|
+
description: m.desc,
|
|
1968
|
+
category: m.category,
|
|
1969
|
+
tags: [m.category.toLowerCase()],
|
|
1970
|
+
tiers: [ACCESS_TIERS.MESH_DOWNLOAD],
|
|
1971
|
+
previewImage: null,
|
|
1972
|
+
sourceGeometry: null,
|
|
1973
|
+
parametricData: null,
|
|
1974
|
+
metadata: { dimensions: { x: 50, y: 50, z: 50 }, polyCount: 1000 },
|
|
1975
|
+
stats: {
|
|
1976
|
+
views: Math.floor(Math.random() * 500),
|
|
1977
|
+
downloads: Math.floor(Math.random() * 100),
|
|
1978
|
+
purchases: Math.floor(Math.random() * 50),
|
|
1979
|
+
rating: (Math.random() * 2 + 3).toFixed(1),
|
|
1980
|
+
reviewCount: Math.floor(Math.random() * 20)
|
|
1981
|
+
},
|
|
1982
|
+
reviews: [],
|
|
1983
|
+
publishedDate: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
1984
|
+
updatedDate: new Date().toISOString(),
|
|
1985
|
+
derivedFromModelId: null,
|
|
1986
|
+
derivativeLicense: false
|
|
1987
|
+
};
|
|
1988
|
+
_allModels.push(model);
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
saveMarketplaceData();
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
export { initMarketplace };
|