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/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 };