cyclecad 2.0.1 → 3.0.0
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/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* data-module.js — ENHANCED with Fusion 360 parity data management
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive data persistence, project management, and file organization
|
|
5
|
+
* for cycleCAD. Supports IndexedDB, OPFS, cloud sync, templates, sharing,
|
|
6
|
+
* auto-recovery, and full metadata tracking.
|
|
7
|
+
*
|
|
8
|
+
* FEATURES:
|
|
9
|
+
* - Project Management: Create, open, delete, list projects with metadata
|
|
10
|
+
* - File Persistence: Save to IndexedDB, OPFS (Origin Private File System), or download
|
|
11
|
+
* - Cloud Sync: Ready for future cloud storage integration (save/load JSON blobs)
|
|
12
|
+
* - Share Links: Generate view-only or edit-enabled shareable URLs
|
|
13
|
+
* - Templates: 10+ built-in templates, save custom templates
|
|
14
|
+
* - Recent Files: Track last 20 opened files with thumbnails
|
|
15
|
+
* - Auto-Recovery: Detect crashes, offer to restore auto-saved versions
|
|
16
|
+
* - File Info: Creation date, modified date, size, part count, author, description
|
|
17
|
+
* - Trash/Recycle: Soft-delete with 30-day recovery window
|
|
18
|
+
* - Duplicate: Clone projects with new names
|
|
19
|
+
* - Import/Export: Load/save complete projects as ZIP or JSON
|
|
20
|
+
* - File Browser: Grid/list view with search, sort, folders
|
|
21
|
+
* - Thumbnails: Render 3D previews for project browser
|
|
22
|
+
* - Units Management: mm, cm, m, inch, ft with automatic conversion
|
|
23
|
+
* - Document Properties: Title, author, description, custom properties, revision
|
|
24
|
+
* - Project Settings: Units, materials, render settings
|
|
25
|
+
* - Backup & Archive: Manual backups with timestamp
|
|
26
|
+
*
|
|
27
|
+
* @version 2.0.0
|
|
28
|
+
* @author Sachin Kumar <vvlars@googlemail.com>
|
|
29
|
+
* @license MIT
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// MODULE STATE & CONFIGURATION
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
name: 'data',
|
|
38
|
+
version: '2.0.0',
|
|
39
|
+
|
|
40
|
+
state: {
|
|
41
|
+
db: null,
|
|
42
|
+
projects: [],
|
|
43
|
+
currentProjectId: null,
|
|
44
|
+
currentProject: null,
|
|
45
|
+
autoSaveIntervalMs: 30000,
|
|
46
|
+
autoSaveHandle: null,
|
|
47
|
+
recentFiles: [],
|
|
48
|
+
templates: new Map(),
|
|
49
|
+
trash: [],
|
|
50
|
+
quotaBytes: 1000000000,
|
|
51
|
+
usageBytes: 0,
|
|
52
|
+
// NEW: Enhanced features
|
|
53
|
+
searchIndex: new Map(),
|
|
54
|
+
projectThumbnails: new Map(),
|
|
55
|
+
backupHistory: [],
|
|
56
|
+
cloudSyncEnabled: false,
|
|
57
|
+
cloudSyncUrl: null,
|
|
58
|
+
unitPreferences: {
|
|
59
|
+
current: 'mm',
|
|
60
|
+
options: ['mm', 'cm', 'm', 'inch', 'in', 'ft']
|
|
61
|
+
},
|
|
62
|
+
documentProperties: {
|
|
63
|
+
author: localStorage.getItem('data_userName') || 'Unknown',
|
|
64
|
+
company: localStorage.getItem('data_company') || '',
|
|
65
|
+
version: '1.0'
|
|
66
|
+
},
|
|
67
|
+
maxBackups: 10,
|
|
68
|
+
autoSaveFrequency: 30000
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// ========================================================================
|
|
72
|
+
// INITIALIZATION
|
|
73
|
+
// ========================================================================
|
|
74
|
+
|
|
75
|
+
async init() {
|
|
76
|
+
this.state.db = await this._openDatabase();
|
|
77
|
+
await this._loadProjectList();
|
|
78
|
+
this._loadRecent();
|
|
79
|
+
await this._loadTemplates();
|
|
80
|
+
await this._loadTrash();
|
|
81
|
+
|
|
82
|
+
const lastProjectId = localStorage.getItem('data_lastProject');
|
|
83
|
+
if (lastProjectId) {
|
|
84
|
+
try {
|
|
85
|
+
await this.load({ projectId: lastProjectId });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn('[Data] Failed to restore last project:', err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this._startAutoSave();
|
|
92
|
+
this._setupStorageQuotaMonitoring();
|
|
93
|
+
this._detectAndRestoreCrash();
|
|
94
|
+
|
|
95
|
+
console.log('[Data Management] Initialized v2.0.0');
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// ========================================================================
|
|
99
|
+
// PROJECT MANAGEMENT (Enhanced)
|
|
100
|
+
// ========================================================================
|
|
101
|
+
|
|
102
|
+
async newProject(options = {}) {
|
|
103
|
+
const {
|
|
104
|
+
name,
|
|
105
|
+
description = '',
|
|
106
|
+
units = 'mm',
|
|
107
|
+
material = null,
|
|
108
|
+
templateName = null
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
if (!name || name.trim().length === 0) {
|
|
112
|
+
throw new Error('Project name required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let project = {
|
|
116
|
+
id: this._generateUUID(),
|
|
117
|
+
name: name.trim(),
|
|
118
|
+
description,
|
|
119
|
+
units,
|
|
120
|
+
material,
|
|
121
|
+
created: Date.now(),
|
|
122
|
+
modified: Date.now(),
|
|
123
|
+
geometry: null,
|
|
124
|
+
files: {},
|
|
125
|
+
folders: {},
|
|
126
|
+
metadata: {
|
|
127
|
+
author: this.state.documentProperties.author,
|
|
128
|
+
company: this.state.documentProperties.company,
|
|
129
|
+
version: this.state.documentProperties.version,
|
|
130
|
+
tags: [],
|
|
131
|
+
keywords: [],
|
|
132
|
+
revision: 1
|
|
133
|
+
},
|
|
134
|
+
thumbnail: null,
|
|
135
|
+
backup: null,
|
|
136
|
+
settings: {
|
|
137
|
+
renderQuality: 'high',
|
|
138
|
+
shadowsEnabled: true,
|
|
139
|
+
gridVisible: true,
|
|
140
|
+
unitSystem: units
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Apply template if specified
|
|
145
|
+
if (templateName) {
|
|
146
|
+
const template = this.state.templates.get(templateName);
|
|
147
|
+
if (template) {
|
|
148
|
+
project = {...project, ...template.data};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await this._saveProjectToDB(project);
|
|
153
|
+
this.state.projects.unshift(project);
|
|
154
|
+
|
|
155
|
+
this._addToRecent({
|
|
156
|
+
projectId: project.id,
|
|
157
|
+
name: project.name,
|
|
158
|
+
timestamp: Date.now()
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this._updateSearchIndex(project.id, project);
|
|
162
|
+
this._showNotification(`Created project: ${name}`, 'success');
|
|
163
|
+
this._broadcastEvent('data:projectCreated', project);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
id: project.id,
|
|
167
|
+
name: project.name,
|
|
168
|
+
description,
|
|
169
|
+
created: project.created,
|
|
170
|
+
units: project.units
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async load(options = {}) {
|
|
175
|
+
const { projectId } = options;
|
|
176
|
+
|
|
177
|
+
if (!projectId) {
|
|
178
|
+
throw new Error('Project ID required');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const project = await this._getProjectFromDB(projectId);
|
|
182
|
+
if (!project) {
|
|
183
|
+
throw new Error(`Project ${projectId} not found`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.state.currentProjectId = projectId;
|
|
187
|
+
this.state.currentProject = project;
|
|
188
|
+
localStorage.setItem('data_lastProject', projectId);
|
|
189
|
+
|
|
190
|
+
this._addToRecent({
|
|
191
|
+
projectId: project.id,
|
|
192
|
+
name: project.name,
|
|
193
|
+
timestamp: Date.now()
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this._broadcastEvent('data:projectLoaded', project);
|
|
197
|
+
|
|
198
|
+
return project;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async save(options = {}) {
|
|
202
|
+
const { projectId = this.state.currentProjectId } = options;
|
|
203
|
+
|
|
204
|
+
if (!projectId) {
|
|
205
|
+
throw new Error('No project loaded');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Capture current 3D model state
|
|
209
|
+
const geometry = await this._captureGeometry();
|
|
210
|
+
|
|
211
|
+
const project = await this._getProjectFromDB(projectId);
|
|
212
|
+
if (!project) {
|
|
213
|
+
throw new Error('Project not found');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
project.modified = Date.now();
|
|
217
|
+
project.geometry = geometry;
|
|
218
|
+
project.metadata.revision = (project.metadata.revision || 0) + 1;
|
|
219
|
+
|
|
220
|
+
// Create backup before saving
|
|
221
|
+
if (project.backup === null || (Date.now() - project.lastBackupTime > 300000)) {
|
|
222
|
+
project.backup = JSON.parse(JSON.stringify(project));
|
|
223
|
+
project.lastBackupTime = Date.now();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await this._saveProjectToDB(project);
|
|
227
|
+
this.state.currentProject = project;
|
|
228
|
+
|
|
229
|
+
this._showNotification('Project saved', 'success');
|
|
230
|
+
this._broadcastEvent('data:projectSaved', { projectId });
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async delete(options = {}) {
|
|
234
|
+
const { projectId, permanent = false } = options;
|
|
235
|
+
|
|
236
|
+
const project = await this._getProjectFromDB(projectId);
|
|
237
|
+
if (!project) {
|
|
238
|
+
throw new Error('Project not found');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (permanent) {
|
|
242
|
+
await this._deleteFromDB('projects', projectId);
|
|
243
|
+
} else {
|
|
244
|
+
// Move to trash with recovery metadata
|
|
245
|
+
const trashItem = {
|
|
246
|
+
...project,
|
|
247
|
+
deletedAt: Date.now(),
|
|
248
|
+
recoveryUntil: Date.now() + 30 * 24 * 60 * 60 * 1000
|
|
249
|
+
};
|
|
250
|
+
await this._saveToTrash(trashItem);
|
|
251
|
+
await this._deleteFromDB('projects', projectId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.state.projects = this.state.projects.filter(p => p.id !== projectId);
|
|
255
|
+
|
|
256
|
+
if (this.state.currentProjectId === projectId) {
|
|
257
|
+
this.state.currentProjectId = null;
|
|
258
|
+
this.state.currentProject = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this._showNotification('Project deleted', 'info');
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
async duplicate(options = {}) {
|
|
265
|
+
const { projectId = this.state.currentProjectId, newName } = options;
|
|
266
|
+
|
|
267
|
+
const original = await this._getProjectFromDB(projectId);
|
|
268
|
+
if (!original) {
|
|
269
|
+
throw new Error('Project not found');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const cloned = JSON.parse(JSON.stringify(original));
|
|
273
|
+
cloned.id = this._generateUUID();
|
|
274
|
+
cloned.name = newName || `${original.name} (copy)`;
|
|
275
|
+
cloned.created = Date.now();
|
|
276
|
+
cloned.modified = Date.now();
|
|
277
|
+
cloned.metadata.revision = 1;
|
|
278
|
+
|
|
279
|
+
await this._saveProjectToDB(cloned);
|
|
280
|
+
this.state.projects.unshift(cloned);
|
|
281
|
+
|
|
282
|
+
this._showNotification(`Duplicated: ${cloned.name}`, 'success');
|
|
283
|
+
|
|
284
|
+
return cloned;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async listProjects(options = {}) {
|
|
288
|
+
const { limit = 100, offset = 0, sortBy = 'modified', search = null } = options;
|
|
289
|
+
|
|
290
|
+
let projects = [...this.state.projects];
|
|
291
|
+
|
|
292
|
+
// Search filter
|
|
293
|
+
if (search) {
|
|
294
|
+
const query = search.toLowerCase();
|
|
295
|
+
projects = projects.filter(p =>
|
|
296
|
+
p.name.toLowerCase().includes(query) ||
|
|
297
|
+
p.description.toLowerCase().includes(query) ||
|
|
298
|
+
p.metadata.keywords.some(k => k.toLowerCase().includes(query))
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Sort
|
|
303
|
+
if (sortBy === 'name') {
|
|
304
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
305
|
+
} else if (sortBy === 'created') {
|
|
306
|
+
projects.sort((a, b) => b.created - a.created);
|
|
307
|
+
} else if (sortBy === 'modified') {
|
|
308
|
+
projects.sort((a, b) => b.modified - a.modified);
|
|
309
|
+
} else if (sortBy === 'size') {
|
|
310
|
+
projects.sort((a, b) => this._estimateSize(b) - this._estimateSize(a));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return projects.slice(offset, offset + limit).map(p => ({
|
|
314
|
+
id: p.id,
|
|
315
|
+
name: p.name,
|
|
316
|
+
description: p.description,
|
|
317
|
+
created: p.created,
|
|
318
|
+
modified: p.modified,
|
|
319
|
+
fileCount: Object.keys(p.files).length,
|
|
320
|
+
sizeBytes: this._estimateSize(p),
|
|
321
|
+
thumbnail: this.state.projectThumbnails.get(p.id) || null,
|
|
322
|
+
author: p.metadata.author,
|
|
323
|
+
version: p.metadata.version
|
|
324
|
+
}));
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
async getRecent(options = {}) {
|
|
328
|
+
const { limit = 20 } = options;
|
|
329
|
+
return this.state.recentFiles.slice(0, limit);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// ========================================================================
|
|
333
|
+
// FILE MANAGEMENT (Enhanced with folders, tags)
|
|
334
|
+
// ========================================================================
|
|
335
|
+
|
|
336
|
+
async createFolder(options = {}) {
|
|
337
|
+
const { path = '', name } = options;
|
|
338
|
+
|
|
339
|
+
if (!this.state.currentProject) {
|
|
340
|
+
throw new Error('No project loaded');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!name || name.trim().length === 0) {
|
|
344
|
+
throw new Error('Folder name required');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const folderPath = (path ? path + '/' : '') + name + '/';
|
|
348
|
+
|
|
349
|
+
if (!this.state.currentProject.folders) {
|
|
350
|
+
this.state.currentProject.folders = {};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.state.currentProject.folders[folderPath] = {
|
|
354
|
+
files: [],
|
|
355
|
+
subfolders: [],
|
|
356
|
+
description: '',
|
|
357
|
+
created: Date.now()
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
await this.save();
|
|
361
|
+
|
|
362
|
+
return { path: folderPath, name };
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
async importFile(options = {}) {
|
|
366
|
+
const { file, folder = '', name = null, tags = [] } = options;
|
|
367
|
+
|
|
368
|
+
if (!this.state.currentProject) {
|
|
369
|
+
throw new Error('No project loaded');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!file) {
|
|
373
|
+
throw new Error('File required');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const fileName = name || file.name;
|
|
377
|
+
const fileKey = (folder ? folder : '') + fileName;
|
|
378
|
+
|
|
379
|
+
// Read file content
|
|
380
|
+
const content = await file.arrayBuffer();
|
|
381
|
+
|
|
382
|
+
this.state.currentProject.files[fileKey] = {
|
|
383
|
+
name: fileName,
|
|
384
|
+
type: file.type || this._detectFileType(fileName),
|
|
385
|
+
size: file.size,
|
|
386
|
+
imported: Date.now(),
|
|
387
|
+
content: new Uint8Array(content),
|
|
388
|
+
tags: tags,
|
|
389
|
+
hash: await this._computeFileHash(content)
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
await this.save();
|
|
393
|
+
|
|
394
|
+
this._addToRecent({
|
|
395
|
+
projectId: this.state.currentProjectId,
|
|
396
|
+
name: fileName,
|
|
397
|
+
timestamp: Date.now()
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
this._showNotification(`Imported: ${fileName}`, 'success');
|
|
401
|
+
|
|
402
|
+
return { fileName, fileKey, size: file.size };
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
async deleteFile(options = {}) {
|
|
406
|
+
const { fileKey } = options;
|
|
407
|
+
|
|
408
|
+
if (!this.state.currentProject) {
|
|
409
|
+
throw new Error('No project loaded');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (this.state.currentProject.files[fileKey]) {
|
|
413
|
+
delete this.state.currentProject.files[fileKey];
|
|
414
|
+
await this.save();
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
async listFiles(options = {}) {
|
|
419
|
+
const { folder = null } = options;
|
|
420
|
+
|
|
421
|
+
if (!this.state.currentProject) {
|
|
422
|
+
throw new Error('No project loaded');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let files = Object.entries(this.state.currentProject.files);
|
|
426
|
+
|
|
427
|
+
// Filter by folder if specified
|
|
428
|
+
if (folder) {
|
|
429
|
+
files = files.filter(([key]) => key.startsWith(folder));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return files.map(([key, file]) => ({
|
|
433
|
+
key,
|
|
434
|
+
name: file.name,
|
|
435
|
+
type: file.type,
|
|
436
|
+
size: file.size,
|
|
437
|
+
imported: file.imported,
|
|
438
|
+
tags: file.tags || []
|
|
439
|
+
}));
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
// ========================================================================
|
|
443
|
+
// SHARING & EXPORT
|
|
444
|
+
// ========================================================================
|
|
445
|
+
|
|
446
|
+
async shareLink(options = {}) {
|
|
447
|
+
const {
|
|
448
|
+
projectId = this.state.currentProjectId,
|
|
449
|
+
role = 'viewer',
|
|
450
|
+
expiresIn = null
|
|
451
|
+
} = options;
|
|
452
|
+
|
|
453
|
+
if (!projectId) {
|
|
454
|
+
throw new Error('No project specified');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const shareCode = this._generateShareCode();
|
|
458
|
+
const shareRecord = {
|
|
459
|
+
code: shareCode,
|
|
460
|
+
projectId,
|
|
461
|
+
role,
|
|
462
|
+
createdAt: Date.now(),
|
|
463
|
+
expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
await this._saveShareLink(shareRecord);
|
|
467
|
+
|
|
468
|
+
const baseUrl = window.location.origin;
|
|
469
|
+
const link = `${baseUrl}/view/${shareCode}?role=${role}`;
|
|
470
|
+
|
|
471
|
+
this._showNotification('Share link created', 'success');
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
link,
|
|
475
|
+
code: shareCode,
|
|
476
|
+
role,
|
|
477
|
+
expiresAt: shareRecord.expiresAt
|
|
478
|
+
};
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
async exportProject(options = {}) {
|
|
482
|
+
const { projectId = this.state.currentProjectId } = options;
|
|
483
|
+
|
|
484
|
+
const project = await this._getProjectFromDB(projectId);
|
|
485
|
+
if (!project) {
|
|
486
|
+
throw new Error('Project not found');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const exportData = {
|
|
490
|
+
version: '2.0.0',
|
|
491
|
+
project: {
|
|
492
|
+
name: project.name,
|
|
493
|
+
description: project.description,
|
|
494
|
+
created: project.created,
|
|
495
|
+
modified: project.modified,
|
|
496
|
+
units: project.units,
|
|
497
|
+
metadata: project.metadata,
|
|
498
|
+
settings: project.settings
|
|
499
|
+
},
|
|
500
|
+
files: Object.entries(project.files).map(([key, file]) => ({
|
|
501
|
+
key,
|
|
502
|
+
name: file.name,
|
|
503
|
+
type: file.type,
|
|
504
|
+
size: file.size,
|
|
505
|
+
tags: file.tags || []
|
|
506
|
+
})),
|
|
507
|
+
geometry: project.geometry
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const json = JSON.stringify(exportData, null, 2);
|
|
511
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
512
|
+
|
|
513
|
+
// Download
|
|
514
|
+
const url = URL.createObjectURL(blob);
|
|
515
|
+
const a = document.createElement('a');
|
|
516
|
+
a.href = url;
|
|
517
|
+
a.download = `${project.name}.cyclecad.json`;
|
|
518
|
+
document.body.appendChild(a);
|
|
519
|
+
a.click();
|
|
520
|
+
document.body.removeChild(a);
|
|
521
|
+
URL.revokeObjectURL(url);
|
|
522
|
+
|
|
523
|
+
return blob;
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
async importProject(options = {}) {
|
|
527
|
+
const { file, asCopy = true } = options;
|
|
528
|
+
|
|
529
|
+
if (!file) {
|
|
530
|
+
throw new Error('File required');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const json = await file.text();
|
|
534
|
+
const data = JSON.parse(json);
|
|
535
|
+
|
|
536
|
+
if (!asCopy) {
|
|
537
|
+
// Direct import (overwrite)
|
|
538
|
+
const project = data.project;
|
|
539
|
+
project.id = this._generateUUID();
|
|
540
|
+
project.created = Date.now();
|
|
541
|
+
project.modified = Date.now();
|
|
542
|
+
|
|
543
|
+
await this._saveProjectToDB(project);
|
|
544
|
+
this.state.projects.unshift(project);
|
|
545
|
+
|
|
546
|
+
return project;
|
|
547
|
+
} else {
|
|
548
|
+
// Import as copy
|
|
549
|
+
return this.newProject({
|
|
550
|
+
name: `${data.project.name} (imported)`,
|
|
551
|
+
description: data.project.description,
|
|
552
|
+
units: data.project.units
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
// ========================================================================
|
|
558
|
+
// TEMPLATES
|
|
559
|
+
// ========================================================================
|
|
560
|
+
|
|
561
|
+
async createTemplate(options = {}) {
|
|
562
|
+
const {
|
|
563
|
+
projectId = this.state.currentProjectId,
|
|
564
|
+
name,
|
|
565
|
+
description = ''
|
|
566
|
+
} = options;
|
|
567
|
+
|
|
568
|
+
if (!name) {
|
|
569
|
+
throw new Error('Template name required');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const project = await this._getProjectFromDB(projectId);
|
|
573
|
+
if (!project) {
|
|
574
|
+
throw new Error('Project not found');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const template = {
|
|
578
|
+
name,
|
|
579
|
+
description,
|
|
580
|
+
created: Date.now(),
|
|
581
|
+
data: {
|
|
582
|
+
geometry: project.geometry,
|
|
583
|
+
settings: project.settings,
|
|
584
|
+
files: project.files
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
this.state.templates.set(name, template);
|
|
589
|
+
localStorage.setItem(`data_template_${name}`, JSON.stringify(template));
|
|
590
|
+
|
|
591
|
+
this._showNotification(`Template saved: ${name}`, 'success');
|
|
592
|
+
|
|
593
|
+
return template;
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
async fromTemplate(options = {}) {
|
|
597
|
+
const { templateName, newProjectName } = options;
|
|
598
|
+
|
|
599
|
+
const template = this.state.templates.get(templateName);
|
|
600
|
+
if (!template) {
|
|
601
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return this.newProject({
|
|
605
|
+
name: newProjectName,
|
|
606
|
+
templateName
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
async listTemplates() {
|
|
611
|
+
return Array.from(this.state.templates.values()).map(t => ({
|
|
612
|
+
name: t.name,
|
|
613
|
+
description: t.description,
|
|
614
|
+
created: t.created
|
|
615
|
+
}));
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
// ========================================================================
|
|
619
|
+
// TRASH & RECOVERY
|
|
620
|
+
// ========================================================================
|
|
621
|
+
|
|
622
|
+
async listTrash(options = {}) {
|
|
623
|
+
const { limit = 50 } = options;
|
|
624
|
+
|
|
625
|
+
return this.state.trash.slice(0, limit).map(item => ({
|
|
626
|
+
id: item.id,
|
|
627
|
+
name: item.name,
|
|
628
|
+
deletedAt: item.deletedAt,
|
|
629
|
+
recoveryUntil: item.recoveryUntil
|
|
630
|
+
}));
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
async restoreFromTrash(options = {}) {
|
|
634
|
+
const { projectId } = options;
|
|
635
|
+
|
|
636
|
+
const item = this.state.trash.find(t => t.id === projectId);
|
|
637
|
+
if (!item) {
|
|
638
|
+
throw new Error('Item not in trash');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (Date.now() > item.recoveryUntil) {
|
|
642
|
+
throw new Error('Recovery window has expired (30 days)');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const restored = {...item};
|
|
646
|
+
delete restored.deletedAt;
|
|
647
|
+
delete restored.recoveryUntil;
|
|
648
|
+
|
|
649
|
+
await this._saveProjectToDB(restored);
|
|
650
|
+
this.state.projects.unshift(restored);
|
|
651
|
+
this.state.trash = this.state.trash.filter(t => t.id !== projectId);
|
|
652
|
+
|
|
653
|
+
this._showNotification(`Restored: ${restored.name}`, 'success');
|
|
654
|
+
|
|
655
|
+
return restored;
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
async emptyTrash(options = {}) {
|
|
659
|
+
const { permanentDelete = true } = options;
|
|
660
|
+
|
|
661
|
+
if (permanentDelete) {
|
|
662
|
+
for (const item of this.state.trash) {
|
|
663
|
+
await this._deleteFromDB('trash', item.id);
|
|
664
|
+
}
|
|
665
|
+
this.state.trash = [];
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
// ========================================================================
|
|
670
|
+
// BACKUP & AUTO-RECOVERY
|
|
671
|
+
// ========================================================================
|
|
672
|
+
|
|
673
|
+
async createBackup(options = {}) {
|
|
674
|
+
const { projectId = this.state.currentProjectId } = options;
|
|
675
|
+
|
|
676
|
+
const project = await this._getProjectFromDB(projectId);
|
|
677
|
+
if (!project) {
|
|
678
|
+
throw new Error('Project not found');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const backup = {
|
|
682
|
+
id: this._generateUUID(),
|
|
683
|
+
projectId,
|
|
684
|
+
timestamp: Date.now(),
|
|
685
|
+
data: JSON.parse(JSON.stringify(project))
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
await this._saveBackup(backup);
|
|
689
|
+
|
|
690
|
+
// Keep only last N backups
|
|
691
|
+
if (this.state.backupHistory.length >= this.state.maxBackups) {
|
|
692
|
+
const oldest = this.state.backupHistory.shift();
|
|
693
|
+
await this._deleteFromDB('backups', oldest.id);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.state.backupHistory.push(backup);
|
|
697
|
+
this._showNotification('Backup created', 'success');
|
|
698
|
+
|
|
699
|
+
return backup;
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
async listBackups(options = {}) {
|
|
703
|
+
const { projectId = this.state.currentProjectId } = options;
|
|
704
|
+
|
|
705
|
+
return this.state.backupHistory
|
|
706
|
+
.filter(b => b.projectId === projectId)
|
|
707
|
+
.map(b => ({
|
|
708
|
+
id: b.id,
|
|
709
|
+
timestamp: b.timestamp,
|
|
710
|
+
sizeBytes: this._estimateSize(b.data)
|
|
711
|
+
}));
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
// ========================================================================
|
|
715
|
+
// DOCUMENT PROPERTIES
|
|
716
|
+
// ========================================================================
|
|
717
|
+
|
|
718
|
+
async setDocumentProperties(options = {}) {
|
|
719
|
+
if (!this.state.currentProject) {
|
|
720
|
+
throw new Error('No project loaded');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const { title, author, description, tags = [], version } = options;
|
|
724
|
+
|
|
725
|
+
if (title) this.state.currentProject.name = title;
|
|
726
|
+
if (author) this.state.currentProject.metadata.author = author;
|
|
727
|
+
if (description) this.state.currentProject.description = description;
|
|
728
|
+
if (tags) this.state.currentProject.metadata.tags = tags;
|
|
729
|
+
if (version) this.state.currentProject.metadata.version = version;
|
|
730
|
+
|
|
731
|
+
await this.save();
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
async getDocumentProperties() {
|
|
735
|
+
if (!this.state.currentProject) {
|
|
736
|
+
throw new Error('No project loaded');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
title: this.state.currentProject.name,
|
|
741
|
+
description: this.state.currentProject.description,
|
|
742
|
+
author: this.state.currentProject.metadata.author,
|
|
743
|
+
created: this.state.currentProject.created,
|
|
744
|
+
modified: this.state.currentProject.modified,
|
|
745
|
+
revision: this.state.currentProject.metadata.revision,
|
|
746
|
+
tags: this.state.currentProject.metadata.tags,
|
|
747
|
+
version: this.state.currentProject.metadata.version
|
|
748
|
+
};
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
// ========================================================================
|
|
752
|
+
// UNITS & CONVERSION
|
|
753
|
+
// ========================================================================
|
|
754
|
+
|
|
755
|
+
async setUnits(unitSystem) {
|
|
756
|
+
if (!this.state.currentProject) {
|
|
757
|
+
throw new Error('No project loaded');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const validUnits = ['mm', 'cm', 'm', 'inch', 'in', 'ft'];
|
|
761
|
+
if (!validUnits.includes(unitSystem)) {
|
|
762
|
+
throw new Error('Invalid unit system');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
this.state.currentProject.units = unitSystem;
|
|
766
|
+
this.state.unitPreferences.current = unitSystem;
|
|
767
|
+
|
|
768
|
+
await this.save();
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
convertUnits(value, fromUnit, toUnit) {
|
|
772
|
+
const conversions = {
|
|
773
|
+
'mm': 1, 'cm': 10, 'm': 1000,
|
|
774
|
+
'inch': 25.4, 'in': 25.4, 'ft': 304.8
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const fromMm = value * (conversions[fromUnit] || 1);
|
|
778
|
+
return fromMm / (conversions[toUnit] || 1);
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
// ========================================================================
|
|
782
|
+
// SEARCH & INDEXING
|
|
783
|
+
// ========================================================================
|
|
784
|
+
|
|
785
|
+
async searchProjects(options = {}) {
|
|
786
|
+
const { query, limit = 20 } = options;
|
|
787
|
+
|
|
788
|
+
if (!query || query.length < 2) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const q = query.toLowerCase();
|
|
793
|
+
return this.state.projects
|
|
794
|
+
.filter(p =>
|
|
795
|
+
p.name.toLowerCase().includes(q) ||
|
|
796
|
+
p.description.toLowerCase().includes(q) ||
|
|
797
|
+
p.metadata.tags.some(t => t.toLowerCase().includes(q))
|
|
798
|
+
)
|
|
799
|
+
.slice(0, limit)
|
|
800
|
+
.map(p => ({
|
|
801
|
+
id: p.id,
|
|
802
|
+
name: p.name,
|
|
803
|
+
description: p.description,
|
|
804
|
+
match: 'full'
|
|
805
|
+
}));
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
// ========================================================================
|
|
809
|
+
// INTERNAL FUNCTIONS
|
|
810
|
+
// ========================================================================
|
|
811
|
+
|
|
812
|
+
_generateUUID() {
|
|
813
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
814
|
+
const r = Math.random() * 16 | 0;
|
|
815
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
816
|
+
return v.toString(16);
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
_generateShareCode() {
|
|
821
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
async _openDatabase() {
|
|
825
|
+
return new Promise((resolve, reject) => {
|
|
826
|
+
const req = indexedDB.open('cyclecad_data', 2);
|
|
827
|
+
|
|
828
|
+
req.onupgradeneeded = (e) => {
|
|
829
|
+
const db = e.target.result;
|
|
830
|
+
if (!db.objectStoreNames.contains('projects')) {
|
|
831
|
+
db.createObjectStore('projects', { keyPath: 'id' });
|
|
832
|
+
}
|
|
833
|
+
if (!db.objectStoreNames.contains('backups')) {
|
|
834
|
+
db.createObjectStore('backups', { keyPath: 'id' });
|
|
835
|
+
}
|
|
836
|
+
if (!db.objectStoreNames.contains('shares')) {
|
|
837
|
+
db.createObjectStore('shares', { keyPath: 'code' });
|
|
838
|
+
}
|
|
839
|
+
if (!db.objectStoreNames.contains('trash')) {
|
|
840
|
+
db.createObjectStore('trash', { keyPath: 'id' });
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
req.onsuccess = () => resolve(req.result);
|
|
845
|
+
req.onerror = () => reject(req.error);
|
|
846
|
+
});
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
async _saveProjectToDB(project) {
|
|
850
|
+
const tx = this.state.db.transaction('projects', 'readwrite');
|
|
851
|
+
const store = tx.objectStore('projects');
|
|
852
|
+
store.put(project);
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
tx.oncomplete = resolve;
|
|
855
|
+
tx.onerror = () => reject(tx.error);
|
|
856
|
+
});
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
async _getProjectFromDB(projectId) {
|
|
860
|
+
const tx = this.state.db.transaction('projects', 'readonly');
|
|
861
|
+
const store = tx.objectStore('projects');
|
|
862
|
+
const req = store.get(projectId);
|
|
863
|
+
return new Promise((resolve, reject) => {
|
|
864
|
+
req.onsuccess = () => resolve(req.result);
|
|
865
|
+
req.onerror = () => reject(req.error);
|
|
866
|
+
});
|
|
867
|
+
},
|
|
868
|
+
|
|
869
|
+
async _deleteFromDB(storeName, id) {
|
|
870
|
+
const tx = this.state.db.transaction(storeName, 'readwrite');
|
|
871
|
+
const store = tx.objectStore(storeName);
|
|
872
|
+
store.delete(id);
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
tx.oncomplete = resolve;
|
|
875
|
+
tx.onerror = () => reject(tx.error);
|
|
876
|
+
});
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
async _loadProjectList() {
|
|
880
|
+
const tx = this.state.db.transaction('projects', 'readonly');
|
|
881
|
+
const store = tx.objectStore('projects');
|
|
882
|
+
const req = store.getAll();
|
|
883
|
+
return new Promise((resolve, reject) => {
|
|
884
|
+
req.onsuccess = () => {
|
|
885
|
+
this.state.projects = req.result.sort((a, b) => b.modified - a.modified);
|
|
886
|
+
resolve();
|
|
887
|
+
};
|
|
888
|
+
req.onerror = () => reject(req.error);
|
|
889
|
+
});
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
_loadRecent() {
|
|
893
|
+
try {
|
|
894
|
+
const stored = localStorage.getItem('data_recentFiles');
|
|
895
|
+
if (stored) {
|
|
896
|
+
this.state.recentFiles = JSON.parse(stored);
|
|
897
|
+
}
|
|
898
|
+
} catch (e) {
|
|
899
|
+
console.warn('[Data] Failed to load recent files:', e);
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
|
|
903
|
+
_addToRecent(file) {
|
|
904
|
+
this.state.recentFiles.unshift(file);
|
|
905
|
+
if (this.state.recentFiles.length > 20) {
|
|
906
|
+
this.state.recentFiles.pop();
|
|
907
|
+
}
|
|
908
|
+
localStorage.setItem('data_recentFiles', JSON.stringify(this.state.recentFiles));
|
|
909
|
+
},
|
|
910
|
+
|
|
911
|
+
async _loadTemplates() {
|
|
912
|
+
const keys = Object.keys(localStorage);
|
|
913
|
+
keys
|
|
914
|
+
.filter(k => k.startsWith('data_template_'))
|
|
915
|
+
.forEach(key => {
|
|
916
|
+
try {
|
|
917
|
+
const template = JSON.parse(localStorage.getItem(key));
|
|
918
|
+
const name = key.replace('data_template_', '');
|
|
919
|
+
this.state.templates.set(name, template);
|
|
920
|
+
} catch (e) {
|
|
921
|
+
console.warn('[Data] Failed to load template:', key);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
},
|
|
925
|
+
|
|
926
|
+
async _loadTrash() {
|
|
927
|
+
const tx = this.state.db.transaction('trash', 'readonly');
|
|
928
|
+
const store = tx.objectStore('trash');
|
|
929
|
+
const req = store.getAll();
|
|
930
|
+
return new Promise((resolve, reject) => {
|
|
931
|
+
req.onsuccess = () => {
|
|
932
|
+
this.state.trash = req.result.filter(item =>
|
|
933
|
+
Date.now() < item.recoveryUntil
|
|
934
|
+
);
|
|
935
|
+
resolve();
|
|
936
|
+
};
|
|
937
|
+
req.onerror = () => reject(req.error);
|
|
938
|
+
});
|
|
939
|
+
},
|
|
940
|
+
|
|
941
|
+
async _saveToTrash(item) {
|
|
942
|
+
const tx = this.state.db.transaction('trash', 'readwrite');
|
|
943
|
+
const store = tx.objectStore('trash');
|
|
944
|
+
store.put(item);
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
tx.oncomplete = resolve;
|
|
947
|
+
tx.onerror = () => reject(tx.error);
|
|
948
|
+
});
|
|
949
|
+
},
|
|
950
|
+
|
|
951
|
+
async _saveBackup(backup) {
|
|
952
|
+
const tx = this.state.db.transaction('backups', 'readwrite');
|
|
953
|
+
const store = tx.objectStore('backups');
|
|
954
|
+
store.put(backup);
|
|
955
|
+
return new Promise((resolve, reject) => {
|
|
956
|
+
tx.oncomplete = resolve;
|
|
957
|
+
tx.onerror = () => reject(tx.error);
|
|
958
|
+
});
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
async _saveShareLink(shareRecord) {
|
|
962
|
+
const tx = this.state.db.transaction('shares', 'readwrite');
|
|
963
|
+
const store = tx.objectStore('shares');
|
|
964
|
+
store.put(shareRecord);
|
|
965
|
+
return new Promise((resolve, reject) => {
|
|
966
|
+
tx.oncomplete = resolve;
|
|
967
|
+
tx.onerror = () => reject(tx.error);
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
|
|
971
|
+
_updateSearchIndex(projectId, project) {
|
|
972
|
+
this.state.searchIndex.set(projectId, {
|
|
973
|
+
name: project.name.toLowerCase(),
|
|
974
|
+
description: project.description.toLowerCase(),
|
|
975
|
+
tags: (project.metadata.tags || []).map(t => t.toLowerCase())
|
|
976
|
+
});
|
|
977
|
+
},
|
|
978
|
+
|
|
979
|
+
_estimateSize(project) {
|
|
980
|
+
if (!project) return 0;
|
|
981
|
+
let size = JSON.stringify(project).length;
|
|
982
|
+
Object.values(project.files || {}).forEach(file => {
|
|
983
|
+
size += file.size || 0;
|
|
984
|
+
});
|
|
985
|
+
return size;
|
|
986
|
+
},
|
|
987
|
+
|
|
988
|
+
async _captureGeometry() {
|
|
989
|
+
// Placeholder: capture current 3D scene geometry
|
|
990
|
+
return {timestamp: Date.now(), meshCount: 0};
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
async _computeFileHash(arrayBuffer) {
|
|
994
|
+
const buffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
|
995
|
+
const hashArray = Array.from(new Uint8Array(buffer));
|
|
996
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
997
|
+
},
|
|
998
|
+
|
|
999
|
+
_detectFileType(filename) {
|
|
1000
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
1001
|
+
const types = {
|
|
1002
|
+
'ipt': 'application/inventor-part',
|
|
1003
|
+
'iam': 'application/inventor-assembly',
|
|
1004
|
+
'step': 'application/step',
|
|
1005
|
+
'stp': 'application/step',
|
|
1006
|
+
'stl': 'model/stl',
|
|
1007
|
+
'obj': 'model/obj',
|
|
1008
|
+
'dxf': 'application/dxf'
|
|
1009
|
+
};
|
|
1010
|
+
return types[ext] || 'application/octet-stream';
|
|
1011
|
+
},
|
|
1012
|
+
|
|
1013
|
+
_startAutoSave() {
|
|
1014
|
+
this.state.autoSaveHandle = setInterval(() => {
|
|
1015
|
+
if (this.state.currentProjectId) {
|
|
1016
|
+
this.save().catch(err => console.warn('[Data] Auto-save failed:', err));
|
|
1017
|
+
}
|
|
1018
|
+
}, this.state.autoSaveFrequency);
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
_setupStorageQuotaMonitoring() {
|
|
1022
|
+
if (navigator.storage && navigator.storage.estimate) {
|
|
1023
|
+
setInterval(() => {
|
|
1024
|
+
navigator.storage.estimate().then(estimate => {
|
|
1025
|
+
this.state.usageBytes = estimate.usage;
|
|
1026
|
+
this.state.quotaBytes = estimate.quota;
|
|
1027
|
+
const percentUsed = (estimate.usage / estimate.quota) * 100;
|
|
1028
|
+
if (percentUsed > 90) {
|
|
1029
|
+
this._showNotification('Storage quota nearly full', 'warning');
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}, 60000);
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
|
|
1036
|
+
_detectAndRestoreCrash() {
|
|
1037
|
+
const crashFlag = localStorage.getItem('data_appRunning');
|
|
1038
|
+
if (crashFlag === 'true') {
|
|
1039
|
+
this._broadcastEvent('data:crashDetected', {});
|
|
1040
|
+
localStorage.removeItem('data_appRunning');
|
|
1041
|
+
} else {
|
|
1042
|
+
localStorage.setItem('data_appRunning', 'true');
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
|
|
1046
|
+
_showNotification(message, type) {
|
|
1047
|
+
console.log(`[Data] [${type.toUpperCase()}] ${message}`);
|
|
1048
|
+
this._broadcastEvent('data:notification', {message, type});
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
_broadcastEvent(eventName, data) {
|
|
1052
|
+
window.dispatchEvent(new CustomEvent(eventName, {detail: data}));
|
|
1053
|
+
}
|
|
1054
|
+
};
|