cyclecad 0.1.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/CNAME +1 -0
- package/app/docs/api-reference.html +1436 -0
- package/app/docs/examples.html +803 -0
- package/app/docs/getting-started.html +1620 -0
- package/app/duo-project-browser.html +1321 -0
- package/app/duo-rebuild-guide.html +861 -0
- package/app/index.html +1635 -0
- package/app/js/ai-chat.js +992 -0
- package/app/js/app.js +724 -0
- package/app/js/export.js +658 -0
- package/app/js/inventor-parser.js +1138 -0
- package/app/js/operations.js +689 -0
- package/app/js/params.js +523 -0
- package/app/js/reverse-engineer.js +1275 -0
- package/app/js/shortcuts.js +350 -0
- package/app/js/sketch.js +899 -0
- package/app/js/tree.js +479 -0
- package/app/js/viewport.js +643 -0
- package/app/samples/Leistenbuerstenblech.ipt +0 -0
- package/app/samples/Rahmen_Seite.iam +0 -0
- package/app/samples/TraegerHoehe1.ipt +0 -0
- package/index.html +1226 -0
- package/package.json +33 -0
package/app/js/app.js
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD - Main Application Entry Point
|
|
3
|
+
* Wires all modules together and manages application state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera } from './viewport.js';
|
|
7
|
+
import { startSketch, endSketch, setTool, getEntities } from './sketch.js';
|
|
8
|
+
import { extrudeProfile, createPrimitive, rebuildFeature } from './operations.js';
|
|
9
|
+
import { initChat, parseCADPrompt } from './ai-chat.js';
|
|
10
|
+
import { initTree, addFeature, selectFeature, onSelect } from './tree.js';
|
|
11
|
+
import { initParams, showParams, onParamChange } from './params.js';
|
|
12
|
+
import { exportSTL, exportOBJ, exportJSON } from './export.js';
|
|
13
|
+
import { initShortcuts, showShortcutsPanel } from './shortcuts.js';
|
|
14
|
+
|
|
15
|
+
// Application state
|
|
16
|
+
const APP = {
|
|
17
|
+
mode: 'idle', // idle, sketch, extrude, operation
|
|
18
|
+
currentSketch: null,
|
|
19
|
+
selectedFeature: null,
|
|
20
|
+
selectedEntity: null,
|
|
21
|
+
history: [],
|
|
22
|
+
historyIndex: -1,
|
|
23
|
+
features: [],
|
|
24
|
+
scene: null,
|
|
25
|
+
camera: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the application
|
|
30
|
+
*/
|
|
31
|
+
export async function initApp() {
|
|
32
|
+
console.log('Initializing cycleCAD...');
|
|
33
|
+
|
|
34
|
+
// Initialize 3D viewport
|
|
35
|
+
const canvas = document.getElementById('three-canvas');
|
|
36
|
+
if (!canvas) {
|
|
37
|
+
console.error('Canvas #three-canvas not found');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
initViewport(canvas);
|
|
42
|
+
APP.scene = getScene();
|
|
43
|
+
APP.camera = getCamera();
|
|
44
|
+
|
|
45
|
+
// Initialize UI panels
|
|
46
|
+
initTree();
|
|
47
|
+
initParams();
|
|
48
|
+
initChat();
|
|
49
|
+
|
|
50
|
+
// Setup toolbar buttons
|
|
51
|
+
setupToolbar();
|
|
52
|
+
|
|
53
|
+
// Setup feature tree selection flow
|
|
54
|
+
onSelect((featureId) => {
|
|
55
|
+
APP.selectedFeature = APP.features.find((f) => f.id === featureId);
|
|
56
|
+
if (APP.selectedFeature) {
|
|
57
|
+
showParams(APP.selectedFeature);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Setup param change flow
|
|
62
|
+
onParamChange((paramName, value) => {
|
|
63
|
+
if (APP.selectedFeature) {
|
|
64
|
+
APP.selectedFeature.params[paramName] = value;
|
|
65
|
+
rebuildFeature(APP.selectedFeature);
|
|
66
|
+
updateScene();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Initialize keyboard shortcuts
|
|
71
|
+
initShortcuts(getShortcutHandlers());
|
|
72
|
+
|
|
73
|
+
// Initialize AI chat integration
|
|
74
|
+
setupAIChat();
|
|
75
|
+
|
|
76
|
+
// Show welcome screen
|
|
77
|
+
showWelcomeScreen();
|
|
78
|
+
|
|
79
|
+
console.log('cycleCAD initialized');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get keyboard shortcut handlers
|
|
84
|
+
* @returns {Object}
|
|
85
|
+
*/
|
|
86
|
+
function getShortcutHandlers() {
|
|
87
|
+
return {
|
|
88
|
+
// Sketch operations
|
|
89
|
+
newSketch: () => {
|
|
90
|
+
if (APP.mode === 'idle') {
|
|
91
|
+
startNewSketch();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
line: () => setTool('line'),
|
|
95
|
+
rect: () => setTool('rect'),
|
|
96
|
+
circle: () => setTool('circle'),
|
|
97
|
+
arc: () => setTool('arc'),
|
|
98
|
+
dimension: () => setTool('dimension'),
|
|
99
|
+
|
|
100
|
+
// 3D operations
|
|
101
|
+
extrude: () => {
|
|
102
|
+
if (APP.currentSketch && APP.mode === 'sketch') {
|
|
103
|
+
startExtrude();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
revolve: () => console.log('Revolve not yet implemented'),
|
|
107
|
+
fillet: () => console.log('Fillet not yet implemented'),
|
|
108
|
+
chamfer: () => console.log('Chamfer not yet implemented'),
|
|
109
|
+
|
|
110
|
+
// Boolean operations
|
|
111
|
+
union: () => console.log('Union not yet implemented'),
|
|
112
|
+
cut: () => console.log('Cut not yet implemented'),
|
|
113
|
+
|
|
114
|
+
// Edit
|
|
115
|
+
undo: () => undo(),
|
|
116
|
+
redo: () => redo(),
|
|
117
|
+
delete: () => deleteSelected(),
|
|
118
|
+
escape: () => cancelOperation(),
|
|
119
|
+
enter: () => confirmOperation(),
|
|
120
|
+
|
|
121
|
+
// Views
|
|
122
|
+
viewFront: () => setView('front'),
|
|
123
|
+
viewBack: () => setView('back'),
|
|
124
|
+
viewRight: () => setView('right'),
|
|
125
|
+
viewLeft: () => setView('left'),
|
|
126
|
+
viewTop: () => setView('top'),
|
|
127
|
+
viewBottom: () => setView('bottom'),
|
|
128
|
+
viewIso: () => setView('iso'),
|
|
129
|
+
|
|
130
|
+
// Display
|
|
131
|
+
toggleGrid: () => toggleGrid(),
|
|
132
|
+
toggleWireframe: () => toggleWireframe(),
|
|
133
|
+
fitAll: () => fitAll(),
|
|
134
|
+
|
|
135
|
+
// File
|
|
136
|
+
save: () => saveProject(),
|
|
137
|
+
exportSTL: () => showExportDialog('stl'),
|
|
138
|
+
|
|
139
|
+
// Help
|
|
140
|
+
showHelp: () => showShortcutsPanel(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Setup toolbar buttons
|
|
146
|
+
*/
|
|
147
|
+
function setupToolbar() {
|
|
148
|
+
const toolbar = document.getElementById('toolbar');
|
|
149
|
+
if (!toolbar) return;
|
|
150
|
+
|
|
151
|
+
// File buttons
|
|
152
|
+
const newBtn = document.getElementById('btn-new');
|
|
153
|
+
if (newBtn) {
|
|
154
|
+
newBtn.onclick = () => {
|
|
155
|
+
if (confirm('Start a new project? Unsaved changes will be lost.')) {
|
|
156
|
+
APP.features = [];
|
|
157
|
+
APP.history = [];
|
|
158
|
+
APP.historyIndex = -1;
|
|
159
|
+
updateScene();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const openBtn = document.getElementById('btn-open');
|
|
165
|
+
if (openBtn) {
|
|
166
|
+
openBtn.onclick = () => {
|
|
167
|
+
const input = document.createElement('input');
|
|
168
|
+
input.type = 'file';
|
|
169
|
+
input.accept = '.json';
|
|
170
|
+
input.onchange = (e) => {
|
|
171
|
+
const file = e.target.files[0];
|
|
172
|
+
if (file) {
|
|
173
|
+
const reader = new FileReader();
|
|
174
|
+
reader.onload = (event) => {
|
|
175
|
+
try {
|
|
176
|
+
const data = JSON.parse(event.target.result);
|
|
177
|
+
loadProject(data);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
alert('Failed to load project: ' + err.message);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
reader.readAsText(file);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
input.click();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const saveBtn = document.getElementById('btn-save');
|
|
190
|
+
if (saveBtn) {
|
|
191
|
+
saveBtn.onclick = () => saveProject();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Export buttons
|
|
195
|
+
const exportSTLBtn = document.getElementById('btn-export-stl');
|
|
196
|
+
if (exportSTLBtn) {
|
|
197
|
+
exportSTLBtn.onclick = () => showExportDialog('stl');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const exportOBJBtn = document.getElementById('btn-export-obj');
|
|
201
|
+
if (exportOBJBtn) {
|
|
202
|
+
exportOBJBtn.onclick = () => showExportDialog('obj');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Sketch tools
|
|
206
|
+
const sketchBtn = document.getElementById('btn-sketch');
|
|
207
|
+
if (sketchBtn) {
|
|
208
|
+
sketchBtn.onclick = () => startNewSketch();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const lineBtn = document.getElementById('btn-line');
|
|
212
|
+
if (lineBtn) {
|
|
213
|
+
lineBtn.onclick = () => setTool('line');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rectBtn = document.getElementById('btn-rect');
|
|
217
|
+
if (rectBtn) {
|
|
218
|
+
rectBtn.onclick = () => setTool('rect');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const circleBtn = document.getElementById('btn-circle');
|
|
222
|
+
if (circleBtn) {
|
|
223
|
+
circleBtn.onclick = () => setTool('circle');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3D operations
|
|
227
|
+
const extrudeBtn = document.getElementById('btn-extrude');
|
|
228
|
+
if (extrudeBtn) {
|
|
229
|
+
extrudeBtn.onclick = () => {
|
|
230
|
+
if (APP.mode === 'sketch') {
|
|
231
|
+
startExtrude();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// View buttons
|
|
237
|
+
const viewFrontBtn = document.getElementById('btn-view-front');
|
|
238
|
+
if (viewFrontBtn) {
|
|
239
|
+
viewFrontBtn.onclick = () => setView('front');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const viewTopBtn = document.getElementById('btn-view-top');
|
|
243
|
+
if (viewTopBtn) {
|
|
244
|
+
viewTopBtn.onclick = () => setView('top');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const viewIsoBtn = document.getElementById('btn-view-iso');
|
|
248
|
+
if (viewIsoBtn) {
|
|
249
|
+
viewIsoBtn.onclick = () => setView('iso');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Edit buttons
|
|
253
|
+
const undoBtn = document.getElementById('btn-undo');
|
|
254
|
+
if (undoBtn) {
|
|
255
|
+
undoBtn.onclick = () => undo();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const redoBtn = document.getElementById('btn-redo');
|
|
259
|
+
if (redoBtn) {
|
|
260
|
+
redoBtn.onclick = () => redo();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Display toggles
|
|
264
|
+
const gridBtn = document.getElementById('btn-grid');
|
|
265
|
+
if (gridBtn) {
|
|
266
|
+
gridBtn.onclick = () => toggleGrid();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const wireframeBtn = document.getElementById('btn-wireframe');
|
|
270
|
+
if (wireframeBtn) {
|
|
271
|
+
wireframeBtn.onclick = () => toggleWireframe();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const fitBtn = document.getElementById('btn-fit-all');
|
|
275
|
+
if (fitBtn) {
|
|
276
|
+
fitBtn.onclick = () => fitAll();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Start a new sketch
|
|
282
|
+
*/
|
|
283
|
+
function startNewSketch() {
|
|
284
|
+
if (APP.mode === 'sketch') {
|
|
285
|
+
alert('Finish current sketch first');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
APP.mode = 'sketch';
|
|
290
|
+
APP.currentSketch = startSketch();
|
|
291
|
+
updateStatusBar(`Sketch mode. Use L/R/C/A for tools. Press Escape to cancel.`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Start extrude operation
|
|
296
|
+
*/
|
|
297
|
+
function startExtrude() {
|
|
298
|
+
const entities = getEntities();
|
|
299
|
+
if (!entities || entities.length === 0) {
|
|
300
|
+
alert('No sketch geometry to extrude');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
APP.mode = 'extrude';
|
|
305
|
+
|
|
306
|
+
// Prompt for height
|
|
307
|
+
const height = prompt('Enter extrusion height:', '10');
|
|
308
|
+
if (height === null) {
|
|
309
|
+
APP.mode = 'sketch';
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const h = parseFloat(height);
|
|
314
|
+
if (isNaN(h)) {
|
|
315
|
+
alert('Invalid height');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const mesh3d = extrudeProfile(entities, h);
|
|
321
|
+
endSketch();
|
|
322
|
+
APP.currentSketch = null;
|
|
323
|
+
APP.mode = 'idle';
|
|
324
|
+
|
|
325
|
+
// Add to scene
|
|
326
|
+
addToScene(mesh3d);
|
|
327
|
+
|
|
328
|
+
// Create feature and add to tree
|
|
329
|
+
const feature = {
|
|
330
|
+
id: 'feature_' + Date.now(),
|
|
331
|
+
name: 'Extrusion',
|
|
332
|
+
type: 'extrude',
|
|
333
|
+
mesh: mesh3d,
|
|
334
|
+
params: { height: h },
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
APP.features.push(feature);
|
|
338
|
+
addFeature(feature);
|
|
339
|
+
pushHistory();
|
|
340
|
+
updateStatusBar('Extrusion created');
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('Extrude failed:', err);
|
|
343
|
+
alert('Extrude failed: ' + err.message);
|
|
344
|
+
APP.mode = 'sketch';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Cancel current operation
|
|
350
|
+
*/
|
|
351
|
+
function cancelOperation() {
|
|
352
|
+
if (APP.mode === 'sketch') {
|
|
353
|
+
endSketch();
|
|
354
|
+
APP.currentSketch = null;
|
|
355
|
+
APP.mode = 'idle';
|
|
356
|
+
updateStatusBar('Sketch cancelled');
|
|
357
|
+
} else if (APP.mode === 'extrude') {
|
|
358
|
+
APP.mode = 'sketch';
|
|
359
|
+
updateStatusBar('Extrude cancelled');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Confirm current operation
|
|
365
|
+
*/
|
|
366
|
+
function confirmOperation() {
|
|
367
|
+
if (APP.mode === 'sketch') {
|
|
368
|
+
// Auto-extrude or prompt
|
|
369
|
+
const entities = getEntities();
|
|
370
|
+
if (entities && entities.length > 0) {
|
|
371
|
+
startExtrude();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Delete selected feature
|
|
378
|
+
*/
|
|
379
|
+
function deleteSelected() {
|
|
380
|
+
if (!APP.selectedFeature) return;
|
|
381
|
+
|
|
382
|
+
if (confirm(`Delete "${APP.selectedFeature.name}"?`)) {
|
|
383
|
+
removeFromScene(APP.selectedFeature.mesh);
|
|
384
|
+
APP.features = APP.features.filter((f) => f.id !== APP.selectedFeature.id);
|
|
385
|
+
APP.selectedFeature = null;
|
|
386
|
+
updateScene();
|
|
387
|
+
pushHistory();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Setup AI chat integration
|
|
393
|
+
*/
|
|
394
|
+
function setupAIChat() {
|
|
395
|
+
// Listen for parsed CAD prompts from AI chat
|
|
396
|
+
const chatInput = document.getElementById('chat-input');
|
|
397
|
+
const chatSend = document.getElementById('chat-send');
|
|
398
|
+
|
|
399
|
+
if (chatSend) {
|
|
400
|
+
chatSend.onclick = () => {
|
|
401
|
+
const text = chatInput?.value || '';
|
|
402
|
+
if (!text.trim()) return;
|
|
403
|
+
|
|
404
|
+
// Parse prompt with AI
|
|
405
|
+
parseCADPrompt(text).then((parsedPrompt) => {
|
|
406
|
+
if (parsedPrompt && parsedPrompt.action) {
|
|
407
|
+
executeParsedPrompt(parsedPrompt);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (chatInput) chatInput.value = '';
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Allow Enter to send
|
|
416
|
+
if (chatInput) {
|
|
417
|
+
chatInput.addEventListener('keypress', (e) => {
|
|
418
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
chatSend?.click();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Execute a parsed CAD prompt from AI
|
|
428
|
+
* @param {Object} prompt
|
|
429
|
+
*/
|
|
430
|
+
function executeParsedPrompt(prompt) {
|
|
431
|
+
try {
|
|
432
|
+
// Create primitive based on parsed prompt
|
|
433
|
+
const primitive = createPrimitive(prompt.type, prompt.params);
|
|
434
|
+
addToScene(primitive.mesh);
|
|
435
|
+
|
|
436
|
+
const feature = {
|
|
437
|
+
id: 'feature_' + Date.now(),
|
|
438
|
+
name: prompt.name || prompt.type,
|
|
439
|
+
type: prompt.type,
|
|
440
|
+
mesh: primitive.mesh,
|
|
441
|
+
params: prompt.params,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
APP.features.push(feature);
|
|
445
|
+
addFeature(feature);
|
|
446
|
+
pushHistory();
|
|
447
|
+
updateStatusBar(`Created ${feature.name}`);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
console.error('Failed to execute prompt:', err);
|
|
450
|
+
alert('Failed to create feature: ' + err.message);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Show welcome screen
|
|
456
|
+
*/
|
|
457
|
+
function showWelcomeScreen() {
|
|
458
|
+
const welcomePanel = document.getElementById('welcome-panel');
|
|
459
|
+
if (!welcomePanel) return;
|
|
460
|
+
|
|
461
|
+
welcomePanel.style.display = 'flex';
|
|
462
|
+
|
|
463
|
+
const newSketchBtn = welcomePanel.querySelector('[data-action="new-sketch"]');
|
|
464
|
+
if (newSketchBtn) {
|
|
465
|
+
newSketchBtn.onclick = () => {
|
|
466
|
+
welcomePanel.style.display = 'none';
|
|
467
|
+
startNewSketch();
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const aiGenerateBtn = welcomePanel.querySelector('[data-action="ai-generate"]');
|
|
472
|
+
if (aiGenerateBtn) {
|
|
473
|
+
aiGenerateBtn.onclick = () => {
|
|
474
|
+
welcomePanel.style.display = 'none';
|
|
475
|
+
const chatInput = document.getElementById('chat-input');
|
|
476
|
+
if (chatInput) chatInput.focus();
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const importBtn = welcomePanel.querySelector('[data-action="import"]');
|
|
481
|
+
if (importBtn) {
|
|
482
|
+
importBtn.onclick = () => {
|
|
483
|
+
welcomePanel.style.display = 'none';
|
|
484
|
+
const input = document.createElement('input');
|
|
485
|
+
input.type = 'file';
|
|
486
|
+
input.accept = '.json';
|
|
487
|
+
input.onchange = (e) => {
|
|
488
|
+
const file = e.target.files[0];
|
|
489
|
+
if (file) {
|
|
490
|
+
const reader = new FileReader();
|
|
491
|
+
reader.onload = (event) => {
|
|
492
|
+
try {
|
|
493
|
+
const data = JSON.parse(event.target.result);
|
|
494
|
+
loadProject(data);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
alert('Failed to load project: ' + err.message);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
reader.readAsText(file);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
input.click();
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Update the 3D scene
|
|
509
|
+
*/
|
|
510
|
+
function updateScene() {
|
|
511
|
+
// Scene will auto-render via viewport's animation loop
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Save project as JSON
|
|
516
|
+
*/
|
|
517
|
+
function saveProject() {
|
|
518
|
+
const data = {
|
|
519
|
+
version: '1.0',
|
|
520
|
+
timestamp: new Date().toISOString(),
|
|
521
|
+
features: APP.features.map((f) => ({
|
|
522
|
+
id: f.id,
|
|
523
|
+
name: f.name,
|
|
524
|
+
type: f.type,
|
|
525
|
+
params: f.params,
|
|
526
|
+
})),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const json = JSON.stringify(data, null, 2);
|
|
530
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
531
|
+
const url = URL.createObjectURL(blob);
|
|
532
|
+
const a = document.createElement('a');
|
|
533
|
+
a.href = url;
|
|
534
|
+
a.download = `cyclecad_project_${Date.now()}.json`;
|
|
535
|
+
a.click();
|
|
536
|
+
URL.revokeObjectURL(url);
|
|
537
|
+
|
|
538
|
+
updateStatusBar('Project saved');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Load project from JSON
|
|
543
|
+
*/
|
|
544
|
+
function loadProject(data) {
|
|
545
|
+
try {
|
|
546
|
+
// Clear current
|
|
547
|
+
APP.features = [];
|
|
548
|
+
APP.history = [];
|
|
549
|
+
APP.historyIndex = -1;
|
|
550
|
+
updateScene();
|
|
551
|
+
|
|
552
|
+
// Rebuild features
|
|
553
|
+
if (data.features && Array.isArray(data.features)) {
|
|
554
|
+
data.features.forEach((featureData) => {
|
|
555
|
+
try {
|
|
556
|
+
const primitive = createPrimitive(featureData.type, featureData.params);
|
|
557
|
+
addToScene(primitive.mesh);
|
|
558
|
+
|
|
559
|
+
const feature = {
|
|
560
|
+
id: featureData.id,
|
|
561
|
+
name: featureData.name,
|
|
562
|
+
type: featureData.type,
|
|
563
|
+
mesh: primitive.mesh,
|
|
564
|
+
params: featureData.params,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
APP.features.push(feature);
|
|
568
|
+
addFeature(feature);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
console.warn(`Failed to load feature ${featureData.name}:`, err);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
pushHistory();
|
|
576
|
+
updateStatusBar(`Loaded project with ${APP.features.length} features`);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
console.error('Load project failed:', err);
|
|
579
|
+
alert('Failed to load project: ' + err.message);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Show export dialog
|
|
585
|
+
*/
|
|
586
|
+
function showExportDialog(format) {
|
|
587
|
+
if (APP.features.length === 0) {
|
|
588
|
+
alert('No features to export');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
let blob;
|
|
594
|
+
let filename;
|
|
595
|
+
|
|
596
|
+
if (format === 'stl') {
|
|
597
|
+
blob = exportSTL(APP.features);
|
|
598
|
+
filename = `cyclecad_model_${Date.now()}.stl`;
|
|
599
|
+
} else if (format === 'obj') {
|
|
600
|
+
blob = exportOBJ(APP.features);
|
|
601
|
+
filename = `cyclecad_model_${Date.now()}.obj`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (blob) {
|
|
605
|
+
const url = URL.createObjectURL(blob);
|
|
606
|
+
const a = document.createElement('a');
|
|
607
|
+
a.href = url;
|
|
608
|
+
a.download = filename;
|
|
609
|
+
a.click();
|
|
610
|
+
URL.revokeObjectURL(url);
|
|
611
|
+
|
|
612
|
+
updateStatusBar(`Exported to ${filename}`);
|
|
613
|
+
}
|
|
614
|
+
} catch (err) {
|
|
615
|
+
console.error('Export failed:', err);
|
|
616
|
+
alert('Export failed: ' + err.message);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Undo last operation
|
|
622
|
+
*/
|
|
623
|
+
function undo() {
|
|
624
|
+
if (APP.historyIndex > 0) {
|
|
625
|
+
APP.historyIndex--;
|
|
626
|
+
restoreFromHistory();
|
|
627
|
+
updateStatusBar('Undo');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Redo last undone operation
|
|
633
|
+
*/
|
|
634
|
+
function redo() {
|
|
635
|
+
if (APP.historyIndex < APP.history.length - 1) {
|
|
636
|
+
APP.historyIndex++;
|
|
637
|
+
restoreFromHistory();
|
|
638
|
+
updateStatusBar('Redo');
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Restore state from history
|
|
644
|
+
*/
|
|
645
|
+
function restoreFromHistory() {
|
|
646
|
+
const state = APP.history[APP.historyIndex];
|
|
647
|
+
if (state) {
|
|
648
|
+
// TODO: Implement full state restoration
|
|
649
|
+
console.log('Restore from history:', state);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Push current state to history
|
|
655
|
+
*/
|
|
656
|
+
function pushHistory() {
|
|
657
|
+
// Trim redo stack if not at end
|
|
658
|
+
if (APP.historyIndex < APP.history.length - 1) {
|
|
659
|
+
APP.history = APP.history.slice(0, APP.historyIndex + 1);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Save state snapshot
|
|
663
|
+
APP.history.push({
|
|
664
|
+
features: JSON.parse(JSON.stringify(APP.features.map((f) => ({
|
|
665
|
+
id: f.id,
|
|
666
|
+
name: f.name,
|
|
667
|
+
type: f.type,
|
|
668
|
+
params: f.params,
|
|
669
|
+
})))),
|
|
670
|
+
timestamp: Date.now(),
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
APP.historyIndex = APP.history.length - 1;
|
|
674
|
+
|
|
675
|
+
// Keep history limited
|
|
676
|
+
if (APP.history.length > 50) {
|
|
677
|
+
APP.history.shift();
|
|
678
|
+
APP.historyIndex--;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Toggle grid visibility
|
|
684
|
+
*/
|
|
685
|
+
function toggleGrid() {
|
|
686
|
+
// TODO: Implement grid toggle in viewport
|
|
687
|
+
console.log('Toggle grid');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Toggle wireframe mode
|
|
692
|
+
*/
|
|
693
|
+
function toggleWireframe() {
|
|
694
|
+
// TODO: Implement wireframe toggle in viewport
|
|
695
|
+
console.log('Toggle wireframe');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Fit all features in view
|
|
700
|
+
*/
|
|
701
|
+
function fitAll() {
|
|
702
|
+
// TODO: Implement fit-all camera animation
|
|
703
|
+
console.log('Fit all');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Update status bar
|
|
708
|
+
*/
|
|
709
|
+
function updateStatusBar(message) {
|
|
710
|
+
const statusBar = document.getElementById('status-bar');
|
|
711
|
+
if (statusBar) {
|
|
712
|
+
statusBar.textContent = message;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Entry point - run when DOM is ready
|
|
718
|
+
*/
|
|
719
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
720
|
+
initApp().catch(console.error);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Export for testing
|
|
724
|
+
export { APP, startNewSketch, startExtrude, cancelOperation };
|