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/index.html ADDED
@@ -0,0 +1,1635 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>cycleCAD - Parametric 3D CAD Modeler</title>
7
+
8
+ <!-- Import Map for Three.js -->
9
+ <script type="importmap">
10
+ {
11
+ "imports": {
12
+ "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
13
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
14
+ }
15
+ }
16
+ </script>
17
+
18
+ <style>
19
+ /* ===== CSS Variables & Root ===== */
20
+ :root {
21
+ --bg-primary: #1e1e1e;
22
+ --bg-secondary: #252526;
23
+ --bg-tertiary: #2d2d30;
24
+ --border-color: #3e3e42;
25
+ --text-primary: #e0e0e0;
26
+ --text-secondary: #a0a0a0;
27
+ --text-muted: #696969;
28
+ --accent-blue: #58a6ff;
29
+ --accent-blue-dark: #1f6feb;
30
+ --accent-green: #3fb950;
31
+ --accent-red: #f85149;
32
+ --accent-yellow: #d29922;
33
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
34
+ --shadow-md: 0 8px 16px rgba(0, 0, 0, 0.4);
35
+ --shadow-lg: 0 16px 32px rgba(0, 0, 0, 0.5);
36
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
37
+ --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
38
+ --toolbar-height: 48px;
39
+ --statusbar-height: 36px;
40
+ --panel-width: 260px;
41
+ --properties-width: 300px;
42
+ }
43
+
44
+ /* ===== Reset & Globals ===== */
45
+ * {
46
+ margin: 0;
47
+ padding: 0;
48
+ box-sizing: border-box;
49
+ }
50
+
51
+ html, body {
52
+ width: 100%;
53
+ height: 100%;
54
+ background: var(--bg-primary);
55
+ color: var(--text-primary);
56
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif;
57
+ font-size: 13px;
58
+ line-height: 1.5;
59
+ overflow: hidden;
60
+ -webkit-font-smoothing: antialiased;
61
+ -moz-osx-font-smoothing: grayscale;
62
+ }
63
+
64
+ button, input, select, textarea {
65
+ font-family: inherit;
66
+ font-size: inherit;
67
+ color: inherit;
68
+ background: none;
69
+ border: none;
70
+ padding: 0;
71
+ margin: 0;
72
+ cursor: pointer;
73
+ }
74
+
75
+ button:disabled {
76
+ opacity: 0.5;
77
+ cursor: not-allowed;
78
+ }
79
+
80
+ /* ===== Main Layout ===== */
81
+ #app {
82
+ display: flex;
83
+ flex-direction: column;
84
+ width: 100%;
85
+ height: 100%;
86
+ }
87
+
88
+ #toolbar {
89
+ height: var(--toolbar-height);
90
+ background: var(--bg-secondary);
91
+ border-bottom: 1px solid var(--border-color);
92
+ display: flex;
93
+ align-items: center;
94
+ padding: 0 8px;
95
+ gap: 8px;
96
+ user-select: none;
97
+ z-index: 100;
98
+ box-shadow: var(--shadow-sm);
99
+ }
100
+
101
+ #content {
102
+ flex: 1;
103
+ display: flex;
104
+ gap: 0;
105
+ overflow: hidden;
106
+ }
107
+
108
+ #left-panel {
109
+ width: var(--panel-width);
110
+ background: var(--bg-secondary);
111
+ border-right: 1px solid var(--border-color);
112
+ display: flex;
113
+ flex-direction: column;
114
+ overflow: hidden;
115
+ z-index: 50;
116
+ }
117
+
118
+ #viewport-container {
119
+ flex: 1;
120
+ position: relative;
121
+ background: var(--bg-primary);
122
+ overflow: hidden;
123
+ }
124
+
125
+ #viewport {
126
+ width: 100%;
127
+ height: 100%;
128
+ }
129
+
130
+ #right-panel {
131
+ width: var(--properties-width);
132
+ background: var(--bg-secondary);
133
+ border-left: 1px solid var(--border-color);
134
+ display: flex;
135
+ flex-direction: column;
136
+ overflow: hidden;
137
+ z-index: 50;
138
+ }
139
+
140
+ #statusbar {
141
+ height: var(--statusbar-height);
142
+ background: var(--bg-secondary);
143
+ border-top: 1px solid var(--border-color);
144
+ display: flex;
145
+ align-items: center;
146
+ padding: 0 12px;
147
+ gap: 24px;
148
+ font-size: 12px;
149
+ user-select: none;
150
+ z-index: 100;
151
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.3);
152
+ }
153
+
154
+ /* ===== Toolbar Styling ===== */
155
+ .toolbar-group {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 0;
159
+ border-right: 1px solid var(--border-color);
160
+ padding-right: 8px;
161
+ margin-right: 8px;
162
+ }
163
+
164
+ .toolbar-group:last-child {
165
+ border-right: none;
166
+ margin-right: 0;
167
+ padding-right: 0;
168
+ }
169
+
170
+ .toolbar-button {
171
+ padding: 6px 8px;
172
+ height: 32px;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ gap: 4px;
177
+ border-radius: 3px;
178
+ background: transparent;
179
+ color: var(--text-primary);
180
+ transition: all var(--transition-fast);
181
+ white-space: nowrap;
182
+ font-size: 12px;
183
+ font-weight: 500;
184
+ }
185
+
186
+ .toolbar-button:hover:not(:disabled) {
187
+ background: var(--bg-tertiary);
188
+ color: var(--accent-blue);
189
+ }
190
+
191
+ .toolbar-button.active {
192
+ background: var(--accent-blue-dark);
193
+ color: var(--accent-blue);
194
+ }
195
+
196
+ .toolbar-button:active:not(:disabled) {
197
+ transform: scale(0.98);
198
+ }
199
+
200
+ .toolbar-icon {
201
+ width: 16px;
202
+ height: 16px;
203
+ display: inline-flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ }
207
+
208
+ .toolbar-label {
209
+ font-size: 11px;
210
+ letter-spacing: 0.3px;
211
+ }
212
+
213
+ /* ===== Left Panel (Feature Tree) ===== */
214
+ #left-panel-header {
215
+ padding: 8px 12px;
216
+ border-bottom: 1px solid var(--border-color);
217
+ font-weight: 600;
218
+ font-size: 12px;
219
+ color: var(--text-secondary);
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.5px;
222
+ }
223
+
224
+ #feature-tree {
225
+ flex: 1;
226
+ overflow-y: auto;
227
+ overflow-x: hidden;
228
+ padding: 4px 0;
229
+ }
230
+
231
+ #feature-tree::-webkit-scrollbar {
232
+ width: 10px;
233
+ }
234
+
235
+ #feature-tree::-webkit-scrollbar-track {
236
+ background: transparent;
237
+ }
238
+
239
+ #feature-tree::-webkit-scrollbar-thumb {
240
+ background: var(--border-color);
241
+ border-radius: 5px;
242
+ }
243
+
244
+ #feature-tree::-webkit-scrollbar-thumb:hover {
245
+ background: var(--text-muted);
246
+ }
247
+
248
+ .tree-item {
249
+ padding: 4px 8px;
250
+ cursor: pointer;
251
+ transition: background var(--transition-fast);
252
+ user-select: none;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 6px;
256
+ font-size: 12px;
257
+ margin: 1px 4px;
258
+ border-radius: 3px;
259
+ }
260
+
261
+ .tree-item:hover {
262
+ background: var(--bg-tertiary);
263
+ }
264
+
265
+ .tree-item.active {
266
+ background: var(--accent-blue-dark);
267
+ color: var(--accent-blue);
268
+ }
269
+
270
+ .tree-icon {
271
+ width: 14px;
272
+ height: 14px;
273
+ flex-shrink: 0;
274
+ opacity: 0.7;
275
+ }
276
+
277
+ /* ===== Right Panel (Properties + Chat) ===== */
278
+ #properties-header {
279
+ padding: 8px 12px;
280
+ border-bottom: 1px solid var(--border-color);
281
+ font-weight: 600;
282
+ font-size: 12px;
283
+ color: var(--text-secondary);
284
+ text-transform: uppercase;
285
+ letter-spacing: 0.5px;
286
+ }
287
+
288
+ #properties-tabs {
289
+ display: flex;
290
+ gap: 0;
291
+ border-bottom: 1px solid var(--border-color);
292
+ padding: 4px 4px 0 4px;
293
+ }
294
+
295
+ .properties-tab {
296
+ flex: 1;
297
+ padding: 6px 8px;
298
+ background: transparent;
299
+ color: var(--text-secondary);
300
+ border-bottom: 2px solid transparent;
301
+ transition: all var(--transition-fast);
302
+ font-size: 11px;
303
+ font-weight: 500;
304
+ text-align: center;
305
+ }
306
+
307
+ .properties-tab:hover {
308
+ color: var(--text-primary);
309
+ }
310
+
311
+ .properties-tab.active {
312
+ color: var(--accent-blue);
313
+ border-bottom-color: var(--accent-blue);
314
+ }
315
+
316
+ #properties-content {
317
+ flex: 1;
318
+ overflow-y: auto;
319
+ padding: 8px;
320
+ min-height: 0;
321
+ }
322
+
323
+ #properties-content::-webkit-scrollbar {
324
+ width: 8px;
325
+ }
326
+
327
+ #properties-content::-webkit-scrollbar-track {
328
+ background: transparent;
329
+ }
330
+
331
+ #properties-content::-webkit-scrollbar-thumb {
332
+ background: var(--border-color);
333
+ border-radius: 4px;
334
+ }
335
+
336
+ #properties-content::-webkit-scrollbar-thumb:hover {
337
+ background: var(--text-muted);
338
+ }
339
+
340
+ .property-group {
341
+ margin-bottom: 12px;
342
+ }
343
+
344
+ .property-group-title {
345
+ font-size: 11px;
346
+ font-weight: 600;
347
+ color: var(--text-secondary);
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.5px;
350
+ margin-bottom: 6px;
351
+ padding-bottom: 4px;
352
+ border-bottom: 1px solid var(--border-color);
353
+ }
354
+
355
+ .property-row {
356
+ display: flex;
357
+ gap: 4px;
358
+ margin-bottom: 6px;
359
+ align-items: center;
360
+ }
361
+
362
+ .property-label {
363
+ min-width: 80px;
364
+ font-size: 11px;
365
+ color: var(--text-secondary);
366
+ font-weight: 500;
367
+ }
368
+
369
+ .property-input {
370
+ flex: 1;
371
+ padding: 4px 6px;
372
+ background: var(--bg-tertiary);
373
+ color: var(--text-primary);
374
+ border: 1px solid var(--border-color);
375
+ border-radius: 3px;
376
+ font-size: 11px;
377
+ transition: border-color var(--transition-fast);
378
+ }
379
+
380
+ .property-input:focus {
381
+ outline: none;
382
+ border-color: var(--accent-blue);
383
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
384
+ }
385
+
386
+ /* ===== Status Bar ===== */
387
+ .statusbar-item {
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 6px;
391
+ color: var(--text-secondary);
392
+ }
393
+
394
+ .statusbar-label {
395
+ font-weight: 500;
396
+ color: var(--text-muted);
397
+ }
398
+
399
+ .statusbar-value {
400
+ color: var(--text-primary);
401
+ font-family: "Menlo", "Monaco", "Courier New", monospace;
402
+ font-size: 11px;
403
+ }
404
+
405
+ .status-indicator {
406
+ width: 8px;
407
+ height: 8px;
408
+ border-radius: 50%;
409
+ background: var(--accent-green);
410
+ animation: pulse 2s infinite;
411
+ }
412
+
413
+ .status-indicator.idle {
414
+ background: var(--text-muted);
415
+ animation: none;
416
+ }
417
+
418
+ .status-indicator.error {
419
+ background: var(--accent-red);
420
+ animation: none;
421
+ }
422
+
423
+ @keyframes pulse {
424
+ 0%, 100% { opacity: 1; }
425
+ 50% { opacity: 0.5; }
426
+ }
427
+
428
+ /* ===== Welcome Splash Screen ===== */
429
+ #welcome-splash {
430
+ position: fixed;
431
+ top: 0;
432
+ left: 0;
433
+ right: 0;
434
+ bottom: 0;
435
+ background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
436
+ display: flex;
437
+ flex-direction: column;
438
+ align-items: center;
439
+ justify-content: center;
440
+ z-index: 1000;
441
+ gap: 32px;
442
+ }
443
+
444
+ #welcome-splash.hidden {
445
+ display: none;
446
+ }
447
+
448
+ .splash-logo {
449
+ font-size: 48px;
450
+ font-weight: 700;
451
+ letter-spacing: -1px;
452
+ text-align: center;
453
+ margin-bottom: 16px;
454
+ }
455
+
456
+ .splash-logo-cycle {
457
+ color: var(--text-primary);
458
+ font-weight: 400;
459
+ }
460
+
461
+ .splash-logo-cad {
462
+ color: var(--accent-blue);
463
+ font-weight: 700;
464
+ }
465
+
466
+ .splash-subtitle {
467
+ font-size: 14px;
468
+ color: var(--text-secondary);
469
+ text-align: center;
470
+ max-width: 400px;
471
+ }
472
+
473
+ .splash-options {
474
+ display: grid;
475
+ grid-template-columns: repeat(3, 1fr);
476
+ gap: 12px;
477
+ max-width: 420px;
478
+ justify-content: center;
479
+ }
480
+
481
+ .splash-button {
482
+ padding: 12px 24px;
483
+ border-radius: 6px;
484
+ font-weight: 600;
485
+ font-size: 14px;
486
+ transition: all var(--transition-base);
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 8px;
490
+ min-width: 180px;
491
+ justify-content: center;
492
+ }
493
+
494
+ .splash-button-primary {
495
+ background: var(--accent-blue);
496
+ color: #000;
497
+ box-shadow: 0 4px 12px rgba(88, 166, 255, 0.2);
498
+ }
499
+
500
+ .splash-button-primary:hover {
501
+ background: #5db3ff;
502
+ box-shadow: 0 6px 16px rgba(88, 166, 255, 0.3);
503
+ }
504
+
505
+ .splash-button-secondary {
506
+ background: var(--bg-tertiary);
507
+ color: var(--text-primary);
508
+ border: 1px solid var(--border-color);
509
+ }
510
+
511
+ .splash-button-secondary:hover {
512
+ background: var(--bg-tertiary);
513
+ border-color: var(--accent-blue);
514
+ color: var(--accent-blue);
515
+ }
516
+
517
+ /* ===== Viewport Overlays ===== */
518
+ #viewport-overlay {
519
+ position: absolute;
520
+ top: 0;
521
+ left: 0;
522
+ right: 0;
523
+ pointer-events: none;
524
+ padding: 12px;
525
+ z-index: 10;
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: 8px;
529
+ }
530
+
531
+ .mode-indicator {
532
+ background: rgba(0, 0, 0, 0.6);
533
+ border: 1px solid var(--accent-blue);
534
+ color: var(--accent-blue);
535
+ padding: 4px 8px;
536
+ border-radius: 3px;
537
+ font-size: 11px;
538
+ font-weight: 600;
539
+ letter-spacing: 0.5px;
540
+ text-transform: uppercase;
541
+ width: fit-content;
542
+ backdrop-filter: blur(4px);
543
+ }
544
+
545
+ .coordinate-display {
546
+ background: rgba(0, 0, 0, 0.6);
547
+ color: var(--text-secondary);
548
+ padding: 6px 8px;
549
+ border-radius: 3px;
550
+ font-family: "Menlo", "Monaco", "Courier New", monospace;
551
+ font-size: 10px;
552
+ width: fit-content;
553
+ backdrop-filter: blur(4px);
554
+ }
555
+
556
+ .coordinate-value {
557
+ color: var(--accent-blue);
558
+ margin: 0 4px;
559
+ }
560
+
561
+ .snap-indicator {
562
+ background: rgba(0, 0, 0, 0.6);
563
+ color: var(--accent-green);
564
+ padding: 4px 8px;
565
+ border-radius: 3px;
566
+ font-size: 10px;
567
+ font-weight: 500;
568
+ width: fit-content;
569
+ display: none;
570
+ backdrop-filter: blur(4px);
571
+ }
572
+
573
+ .snap-indicator.active {
574
+ display: block;
575
+ }
576
+
577
+ /* ===== Sketch Canvas Overlay ===== */
578
+ #sketch-canvas-overlay {
579
+ position: absolute;
580
+ top: 0;
581
+ left: 0;
582
+ display: none;
583
+ z-index: 20;
584
+ cursor: crosshair;
585
+ }
586
+
587
+ #sketch-canvas-overlay.active {
588
+ display: block;
589
+ }
590
+
591
+ /* ===== Loading Spinner ===== */
592
+ #kernel-spinner {
593
+ position: fixed;
594
+ top: 50%;
595
+ left: 50%;
596
+ transform: translate(-50%, -50%);
597
+ z-index: 2000;
598
+ display: none;
599
+ }
600
+
601
+ #kernel-spinner.active {
602
+ display: flex;
603
+ }
604
+
605
+ .spinner-container {
606
+ display: flex;
607
+ flex-direction: column;
608
+ align-items: center;
609
+ gap: 16px;
610
+ background: var(--bg-secondary);
611
+ padding: 32px;
612
+ border-radius: 8px;
613
+ box-shadow: var(--shadow-lg);
614
+ border: 1px solid var(--border-color);
615
+ }
616
+
617
+ .spinner {
618
+ width: 40px;
619
+ height: 40px;
620
+ border: 3px solid var(--border-color);
621
+ border-top-color: var(--accent-blue);
622
+ border-radius: 50%;
623
+ animation: spin 1s linear infinite;
624
+ }
625
+
626
+ @keyframes spin {
627
+ to { transform: rotate(360deg); }
628
+ }
629
+
630
+ .spinner-text {
631
+ color: var(--text-secondary);
632
+ font-size: 12px;
633
+ font-weight: 500;
634
+ }
635
+
636
+ /* ===== Scrollbars Global ===== */
637
+ ::-webkit-scrollbar {
638
+ width: 10px;
639
+ height: 10px;
640
+ }
641
+
642
+ ::-webkit-scrollbar-track {
643
+ background: transparent;
644
+ }
645
+
646
+ ::-webkit-scrollbar-thumb {
647
+ background: var(--border-color);
648
+ border-radius: 5px;
649
+ }
650
+
651
+ ::-webkit-scrollbar-thumb:hover {
652
+ background: var(--text-muted);
653
+ }
654
+
655
+ /* ===== Context Menu ===== */
656
+ #context-menu {
657
+ position: fixed;
658
+ background: var(--bg-secondary);
659
+ border: 1px solid var(--border-color);
660
+ border-radius: 4px;
661
+ box-shadow: var(--shadow-lg);
662
+ z-index: 300;
663
+ display: none;
664
+ min-width: 180px;
665
+ }
666
+
667
+ #context-menu.visible {
668
+ display: block;
669
+ }
670
+
671
+ .context-item {
672
+ padding: 6px 12px;
673
+ cursor: pointer;
674
+ color: var(--text-primary);
675
+ font-size: 12px;
676
+ transition: background var(--transition-fast);
677
+ display: flex;
678
+ align-items: center;
679
+ gap: 8px;
680
+ white-space: nowrap;
681
+ }
682
+
683
+ .context-item:hover {
684
+ background: var(--bg-tertiary);
685
+ color: var(--accent-blue);
686
+ }
687
+
688
+ .context-item.separator {
689
+ height: 1px;
690
+ background: var(--border-color);
691
+ margin: 2px 0;
692
+ cursor: default;
693
+ }
694
+
695
+ .context-item.separator:hover {
696
+ background: var(--border-color);
697
+ }
698
+
699
+ /* ===== Modals & Dialogs ===== */
700
+ .modal-overlay {
701
+ position: fixed;
702
+ top: 0;
703
+ left: 0;
704
+ right: 0;
705
+ bottom: 0;
706
+ background: rgba(0, 0, 0, 0.6);
707
+ display: none;
708
+ align-items: center;
709
+ justify-content: center;
710
+ z-index: 500;
711
+ backdrop-filter: blur(2px);
712
+ }
713
+
714
+ .modal-overlay.visible {
715
+ display: flex;
716
+ }
717
+
718
+ .modal {
719
+ background: var(--bg-secondary);
720
+ border: 1px solid var(--border-color);
721
+ border-radius: 8px;
722
+ box-shadow: var(--shadow-lg);
723
+ max-width: 500px;
724
+ width: 90%;
725
+ max-height: 80vh;
726
+ overflow-y: auto;
727
+ padding: 0;
728
+ display: flex;
729
+ flex-direction: column;
730
+ }
731
+
732
+ .modal-header {
733
+ padding: 16px;
734
+ border-bottom: 1px solid var(--border-color);
735
+ font-weight: 600;
736
+ font-size: 14px;
737
+ display: flex;
738
+ justify-content: space-between;
739
+ align-items: center;
740
+ }
741
+
742
+ .modal-close-btn {
743
+ background: transparent;
744
+ color: var(--text-secondary);
745
+ font-size: 18px;
746
+ cursor: pointer;
747
+ transition: color var(--transition-fast);
748
+ width: 24px;
749
+ height: 24px;
750
+ display: flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ }
754
+
755
+ .modal-close-btn:hover {
756
+ color: var(--text-primary);
757
+ }
758
+
759
+ .modal-body {
760
+ padding: 16px;
761
+ flex: 1;
762
+ overflow-y: auto;
763
+ }
764
+
765
+ .modal-footer {
766
+ padding: 12px 16px;
767
+ border-top: 1px solid var(--border-color);
768
+ display: flex;
769
+ gap: 8px;
770
+ justify-content: flex-end;
771
+ }
772
+
773
+ /* ===== Buttons ===== */
774
+ .btn {
775
+ padding: 6px 12px;
776
+ border-radius: 3px;
777
+ font-weight: 500;
778
+ font-size: 12px;
779
+ transition: all var(--transition-fast);
780
+ cursor: pointer;
781
+ border: 1px solid transparent;
782
+ display: inline-flex;
783
+ align-items: center;
784
+ gap: 4px;
785
+ }
786
+
787
+ .btn-primary {
788
+ background: var(--accent-blue);
789
+ color: #000;
790
+ }
791
+
792
+ .btn-primary:hover:not(:disabled) {
793
+ background: #5db3ff;
794
+ box-shadow: 0 2px 8px rgba(88, 166, 255, 0.2);
795
+ }
796
+
797
+ .btn-secondary {
798
+ background: var(--bg-tertiary);
799
+ color: var(--text-primary);
800
+ border-color: var(--border-color);
801
+ }
802
+
803
+ .btn-secondary:hover:not(:disabled) {
804
+ border-color: var(--accent-blue);
805
+ color: var(--accent-blue);
806
+ }
807
+
808
+ .btn-danger {
809
+ background: var(--accent-red);
810
+ color: #fff;
811
+ }
812
+
813
+ .btn-danger:hover:not(:disabled) {
814
+ background: #ff7b72;
815
+ }
816
+
817
+ /* ===== Input Fields ===== */
818
+ input[type="text"],
819
+ input[type="number"],
820
+ textarea,
821
+ select {
822
+ padding: 6px 8px;
823
+ background: var(--bg-tertiary);
824
+ color: var(--text-primary);
825
+ border: 1px solid var(--border-color);
826
+ border-radius: 3px;
827
+ font-size: 12px;
828
+ transition: border-color var(--transition-fast);
829
+ }
830
+
831
+ input[type="text"]:focus,
832
+ input[type="number"]:focus,
833
+ textarea:focus,
834
+ select:focus {
835
+ outline: none;
836
+ border-color: var(--accent-blue);
837
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
838
+ }
839
+
840
+ textarea {
841
+ resize: none;
842
+ font-family: "Menlo", "Monaco", "Courier New", monospace;
843
+ }
844
+
845
+ /* ===== Chat Panel ===== */
846
+ #chat-panel {
847
+ display: flex;
848
+ flex-direction: column;
849
+ flex: 1;
850
+ min-height: 0;
851
+ }
852
+
853
+ #chat-messages {
854
+ flex: 1;
855
+ overflow-y: auto;
856
+ padding: 8px;
857
+ display: flex;
858
+ flex-direction: column;
859
+ gap: 8px;
860
+ min-height: 0;
861
+ }
862
+
863
+ #chat-messages::-webkit-scrollbar {
864
+ width: 8px;
865
+ }
866
+
867
+ #chat-messages::-webkit-scrollbar-thumb {
868
+ background: var(--border-color);
869
+ border-radius: 4px;
870
+ }
871
+
872
+ .chat-message {
873
+ padding: 8px;
874
+ border-radius: 4px;
875
+ font-size: 11px;
876
+ line-height: 1.4;
877
+ }
878
+
879
+ .chat-message.user {
880
+ background: var(--accent-blue-dark);
881
+ color: var(--text-primary);
882
+ margin-left: 12px;
883
+ border: 1px solid var(--accent-blue);
884
+ }
885
+
886
+ .chat-message.assistant {
887
+ background: var(--bg-tertiary);
888
+ color: var(--text-primary);
889
+ margin-right: 12px;
890
+ border: 1px solid var(--border-color);
891
+ }
892
+
893
+ #chat-input-area {
894
+ padding: 8px;
895
+ border-top: 1px solid var(--border-color);
896
+ display: flex;
897
+ gap: 4px;
898
+ flex-shrink: 0;
899
+ }
900
+
901
+ #chat-input {
902
+ flex: 1;
903
+ padding: 6px 8px;
904
+ background: var(--bg-tertiary);
905
+ color: var(--text-primary);
906
+ border: 1px solid var(--border-color);
907
+ border-radius: 3px;
908
+ font-size: 11px;
909
+ resize: none;
910
+ max-height: 80px;
911
+ }
912
+
913
+ #chat-input:focus {
914
+ outline: none;
915
+ border-color: var(--accent-blue);
916
+ }
917
+
918
+ #chat-send-btn {
919
+ padding: 4px 8px;
920
+ background: var(--accent-blue);
921
+ color: #000;
922
+ border-radius: 3px;
923
+ font-weight: 600;
924
+ font-size: 11px;
925
+ transition: all var(--transition-fast);
926
+ flex-shrink: 0;
927
+ }
928
+
929
+ #chat-send-btn:hover:not(:disabled) {
930
+ background: #5db3ff;
931
+ }
932
+
933
+ /* ===== Responsive ===== */
934
+ @media (max-width: 1280px) {
935
+ :root {
936
+ --panel-width: 220px;
937
+ --properties-width: 260px;
938
+ }
939
+ }
940
+
941
+ @media (max-width: 1024px) {
942
+ :root {
943
+ --panel-width: 200px;
944
+ --properties-width: 240px;
945
+ }
946
+ }
947
+
948
+ </style>
949
+ </head>
950
+ <body>
951
+ <div id="app">
952
+ <!-- Welcome Splash Screen -->
953
+ <div id="welcome-splash">
954
+ <div>
955
+ <div class="splash-logo">
956
+ <span class="splash-logo-cycle">cycle</span><span class="splash-logo-cad">CAD</span>
957
+ </div>
958
+ <p class="splash-subtitle">Parametric 3D CAD Modeler for the Mechanical Designer</p>
959
+ </div>
960
+ <div class="splash-options">
961
+ <button class="splash-button splash-button-primary" id="btn-empty-project" style="grid-column: 1 / -1;">
962
+ <span>📦</span> Empty Project
963
+ </button>
964
+ <button class="splash-button splash-button-secondary" id="btn-new-sketch">
965
+ <span>📐</span> New Sketch
966
+ </button>
967
+ <button class="splash-button splash-button-secondary" id="btn-import-inventor">
968
+ <span>🏭</span> Import Inventor
969
+ </button>
970
+ <button class="splash-button splash-button-secondary" id="btn-ai-generate">
971
+ <span>✨</span> AI Generate
972
+ </button>
973
+ <button class="splash-button splash-button-secondary" id="btn-open-browser" style="grid-column: 1 / -1; background: rgba(255,140,0,0.1); border-color: rgba(255,140,0,0.3);">
974
+ <span>📂</span> DUO Project Browser (473 Parts)
975
+ </button>
976
+ </div>
977
+ </div>
978
+
979
+ <!-- Top Toolbar -->
980
+ <div id="toolbar">
981
+ <!-- Sketch Tools -->
982
+ <div class="toolbar-group">
983
+ <button class="toolbar-button" id="tool-sketch" title="Start Sketch (S)">
984
+ <span class="toolbar-icon">📐</span>
985
+ <span class="toolbar-label">Sketch</span>
986
+ </button>
987
+ <button class="toolbar-button" id="tool-line" title="Draw Line (L)">
988
+ <span class="toolbar-icon">/</span>
989
+ <span class="toolbar-label">Line</span>
990
+ </button>
991
+ <button class="toolbar-button" id="tool-rect" title="Draw Rectangle (R)">
992
+ <span class="toolbar-icon">▭</span>
993
+ <span class="toolbar-label">Rect</span>
994
+ </button>
995
+ <button class="toolbar-button" id="tool-circle" title="Draw Circle (C)">
996
+ <span class="toolbar-icon">⭕</span>
997
+ <span class="toolbar-label">Circle</span>
998
+ </button>
999
+ <button class="toolbar-button" id="tool-arc" title="Draw Arc (A)">
1000
+ <span class="toolbar-icon">⌢</span>
1001
+ <span class="toolbar-label">Arc</span>
1002
+ </button>
1003
+ <button class="toolbar-button" id="tool-dimension" title="Add Dimension (D)">
1004
+ <span class="toolbar-icon">↔</span>
1005
+ <span class="toolbar-label">Dim</span>
1006
+ </button>
1007
+ </div>
1008
+
1009
+ <!-- Modeling Tools -->
1010
+ <div class="toolbar-group">
1011
+ <button class="toolbar-button" id="tool-extrude" title="Extrude (E)">
1012
+ <span class="toolbar-icon">⬆</span>
1013
+ <span class="toolbar-label">Extrude</span>
1014
+ </button>
1015
+ <button class="toolbar-button" id="tool-revolve" title="Revolve">
1016
+ <span class="toolbar-icon">🔄</span>
1017
+ <span class="toolbar-label">Revolve</span>
1018
+ </button>
1019
+ <button class="toolbar-button" id="tool-fillet" title="Fillet">
1020
+ <span class="toolbar-icon">⌒</span>
1021
+ <span class="toolbar-label">Fillet</span>
1022
+ </button>
1023
+ <button class="toolbar-button" id="tool-chamfer" title="Chamfer">
1024
+ <span class="toolbar-icon">⌉</span>
1025
+ <span class="toolbar-label">Chamfer</span>
1026
+ </button>
1027
+ <button class="toolbar-button" id="tool-cut" title="Boolean Cut">
1028
+ <span class="toolbar-icon">−</span>
1029
+ <span class="toolbar-label">Cut</span>
1030
+ </button>
1031
+ <button class="toolbar-button" id="tool-union" title="Boolean Union">
1032
+ <span class="toolbar-icon">+</span>
1033
+ <span class="toolbar-label">Union</span>
1034
+ </button>
1035
+ </div>
1036
+
1037
+ <!-- Export Tools -->
1038
+ <div class="toolbar-group">
1039
+ <button class="toolbar-button" id="export-stl" title="Export STL">
1040
+ <span class="toolbar-icon">💾</span>
1041
+ <span class="toolbar-label">STL</span>
1042
+ </button>
1043
+ <button class="toolbar-button" id="export-step" title="Export STEP">
1044
+ <span class="toolbar-icon">💾</span>
1045
+ <span class="toolbar-label">STEP</span>
1046
+ </button>
1047
+ </div>
1048
+
1049
+ <!-- Reverse Engineer -->
1050
+ <div class="toolbar-group">
1051
+ <button class="toolbar-button" id="btn-reverse-engineer" title="Reverse Engineer a Part (Import STL → Detect Features → Step-by-Step Rebuild Guide)" style="background:rgba(88,166,255,0.12);border:1px solid rgba(88,166,255,0.3);">
1052
+ <span class="toolbar-icon">🔍</span>
1053
+ <span class="toolbar-label">Reverse Engineer</span>
1054
+ </button>
1055
+ <button class="toolbar-button" id="btn-inventor-import" title="Import Autodesk Inventor .ipt/.iam — Parse Native Format" style="background:rgba(255,140,0,0.12);border:1px solid rgba(255,140,0,0.3);">
1056
+ <span class="toolbar-icon">🏭</span>
1057
+ <span class="toolbar-label">Inventor</span>
1058
+ </button>
1059
+ </div>
1060
+
1061
+ <!-- Edit Operations -->
1062
+ <div class="toolbar-group">
1063
+ <button class="toolbar-button" id="btn-undo" title="Undo (Ctrl+Z)">
1064
+ <span class="toolbar-icon">↶</span>
1065
+ </button>
1066
+ <button class="toolbar-button" id="btn-redo" title="Redo (Ctrl+Y)">
1067
+ <span class="toolbar-icon">↷</span>
1068
+ </button>
1069
+ </div>
1070
+
1071
+ <!-- View Buttons -->
1072
+ <div class="toolbar-group">
1073
+ <button class="toolbar-button" id="view-front" title="Front View (V+F)">
1074
+ <span class="toolbar-label">Front</span>
1075
+ </button>
1076
+ <button class="toolbar-button" id="view-top" title="Top View (V+T)">
1077
+ <span class="toolbar-label">Top</span>
1078
+ </button>
1079
+ <button class="toolbar-button" id="view-right" title="Right View (V+R)">
1080
+ <span class="toolbar-label">Right</span>
1081
+ </button>
1082
+ <button class="toolbar-button" id="view-iso" title="Isometric View (V+I)">
1083
+ <span class="toolbar-label">Iso</span>
1084
+ </button>
1085
+ <button class="toolbar-button" id="view-fit" title="Fit All (V)">
1086
+ <span class="toolbar-label">Fit</span>
1087
+ </button>
1088
+ </div>
1089
+
1090
+ </div>
1091
+
1092
+ <!-- Main Content Area -->
1093
+ <div id="content">
1094
+ <!-- Left Panel: Feature Tree -->
1095
+ <div id="left-panel">
1096
+ <div id="left-panel-header">Model Tree</div>
1097
+ <div id="feature-tree">
1098
+ <!-- Populated by JavaScript -->
1099
+ </div>
1100
+ </div>
1101
+
1102
+ <!-- Center: Viewport -->
1103
+ <div id="viewport-container">
1104
+
1105
+ <!-- Viewport Overlays -->
1106
+ <div id="viewport-overlay">
1107
+ <div class="mode-indicator" id="mode-indicator">Ready</div>
1108
+ <div class="coordinate-display">
1109
+ <span>X:</span> <span class="coordinate-value" id="coord-x">0.00</span>
1110
+ <span>Y:</span> <span class="coordinate-value" id="coord-y">0.00</span>
1111
+ <span>Z:</span> <span class="coordinate-value" id="coord-z">0.00</span>
1112
+ </div>
1113
+ <div class="snap-indicator" id="snap-indicator">Snap: Grid</div>
1114
+ </div>
1115
+
1116
+ <!-- Sketch Canvas Overlay -->
1117
+ <canvas id="sketch-canvas-overlay"></canvas>
1118
+
1119
+ <!-- Loading Spinner -->
1120
+ <div id="kernel-spinner">
1121
+ <div class="spinner-container">
1122
+ <div class="spinner"></div>
1123
+ <div class="spinner-text">Loading Kernel...</div>
1124
+ </div>
1125
+ </div>
1126
+ </div>
1127
+
1128
+ <!-- Right Panel: Properties + Chat -->
1129
+ <div id="right-panel">
1130
+ <div id="properties-header">Properties</div>
1131
+
1132
+ <!-- Tab Navigation -->
1133
+ <div id="properties-tabs">
1134
+ <button class="properties-tab active" data-tab="properties">Properties</button>
1135
+ <button class="properties-tab" data-tab="chat">Chat</button>
1136
+ </div>
1137
+
1138
+ <!-- Properties Content -->
1139
+ <div id="properties-content">
1140
+ <div id="tab-properties">
1141
+ <!-- Properties populated by JavaScript -->
1142
+ </div>
1143
+ <div id="tab-chat" style="display: none;">
1144
+ <!-- Chat tab populated by JavaScript -->
1145
+ </div>
1146
+ </div>
1147
+ </div>
1148
+ </div>
1149
+
1150
+ <!-- Bottom Status Bar -->
1151
+ <div id="statusbar">
1152
+ <div class="statusbar-item">
1153
+ <span class="status-indicator" id="kernel-status"></span>
1154
+ <span class="statusbar-label">Kernel:</span>
1155
+ <span class="statusbar-value" id="kernel-status-text">Idle</span>
1156
+ </div>
1157
+ <div class="statusbar-item">
1158
+ <span class="statusbar-label">Mode:</span>
1159
+ <span class="statusbar-value" id="mode-value">Normal</span>
1160
+ </div>
1161
+ <div class="statusbar-item">
1162
+ <span class="statusbar-label">Units:</span>
1163
+ <span class="statusbar-value" id="units-value">mm</span>
1164
+ </div>
1165
+ <div class="statusbar-item">
1166
+ <span class="statusbar-label">Grid:</span>
1167
+ <span class="statusbar-value" id="grid-value">1.0</span>
1168
+ </div>
1169
+ <div class="statusbar-item">
1170
+ <span class="statusbar-label">FPS:</span>
1171
+ <span class="statusbar-value" id="fps-value">0</span>
1172
+ </div>
1173
+ <div style="margin-left:auto;">
1174
+ <button id="btn-hard-refresh" title="Hard Refresh — Nuke Cache & Reload" style="background:rgba(248,81,73,0.12);border:1px solid rgba(248,81,73,0.3);color:#f85149;font-size:11px;padding:2px 10px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;">
1175
+ <span style="font-size:12px;">🔄</span> Hard Refresh
1176
+ </button>
1177
+ </div>
1178
+ </div>
1179
+ </div>
1180
+
1181
+ <!-- Context Menu -->
1182
+ <div id="context-menu"></div>
1183
+
1184
+ <!-- Module Loader -->
1185
+ <script type="module">
1186
+ import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject } from './js/viewport.js';
1187
+ import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js';
1188
+ import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js';
1189
+ import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js';
1190
+ import { initTree, addFeature, selectFeature, onSelect } from './js/tree.js';
1191
+ import { initParams, showParams, onParamChange } from './js/params.js';
1192
+ import { exportSTL, exportOBJ, exportJSON } from './js/export.js';
1193
+ import { initShortcuts } from './js/shortcuts.js';
1194
+ import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js';
1195
+ import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js';
1196
+
1197
+ // ========== Application State ==========
1198
+ const APP = {
1199
+ mode: 'idle',
1200
+ currentSketch: null,
1201
+ selectedFeature: null,
1202
+ features: [],
1203
+ history: [],
1204
+ historyIndex: -1,
1205
+ };
1206
+
1207
+ // ========== Initialization ==========
1208
+ async function init() {
1209
+ try {
1210
+ console.log('Initializing cycleCAD...');
1211
+
1212
+ // 1. Initialize 3D viewport
1213
+ initViewport('viewport-container');
1214
+ document.getElementById('kernel-status').classList.add('ready');
1215
+ document.getElementById('kernel-status-text').textContent = 'Ready';
1216
+
1217
+ // 2. Initialize feature tree
1218
+ const treeContainer = document.getElementById('feature-tree');
1219
+ if (treeContainer) initTree(treeContainer);
1220
+
1221
+ // 3. Initialize properties panel
1222
+ const propsContainer = document.getElementById('tab-properties');
1223
+ if (propsContainer) initParams(propsContainer);
1224
+
1225
+ // 4. Initialize AI chat
1226
+ const chatTab = document.getElementById('tab-chat');
1227
+ if (chatTab) {
1228
+ // Create chat UI structure
1229
+ chatTab.innerHTML = `
1230
+ <div id="chat-messages" style="flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:6px;min-height:0;"></div>
1231
+ <div style="display:flex;gap:4px;padding:8px;border-top:1px solid var(--border-color);">
1232
+ <input id="chat-input" type="text" placeholder="Describe a part..." style="flex:1;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-primary);font-size:12px;">
1233
+ <button id="chat-send" style="padding:6px 12px;background:var(--accent-blue);color:#fff;border-radius:4px;font-size:12px;">Send</button>
1234
+ </div>
1235
+ `;
1236
+ chatTab.style.display = 'none';
1237
+ chatTab.style.flexDirection = 'column';
1238
+ chatTab.style.height = '100%';
1239
+
1240
+ initChat(
1241
+ document.getElementById('chat-messages'),
1242
+ document.getElementById('chat-input'),
1243
+ document.getElementById('chat-send'),
1244
+ (cmd) => executeParsedPrompt(cmd)
1245
+ );
1246
+ }
1247
+
1248
+ // 5. Wire up toolbar buttons
1249
+ setupToolbar();
1250
+
1251
+ // 6. Wire up tab switching
1252
+ setupTabs();
1253
+
1254
+ // 7. Wire up tree selection → params
1255
+ onSelect((featureId) => {
1256
+ APP.selectedFeature = APP.features.find(f => f.id === featureId);
1257
+ if (APP.selectedFeature) showParams(APP.selectedFeature);
1258
+ });
1259
+
1260
+ onParamChange((paramName, value) => {
1261
+ if (APP.selectedFeature) {
1262
+ APP.selectedFeature.params[paramName] = value;
1263
+ rebuildFeature(APP.selectedFeature);
1264
+ }
1265
+ });
1266
+
1267
+ // 8. Initialize keyboard shortcuts
1268
+ initShortcuts({
1269
+ newSketch: () => startNewSketch(),
1270
+ line: () => setTool('line'),
1271
+ rect: () => setTool('rect'),
1272
+ circle: () => setTool('circle'),
1273
+ arc: () => setTool('arc'),
1274
+ extrude: () => doExtrude(),
1275
+ undo: () => { /* TODO */ },
1276
+ redo: () => { /* TODO */ },
1277
+ delete: () => deleteSelected(),
1278
+ escape: () => cancelOperation(),
1279
+ enter: () => confirmOperation(),
1280
+ viewFront: () => setView('front'),
1281
+ viewBack: () => setView('back'),
1282
+ viewRight: () => setView('right'),
1283
+ viewLeft: () => setView('left'),
1284
+ viewTop: () => setView('top'),
1285
+ viewBottom: () => setView('bottom'),
1286
+ viewIso: () => setView('iso'),
1287
+ toggleGrid: () => vpToggleGrid(),
1288
+ fitAll: () => { /* TODO */ },
1289
+ save: () => saveProject(),
1290
+ exportSTL: () => doExportSTL(),
1291
+ });
1292
+
1293
+ // 9. Setup welcome splash
1294
+ setupWelcome();
1295
+
1296
+ // 10. Hard Refresh button — nukes all caches, service workers, and reloads
1297
+ const hardRefreshBtn = document.getElementById('btn-hard-refresh');
1298
+ if (hardRefreshBtn) hardRefreshBtn.addEventListener('click', async () => {
1299
+ hardRefreshBtn.textContent = 'Clearing...';
1300
+ hardRefreshBtn.style.opacity = '0.6';
1301
+ // Kill service workers
1302
+ if ('serviceWorker' in navigator) {
1303
+ const regs = await navigator.serviceWorker.getRegistrations();
1304
+ for (const r of regs) await r.unregister();
1305
+ }
1306
+ // Kill Cache API caches
1307
+ if ('caches' in window) {
1308
+ const names = await caches.keys();
1309
+ for (const n of names) await caches.delete(n);
1310
+ }
1311
+ // Reload with timestamp cache bust
1312
+ const url = new URL(window.location.href);
1313
+ url.searchParams.set('v', Date.now());
1314
+ window.location.replace(url.toString());
1315
+ });
1316
+
1317
+ console.log('cycleCAD initialized successfully');
1318
+ updateStatus('Ready');
1319
+
1320
+ } catch (error) {
1321
+ console.error('Failed to initialize cycleCAD:', error);
1322
+ document.getElementById('kernel-status').classList.add('error');
1323
+ document.getElementById('kernel-status-text').textContent = 'Error';
1324
+ }
1325
+ }
1326
+
1327
+ // ========== Toolbar Wiring ==========
1328
+ function setupToolbar() {
1329
+ const bind = (id, fn) => {
1330
+ const el = document.getElementById(id);
1331
+ if (el) el.addEventListener('click', fn);
1332
+ };
1333
+
1334
+ // Sketch tools
1335
+ bind('tool-sketch', () => startNewSketch());
1336
+ bind('tool-line', () => { if (APP.mode === 'sketch') setTool('line'); else { startNewSketch(); setTool('line'); } });
1337
+ bind('tool-rect', () => { if (APP.mode === 'sketch') setTool('rect'); else { startNewSketch(); setTool('rect'); } });
1338
+ bind('tool-circle', () => { if (APP.mode === 'sketch') setTool('circle'); else { startNewSketch(); setTool('circle'); } });
1339
+ bind('tool-arc', () => { if (APP.mode === 'sketch') setTool('arc'); else { startNewSketch(); setTool('arc'); } });
1340
+ bind('tool-dimension', () => setTool('dimension'));
1341
+
1342
+ // 3D operations
1343
+ bind('tool-extrude', () => doExtrude());
1344
+ bind('tool-revolve', () => updateStatus('Revolve: coming soon'));
1345
+ bind('tool-fillet', () => updateStatus('Fillet: coming soon'));
1346
+ bind('tool-chamfer', () => updateStatus('Chamfer: coming soon'));
1347
+ bind('tool-cut', () => updateStatus('Boolean Cut: coming soon'));
1348
+ bind('tool-union', () => updateStatus('Boolean Union: coming soon'));
1349
+
1350
+ // Export
1351
+ bind('export-stl', () => doExportSTL());
1352
+ bind('export-step', () => updateStatus('STEP export: coming soon'));
1353
+
1354
+ // Edit
1355
+ bind('btn-undo', () => updateStatus('Undo: coming soon'));
1356
+ bind('btn-redo', () => updateStatus('Redo: coming soon'));
1357
+
1358
+ // Views
1359
+ bind('view-front', () => setView('front'));
1360
+ bind('view-top', () => setView('top'));
1361
+ bind('view-right', () => setView('right'));
1362
+ bind('view-iso', () => setView('iso'));
1363
+ bind('view-fit', () => updateStatus('Fit All'));
1364
+
1365
+ // Reverse Engineer
1366
+ bind('btn-reverse-engineer', () => {
1367
+ const splash = document.getElementById('welcome-splash');
1368
+ if (splash) splash.classList.add('hidden');
1369
+ createReverseEngineerPanel(getScene());
1370
+ });
1371
+
1372
+ // Inventor Import (.ipt / .iam)
1373
+ bind('btn-inventor-import', () => {
1374
+ const splash = document.getElementById('welcome-splash');
1375
+ if (splash) splash.classList.add('hidden');
1376
+ createInventorPanel((parsedData) => {
1377
+ console.log('Inventor file parsed:', parsedData);
1378
+ // Add parsed features to tree
1379
+ if (parsedData.features && parsedData.features.length > 0) {
1380
+ parsedData.features.forEach((f, i) => {
1381
+ addFeature({
1382
+ id: `inv-${i}`,
1383
+ type: f.type || 'Unknown',
1384
+ name: f.name || `Feature ${i + 1}`,
1385
+ params: f.parameters || {},
1386
+ icon: f.icon || '📦'
1387
+ });
1388
+ });
1389
+ }
1390
+ updateStatus(`Inventor file loaded: ${parsedData.metadata?.fileName || 'unknown'} — ${parsedData.features?.length || 0} features found`);
1391
+ });
1392
+ });
1393
+ }
1394
+
1395
+ // ========== Tab Switching ==========
1396
+ function setupTabs() {
1397
+ document.querySelectorAll('.properties-tab').forEach(tab => {
1398
+ tab.addEventListener('click', () => {
1399
+ document.querySelectorAll('.properties-tab').forEach(t => t.classList.remove('active'));
1400
+ tab.classList.add('active');
1401
+ const target = tab.getAttribute('data-tab');
1402
+ document.getElementById('tab-properties').style.display = target === 'properties' ? 'block' : 'none';
1403
+ document.getElementById('tab-chat').style.display = target === 'chat' ? 'flex' : 'none';
1404
+ });
1405
+ });
1406
+ }
1407
+
1408
+ // ========== Welcome Splash ==========
1409
+ function setupWelcome() {
1410
+ const splash = document.getElementById('welcome-splash');
1411
+ if (!splash) return;
1412
+
1413
+ const hide = () => splash.classList.add('hidden');
1414
+
1415
+ // Empty Project — just dismiss splash and show empty workspace
1416
+ const emptyBtn = document.getElementById('btn-empty-project');
1417
+ if (emptyBtn) emptyBtn.addEventListener('click', () => {
1418
+ hide();
1419
+ updateStatus('Empty project — use toolbar to sketch, import, or generate');
1420
+ });
1421
+
1422
+ // New Sketch — start sketching immediately
1423
+ const newSketchBtn = document.getElementById('btn-new-sketch');
1424
+ if (newSketchBtn) newSketchBtn.addEventListener('click', () => { hide(); startNewSketch(); });
1425
+
1426
+ // Import Inventor — open the Inventor parser panel
1427
+ const invBtn = document.getElementById('btn-import-inventor');
1428
+ if (invBtn) invBtn.addEventListener('click', () => {
1429
+ hide();
1430
+ createInventorPanel((parsedData) => {
1431
+ console.log('Inventor file parsed:', parsedData);
1432
+ if (parsedData.allFeatures && parsedData.allFeatures.length > 0) {
1433
+ parsedData.allFeatures.forEach((f, i) => {
1434
+ addFeature({
1435
+ id: `inv-${i}`,
1436
+ type: f.type || 'Unknown',
1437
+ name: `${f.icon || '📦'} ${f.type} ${i + 1}`,
1438
+ params: {},
1439
+ icon: f.icon || '📦'
1440
+ });
1441
+ });
1442
+ }
1443
+ updateStatus(`Inventor file loaded: ${parsedData.filename} — ${parsedData.allFeatures?.length || 0} features`);
1444
+ });
1445
+ });
1446
+
1447
+ // AI Generate — switch to chat tab
1448
+ const aiBtn = document.getElementById('btn-ai-generate');
1449
+ if (aiBtn) aiBtn.addEventListener('click', () => {
1450
+ hide();
1451
+ document.querySelectorAll('.properties-tab').forEach(t => t.classList.remove('active'));
1452
+ const chatTabBtn = document.querySelector('[data-tab="chat"]');
1453
+ if (chatTabBtn) chatTabBtn.classList.add('active');
1454
+ document.getElementById('tab-properties').style.display = 'none';
1455
+ document.getElementById('tab-chat').style.display = 'flex';
1456
+ const chatInput = document.getElementById('chat-input');
1457
+ if (chatInput) chatInput.focus();
1458
+ });
1459
+
1460
+ // DUO Project Browser — open in new tab
1461
+ const browserBtn = document.getElementById('btn-open-browser');
1462
+ if (browserBtn) browserBtn.addEventListener('click', () => {
1463
+ window.open('duo-project-browser.html', '_blank');
1464
+ });
1465
+ }
1466
+
1467
+ // ========== Core Operations ==========
1468
+ function startNewSketch() {
1469
+ if (APP.mode === 'sketch') {
1470
+ updateStatus('Finish current sketch first (Enter=extrude, Esc=cancel)');
1471
+ return;
1472
+ }
1473
+ const splash = document.getElementById('welcome-splash');
1474
+ if (splash) splash.classList.add('hidden');
1475
+
1476
+ APP.mode = 'sketch';
1477
+ APP.currentSketch = startSketch('XY', getCamera(), getControls());
1478
+ updateStatus('Sketch mode — L=Line, R=Rect, C=Circle, A=Arc. Enter=Extrude, Esc=Cancel');
1479
+ document.getElementById('mode-indicator').textContent = 'Sketch: XY';
1480
+ document.getElementById('mode-value').textContent = 'Sketch';
1481
+ }
1482
+
1483
+ function doExtrude() {
1484
+ if (APP.mode !== 'sketch') {
1485
+ updateStatus('Start a sketch first');
1486
+ return;
1487
+ }
1488
+ const entities = getEntities();
1489
+ if (!entities || entities.length === 0) {
1490
+ updateStatus('No sketch geometry to extrude — draw something first');
1491
+ return;
1492
+ }
1493
+
1494
+ const height = prompt('Enter extrusion height (mm):', '20');
1495
+ if (height === null) return;
1496
+ const h = parseFloat(height);
1497
+ if (isNaN(h) || h === 0) { updateStatus('Invalid height'); return; }
1498
+
1499
+ try {
1500
+ const mesh = extrudeProfile(entities, h);
1501
+ endSketch();
1502
+ APP.currentSketch = null;
1503
+ APP.mode = 'idle';
1504
+ addToScene(mesh);
1505
+
1506
+ const feature = {
1507
+ id: 'feature_' + Date.now(),
1508
+ name: `Extrusion (${h}mm)`,
1509
+ type: 'extrude',
1510
+ mesh,
1511
+ params: { height: h },
1512
+ };
1513
+ APP.features.push(feature);
1514
+ addFeature(feature);
1515
+
1516
+ updateStatus(`Created extrusion: ${h}mm`);
1517
+ document.getElementById('mode-indicator').textContent = 'Ready';
1518
+ document.getElementById('mode-value').textContent = 'Normal';
1519
+ } catch (err) {
1520
+ console.error('Extrude failed:', err);
1521
+ updateStatus('Extrude failed: ' + err.message);
1522
+ }
1523
+ }
1524
+
1525
+ function executeParsedPrompt(prompt) {
1526
+ try {
1527
+ const splash = document.getElementById('welcome-splash');
1528
+ if (splash) splash.classList.add('hidden');
1529
+
1530
+ const result = createPrimitive(prompt.type || prompt.action, prompt.params || prompt);
1531
+ addToScene(result.mesh || result);
1532
+
1533
+ const feature = {
1534
+ id: 'feature_' + Date.now(),
1535
+ name: prompt.name || prompt.type || prompt.action || 'Part',
1536
+ type: prompt.type || prompt.action,
1537
+ mesh: result.mesh || result,
1538
+ params: prompt.params || prompt,
1539
+ };
1540
+ APP.features.push(feature);
1541
+ addFeature(feature);
1542
+ updateStatus(`Created: ${feature.name}`);
1543
+ } catch (err) {
1544
+ console.error('AI create failed:', err);
1545
+ updateStatus('Failed: ' + err.message);
1546
+ }
1547
+ }
1548
+
1549
+ function cancelOperation() {
1550
+ if (APP.mode === 'sketch') {
1551
+ endSketch();
1552
+ APP.currentSketch = null;
1553
+ APP.mode = 'idle';
1554
+ updateStatus('Sketch cancelled');
1555
+ document.getElementById('mode-indicator').textContent = 'Ready';
1556
+ document.getElementById('mode-value').textContent = 'Normal';
1557
+ }
1558
+ }
1559
+
1560
+ function confirmOperation() {
1561
+ if (APP.mode === 'sketch') {
1562
+ const entities = getEntities();
1563
+ if (entities && entities.length > 0) doExtrude();
1564
+ }
1565
+ }
1566
+
1567
+ function deleteSelected() {
1568
+ if (!APP.selectedFeature) return;
1569
+ removeFromScene(APP.selectedFeature.mesh);
1570
+ APP.features = APP.features.filter(f => f.id !== APP.selectedFeature.id);
1571
+ APP.selectedFeature = null;
1572
+ updateStatus('Feature deleted');
1573
+ }
1574
+
1575
+ function doExportSTL() {
1576
+ if (APP.features.length === 0) { updateStatus('Nothing to export'); return; }
1577
+ try {
1578
+ exportSTL(APP.features);
1579
+ updateStatus('Exported STL');
1580
+ } catch (err) {
1581
+ updateStatus('Export failed: ' + err.message);
1582
+ }
1583
+ }
1584
+
1585
+ function saveProject() {
1586
+ const data = {
1587
+ version: '1.0',
1588
+ timestamp: new Date().toISOString(),
1589
+ features: APP.features.map(f => ({ id: f.id, name: f.name, type: f.type, params: f.params })),
1590
+ };
1591
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1592
+ const url = URL.createObjectURL(blob);
1593
+ const a = document.createElement('a');
1594
+ a.href = url;
1595
+ a.download = `cyclecad_project_${Date.now()}.json`;
1596
+ a.click();
1597
+ URL.revokeObjectURL(url);
1598
+ updateStatus('Project saved');
1599
+ }
1600
+
1601
+ function updateStatus(msg) {
1602
+ const el = document.getElementById('status-bar');
1603
+ if (el) el.textContent = msg;
1604
+ // Also update mode indicator if it exists
1605
+ const modeEl = document.getElementById('mode-indicator');
1606
+ if (modeEl && APP.mode === 'idle') modeEl.textContent = 'Ready';
1607
+ }
1608
+
1609
+ // ========== FPS Counter ==========
1610
+ let frameCount = 0;
1611
+ let lastFPSTime = performance.now();
1612
+ function updateFPS() {
1613
+ frameCount++;
1614
+ const now = performance.now();
1615
+ if (now - lastFPSTime >= 1000) {
1616
+ const fpsEl = document.getElementById('fps-value');
1617
+ if (fpsEl) fpsEl.textContent = frameCount;
1618
+ frameCount = 0;
1619
+ lastFPSTime = now;
1620
+ }
1621
+ requestAnimationFrame(updateFPS);
1622
+ }
1623
+ requestAnimationFrame(updateFPS);
1624
+
1625
+ // ========== Start ==========
1626
+ if (document.readyState === 'loading') {
1627
+ document.addEventListener('DOMContentLoaded', init);
1628
+ } else {
1629
+ init();
1630
+ }
1631
+
1632
+ window.cycleCAD = { version: '1.0.0', APP, init };
1633
+ </script>
1634
+ </body>
1635
+ </html>