cyclecad 0.1.9 → 0.2.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.
@@ -1,1312 +1,2008 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>cycleCAD Agent Demo — Watch an AI design a part</title>
7
- <style>
8
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
9
- :root {
10
- --bg: #0A1628; --bg2: #122240; --bg3: #1A2D50;
11
- --text: #F0F0E8; --muted: #8B9AB5; --dim: #5A6B85;
12
- --gold: #D4A843; --blue: #2E86DE; --teal: #3AAFA9;
13
- --purple: #8B6FC0; --warn: #E8963A; --coral: #E05555;
14
- --green: #2ECC71;
15
- }
16
- body { background: var(--bg); color: var(--text); font-family: 'Inter', -apple-system, sans-serif; min-height: 100vh; }
17
-
18
- .header { padding: 20px 40px; display: flex; align-items: center; gap: 16px; border-bottom: 1px solid rgba(255,255,255,0.06); }
19
- .header .logo { font-size: 20px; font-weight: 800; }
20
- .header .logo .cy { color: var(--gold); }
21
- .header .logo .ca { color: var(--blue); }
22
- .header .tag { color: var(--muted); font-size: 13px; margin-left: auto; }
23
-
24
- .main { display: grid; grid-template-columns: 1fr 1fr; height: calc(100vh - 65px); }
25
-
26
- /* LEFT: Agent terminal */
27
- .terminal { background: #0D1117; border-right: 1px solid rgba(255,255,255,0.06); display: flex; flex-direction: column; }
28
- .term-header { padding: 12px 20px; border-bottom: 1px solid rgba(255,255,255,0.06); display: flex; align-items: center; gap: 10px; }
29
- .term-header .dot { width: 10px; height: 10px; border-radius: 50%; }
30
- .dot-r { background: var(--coral); } .dot-y { background: var(--warn); } .dot-g { background: var(--green); }
31
- .term-header span { color: var(--muted); font-size: 12px; margin-left: 8px; }
32
- .term-body { flex: 1; overflow-y: auto; padding: 20px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 13px; line-height: 1.8; }
33
- .term-body .cmd { color: var(--green); }
34
- .term-body .cmd::before { content: '> '; color: var(--dim); }
35
- .term-body .res { color: var(--muted); padding-left: 16px; }
36
- .term-body .res.ok { color: var(--teal); }
37
- .term-body .res.err { color: var(--coral); }
38
- .term-body .comment { color: var(--dim); font-style: italic; }
39
- .term-body .agent { color: var(--purple); font-weight: 600; }
40
- .term-body .divider { border-top: 1px solid rgba(255,255,255,0.06); margin: 12px 0; }
41
- .cursor { display: inline-block; width: 8px; height: 16px; background: var(--green); animation: blink 1s step-end infinite; vertical-align: middle; margin-left: 2px; }
42
- @keyframes blink { 50% { opacity: 0; } }
43
-
44
- /* RIGHT: 3D viewport */
45
- .viewport { position: relative; background: var(--bg); }
46
- .viewport iframe { width: 100%; height: 100%; border: none; }
47
- .viewport .overlay { position: absolute; top: 12px; right: 12px; background: rgba(10,22,40,0.85); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 12px 16px; }
48
- .viewport .overlay h4 { font-size: 11px; color: var(--gold); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
49
- .viewport .overlay .stat { font-size: 12px; color: var(--muted); margin: 3px 0; }
50
- .viewport .overlay .stat b { color: var(--text); }
51
-
52
- /* Controls */
53
- .controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px; z-index: 10; }
54
- .controls button {
55
- padding: 10px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;
56
- }
57
- .btn-run { background: linear-gradient(135deg, var(--gold), var(--blue)); color: #000; }
58
- .btn-run:hover { opacity: 0.9; transform: translateY(-1px); }
59
- .btn-reset { background: var(--bg2); color: var(--muted); border: 1px solid rgba(255,255,255,0.1) !important; }
60
- .btn-reset:hover { color: var(--text); }
61
- .btn-schema { background: var(--bg2); color: var(--purple); border: 1px solid rgba(139,111,192,0.3) !important; }
62
-
63
- /* Progress bar */
64
- .progress { position: absolute; bottom: 0; left: 0; width: 100%; height: 3px; background: var(--bg2); }
65
- .progress-bar { height: 100%; width: 0%; background: linear-gradient(90deg, var(--gold), var(--blue), var(--teal)); transition: width 0.3s; }
66
- </style>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>cycleCAD Agent Demo v2.0</title>
7
+ <script async src="https://cdn.jsdelivr.net/npm/three@r170/build/three.min.js"></script>
8
+ <script async src="https://cdn.jsdelivr.net/npm/three@r170/examples/js/controls/OrbitControls.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
18
+ background: #1a1a1a;
19
+ color: #e0e0e0;
20
+ height: 100vh;
21
+ overflow: hidden;
22
+ }
23
+
24
+ .container {
25
+ display: flex;
26
+ flex-direction: column;
27
+ height: 100vh;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #2a2a3e 0%, #1f1f2e 100%);
32
+ padding: 12px 16px;
33
+ border-bottom: 1px solid #404050;
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .header h1 {
41
+ font-size: 18px;
42
+ font-weight: 600;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 10px;
46
+ }
47
+
48
+ .header-icon {
49
+ width: 24px;
50
+ height: 24px;
51
+ background: linear-gradient(135deg, #00d4ff, #0099ff);
52
+ border-radius: 4px;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ font-size: 12px;
57
+ color: white;
58
+ font-weight: bold;
59
+ }
60
+
61
+ .main {
62
+ display: flex;
63
+ flex: 1;
64
+ overflow: hidden;
65
+ }
66
+
67
+ .split-pane {
68
+ display: flex;
69
+ width: 100%;
70
+ position: relative;
71
+ }
72
+
73
+ .terminal-section {
74
+ background: #0d0d0d;
75
+ border-right: 1px solid #404050;
76
+ display: flex;
77
+ flex-direction: column;
78
+ overflow: hidden;
79
+ }
80
+
81
+ .viewport-section {
82
+ flex: 1;
83
+ display: flex;
84
+ flex-direction: column;
85
+ background: #1a1a1a;
86
+ overflow: hidden;
87
+ }
88
+
89
+ .resize-handle {
90
+ width: 4px;
91
+ cursor: col-resize;
92
+ background: #404050;
93
+ transition: background 0.2s;
94
+ }
95
+
96
+ .resize-handle:hover {
97
+ background: #00d4ff;
98
+ }
99
+
100
+ /* Terminal Styles */
101
+ .terminal-header {
102
+ background: #1a1a2e;
103
+ padding: 10px 12px;
104
+ border-bottom: 1px solid #404050;
105
+ font-size: 13px;
106
+ font-weight: 600;
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.5px;
109
+ color: #00d4ff;
110
+ flex-shrink: 0;
111
+ }
112
+
113
+ .terminal-output {
114
+ flex: 1;
115
+ overflow-y: auto;
116
+ padding: 12px;
117
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
118
+ font-size: 12px;
119
+ line-height: 1.5;
120
+ }
121
+
122
+ .terminal-output::-webkit-scrollbar {
123
+ width: 8px;
124
+ }
125
+
126
+ .terminal-output::-webkit-scrollbar-track {
127
+ background: #0d0d0d;
128
+ }
129
+
130
+ .terminal-output::-webkit-scrollbar-thumb {
131
+ background: #404050;
132
+ border-radius: 4px;
133
+ }
134
+
135
+ .terminal-output::-webkit-scrollbar-thumb:hover {
136
+ background: #505060;
137
+ }
138
+
139
+ .terminal-line {
140
+ margin-bottom: 8px;
141
+ padding: 4px 8px;
142
+ border-radius: 3px;
143
+ word-break: break-word;
144
+ }
145
+
146
+ .terminal-line.input {
147
+ color: #00d4ff;
148
+ background: rgba(0, 212, 255, 0.05);
149
+ }
150
+
151
+ .terminal-line.output {
152
+ color: #e0e0e0;
153
+ }
154
+
155
+ .terminal-line.error {
156
+ color: #ff6b6b;
157
+ background: rgba(255, 107, 107, 0.05);
158
+ }
159
+
160
+ .terminal-line.success {
161
+ color: #51cf66;
162
+ background: rgba(81, 207, 102, 0.05);
163
+ }
164
+
165
+ .terminal-line.agent {
166
+ display: flex;
167
+ gap: 8px;
168
+ align-items: flex-start;
169
+ padding: 6px 8px;
170
+ background: rgba(100, 100, 120, 0.15);
171
+ border-left: 3px solid #6c7a9e;
172
+ }
173
+
174
+ .agent-badge {
175
+ flex-shrink: 0;
176
+ width: 24px;
177
+ height: 24px;
178
+ border-radius: 50%;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ font-size: 10px;
183
+ font-weight: bold;
184
+ color: white;
185
+ }
186
+
187
+ .agent-badge.design {
188
+ background: #ffd700;
189
+ }
190
+
191
+ .agent-badge.manufacturing {
192
+ background: #20b2aa;
193
+ }
194
+
195
+ .agent-badge.review {
196
+ background: #9370db;
197
+ }
198
+
199
+ .agent-content {
200
+ flex: 1;
201
+ }
202
+
203
+ /* Input Section */
204
+ .input-section {
205
+ background: #1a1a2e;
206
+ border-top: 1px solid #404050;
207
+ padding: 10px 12px;
208
+ display: flex;
209
+ gap: 8px;
210
+ flex-shrink: 0;
211
+ }
212
+
213
+ .input-wrapper {
214
+ flex: 1;
215
+ display: flex;
216
+ gap: 8px;
217
+ align-items: center;
218
+ position: relative;
219
+ }
220
+
221
+ .voice-indicator {
222
+ width: 24px;
223
+ height: 24px;
224
+ border-radius: 50%;
225
+ background: #404050;
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ cursor: pointer;
230
+ transition: all 0.2s;
231
+ flex-shrink: 0;
232
+ }
233
+
234
+ .voice-indicator:hover {
235
+ background: #505060;
236
+ }
237
+
238
+ .voice-indicator.active {
239
+ background: #ff3333;
240
+ box-shadow: 0 0 10px rgba(255, 51, 51, 0.5);
241
+ animation: pulse 1s infinite;
242
+ }
243
+
244
+ @keyframes pulse {
245
+ 0%, 100% { opacity: 1; }
246
+ 50% { opacity: 0.7; }
247
+ }
248
+
249
+ .voice-waveform {
250
+ width: 32px;
251
+ height: 20px;
252
+ display: flex;
253
+ align-items: flex-end;
254
+ gap: 2px;
255
+ justify-content: center;
256
+ }
257
+
258
+ .voice-bar {
259
+ width: 2px;
260
+ background: #ff3333;
261
+ border-radius: 1px;
262
+ animation: waveform 0.3s ease-in-out infinite;
263
+ }
264
+
265
+ @keyframes waveform {
266
+ 0%, 100% { height: 4px; }
267
+ 50% { height: 12px; }
268
+ }
269
+
270
+ .command-input {
271
+ flex: 1;
272
+ background: #2a2a3e;
273
+ border: 1px solid #404050;
274
+ color: #e0e0e0;
275
+ padding: 8px 12px;
276
+ border-radius: 4px;
277
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
278
+ font-size: 12px;
279
+ }
280
+
281
+ .command-input:focus {
282
+ outline: none;
283
+ border-color: #00d4ff;
284
+ box-shadow: 0 0 8px rgba(0, 212, 255, 0.2);
285
+ }
286
+
287
+ .autocomplete-list {
288
+ position: absolute;
289
+ bottom: 50px;
290
+ left: 12px;
291
+ background: #2a2a3e;
292
+ border: 1px solid #404050;
293
+ border-radius: 4px;
294
+ max-height: 150px;
295
+ overflow-y: auto;
296
+ z-index: 100;
297
+ min-width: 200px;
298
+ display: none;
299
+ }
300
+
301
+ .autocomplete-list.visible {
302
+ display: block;
303
+ }
304
+
305
+ .autocomplete-item {
306
+ padding: 6px 12px;
307
+ cursor: pointer;
308
+ border-bottom: 1px solid #404050;
309
+ transition: background 0.2s;
310
+ }
311
+
312
+ .autocomplete-item:hover,
313
+ .autocomplete-item.selected {
314
+ background: #00d4ff;
315
+ color: #0d0d0d;
316
+ }
317
+
318
+ .input-actions {
319
+ display: flex;
320
+ gap: 6px;
321
+ }
322
+
323
+ .input-btn {
324
+ background: #2a2a3e;
325
+ border: 1px solid #404050;
326
+ color: #e0e0e0;
327
+ padding: 8px 12px;
328
+ border-radius: 4px;
329
+ cursor: pointer;
330
+ transition: all 0.2s;
331
+ font-size: 12px;
332
+ white-space: nowrap;
333
+ }
334
+
335
+ .input-btn:hover {
336
+ background: #404050;
337
+ border-color: #00d4ff;
338
+ }
339
+
340
+ .input-btn.primary {
341
+ background: #00d4ff;
342
+ color: #0d0d0d;
343
+ border-color: #00d4ff;
344
+ font-weight: 600;
345
+ }
346
+
347
+ .input-btn.primary:hover {
348
+ background: #00a8cc;
349
+ border-color: #00a8cc;
350
+ }
351
+
352
+ /* Viewport Styles */
353
+ .viewport-toolbar {
354
+ background: #1a1a2e;
355
+ border-bottom: 1px solid #404050;
356
+ padding: 8px 12px;
357
+ display: flex;
358
+ gap: 8px;
359
+ flex-shrink: 0;
360
+ }
361
+
362
+ .toolbar-group {
363
+ display: flex;
364
+ gap: 4px;
365
+ align-items: center;
366
+ }
367
+
368
+ .toolbar-separator {
369
+ width: 1px;
370
+ height: 24px;
371
+ background: #404050;
372
+ }
373
+
374
+ .viewport-btn {
375
+ background: #2a2a3e;
376
+ border: 1px solid #404050;
377
+ color: #e0e0e0;
378
+ padding: 6px 10px;
379
+ border-radius: 3px;
380
+ cursor: pointer;
381
+ transition: all 0.2s;
382
+ font-size: 11px;
383
+ white-space: nowrap;
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 4px;
387
+ }
388
+
389
+ .viewport-btn:hover {
390
+ background: #404050;
391
+ border-color: #00d4ff;
392
+ }
393
+
394
+ .viewport-btn.active {
395
+ background: #00d4ff;
396
+ color: #0d0d0d;
397
+ border-color: #00d4ff;
398
+ font-weight: 600;
399
+ }
400
+
401
+ #viewport {
402
+ flex: 1;
403
+ background: linear-gradient(135deg, #1a1a1a 0%, #252535 100%);
404
+ }
405
+
406
+ .viewport-status {
407
+ background: #1a1a2e;
408
+ border-top: 1px solid #404050;
409
+ padding: 8px 12px;
410
+ display: flex;
411
+ justify-content: space-between;
412
+ align-items: center;
413
+ flex-shrink: 0;
414
+ font-size: 11px;
415
+ }
416
+
417
+ .status-item {
418
+ display: flex;
419
+ gap: 4px;
420
+ align-items: center;
421
+ }
422
+
423
+ .status-label {
424
+ color: #808090;
425
+ text-transform: uppercase;
426
+ letter-spacing: 0.3px;
427
+ font-weight: 600;
428
+ }
429
+
430
+ .status-value {
431
+ color: #00d4ff;
432
+ font-weight: 600;
433
+ }
434
+
435
+ /* Feature Tree Sidebar */
436
+ .feature-tree {
437
+ background: #1a1a2e;
438
+ border-right: 1px solid #404050;
439
+ width: 220px;
440
+ display: flex;
441
+ flex-direction: column;
442
+ flex-shrink: 0;
443
+ transition: all 0.3s;
444
+ }
445
+
446
+ .feature-tree.collapsed {
447
+ width: 0;
448
+ overflow: hidden;
449
+ border-right: none;
450
+ }
451
+
452
+ .feature-tree-header {
453
+ background: #0d0d0d;
454
+ padding: 10px 12px;
455
+ border-bottom: 1px solid #404050;
456
+ font-size: 12px;
457
+ font-weight: 600;
458
+ text-transform: uppercase;
459
+ letter-spacing: 0.5px;
460
+ color: #00d4ff;
461
+ display: flex;
462
+ justify-content: space-between;
463
+ align-items: center;
464
+ }
465
+
466
+ .tree-toggle-btn {
467
+ background: none;
468
+ border: none;
469
+ color: #00d4ff;
470
+ cursor: pointer;
471
+ font-size: 12px;
472
+ padding: 0;
473
+ }
474
+
475
+ .feature-tree-list {
476
+ flex: 1;
477
+ overflow-y: auto;
478
+ padding: 8px 0;
479
+ }
480
+
481
+ .feature-tree-list::-webkit-scrollbar {
482
+ width: 6px;
483
+ }
484
+
485
+ .feature-tree-list::-webkit-scrollbar-track {
486
+ background: #1a1a2e;
487
+ }
488
+
489
+ .feature-tree-list::-webkit-scrollbar-thumb {
490
+ background: #404050;
491
+ border-radius: 3px;
492
+ }
493
+
494
+ .tree-item {
495
+ padding: 8px 12px;
496
+ border-left: 3px solid transparent;
497
+ cursor: pointer;
498
+ transition: all 0.2s;
499
+ font-size: 11px;
500
+ white-space: nowrap;
501
+ overflow: hidden;
502
+ text-overflow: ellipsis;
503
+ }
504
+
505
+ .tree-item:hover {
506
+ background: rgba(0, 212, 255, 0.1);
507
+ border-left-color: #00d4ff;
508
+ }
509
+
510
+ .tree-item.active {
511
+ background: rgba(0, 212, 255, 0.2);
512
+ border-left-color: #00d4ff;
513
+ color: #00d4ff;
514
+ font-weight: 600;
515
+ }
516
+
517
+ .tree-item-icon {
518
+ margin-right: 6px;
519
+ font-size: 10px;
520
+ }
521
+
522
+ /* Responsive */
523
+ @media (max-width: 768px) {
524
+ .split-pane {
525
+ flex-direction: column;
526
+ }
527
+
528
+ .terminal-section {
529
+ height: 40%;
530
+ border-right: none;
531
+ border-bottom: 1px solid #404050;
532
+ }
533
+
534
+ .viewport-section {
535
+ height: 60%;
536
+ }
537
+
538
+ .resize-handle {
539
+ width: 100%;
540
+ height: 4px;
541
+ cursor: row-resize;
542
+ }
543
+
544
+ .feature-tree {
545
+ width: 100%;
546
+ max-height: 150px;
547
+ }
548
+
549
+ .feature-tree.collapsed {
550
+ max-height: 0;
551
+ }
552
+ }
553
+ </style>
67
554
  </head>
68
555
  <body>
69
- <div class="header">
70
- <div class="logo"><span class="cy">cycle</span><span class="ca">CAD</span></div>
71
- <div style="color:var(--dim);font-size:13px;">Agent Demo</div>
72
- <div class="tag">The Agent-First OS for Manufacturing</div>
73
- </div>
74
-
75
- <div class="main">
76
- <!-- LEFT: Terminal showing agent commands -->
77
- <div class="terminal">
78
- <div class="term-header">
79
- <div class="dot dot-r"></div><div class="dot dot-y"></div><div class="dot dot-g"></div>
80
- <span>AI Agent — Bracket Design Task</span>
81
- </div>
82
- <div class="term-body" id="term"></div>
83
- <div class="progress"><div class="progress-bar" id="progress"></div></div>
556
+ <div class="container">
557
+ <div class="header">
558
+ <div style="display: flex; align-items: center; gap: 12px; flex: 1;">
559
+ <div class="header-icon">🤖</div>
560
+ <h1>cycleCAD Agent Demo</h1>
561
+ <span style="color: #808090; font-size: 12px; font-weight: normal;">v2.0 · Agent-First CAD</span>
562
+ </div>
563
+ <div style="color: #808090; font-size: 11px;">
564
+ <span id="session-time">Session: 00:00</span>
565
+ </div>
566
+ </div>
567
+
568
+ <div class="main">
569
+ <div class="feature-tree">
570
+ <div class="feature-tree-header">
571
+ Features
572
+ <button class="tree-toggle-btn" onclick="toggleFeatureTree()">−</button>
573
+ </div>
574
+ <div class="feature-tree-list" id="feature-tree-list">
575
+ <div style="padding: 12px; color: #808090; text-align: center; font-size: 11px;">
576
+ Create something to begin
577
+ </div>
578
+ </div>
579
+ </div>
580
+
581
+ <div class="split-pane" id="split-pane">
582
+ <div class="terminal-section" style="width: 35%;">
583
+ <div class="terminal-header">Terminal</div>
584
+ <div class="terminal-output" id="terminal-output">
585
+ <div class="terminal-line success">
586
+ Welcome to cycleCAD Agent Demo v2.0
587
+ </div>
588
+ <div class="terminal-line success">
589
+ Try: "create cylinder 50mm diameter 80mm tall"
590
+ </div>
591
+ </div>
592
+ <div class="autocomplete-list" id="autocomplete-list"></div>
593
+ <div class="input-section">
594
+ <div class="input-wrapper">
595
+ <div class="voice-indicator" id="voice-btn" title="Click to toggle voice input">
596
+ 🎤
597
+ </div>
598
+ <div class="voice-waveform" id="voice-waveform" style="display: none;">
599
+ <div class="voice-bar" style="animation-delay: 0s;"></div>
600
+ <div class="voice-bar" style="animation-delay: 0.1s;"></div>
601
+ <div class="voice-bar" style="animation-delay: 0.2s;"></div>
602
+ </div>
603
+ <input
604
+ type="text"
605
+ id="command-input"
606
+ class="command-input"
607
+ placeholder="Enter command or speak..."
608
+ autocomplete="off"
609
+ >
610
+ </div>
611
+ <div class="input-actions">
612
+ <button class="input-btn primary" onclick="sendCommand()">Send</button>
613
+ <button class="input-btn" onclick="showExamples()" title="Show example commands">Examples</button>
614
+ </div>
615
+ </div>
616
+ </div>
617
+
618
+ <div class="resize-handle" id="resize-handle"></div>
619
+
620
+ <div class="viewport-section">
621
+ <div class="viewport-toolbar">
622
+ <div class="toolbar-group">
623
+ <button class="viewport-btn" onclick="resetView()" title="Reset camera view">Reset View</button>
624
+ <button class="viewport-btn" onclick="fitToObject()" title="Fit to object">Fit All</button>
625
+ </div>
626
+ <div class="toolbar-separator"></div>
627
+ <div class="toolbar-group">
628
+ <button class="viewport-btn" id="wireframe-btn" onclick="toggleWireframe()" title="Toggle wireframe">Wireframe</button>
629
+ <button class="viewport-btn" id="grid-btn" onclick="toggleGrid()" title="Toggle grid">Grid</button>
630
+ <button class="viewport-btn" id="shadows-btn" onclick="toggleShadows()" title="Toggle shadows">Shadows</button>
631
+ </div>
632
+ <div class="toolbar-separator"></div>
633
+ <div class="toolbar-group">
634
+ <button class="viewport-btn" onclick="undoOperation()" title="Undo (Ctrl+Z)">↶ Undo</button>
635
+ <button class="viewport-btn" onclick="redoOperation()" title="Redo (Ctrl+Y)">↷ Redo</button>
636
+ </div>
637
+ </div>
638
+ <canvas id="viewport"></canvas>
639
+ <div class="viewport-status">
640
+ <div class="status-item">
641
+ <span class="status-label">Part:</span>
642
+ <span class="status-value" id="status-part">None</span>
643
+ </div>
644
+ <div class="status-item">
645
+ <span class="status-label">Features:</span>
646
+ <span class="status-value" id="status-features">0</span>
647
+ </div>
648
+ <div class="status-item">
649
+ <span class="status-label">Latency:</span>
650
+ <span class="status-value" id="status-latency">0ms</span>
651
+ </div>
652
+ <div class="status-item">
653
+ <span class="status-label">FPS:</span>
654
+ <span class="status-value" id="status-fps">0</span>
655
+ </div>
656
+ </div>
657
+ </div>
658
+ </div>
659
+ </div>
84
660
  </div>
85
661
 
86
- <!-- RIGHT: 3D viewport (placeholder) -->
87
- <div class="viewport">
88
- <canvas id="viewport3d" style="width:100%;height:100%;"></canvas>
89
- <div class="overlay" id="stats-overlay" style="display:none;">
90
- <h4>Part Stats</h4>
91
- <div class="stat">Size: <b id="stat-size">—</b></div>
92
- <div class="stat">Material: <b id="stat-mat">—</b></div>
93
- <div class="stat">Printable: <b id="stat-print">—</b></div>
94
- <div class="stat">Cost: <b id="stat-cost">—</b></div>
95
- <div class="stat">Commands: <b id="stat-cmds">0</b></div>
96
- <div class="stat">Time: <b id="stat-time">—</b></div>
97
- </div>
98
- </div>
99
- </div>
100
-
101
- <!-- Example chips -->
102
- <div id="example-chips" style="position:absolute;bottom:118px;left:50%;transform:translateX(-50%);z-index:11;display:flex;gap:6px;flex-wrap:wrap;justify-content:center;max-width:720px;">
103
- <button class="chip" onclick="fillExample('build a cylinder 50mm diameter 80mm tall in steel')">cylinder</button>
104
- <button class="chip" onclick="fillExample('gear 24 teeth module 2.5 in brass')">gear</button>
105
- <button class="chip" onclick="fillExample('bracket 120x60 4 M8 holes 15mm from edge fillet 8')">bracket</button>
106
- <button class="chip" onclick="fillExample('hex bolt M12 40mm long')">hex bolt</button>
107
- <button class="chip" onclick="fillExample('flange 100mm diameter inner 40 6 M6 holes')">flange</button>
108
- <button class="chip" onclick="fillExample('tube outer 50 inner 40 height 80 in aluminum')">tube</button>
109
- <button class="chip" onclick="fillExample('sphere radius 30 in titanium')">sphere</button>
110
- <button class="chip" onclick="fillExample('add a hole radius 8')">+ hole</button>
111
- <button class="chip" onclick="fillExample('fillet 5')">+ fillet</button>
112
- <button class="chip" onclick="fillExample('shell thickness 2')">+ shell</button>
113
- <button class="chip" onclick="fillExample('pattern circular 6 copies')">+ pattern</button>
114
- <button class="chip" onclick="fillExample('change material to steel')">material</button>
115
- <button class="chip" onclick="fillExample('validate')">validate</button>
116
- <button class="chip" onclick="fillExample('export stl')">export</button>
117
- </div>
118
- <style>
119
- .chip { padding:5px 12px;border:1px solid rgba(255,255,255,0.12);border-radius:16px;background:rgba(18,34,64,0.8);color:var(--muted);font-size:11px;cursor:pointer;transition:all 0.15s;font-family:inherit; }
120
- .chip:hover { background:rgba(46,134,222,0.2);color:var(--text);border-color:rgba(46,134,222,0.4); }
121
- </style>
122
-
123
- <!-- Voice input bar -->
124
- <div id="voice-bar" style="position:absolute;bottom:70px;left:50%;transform:translateX(-50%);z-index:11;display:flex;align-items:center;gap:10px;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:8px 16px;min-width:500px;max-width:720px;">
125
- <button id="btn-mic" onclick="toggleVoice()" style="width:40px;height:40px;border-radius:50%;border:2px solid var(--coral);background:transparent;color:var(--coral);font-size:18px;cursor:pointer;transition:all 0.2s;flex-shrink:0;">🎤</button>
126
- <input id="voice-input" type="text" placeholder="Describe what to build or modify..." style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;font-family:inherit;outline:none;" onkeydown="if(event.key==='Enter')executeVoiceCommand()">
127
- <button onclick="executeVoiceCommand()" style="padding:8px 16px;border:none;border-radius:8px;background:linear-gradient(135deg,var(--gold),var(--blue));color:#000;font-size:13px;font-weight:700;cursor:pointer;flex-shrink:0;">Go</button>
128
- <div id="voice-status" style="position:absolute;top:-22px;left:16px;font-size:11px;color:var(--dim);"></div>
129
- </div>
130
-
131
- <!-- Feature count badge -->
132
- <div id="feature-badge" style="position:absolute;bottom:70px;right:20px;z-index:11;display:none;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;font-size:11px;color:var(--muted);max-width:180px;">
133
- <div style="color:var(--gold);font-weight:600;margin-bottom:4px;">FEATURES</div>
134
- <div id="feature-list" style="line-height:1.6;"></div>
135
- </div>
136
-
137
- <div class="controls">
138
- <button class="btn-run" id="btn-run" onclick="runDemo()">▶ Run Agent Demo</button>
139
- <button class="btn-reset" onclick="resetDemo()">↺ Reset</button>
140
- <button class="btn-schema" onclick="showSchema()">{ } API Schema</button>
141
- </div>
142
-
143
- <script type="importmap">
144
- { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js" } }
145
- </script>
146
- <script type="module">
147
- import * as THREE from 'three';
148
- import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/controls/OrbitControls.js';
149
-
150
- // ========== Mini 3D viewport ==========
151
- const canvas = document.getElementById('viewport3d');
152
- const scene = new THREE.Scene();
153
- scene.background = new THREE.Color(0x0A1628);
154
- const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
155
- camera.position.set(60, 50, 80);
156
- const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
157
- renderer.setSize(canvas.clientWidth, canvas.clientHeight);
158
- renderer.setPixelRatio(window.devicePixelRatio);
159
- const controls = new OrbitControls(camera, renderer.domElement);
160
- controls.enableDamping = true;
161
- controls.dampingFactor = 0.05;
162
-
163
- // Lights
164
- scene.add(new THREE.AmbientLight(0xffffff, 0.4));
165
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
166
- dirLight.position.set(30, 50, 40);
167
- scene.add(dirLight);
168
-
169
- // Grid
170
- const grid = new THREE.GridHelper(200, 40, 0x1E3A5F, 0x122240);
171
- scene.add(grid);
172
-
173
- // Animate
174
- function animate() {
175
- requestAnimationFrame(animate);
176
- controls.update();
177
- renderer.render(scene, camera);
178
- }
179
- animate();
180
- window.addEventListener('resize', () => {
181
- camera.aspect = canvas.clientWidth / canvas.clientHeight;
182
- camera.updateProjectionMatrix();
183
- renderer.setSize(canvas.clientWidth, canvas.clientHeight);
184
- });
185
-
186
- // ========== Expose for demo script ==========
187
- window._scene = scene;
188
- window._camera = camera;
189
- window._renderer = renderer;
190
-
191
- window.addMeshToScene = (mesh) => { scene.add(mesh); };
192
- window.clearScene = () => {
193
- const toRemove = [];
194
- scene.traverse(c => { if (c.isMesh) toRemove.push(c); });
195
- toRemove.forEach(m => { scene.remove(m); m.geometry?.dispose(); m.material?.dispose(); });
196
- };
197
- window.THREE = THREE;
198
- </script>
199
-
200
- <script>
201
- const term = document.getElementById('term');
202
- const progress = document.getElementById('progress');
203
- let running = false;
204
- let cmdCount = 0;
205
- let startTime = 0;
206
-
207
- function addLine(html, cls = '') {
208
- const div = document.createElement('div');
209
- div.className = cls;
210
- div.innerHTML = html;
211
- term.appendChild(div);
212
- term.scrollTop = term.scrollHeight;
213
- }
214
-
215
- function addDivider() {
216
- const div = document.createElement('div');
217
- div.className = 'divider';
218
- term.appendChild(div);
219
- }
220
-
221
- function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
222
-
223
- function updateStats(data) {
224
- document.getElementById('stats-overlay').style.display = 'block';
225
- if (data.size) document.getElementById('stat-size').textContent = data.size;
226
- if (data.material) document.getElementById('stat-mat').textContent = data.material;
227
- if (data.printable !== undefined) document.getElementById('stat-print').textContent = data.printable ? '✅ Yes' : '❌ No';
228
- if (data.cost) document.getElementById('stat-cost').textContent = data.cost;
229
- document.getElementById('stat-cmds').textContent = cmdCount;
230
- document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
231
- }
232
-
233
- // ========== Demo Script ==========
234
- // This simulates what an AI agent does when it calls cycleCAD's API
235
-
236
- const DEMO_STEPS = [
237
- // Agent thinks
238
- { type: 'agent', text: '🤖 Agent received task: "Design a mounting bracket for the DUO cycleWASH with 4 M6 bolt holes"' },
239
- { type: 'comment', text: '// Agent analyzes requirements: 80×40mm base plate, 5mm thick, aluminum, 4 corner holes' },
240
- { type: 'delay', ms: 600 },
241
-
242
- // Phase 1: Sketch
243
- { type: 'divider' },
244
- { type: 'agent', text: '📐 Phase 1: Sketch the base profile' },
245
- { type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } },
246
- { type: 'cmd', method: 'sketch.rect', params: { x: -40, y: -20, width: 80, height: 40 } },
247
- { type: 'delay', ms: 300 },
248
-
249
- // Phase 2: Extrude
250
- { type: 'divider' },
251
- { type: 'agent', text: '📦 Phase 2: Extrude to 3D solid' },
252
- { type: 'cmd', method: 'ops.extrude', params: { height: 5, material: 'aluminum' }, mesh: 'bracket' },
253
- { type: 'delay', ms: 400 },
254
-
255
- // Phase 3: Add bolt holes (primitives positioned at corners)
256
- { type: 'divider' },
257
- { type: 'agent', text: '🔩 Phase 3: Add 4 M6 bolt holes at corners' },
258
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole1', pos: [-30, 2.5, -12] },
259
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole2', pos: [30, 2.5, -12] },
260
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole3', pos: [-30, 2.5, 12] },
261
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole4', pos: [30, 2.5, 12] },
262
- { type: 'delay', ms: 300 },
263
-
264
- // Phase 4: Fillet edges
265
- { type: 'divider' },
266
- { type: 'agent', text: '✨ Phase 4: Fillet edges for stress relief' },
267
- { type: 'cmd', method: 'ops.fillet', params: { target: 'bracket', radius: 15 }, mesh: 'fillet' },
268
- { type: 'delay', ms: 300 },
269
-
270
- // Phase 5: Validate
271
- { type: 'divider' },
272
- { type: 'agent', text: '🔍 Phase 5: Validate for manufacturing' },
273
- { type: 'cmd', method: 'validate.dimensions', params: { target: 'bracket' }, stat: 'size' },
274
- { type: 'cmd', method: 'validate.printability', params: { target: 'bracket', process: 'CNC' }, stat: 'printable' },
275
- { type: 'cmd', method: 'validate.cost', params: { target: 'bracket', process: 'CNC', material: 'aluminum' }, stat: 'cost' },
276
- { type: 'delay', ms: 400 },
277
-
278
- // Phase 6: Export
279
- { type: 'divider' },
280
- { type: 'agent', text: '📤 Phase 6: Export for manufacturing' },
281
- { type: 'cmd', method: 'export.stl', params: { filename: 'duo-bracket.stl', binary: true } },
282
- { type: 'delay', ms: 200 },
283
-
284
- // Done
285
- { type: 'divider' },
286
- { type: 'agent', text: '✅ Task complete. Bracket designed, validated, and exported in {TIME}s using {CMDS} API calls.' },
287
- { type: 'comment', text: '// No human touched a mouse. No GUI was opened. The agent designed through cycleCAD.' },
288
- ];
289
-
290
- async function runDemo() {
291
- if (running) return;
292
- running = true;
293
- cmdCount = 0;
294
- startTime = Date.now();
295
- document.getElementById('btn-run').disabled = true;
296
- document.getElementById('btn-run').textContent = '⏳ Running...';
297
- term.innerHTML = '';
298
- window.clearScene();
299
- document.getElementById('stats-overlay').style.display = 'none';
300
-
301
- const totalSteps = DEMO_STEPS.filter(s => s.type === 'cmd').length;
302
- let cmdIdx = 0;
303
-
304
- for (const step of DEMO_STEPS) {
305
- if (!running) break;
306
-
307
- if (step.type === 'agent') {
308
- let text = step.text
309
- .replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1))
310
- .replace('{CMDS}', cmdCount);
311
- addLine(text, 'agent');
312
- await delay(200);
313
- }
314
- else if (step.type === 'comment') {
315
- addLine(step.text, 'comment');
316
- await delay(100);
317
- }
318
- else if (step.type === 'divider') {
319
- addDivider();
320
- }
321
- else if (step.type === 'delay') {
322
- await delay(step.ms);
323
- }
324
- else if (step.type === 'cmd') {
325
- cmdCount++;
326
- cmdIdx++;
327
- progress.style.width = (cmdIdx / totalSteps * 100) + '%';
328
-
329
- const cmdStr = `cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`;
330
- addLine(cmdStr, 'cmd');
331
- await delay(150);
332
-
333
- // Simulate the result
334
- const result = simulateCommand(step);
335
- const resStr = JSON.stringify(result, null, 0);
336
- addLine(`→ ${resStr}`, 'res ok');
337
-
338
- // Update stats
339
- if (step.stat === 'size') updateStats({ size: '80 × 40 × 5 mm' });
340
- if (step.stat === 'printable') updateStats({ printable: true });
341
- if (step.stat === 'cost') updateStats({ cost: '$12.40 (CNC)' });
342
- updateStats({ material: 'Aluminum' });
343
-
344
- // Add 3D geometry
345
- if (step.mesh) {
346
- createMesh(step);
662
+ <script>
663
+ // Three.js Setup
664
+ let scene, camera, renderer, controls, currentMesh, gridHelper, shadowPlane;
665
+ let commandHistory = [];
666
+ let historyIndex = -1;
667
+ let isListening = false;
668
+ let recognition = null;
669
+ let lastCommandTime = 0;
670
+ let operationHistory = [];
671
+ let historyPointer = 0;
672
+ let wireframeMode = false;
673
+ let gridVisible = true;
674
+ let shadowsVisible = true;
675
+
676
+ // Scene State
677
+ const sceneState = {
678
+ currentPart: 'Unnamed Part',
679
+ features: [],
680
+ material: 'Steel',
681
+ dimensions: {}
682
+ };
683
+
684
+ // Initialize Three.js
685
+ function initThreeJS() {
686
+ const canvas = document.getElementById('viewport');
687
+
688
+ scene = new THREE.Scene();
689
+ scene.background = new THREE.Color(0x1a1a1a);
690
+ scene.fog = new THREE.Fog(0x1a1a1a, 500, 1000);
691
+
692
+ camera = new THREE.PerspectiveCamera(
693
+ 75,
694
+ canvas.clientWidth / canvas.clientHeight,
695
+ 0.1,
696
+ 10000
697
+ );
698
+ camera.position.set(200, 150, 200);
699
+
700
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
701
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
702
+ renderer.setPixelRatio(window.devicePixelRatio);
703
+ renderer.shadowMap.enabled = true;
704
+ renderer.shadowMap.type = THREE.PCFShadowShadowMap;
705
+
706
+ // Lights
707
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
708
+ scene.add(ambientLight);
709
+
710
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
711
+ directionalLight.position.set(100, 150, 100);
712
+ directionalLight.castShadow = true;
713
+ directionalLight.shadow.camera.left = -300;
714
+ directionalLight.shadow.camera.right = 300;
715
+ directionalLight.shadow.camera.top = 300;
716
+ directionalLight.shadow.camera.bottom = -300;
717
+ directionalLight.shadow.mapSize.width = 2048;
718
+ directionalLight.shadow.mapSize.height = 2048;
719
+ scene.add(directionalLight);
720
+
721
+ // Controls
722
+ controls = new THREE.OrbitControls(camera, renderer.domElement);
723
+ controls.enableDamping = true;
724
+ controls.dampingFactor = 0.05;
725
+ controls.autoRotate = false;
726
+
727
+ // Grid
728
+ gridHelper = new THREE.GridHelper(400, 40, 0x404050, 0x303040);
729
+ scene.add(gridHelper);
730
+
731
+ // Shadow plane
732
+ const planeGeometry = new THREE.PlaneGeometry(500, 500);
733
+ const planeMaterial = new THREE.ShadowMaterial({ opacity: 0.3 });
734
+ shadowPlane = new THREE.Mesh(planeGeometry, planeMaterial);
735
+ shadowPlane.rotateX(-Math.PI / 2);
736
+ shadowPlane.receiveShadow = true;
737
+ scene.add(shadowPlane);
738
+
739
+ // Handle resize
740
+ window.addEventListener('resize', onWindowResize);
741
+
742
+ // Animation loop
743
+ animate();
744
+ }
745
+
746
+ function onWindowResize() {
747
+ const canvas = document.getElementById('viewport');
748
+ if (!canvas) return;
749
+
750
+ const width = canvas.clientWidth;
751
+ const height = canvas.clientHeight;
752
+
753
+ camera.aspect = width / height;
754
+ camera.updateProjectionMatrix();
755
+ renderer.setSize(width, height);
756
+ }
757
+
758
+ function animate() {
759
+ requestAnimationFrame(animate);
760
+ controls.update();
761
+
762
+ // Update FPS
763
+ const now = performance.now();
764
+ if (!animate.lastTime) animate.lastTime = now;
765
+ const delta = now - animate.lastTime;
766
+ animate.lastTime = now;
767
+
768
+ if (delta > 0) {
769
+ const fps = Math.round(1000 / delta);
770
+ document.getElementById('status-fps').textContent = fps;
771
+ }
772
+
773
+ renderer.render(scene, camera);
774
+ }
775
+
776
+ // Voice Recognition
777
+ function initVoiceRecognition() {
778
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
779
+
780
+ if (!SpeechRecognition) {
781
+ logTerminal('Voice recognition not supported in your browser', 'error');
782
+ return;
783
+ }
784
+
785
+ recognition = new SpeechRecognition();
786
+ recognition.continuous = false;
787
+ recognition.interimResults = true;
788
+ recognition.lang = 'en-US';
789
+
790
+ const input = document.getElementById('command-input');
791
+
792
+ recognition.onstart = () => {
793
+ isListening = true;
794
+ document.getElementById('voice-btn').classList.add('active');
795
+ document.getElementById('voice-waveform').style.display = 'flex';
796
+ input.placeholder = 'Listening...';
797
+ };
798
+
799
+ recognition.onresult = (event) => {
800
+ let interimTranscript = '';
801
+ for (let i = event.resultIndex; i < event.results.length; i++) {
802
+ const transcript = event.results[i].transcript;
803
+ if (event.results[i].isFinal) {
804
+ input.value = transcript;
805
+ sendCommand();
806
+ } else {
807
+ interimTranscript += transcript;
808
+ }
809
+ }
810
+ if (interimTranscript) {
811
+ input.value = interimTranscript;
812
+ }
813
+ };
814
+
815
+ recognition.onerror = (event) => {
816
+ logTerminal(`Voice error: ${event.error}`, 'error');
817
+ };
818
+
819
+ recognition.onend = () => {
820
+ isListening = false;
821
+ document.getElementById('voice-btn').classList.remove('active');
822
+ document.getElementById('voice-waveform').style.display = 'none';
823
+ input.placeholder = 'Enter command or speak...';
824
+ };
825
+ }
826
+
827
+ // Voice Button Toggle
828
+ document.getElementById('voice-btn').addEventListener('click', () => {
829
+ if (!recognition) initVoiceRecognition();
830
+ if (isListening) {
831
+ recognition.stop();
832
+ } else {
833
+ try {
834
+ recognition.start();
835
+ } catch (e) {
836
+ logTerminal('Microphone access denied. Use text input instead.', 'error');
837
+ }
838
+ }
839
+ });
840
+
841
+ // Command Input
842
+ document.getElementById('command-input').addEventListener('keydown', (e) => {
843
+ if (e.key === 'Enter') {
844
+ sendCommand();
845
+ } else if (e.key === 'ArrowUp') {
846
+ e.preventDefault();
847
+ historyIndex = Math.min(historyIndex + 1, commandHistory.length - 1);
848
+ if (historyIndex >= 0) {
849
+ e.target.value = commandHistory[commandHistory.length - 1 - historyIndex];
850
+ }
851
+ } else if (e.key === 'ArrowDown') {
852
+ e.preventDefault();
853
+ historyIndex = Math.max(historyIndex - 1, -1);
854
+ if (historyIndex >= 0) {
855
+ e.target.value = commandHistory[commandHistory.length - 1 - historyIndex];
856
+ } else {
857
+ e.target.value = '';
858
+ }
859
+ } else if (e.key === 'Tab') {
860
+ e.preventDefault();
861
+ showAutocomplete(e.target.value);
862
+ }
863
+ });
864
+
865
+ document.getElementById('command-input').addEventListener('input', (e) => {
866
+ updateAutocomplete(e.target.value);
867
+ });
868
+
869
+ // Autocomplete
870
+ function updateAutocomplete(value) {
871
+ const list = document.getElementById('autocomplete-list');
872
+ if (value.length < 2) {
873
+ list.classList.remove('visible');
874
+ return;
875
+ }
876
+
877
+ const commands = [
878
+ 'create cylinder', 'create box', 'create sphere', 'create cone', 'create torus',
879
+ 'create plate', 'create gear', 'create bracket', 'create washer', 'create hexbolt', 'create flange',
880
+ 'draw circle', 'make a cylinder', 'build box',
881
+ 'add hole', 'drill hole', 'add fillet', 'round edges', 'add chamfer', 'bevel',
882
+ 'shell', 'hollow out', 'mirror', 'pattern', 'add thread',
883
+ 'extrude', 'revolve', 'sweep', 'loft', 'boolean cut', 'boolean union',
884
+ 'material steel', 'material brass', 'material aluminum', 'color red', 'color chrome',
885
+ 'move', 'rotate', 'scale', 'copy', 'delete',
886
+ 'measure', 'section', 'wireframe', 'grid', 'shadows',
887
+ 'sketch line', 'sketch rect', 'constraint parallel',
888
+ 'export stl', 'export step', 'export gltf',
889
+ 'undo', 'redo', 'reset view', 'help', 'history', 'clear'
890
+ ];
891
+
892
+ const matches = commands.filter(cmd => cmd.includes(value.toLowerCase()));
893
+
894
+ if (matches.length > 0) {
895
+ list.innerHTML = matches.slice(0, 8)
896
+ .map(m => `<div class="autocomplete-item" onclick="selectAutocomplete('${m}')">${m}</div>`)
897
+ .join('');
898
+ list.classList.add('visible');
899
+ } else {
900
+ list.classList.remove('visible');
901
+ }
902
+ }
903
+
904
+ function selectAutocomplete(cmd) {
905
+ document.getElementById('command-input').value = cmd;
906
+ document.getElementById('autocomplete-list').classList.remove('visible');
907
+ }
908
+
909
+ // Command Processing
910
+ function sendCommand() {
911
+ const input = document.getElementById('command-input');
912
+ const command = input.value.trim();
913
+
914
+ if (!command) return;
915
+
916
+ const startTime = performance.now();
917
+ logTerminal(command, 'input');
918
+
919
+ commandHistory.push(command);
920
+ historyIndex = -1;
921
+ input.value = '';
922
+
923
+ processCommand(command);
924
+
925
+ const latency = Math.round(performance.now() - startTime);
926
+ document.getElementById('status-latency').textContent = latency + 'ms';
927
+ }
928
+
929
+ // ─── NATURAL LANGUAGE PARSER ───
930
+ // Maps synonyms/phrases to canonical intents
931
+ const NLP_SHAPES = {
932
+ cylinder: ['cylinder', 'cylnder', 'cylindar', 'cyl', 'tube', 'pipe', 'rod', 'shaft', 'barrel', 'piston'],
933
+ box: ['box', 'cube', 'block', 'brick', 'rectangular', 'rect', 'cuboid', 'prism'],
934
+ sphere: ['sphere', 'ball', 'globe', 'orb'],
935
+ cone: ['cone', 'funnel', 'tapered'],
936
+ torus: ['torus', 'donut', 'doughnut', 'ring', 'o-ring'],
937
+ circle: ['circle', 'disc', 'disk', 'round', 'circular'],
938
+ plate: ['plate', 'slab', 'flat', 'panel', 'sheet'],
939
+ washer: ['washer', 'spacer', 'shim'],
940
+ hexbolt: ['hexbolt', 'hex bolt', 'bolt', 'hex head'],
941
+ gear: ['gear', 'cog', 'sprocket'],
942
+ flange: ['flange', 'collar', 'rim'],
943
+ bracket: ['bracket', 'l-bracket', 'angle'],
944
+ };
945
+
946
+ const NLP_ACTIONS = {
947
+ create: ['create', 'make', 'build', 'draw', 'add a', 'add an', 'generate', 'design', 'model', 'sketch', 'construct', 'new'],
948
+ hole: ['hole', 'bore', 'drill', 'pierce', 'through hole', 'counterbore', 'countersink'],
949
+ fillet: ['fillet', 'round', 'round off', 'smooth', 'radius edge', 'blend'],
950
+ chamfer: ['chamfer', 'bevel', 'chamfr', 'edge break', 'cut edge', 'angled edge'],
951
+ shell: ['shell', 'hollow', 'thin wall', 'hollow out', 'scoop', 'empty'],
952
+ mirror: ['mirror', 'reflect', 'flip', 'symmetry'],
953
+ pattern: ['pattern', 'array', 'repeat', 'duplicate', 'copy array', 'replicate', 'linear pattern', 'circular pattern'],
954
+ extrude: ['extrude', 'pull', 'push', 'extend', 'raise', 'boss'],
955
+ revolve: ['revolve', 'rotate shape', 'spin', 'lathe', 'turn'],
956
+ sweep: ['sweep', 'sweep along', 'follow path', 'rail'],
957
+ loft: ['loft', 'blend between', 'transition', 'morph'],
958
+ thread: ['thread', 'screw thread', 'threading', 'helix', 'helical'],
959
+ boolean: ['boolean', 'cut', 'subtract', 'union', 'merge', 'intersect', 'combine', 'join'],
960
+ measure: ['measure', 'distance', 'angle', 'how far', 'how big', 'dimension'],
961
+ section: ['section', 'cross section', 'cut plane', 'slice', 'clip'],
962
+ move: ['move', 'translate', 'shift', 'relocate', 'reposition', 'offset'],
963
+ rotate: ['rotate', 'spin', 'turn', 'orient', 'tilt', 'angle'],
964
+ scale: ['scale', 'resize', 'enlarge', 'shrink', 'bigger', 'smaller'],
965
+ copy: ['copy', 'clone', 'duplicate'],
966
+ delete: ['delete', 'remove', 'erase', 'clear part', 'destroy'],
967
+ material: ['material', 'color', 'colour', 'paint', 'steel', 'aluminum', 'aluminium', 'plastic', 'brass', 'titanium', 'copper', 'wood', 'glass'],
968
+ export: ['export', 'save as', 'download', 'output'],
969
+ sketch: ['sketch', '2d', 'draw line', 'draw rect', 'draw arc', 'draw spline', 'pencil'],
970
+ constraint: ['constraint', 'constrain', 'lock', 'fix', 'parallel', 'perpendicular', 'tangent', 'concentric', 'coincident', 'equal', 'horizontal', 'vertical'],
971
+ undo: ['undo', 'go back', 'ctrl z', 'revert'],
972
+ redo: ['redo', 'go forward', 'ctrl y'],
973
+ reset: ['reset', 'home', 'reset view', 'default view', 'fit all', 'zoom fit'],
974
+ wireframe:['wireframe', 'wire', 'skeleton', 'mesh view'],
975
+ grid: ['grid', 'show grid', 'toggle grid'],
976
+ shadows: ['shadow', 'shadows', 'toggle shadows'],
977
+ help: ['help', '?', 'commands', 'how to', 'what can'],
978
+ history: ['history', 'log', 'past commands'],
979
+ clear: ['clear', 'cls', 'clean'],
980
+ };
981
+
982
+ const MATERIAL_MAP = {
983
+ steel: { color: 0xb0b8c4, metalness: 0.8, roughness: 0.3, name: 'Steel' },
984
+ aluminum: { color: 0xd6dce4, metalness: 0.7, roughness: 0.25, name: 'Aluminum' },
985
+ aluminium: { color: 0xd6dce4, metalness: 0.7, roughness: 0.25, name: 'Aluminum' },
986
+ brass: { color: 0xc8a84e, metalness: 0.9, roughness: 0.2, name: 'Brass' },
987
+ copper: { color: 0xc87533, metalness: 0.85, roughness: 0.25, name: 'Copper' },
988
+ titanium: { color: 0x878c96, metalness: 0.75, roughness: 0.35, name: 'Titanium' },
989
+ plastic: { color: 0x2277cc, metalness: 0.0, roughness: 0.6, name: 'ABS Plastic' },
990
+ wood: { color: 0x9e7c4a, metalness: 0.0, roughness: 0.8, name: 'Wood' },
991
+ glass: { color: 0xaaddff, metalness: 0.1, roughness: 0.05, name: 'Glass' },
992
+ red: { color: 0xdd3333, metalness: 0.1, roughness: 0.5, name: 'Red' },
993
+ blue: { color: 0x3355dd, metalness: 0.1, roughness: 0.5, name: 'Blue' },
994
+ green: { color: 0x33aa33, metalness: 0.1, roughness: 0.5, name: 'Green' },
995
+ black: { color: 0x222222, metalness: 0.2, roughness: 0.4, name: 'Black' },
996
+ white: { color: 0xeeeeee, metalness: 0.1, roughness: 0.5, name: 'White' },
997
+ gold: { color: 0xffd700, metalness: 0.95, roughness: 0.15, name: 'Gold' },
998
+ chrome: { color: 0xcccccc, metalness: 1.0, roughness: 0.05, name: 'Chrome' },
999
+ };
1000
+
1001
+ function detectIntent(cmd) {
1002
+ // Check each action category
1003
+ for (const [action, keywords] of Object.entries(NLP_ACTIONS)) {
1004
+ for (const kw of keywords) {
1005
+ if (cmd.includes(kw)) return action;
1006
+ }
347
1007
  }
1008
+ // Check if it's a shape name on its own (e.g. "cylinder 50mm")
1009
+ for (const [shape, aliases] of Object.entries(NLP_SHAPES)) {
1010
+ for (const alias of aliases) {
1011
+ if (cmd.includes(alias)) return 'create';
1012
+ }
1013
+ }
1014
+ return null;
1015
+ }
348
1016
 
349
- await delay(250);
350
- }
351
- }
352
-
353
- running = false;
354
- document.getElementById('btn-run').disabled = false;
355
- document.getElementById('btn-run').textContent = '▶ Run Again';
356
- }
357
-
358
- function simulateCommand(step) {
359
- const m = step.method;
360
- if (m === 'sketch.start') return { ok: true, result: { plane: 'XY', status: 'active' } };
361
- if (m === 'sketch.rect') return { ok: true, result: { id: 'rect_1', type: 'rect', width: step.params.width, height: step.params.height } };
362
- if (m === 'ops.extrude') return { ok: true, result: { id: 'extrude_1', type: 'extrude', height: step.params.height, material: step.params.material, bbox: { width: 80, height: 5, depth: 40 } } };
363
- if (m === 'ops.primitive') return { ok: true, result: { id: step.mesh, type: 'cylinder', shape: 'cylinder' } };
364
- if (m === 'ops.fillet') return { ok: true, result: { target: 'bracket', radius: step.params.radius, applied: true } };
365
- if (m === 'validate.dimensions') return { ok: true, result: { width: 80, height: 5, depth: 40, volume: 16000, fitsInPrintBed: true } };
366
- if (m === 'validate.printability') return { ok: true, result: { printable: true, process: 'CNC', issues: [] } };
367
- if (m === 'validate.cost') return { ok: true, result: { process: 'CNC', unitCost: 12.40, batchOf100: 892 } };
368
- if (m === 'export.stl') return { ok: true, result: { format: 'stl', filename: step.params.filename, featureCount: 5 } };
369
- return { ok: true };
370
- }
371
-
372
- function createMesh(step) {
373
- const THREE = window.THREE;
374
- if (step.mesh === 'bracket') {
375
- // Main bracket body — sharp box (will be replaced by fillet step)
376
- const geo = new THREE.BoxGeometry(80, 5, 40);
377
- const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
378
- const mesh = new THREE.Mesh(geo, mat);
379
- mesh.position.y = 2.5;
380
- mesh.name = 'bracket';
381
- window.addMeshToScene(mesh);
382
- }
383
- else if (step.mesh === 'fillet') {
384
- // Replace sharp bracket with rounded version — visible fillet r=15
385
- const scene = window._scene;
386
- // Remove old bracket
387
- const old = scene.getObjectByName('bracket');
388
- if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
389
-
390
- const r = step.params?.radius || 15; // fillet radius from command
391
- const w = 80, h = 5, d = 40;
392
- const shape = new THREE.Shape();
393
- // Rounded rectangle in XZ plane (we'll extrude along Y)
394
- const hw = w / 2, hd = d / 2;
395
- shape.moveTo(-hw + r, -hd);
396
- shape.lineTo(hw - r, -hd);
397
- shape.quadraticCurveTo(hw, -hd, hw, -hd + r);
398
- shape.lineTo(hw, hd - r);
399
- shape.quadraticCurveTo(hw, hd, hw - r, hd);
400
- shape.lineTo(-hw + r, hd);
401
- shape.quadraticCurveTo(-hw, hd, -hw, hd - r);
402
- shape.lineTo(-hw, -hd + r);
403
- shape.quadraticCurveTo(-hw, -hd, -hw + r, -hd);
404
-
405
- const extrudeSettings = { depth: h, bevelEnabled: false, curveSegments: 16 };
406
- const geo = new THREE.ExtrudeGeometry(shape, extrudeSettings);
407
- // Rotate so extrude goes along Y axis
408
- geo.rotateX(-Math.PI / 2);
409
- const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
410
- const mesh = new THREE.Mesh(geo, mat);
411
- mesh.position.y = 5; // align with bolt holes
412
- mesh.name = 'bracket';
413
- window.addMeshToScene(mesh);
414
- }
415
- else if (step.mesh && step.mesh.startsWith('hole')) {
416
- // Bolt holes — dark cylinders subtracted visually
417
- const geo = new THREE.CylinderGeometry(3.2, 3.2, 6, 24);
418
- const mat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
419
- const mesh = new THREE.Mesh(geo, mat);
420
- if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
421
- window.addMeshToScene(mesh);
422
- }
423
- }
424
-
425
- function resetDemo() {
426
- running = false;
427
- cmdCount = 0;
428
- startTime = 0;
429
- sceneState = { shape: null, material: 'aluminum', matClr: 0xccccdd, dims: {}, features: [], holeIdx: 0 };
430
- term.innerHTML = '<div class="comment">// Ready. Type a command like "build a cylinder 50mm diameter 80 tall in steel"</div><div class="comment">// Then: "add a hole radius 10" → "fillet 5" → "export stl"</div><div class="comment">// Each command builds on the last. Say "start over" to reset.</div>';
431
- window.clearScene();
432
- document.getElementById('stats-overlay').style.display = 'none';
433
- progress.style.width = '0%';
434
- document.getElementById('btn-run').disabled = false;
435
- document.getElementById('btn-run').textContent = '▶ Run Agent Demo';
436
- }
437
-
438
- function showSchema() {
439
- term.innerHTML = '';
440
- const schema = {
441
- sketch: ['sketch.start', 'sketch.end', 'sketch.line', 'sketch.rect', 'sketch.circle', 'sketch.arc', 'sketch.clear'],
442
- ops: ['ops.extrude', 'ops.revolve', 'ops.primitive', 'ops.fillet', 'ops.chamfer', 'ops.boolean', 'ops.shell', 'ops.pattern', 'ops.material', 'ops.sweep', 'ops.loft', 'ops.spring', 'ops.thread', 'ops.bend'],
443
- transform: ['transform.move', 'transform.rotate', 'transform.scale'],
444
- view: ['view.set', 'view.fit', 'view.wireframe', 'view.grid'],
445
- export: ['export.stl', 'export.obj', 'export.gltf', 'export.json'],
446
- validate: ['validate.dimensions', 'validate.wallThickness', 'validate.printability', 'validate.cost'],
447
- query: ['query.features', 'query.bbox', 'query.materials', 'query.session', 'query.log'],
448
- scene: ['scene.clear', 'scene.snapshot'],
449
- meta: ['meta.ping', 'meta.version', 'meta.schema']
450
- };
451
- addLine('📋 cycleCAD Agent API — 46 commands across 9 namespaces', 'agent');
452
- addDivider();
453
- for (const [ns, methods] of Object.entries(schema)) {
454
- addLine(`<span style="color:var(--gold)">${ns}</span> (${methods.length})`, '');
455
- methods.forEach(m => addLine(` ${m}`, 'res'));
456
- }
457
- addDivider();
458
- addLine('Usage: window.cycleCAD.execute({ method: "ops.extrude", params: { height: 10 } })', 'comment');
459
- addLine('Full schema: window.cycleCAD.getSchema()', 'comment');
460
- }
461
-
462
- // ========== Voice Command System ==========
463
- let recognition = null;
464
- let isListening = false;
465
-
466
- function toggleVoice() {
467
- if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
468
- document.getElementById('voice-status').textContent = 'Speech recognition not supported — type your command instead';
469
- return;
470
- }
471
- if (isListening) { stopVoice(); return; }
472
-
473
- const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
474
- recognition = new SR();
475
- recognition.continuous = false;
476
- recognition.interimResults = true;
477
- recognition.lang = 'en-US';
478
-
479
- const btn = document.getElementById('btn-mic');
480
- const status = document.getElementById('voice-status');
481
- const input = document.getElementById('voice-input');
482
-
483
- recognition.onstart = () => {
484
- isListening = true;
485
- btn.style.background = 'var(--coral)';
486
- btn.style.color = '#fff';
487
- btn.style.animation = 'pulse 1s ease-in-out infinite';
488
- status.textContent = 'Listening...';
489
- };
490
- recognition.onresult = (e) => {
491
- let transcript = '';
492
- for (let i = 0; i < e.results.length; i++) transcript = e.results[i][0].transcript;
493
- input.value = transcript;
494
- if (e.results[0].isFinal) status.textContent = 'Got it! Press "Build It" or Enter.';
495
- };
496
- recognition.onerror = (e) => {
497
- if (e.error === 'not-allowed') {
498
- status.textContent = 'Mic needs HTTPS (works on cyclecad.com) — type your command below';
499
- } else {
500
- status.textContent = 'Mic unavailable — type your command below';
501
- }
502
- stopVoice();
503
- };
504
- recognition.onend = () => { stopVoice(); };
505
- try { recognition.start(); } catch(err) {
506
- document.getElementById('voice-status').textContent = 'Mic blocked — type your command instead';
507
- stopVoice();
508
- }
509
- }
510
-
511
- function stopVoice() {
512
- isListening = false;
513
- if (recognition) try { recognition.stop(); } catch(e) {}
514
- const btn = document.getElementById('btn-mic');
515
- btn.style.background = 'transparent';
516
- btn.style.color = 'var(--coral)';
517
- btn.style.animation = 'none';
518
- }
519
-
520
- // ========== STATEFUL NLP COMMAND SYSTEM v3 ==========
521
- // Iterative: each command builds on the previous.
522
- // "build cylinder 50mm diameter 80 tall" → creates cylinder (clears scene)
523
- // "add a hole radius 10" → cuts hole into existing part
524
- // "fillet 5" → rounds edges
525
- // "export stl" → exports (only when asked)
526
- // "validate" → checks manufacturing (only when asked)
527
- // "change material to steel" → updates material
528
- // "start over" / "reset" / "new" → clears everything
529
-
530
- // ---- Persistent scene state ----
531
- let sceneState = {
532
- shape: null, // 'cylinder', 'bracket', 'sphere', etc.
533
- material: 'aluminum',
534
- matClr: 0xccccdd,
535
- dims: {}, // shape-specific dimensions
536
- features: [], // log of operations applied
537
- holeIdx: 0, // counter for unique hole names
538
- };
539
-
540
- function parseNum(t, ...patterns) {
541
- for (const p of patterns) {
542
- const m = t.match(p);
543
- if (m) return parseFloat(m[1]);
544
- }
545
- return null;
546
- }
547
-
548
- function parseMaterial(t) {
549
- if (/steel|stainless/.test(t)) return 'steel';
550
- if (/brass/.test(t)) return 'brass';
551
- if (/titanium/.test(t)) return 'titanium';
552
- if (/copper/.test(t)) return 'copper';
553
- if (/abs|plastic/.test(t)) return 'ABS';
554
- if (/nylon/.test(t)) return 'nylon';
555
- if (/wood/.test(t)) return 'wood';
556
- if (/carbon/.test(t)) return 'carbon fiber';
557
- return null; // null = don't change
558
- }
559
-
560
- function materialColor(mat) {
561
- const colors = {
562
- aluminum: 0xccccdd, steel: 0x888899, brass: 0xd4a843, titanium: 0xaabbcc,
563
- copper: 0xb87333, ABS: 0xe8e8e0, nylon: 0xf0f0e0, wood: 0x8B6914, 'carbon fiber': 0x333333
564
- };
565
- return colors[mat] || 0xccccdd;
566
- }
567
-
568
- const costMap = { aluminum: 12.40, steel: 8.90, brass: 18.50, titanium: 45.00, ABS: 3.20, nylon: 4.80, copper: 22.10, wood: 5.50, 'carbon fiber': 65.00 };
569
-
570
- function detectShape(t) {
571
- if (/hollow\s*cylinder|tube|pipe/.test(t)) return 'tube';
572
- if (/cylinder|cylind|cylindar/.test(t)) return 'cylinder';
573
- if (/disk|disc|puck/.test(t)) return 'disk';
574
- if (/sphere|ball/.test(t)) return 'sphere';
575
- if (/cone|taper/.test(t)) return 'cone';
576
- if (/gear|sprocket/.test(t)) return 'gear';
577
- if (/hex\s*bolt|bolt/.test(t)) return 'hexbolt';
578
- if (/washer/.test(t)) return 'washer';
579
- if (/\bring\b/.test(t)) return 'ring';
580
- if (/flange/.test(t)) return 'flange';
581
- if (/plate/.test(t)) return 'plate';
582
- if (/block|cube/.test(t)) return 'box';
583
- if (/bracket|mount/.test(t)) return 'bracket';
584
- if (/\d+\s*x\s*\d+/.test(t)) return 'bracket';
585
- if (/diameter|radius/.test(t)) return 'cylinder';
586
- return null; // unknown — might be a modify command
587
- }
588
-
589
- // ---- Detect what KIND of command this is ----
590
- function detectIntent(t) {
591
- if (/^(start\s*over|reset|clear|new\s*part|new\s*design)/.test(t)) return 'reset';
592
- if (/export|save|download/.test(t)) return 'export';
593
- if (/validate|check|verify|inspect|analyze/.test(t)) return 'validate';
594
- if (/change\s*material|set\s*material|make\s*it\s*(steel|brass|aluminum|titanium|nylon|abs|plastic|copper|wood|carbon)/.test(t)) return 'material';
595
- if (/undo/.test(t)) return 'undo';
596
- // Modify operations on existing part
597
- if (/add\s*(a\s*)?hole|cut\s*(a\s*)?hole|drill|bore|extrude\s*(a\s*)?hole|punch/.test(t)) return 'hole';
598
- if (/fillet|round\s*(the\s*)?edge/.test(t)) return 'fillet';
599
- if (/chamfer/.test(t)) return 'chamfer';
600
- if (/add\s*(a\s*)?(slot|groove|channel)/.test(t)) return 'slot';
601
- if (/add\s*(a\s*)?(boss|peg|pin|post)/.test(t)) return 'boss';
602
- if (/shell|hollow\s*out|thin\s*wall/.test(t)) return 'shell';
603
- if (/pattern|array|copies|repeat/.test(t)) return 'pattern';
604
- if (/counterbore|counter\s*bore|c'?bore/.test(t)) return 'counterbore';
605
- if (/thread|tap/.test(t)) return 'thread';
606
- if (/mirror|symmetr/.test(t)) return 'mirror';
607
- // If a known shape word is present → create new
608
- if (detectShape(t)) return 'create';
609
- // Default: try to create
610
- return 'create';
611
- }
612
-
613
- // ---- Parse dimensions from text ----
614
- function extractDims(t) {
615
- return {
616
- diameter: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*(?:dia|diameter)/, /dia(?:meter)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/, /[øo]\s*(\d+(?:\.\d+)?)/) || 0,
617
- radius: parseNum(t, /radius\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*radius/) || 0,
618
- height: parseNum(t, /height\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:tall|high|height|long|deep)/) || 0,
619
- thick: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*thick/, /thick(?:ness)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
620
- outerD: parseNum(t, /outer\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /od\s*(\d+(?:\.\d+)?)/) || 0,
621
- innerD: parseNum(t, /inner\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /id\s*(\d+(?:\.\d+)?)/, /bore\s*(\d+(?:\.\d+)?)/) || 0,
622
- topD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*top/, /top\s*(\d+(?:\.\d+)?)/) || 0,
623
- baseD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*base/, /base\s*(\d+(?:\.\d+)?)/) || 0,
624
- teeth: parseNum(t, /(\d+)\s*teeth/, /teeth\s*(\d+)/) || 0,
625
- modl: parseNum(t, /module\s*(\d+(?:\.\d+)?)/) || 0,
626
- mBolt: parseNum(t, /\bm(\d+)\b/) || 0,
627
- filletR: parseNum(t, /fillet\s*(?:radius\s*(?:of\s*)?)?(\d+(?:\.\d+)?)/, /round\s*(\d+(?:\.\d+)?)/) || 0,
628
- chamferS: parseNum(t, /chamfer\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
629
- dimMatch: t.match(/(\d+)\s*(?:x|by)\s*(\d+)/),
630
- holeCountMatch: t.match(/(\d+)\s*(?:m\d+\s*)?(?:bolt\s*)?holes?/),
631
- holeOffset: parseNum(t, /(\d+)\s*mm?\s*from\s*(?:the\s*)?edge/) || 10,
632
- posX: parseNum(t, /(?:at|x)\s*(-?\d+(?:\.\d+)?)/) || 0,
633
- posZ: parseNum(t, /(?:,\s*|y\s*)(-?\d+(?:\.\d+)?)/) || 0,
634
- shellThick: parseNum(t, /shell\s*(?:thickness\s*)?(\d+(?:\.\d+)?)/, /thick(?:ness)?\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:shell|wall)/) || 0,
635
- copies: parseNum(t, /(\d+)\s*cop(?:y|ies)/, /(\d+)\s*times/, /pattern\s*(?:of\s*)?(\d+)/) || 0,
636
- patternType: /circular|radial|around/.test(t) ? 'circular' : (/linear|row|line/.test(t) ? 'linear' : 'circular'),
637
- spacing: parseNum(t, /spacing\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:apart|spacing)/) || 0,
638
- threadPitch: parseNum(t, /pitch\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*pitch/) || 0,
639
- };
640
- }
641
-
642
- // ======= STEP BUILDERS per intent =======
643
-
644
- function buildCreateSteps(text, t, ex) {
645
- const steps = [];
646
- const shape = detectShape(t) || 'bracket';
647
- const mat = parseMaterial(t) || 'aluminum';
648
- const matClr = materialColor(mat);
649
-
650
- // Update scene state — this is a NEW part
651
- sceneState = { shape, material: mat, matClr, dims: {}, features: [], holeIdx: 0 };
652
- const d = sceneState.dims;
653
-
654
- if (shape === 'cylinder' || shape === 'disk') {
655
- d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 25);
656
- d.h = ex.height || ex.thick || (shape === 'disk' ? 5 : 60);
657
- d.sizeLabel = `ø${d.r * 2} × ${d.h}mm`;
658
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
659
- steps.push({ type: 'comment', text: `// New ${shape}: ø${d.r * 2}mm × ${d.h}mm in ${mat}` });
660
- steps.push({ type: 'delay', ms: 300 });
661
- steps.push({ type: 'divider' });
662
- steps.push({ type: 'agent', text: `📐 Sketch circle ø${d.r * 2}mm` });
663
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
664
- steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.r } });
665
- steps.push({ type: 'delay', ms: 200 });
666
- steps.push({ type: 'divider' });
667
- steps.push({ type: 'agent', text: `📦 Extrude ${d.h}mm in ${mat}` });
668
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
669
- }
670
- else if (shape === 'tube' || shape === 'ring') {
671
- d.outerR = ex.outerD ? ex.outerD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
672
- d.innerR = ex.innerD ? ex.innerD / 2 : d.outerR * 0.7;
673
- d.h = ex.height || ex.thick || (shape === 'ring' ? 10 : 60);
674
- d.sizeLabel = `OD${d.outerR * 2} × ID${d.innerR * 2} × ${d.h}mm`;
675
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
676
- steps.push({ type: 'comment', text: `// New ${shape}: OD${d.outerR * 2} ID${d.innerR * 2} × ${d.h}mm` });
677
- steps.push({ type: 'delay', ms: 300 });
678
- steps.push({ type: 'divider' });
679
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
680
- steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.outerR } });
681
- steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.innerR, type: 'inner' } });
682
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, hollow: true, material: mat }, mesh: 'main' });
683
- }
684
- else if (shape === 'sphere') {
685
- d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 30);
686
- d.h = d.r * 2;
687
- d.sizeLabel = `ø${d.r * 2}mm sphere`;
688
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
689
- steps.push({ type: 'comment', text: `// New sphere ø${d.r * 2}mm in ${mat}` });
690
- steps.push({ type: 'delay', ms: 300 });
691
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'sphere', radius: d.r, material: mat }, mesh: 'main' });
692
- }
693
- else if (shape === 'cone') {
694
- d.baseR = ex.baseD ? ex.baseD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
695
- d.topR = ex.topD ? ex.topD / 2 : 5;
696
- d.h = ex.height || 50;
697
- d.sizeLabel = `ø${d.baseR * 2}→ø${d.topR * 2} × ${d.h}mm`;
698
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
699
- steps.push({ type: 'comment', text: `// New cone: base ø${d.baseR * 2} → top ø${d.topR * 2} × ${d.h}mm` });
700
- steps.push({ type: 'delay', ms: 300 });
701
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
702
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, taper: d.topR, material: mat }, mesh: 'main' });
703
- }
704
- else if (shape === 'gear') {
705
- d.teeth = ex.teeth || 20;
706
- d.module = ex.modl || 3;
707
- d.r = (d.teeth * d.module) / 2;
708
- d.h = ex.thick || ex.height || 10;
709
- d.sizeLabel = `${d.teeth}T m${d.module} × ${d.h}mm`;
710
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
711
- steps.push({ type: 'comment', text: `// Gear: ${d.teeth} teeth, module ${d.module}, ø${d.r * 2}mm` });
712
- steps.push({ type: 'delay', ms: 300 });
713
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'gear', teeth: d.teeth, module: d.module, material: mat }, mesh: 'main' });
714
- }
715
- else if (shape === 'hexbolt') {
716
- d.m = ex.mBolt || 10;
717
- d.headR = d.m * 0.9; d.headH = d.m * 0.65;
718
- d.shankR = d.m / 2; d.shankH = ex.height || d.m * 2;
719
- d.h = d.headH + d.shankH;
720
- d.sizeLabel = `M${d.m} × ${d.shankH}mm bolt`;
721
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
722
- steps.push({ type: 'comment', text: `// Hex bolt M${d.m} × ${d.shankH}mm` });
723
- steps.push({ type: 'delay', ms: 300 });
724
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'hexbolt', size: d.m, length: d.shankH, material: mat }, mesh: 'main' });
725
- }
726
- else if (shape === 'washer') {
727
- d.m = ex.mBolt || 10;
728
- d.outerR = d.m * 1.1; d.innerR = d.m / 2 + 0.5;
729
- d.h = ex.thick || 2;
730
- d.sizeLabel = `M${d.m} washer`;
731
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
732
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'washer', size: d.m, material: mat }, mesh: 'main' });
733
- }
734
- else {
735
- // bracket / plate / box
736
- d.w = ex.dimMatch ? parseInt(ex.dimMatch[1]) : 80;
737
- d.d = ex.dimMatch ? parseInt(ex.dimMatch[2]) : 40;
738
- d.h = ex.thick || ex.height || 5;
739
- d.sizeLabel = `${d.w} × ${d.d} × ${d.h}mm`;
740
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
741
- steps.push({ type: 'comment', text: `// New ${shape}: ${d.w}×${d.d}×${d.h}mm in ${mat}` });
742
- steps.push({ type: 'delay', ms: 300 });
743
- steps.push({ type: 'divider' });
744
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
745
- steps.push({ type: 'cmd', method: 'sketch.rect', params: { x: -d.w / 2, y: -d.d / 2, width: d.w, height: d.d } });
746
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
747
- }
748
- sceneState.features.push(`Created ${shape}`);
749
- steps.push({ type: 'delay', ms: 300 });
750
- steps.push({ type: 'agent', text: `✅ ${shape} created. Keep going — add holes, fillets, or say "export".` });
751
- return { steps, clearScene: true };
752
- }
753
-
754
- function buildHoleSteps(text, t, ex) {
755
- const steps = [];
756
- if (!sceneState.shape) {
757
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first — e.g. "build a cylinder 50mm diameter 80 tall"' });
758
- return { steps, clearScene: false };
759
- }
760
- const hr = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
761
- const hh = ex.height || sceneState.dims.h || 20;
762
- const px = ex.posX || 0;
763
- const pz = ex.posZ || 0;
764
- const py = (sceneState.dims.h || hh) / 2;
765
- sceneState.holeIdx++;
766
- const holeName = `hole${sceneState.holeIdx}`;
767
-
768
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
769
- steps.push({ type: 'comment', text: `// Adding hole ø${hr * 2}mm through part at (${px}, ${pz})` });
770
- steps.push({ type: 'delay', ms: 200 });
771
- steps.push({ type: 'divider' });
772
- steps.push({ type: 'agent', text: `🕳️ Extruding cut: ø${hr * 2}mm hole` });
773
- steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: hr, height: hh + 2, x: px, z: pz }, mesh: holeName, pos: [px, py, pz] });
774
- steps.push({ type: 'delay', ms: 200 });
775
- sceneState.features.push(`Hole ø${hr * 2} at (${px},${pz})`);
776
- steps.push({ type: 'agent', text: `✅ Hole added. ${sceneState.features.length} features total. Keep going!` });
777
- return { steps, clearScene: false };
778
- }
779
-
780
- function buildFilletSteps(text, t, ex) {
781
- const steps = [];
782
- if (!sceneState.shape) {
783
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
784
- return { steps, clearScene: false };
785
- }
786
- const fr = ex.filletR || parseNum(t, /(\d+(?:\.\d+)?)/) || 3;
787
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
788
- steps.push({ type: 'divider' });
789
- steps.push({ type: 'agent', text: `✨ Fillet edges r=${fr}mm` });
790
- steps.push({ type: 'cmd', method: 'ops.fillet', params: { target: 'main', radius: fr }, mesh: 'fillet' });
791
- steps.push({ type: 'delay', ms: 200 });
792
- sceneState.dims.filletR = fr;
793
- sceneState.features.push(`Fillet r=${fr}`);
794
- steps.push({ type: 'agent', text: `✅ Filleted. ${sceneState.features.length} features. Keep going!` });
795
- return { steps, clearScene: false };
796
- }
797
-
798
- function buildExportSteps(text, t) {
799
- const steps = [];
800
- if (!sceneState.shape) {
801
- steps.push({ type: 'agent', text: '⚠️ No part to export. Build something first.' });
802
- return { steps, clearScene: false };
803
- }
804
- const fmt = /obj/.test(t) ? 'obj' : (/gltf|glb/.test(t) ? 'gltf' : 'stl');
805
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
806
- steps.push({ type: 'divider' });
807
- steps.push({ type: 'agent', text: `📤 Exporting as ${fmt.toUpperCase()}` });
808
- steps.push({ type: 'cmd', method: `export.${fmt}`, params: { filename: `${sceneState.shape}.${fmt}`, binary: true } });
809
- steps.push({ type: 'delay', ms: 200 });
810
- steps.push({ type: 'agent', text: `✅ Exported ${sceneState.shape}.${fmt}` });
811
- return { steps, clearScene: false };
812
- }
813
-
814
- function buildValidateSteps(text) {
815
- const steps = [];
816
- if (!sceneState.shape) {
817
- steps.push({ type: 'agent', text: '⚠️ No part to validate. Build something first.' });
818
- return { steps, clearScene: false };
819
- }
820
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
821
- steps.push({ type: 'divider' });
822
- steps.push({ type: 'agent', text: '🔍 Validating for manufacturing' });
823
- steps.push({ type: 'cmd', method: 'validate.dimensions', params: { target: 'main' }, stat: 'size' });
824
- steps.push({ type: 'cmd', method: 'validate.printability', params: { target: 'main', process: 'CNC' }, stat: 'printable' });
825
- steps.push({ type: 'cmd', method: 'validate.cost', params: { target: 'main', process: 'CNC', material: sceneState.material }, stat: 'cost' });
826
- steps.push({ type: 'delay', ms: 300 });
827
- steps.push({ type: 'agent', text: '✅ Validation complete.' });
828
- return { steps, clearScene: false };
829
- }
830
-
831
- function buildMaterialSteps(text, t) {
832
- const steps = [];
833
- const newMat = parseMaterial(t) || 'aluminum';
834
- sceneState.material = newMat;
835
- sceneState.matClr = materialColor(newMat);
836
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
837
- steps.push({ type: 'cmd', method: 'ops.material', params: { material: newMat }, mesh: 'recolor' });
838
- steps.push({ type: 'agent', text: `✅ Material changed to ${newMat}. Keep going!` });
839
- return { steps, clearScene: false };
840
- }
841
-
842
- function buildBossSteps(text, t, ex) {
843
- const steps = [];
844
- if (!sceneState.shape) {
845
- steps.push({ type: 'agent', text: '⚠️ No part yet.' });
846
- return { steps, clearScene: false };
847
- }
848
- const br = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
849
- const bh = ex.height || 15;
850
- const px = ex.posX || 0;
851
- const pz = ex.posZ || 0;
852
- const py = (sceneState.dims.h || 5) + bh / 2;
853
- sceneState.holeIdx++;
854
- const bossName = `boss${sceneState.holeIdx}`;
855
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
856
- steps.push({ type: 'divider' });
857
- steps.push({ type: 'agent', text: `📌 Adding boss ø${br * 2} × ${bh}mm` });
858
- steps.push({ type: 'cmd', method: 'ops.boss', params: { radius: br, height: bh, x: px, z: pz }, mesh: bossName, pos: [px, py, pz] });
859
- sceneState.features.push(`Boss ø${br * 2} at (${px},${pz})`);
860
- steps.push({ type: 'agent', text: `✅ Boss added. Keep going!` });
861
- return { steps, clearScene: false };
862
- }
863
-
864
- function buildShellSteps(text, t, ex) {
865
- const steps = [];
866
- if (!sceneState.shape) {
867
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
868
- return { steps, clearScene: false };
869
- }
870
- const thick = ex.shellThick || ex.thick || 2;
871
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
872
- steps.push({ type: 'divider' });
873
- steps.push({ type: 'agent', text: `🥚 Shell — hollow out with ${thick}mm wall thickness` });
874
- steps.push({ type: 'cmd', method: 'ops.shell', params: { target: 'main', thickness: thick }, mesh: 'shell' });
875
- steps.push({ type: 'delay', ms: 200 });
876
- sceneState.dims.shellThick = thick;
877
- sceneState.features.push(`Shell t=${thick}`);
878
- steps.push({ type: 'agent', text: `✅ Shelled. ${sceneState.features.length} features. Keep going!` });
879
- return { steps, clearScene: false };
880
- }
881
-
882
- function buildPatternSteps(text, t, ex) {
883
- const steps = [];
884
- if (!sceneState.shape) {
885
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
886
- return { steps, clearScene: false };
887
- }
888
- const copies = ex.copies || 6;
889
- const pType = ex.patternType || 'circular';
890
- const spacing = ex.spacing || 30;
891
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
892
- steps.push({ type: 'divider' });
893
- steps.push({ type: 'agent', text: `🔄 ${pType.charAt(0).toUpperCase() + pType.slice(1)} pattern — ${copies} copies` });
894
- steps.push({ type: 'cmd', method: 'ops.pattern', params: { target: 'main', type: pType, copies, spacing }, mesh: 'pattern' });
895
- steps.push({ type: 'delay', ms: 200 });
896
- sceneState.dims.patternCopies = copies;
897
- sceneState.dims.patternType = pType;
898
- sceneState.features.push(`${pType} pattern ×${copies}`);
899
- steps.push({ type: 'agent', text: `✅ Pattern applied. ${sceneState.features.length} features. Keep going!` });
900
- return { steps, clearScene: false };
901
- }
902
-
903
- function buildCounterboreSteps(text, t, ex) {
904
- const steps = [];
905
- if (!sceneState.shape) {
906
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
907
- return { steps, clearScene: false };
908
- }
909
- const boreR = ex.diameter ? ex.diameter / 2 : (ex.radius || 6);
910
- const headR = boreR * 1.6;
911
- const headDepth = ex.thick || 4;
912
- const px = ex.posX || 0, pz = ex.posZ || 0;
913
- const py = (sceneState.dims.h || 10) / 2;
914
- sceneState.holeIdx++;
915
- const name = `cbore${sceneState.holeIdx}`;
916
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
917
- steps.push({ type: 'divider' });
918
- steps.push({ type: 'agent', text: `🔩 Counterbore: ø${boreR * 2}mm through + ø${headR * 2}mm × ${headDepth}mm pocket` });
919
- steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: boreR, height: (sceneState.dims.h || 10) + 2, x: px, z: pz }, mesh: `hole${sceneState.holeIdx}`, pos: [px, py, pz] });
920
- steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: headR, height: headDepth, x: px, z: pz, counterbore: true }, mesh: name, pos: [px, (sceneState.dims.h || 10) - headDepth / 2, pz] });
921
- steps.push({ type: 'delay', ms: 200 });
922
- sceneState.features.push(`Counterbore ø${boreR * 2} at (${px},${pz})`);
923
- steps.push({ type: 'agent', text: `✅ Counterbore added. ${sceneState.features.length} features. Keep going!` });
924
- return { steps, clearScene: false };
925
- }
926
-
927
- function buildThreadSteps(text, t, ex) {
928
- const steps = [];
929
- if (!sceneState.shape) {
930
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
931
- return { steps, clearScene: false };
932
- }
933
- const pitch = ex.threadPitch || 1.5;
934
- const threadR = ex.diameter ? ex.diameter / 2 : (ex.radius || (sceneState.dims.r || 10));
935
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
936
- steps.push({ type: 'divider' });
937
- steps.push({ type: 'agent', text: `🔩 Thread: ø${threadR * 2}mm, pitch ${pitch}mm` });
938
- steps.push({ type: 'cmd', method: 'ops.thread', params: { target: 'main', radius: threadR, pitch, external: true }, mesh: 'thread' });
939
- steps.push({ type: 'delay', ms: 200 });
940
- sceneState.dims.threadPitch = pitch;
941
- sceneState.features.push(`Thread p=${pitch} ø${threadR * 2}`);
942
- steps.push({ type: 'agent', text: `✅ Thread applied. ${sceneState.features.length} features. Keep going!` });
943
- return { steps, clearScene: false };
944
- }
945
-
946
- function buildMirrorSteps(text, t, ex) {
947
- const steps = [];
948
- if (!sceneState.shape) {
949
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
950
- return { steps, clearScene: false };
951
- }
952
- const axis = /[yz]/i.test(t) ? (t.match(/([yz])/i)[1].toUpperCase()) : 'X';
953
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
954
- steps.push({ type: 'divider' });
955
- steps.push({ type: 'agent', text: `🪞 Mirror across ${axis} axis` });
956
- steps.push({ type: 'cmd', method: 'ops.mirror', params: { target: 'main', axis }, mesh: 'mirror' });
957
- steps.push({ type: 'delay', ms: 200 });
958
- sceneState.features.push(`Mirror ${axis}`);
959
- steps.push({ type: 'agent', text: `✅ Mirrored. ${sceneState.features.length} features. Keep going!` });
960
- return { steps, clearScene: false };
961
- }
962
-
963
- // ======= EXECUTE =======
964
- async function executeVoiceCommand() {
965
- const input = document.getElementById('voice-input');
966
- const text = input.value.trim();
967
- if (!text || running) return;
968
- stopVoice();
969
- input.value = '';
970
-
971
- const t = text.toLowerCase();
972
- const intent = detectIntent(t);
973
- const ex = extractDims(t);
974
-
975
- console.log('[cycleCAD NLP]', { intent, text, extracted: ex, sceneState: sceneState.shape });
976
-
977
- // Handle reset
978
- if (intent === 'reset') {
979
- resetDemo();
980
- addLine('🔄 Scene cleared. Start fresh!', 'agent');
981
- return;
982
- }
983
-
984
- let result;
985
- if (intent === 'create') result = buildCreateSteps(text, t, ex);
986
- else if (intent === 'hole') result = buildHoleSteps(text, t, ex);
987
- else if (intent === 'fillet') result = buildFilletSteps(text, t, ex);
988
- else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex);
989
- else if (intent === 'export') result = buildExportSteps(text, t);
990
- else if (intent === 'validate') result = buildValidateSteps(text);
991
- else if (intent === 'material') result = buildMaterialSteps(text, t);
992
- else if (intent === 'boss') result = buildBossSteps(text, t, ex);
993
- else if (intent === 'shell') result = buildShellSteps(text, t, ex);
994
- else if (intent === 'pattern') result = buildPatternSteps(text, t, ex);
995
- else if (intent === 'counterbore') result = buildCounterboreSteps(text, t, ex);
996
- else if (intent === 'thread') result = buildThreadSteps(text, t, ex);
997
- else if (intent === 'mirror') result = buildMirrorSteps(text, t, ex);
998
- else result = buildCreateSteps(text, t, ex);
999
-
1000
- const voiceSteps = result.steps;
1001
- if (result.clearScene) {
1002
- window.clearScene();
1003
- document.getElementById('stats-overlay').style.display = 'none';
1004
- }
1005
-
1006
- // Don't clear terminal — append (iterative)
1007
- addDivider();
1008
- running = true;
1009
- if (!startTime) startTime = Date.now();
1010
-
1011
- const totalSteps = voiceSteps.filter(s => s.type === 'cmd').length;
1012
- let localCmdIdx = 0;
1013
-
1014
- for (const step of voiceSteps) {
1015
- if (!running) break;
1016
- if (step.type === 'agent') {
1017
- addLine(step.text.replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1)).replace('{CMDS}', cmdCount), 'agent');
1018
- await delay(150);
1019
- } else if (step.type === 'comment') {
1020
- addLine(step.text, 'comment'); await delay(80);
1021
- } else if (step.type === 'divider') {
1022
- addDivider();
1023
- } else if (step.type === 'delay') {
1024
- await delay(step.ms);
1025
- } else if (step.type === 'cmd') {
1026
- cmdCount++; localCmdIdx++;
1027
- addLine(`cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`, 'cmd');
1028
- await delay(120);
1029
- const res = simulateResult(step);
1030
- addLine(`→ ${JSON.stringify(res, null, 0)}`, 'res ok');
1031
- // Update stats
1032
- if (step.stat === 'size') updateStats({ size: sceneState.dims.sizeLabel || 'computed' });
1033
- if (step.stat === 'printable') updateStats({ printable: true });
1034
- if (step.stat === 'cost') updateStats({ cost: `$${(costMap[sceneState.material] || 12.40).toFixed(2)} (CNC)` });
1035
- updateStats({ material: sceneState.material.charAt(0).toUpperCase() + sceneState.material.slice(1) });
1036
- document.getElementById('stat-cmds').textContent = cmdCount;
1037
- document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
1038
- document.getElementById('stats-overlay').style.display = 'block';
1039
- // Build 3D
1040
- if (step.mesh) buildMesh(step);
1041
- await delay(180);
1042
- }
1043
- }
1044
- running = false;
1045
- updateFeatureBadge();
1046
- }
1047
-
1048
- function simulateResult(step) {
1049
- const m = step.method;
1050
- if (m === 'sketch.start') return { ok: true, plane: 'XY' };
1051
- if (m === 'sketch.rect') return { ok: true, id: 'rect_1' };
1052
- if (m === 'sketch.circle') return { ok: true, id: 'circle_1', radius: step.params.radius };
1053
- if (m === 'ops.extrude') return { ok: true, id: 'extrude_1', height: step.params.height };
1054
- if (m === 'ops.primitive') return { ok: true, id: step.mesh, shape: step.params.shape };
1055
- if (m === 'ops.cut') return { ok: true, id: step.mesh, type: 'hole' };
1056
- if (m === 'ops.fillet') return { ok: true, radius: step.params.radius, applied: true };
1057
- if (m === 'ops.material') return { ok: true, material: step.params.material };
1058
- if (m === 'ops.boss') return { ok: true, id: step.mesh };
1059
- if (m === 'ops.shell') return { ok: true, thickness: step.params.thickness, hollowed: true };
1060
- if (m === 'ops.pattern') return { ok: true, type: step.params.type, copies: step.params.copies };
1061
- if (m === 'ops.thread') return { ok: true, pitch: step.params.pitch, external: true };
1062
- if (m === 'ops.mirror') return { ok: true, axis: step.params.axis, mirrored: true };
1063
- if (m === 'validate.dimensions') return { ok: true, size: sceneState.dims.sizeLabel };
1064
- if (m === 'validate.printability') return { ok: true, printable: true, issues: [] };
1065
- if (m === 'validate.cost') return { ok: true, unitCost: costMap[sceneState.material] || 12.40 };
1066
- if (m.startsWith('export.')) return { ok: true, filename: step.params.filename };
1067
- return { ok: true };
1068
- }
1069
-
1070
- // ======= 3D MESH BUILDER =======
1071
- function buildMesh(step) {
1072
- const THREE = window.THREE;
1073
- const sc = sceneState;
1074
- const d = sc.dims;
1075
- const matOpts = { color: sc.matClr, metalness: 0.7, roughness: 0.3 };
1076
- const darkMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
1077
-
1078
- if (step.mesh === 'main' || step.mesh === 'main_ext') {
1079
- const scene = window._scene;
1080
- const old = scene.getObjectByName('main');
1081
- if (old && step.mesh !== 'main_ext') { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
1082
-
1083
- let geo, mesh;
1084
- const s = sc.shape;
1085
- if (s === 'cylinder' || s === 'disk') {
1086
- geo = new THREE.CylinderGeometry(d.r, d.r, d.h, 48);
1087
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
1088
- mesh.position.y = d.h / 2;
1089
- } else if (s === 'tube' || s === 'ring' || s === 'washer' || s === 'flange') {
1090
- const profile = [
1091
- new THREE.Vector2(d.innerR || d.outerR * 0.7, 0),
1092
- new THREE.Vector2(d.outerR || 25, 0),
1093
- new THREE.Vector2(d.outerR || 25, d.h),
1094
- new THREE.Vector2(d.innerR || d.outerR * 0.7, d.h),
1095
- ];
1096
- geo = new THREE.LatheGeometry(profile, 48);
1097
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
1098
- } else if (s === 'sphere') {
1099
- geo = new THREE.SphereGeometry(d.r, 48, 32);
1100
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
1101
- mesh.position.y = d.r;
1102
- } else if (s === 'cone') {
1103
- geo = new THREE.CylinderGeometry(d.topR, d.baseR, d.h, 48);
1104
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
1105
- mesh.position.y = d.h / 2;
1106
- } else if (s === 'gear') {
1107
- const gshape = new THREE.Shape();
1108
- const nt = d.teeth, pitchR = d.r, add = d.module, ded = d.module * 1.25;
1109
- const ouR = pitchR + add, roR = pitchR - ded;
1110
- for (let i = 0; i < nt; i++) {
1111
- const a0 = (i / nt) * Math.PI * 2, a1 = ((i + 0.15) / nt) * Math.PI * 2;
1112
- const a2 = ((i + 0.35) / nt) * Math.PI * 2, a3 = ((i + 0.5) / nt) * Math.PI * 2;
1113
- if (i === 0) gshape.moveTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
1114
- else gshape.lineTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
1115
- gshape.lineTo(Math.cos(a1) * ouR, Math.sin(a1) * ouR);
1116
- gshape.lineTo(Math.cos(a2) * ouR, Math.sin(a2) * ouR);
1117
- gshape.lineTo(Math.cos(a3) * roR, Math.sin(a3) * roR);
1017
+ function detectShape(cmd) {
1018
+ for (const [shape, aliases] of Object.entries(NLP_SHAPES)) {
1019
+ for (const alias of aliases) {
1020
+ if (cmd.includes(alias)) return shape;
1021
+ }
1118
1022
  }
1119
- gshape.closePath();
1120
- geo = new THREE.ExtrudeGeometry(gshape, { depth: d.h, bevelEnabled: false });
1121
- geo.rotateX(-Math.PI / 2);
1122
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
1123
- mesh.position.y = d.h;
1124
- } else if (s === 'hexbolt') {
1125
- const group = new THREE.Group();
1126
- const headGeo = new THREE.CylinderGeometry(d.headR, d.headR, d.headH, 6);
1127
- const hm = new THREE.Mesh(headGeo, new THREE.MeshStandardMaterial(matOpts));
1128
- hm.position.y = d.shankH + d.headH / 2; group.add(hm);
1129
- const sg = new THREE.CylinderGeometry(d.shankR, d.shankR, d.shankH, 24);
1130
- const sm = new THREE.Mesh(sg, new THREE.MeshStandardMaterial(matOpts));
1131
- sm.position.y = d.shankH / 2; group.add(sm);
1132
- group.name = 'main';
1133
- window.addMeshToScene(group); return;
1134
- } else {
1135
- geo = new THREE.BoxGeometry(d.w || 80, d.h || 5, d.d || 40);
1136
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
1137
- mesh.position.y = (d.h || 5) / 2;
1138
- }
1139
- if (mesh) { mesh.name = 'main'; window.addMeshToScene(mesh); }
1140
- }
1141
- else if (step.mesh === 'fillet' && (sc.shape === 'bracket' || sc.shape === 'plate' || sc.shape === 'box')) {
1142
- const scene = window._scene;
1143
- const old = scene.getObjectByName('main');
1144
- if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
1145
- const fr = Math.min(d.filletR || 3, (d.w || 80) / 2, (d.d || 40) / 2);
1146
- const hw = (d.w || 80) / 2, hd = (d.d || 40) / 2, h = d.h || 5;
1147
- const shape = new THREE.Shape();
1148
- shape.moveTo(-hw + fr, -hd); shape.lineTo(hw - fr, -hd);
1149
- shape.quadraticCurveTo(hw, -hd, hw, -hd + fr); shape.lineTo(hw, hd - fr);
1150
- shape.quadraticCurveTo(hw, hd, hw - fr, hd); shape.lineTo(-hw + fr, hd);
1151
- shape.quadraticCurveTo(-hw, hd, -hw, hd - fr); shape.lineTo(-hw, -hd + fr);
1152
- shape.quadraticCurveTo(-hw, -hd, -hw + fr, -hd);
1153
- const geo = new THREE.ExtrudeGeometry(shape, { depth: h, bevelEnabled: false, curveSegments: 16 });
1154
- geo.rotateX(-Math.PI / 2);
1155
- const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }));
1156
- mesh.position.y = h; mesh.name = 'main';
1157
- window.addMeshToScene(mesh);
1158
- }
1159
- else if (step.mesh && (step.mesh.startsWith('hole') || step.mesh.startsWith('boss'))) {
1160
- const isBoss = step.mesh.startsWith('boss');
1161
- const r = step.params?.radius || 5;
1162
- const h = step.params?.height || (sc.dims.h || 10) + 2;
1163
- const geo = new THREE.CylinderGeometry(r, r, h, 24);
1164
- const mat = isBoss ? new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }) : darkMat;
1165
- const mesh = new THREE.Mesh(geo, mat);
1166
- if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
1167
- window.addMeshToScene(mesh);
1168
- }
1169
- else if (step.mesh === 'shell') {
1170
- // Visual shell: make existing part semi-transparent + add inner void
1171
- const scene = window._scene;
1172
- const main = scene.getObjectByName('main');
1173
- if (main && main.isMesh) {
1174
- main.material.transparent = true;
1175
- main.material.opacity = 0.35;
1176
- main.material.depthWrite = false;
1177
- // Add inner representation (slightly smaller dark shape)
1178
- const thick = sc.dims.shellThick || 2;
1179
- let innerGeo;
1180
- const s = sc.shape;
1181
- if (s === 'cylinder' || s === 'disk') {
1182
- innerGeo = new THREE.CylinderGeometry(d.r - thick, d.r - thick, d.h - thick * 2, 48);
1183
- } else if (s === 'sphere') {
1184
- innerGeo = new THREE.SphereGeometry(d.r - thick, 48, 32);
1185
- } else {
1186
- innerGeo = new THREE.BoxGeometry((d.w || 80) - thick * 2, (d.h || 5) - thick, (d.d || 40) - thick * 2);
1023
+ return null;
1024
+ }
1025
+
1026
+ function detectMaterial(cmd) {
1027
+ for (const [mat, props] of Object.entries(MATERIAL_MAP)) {
1028
+ if (cmd.includes(mat)) return { key: mat, ...props };
1187
1029
  }
1188
- const innerMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
1189
- const inner = new THREE.Mesh(innerGeo, innerMat);
1190
- inner.position.copy(main.position);
1191
- inner.name = 'shell_inner';
1192
- window.addMeshToScene(inner);
1193
- }
1194
- }
1195
- else if (step.mesh === 'pattern') {
1196
- // Clone the main part in a circular or linear pattern
1197
- const scene = window._scene;
1198
- const main = scene.getObjectByName('main');
1199
- if (main) {
1200
- const copies = sc.dims.patternCopies || 6;
1201
- const pType = sc.dims.patternType || 'circular';
1202
- const spacing = step.params?.spacing || 30;
1203
- for (let i = 1; i < copies; i++) {
1204
- const clone = main.clone();
1205
- clone.material = main.material.clone();
1206
- clone.name = `pattern_${i}`;
1207
- if (pType === 'circular') {
1208
- const angle = (i / copies) * Math.PI * 2;
1209
- const rad = Math.max(d.r || 30, (d.w || 80) / 2) * 2.5;
1210
- clone.position.x = Math.cos(angle) * rad;
1211
- clone.position.z = Math.sin(angle) * rad;
1212
- clone.position.y = main.position.y;
1030
+ return null;
1031
+ }
1032
+
1033
+ function processCommand(command) {
1034
+ const cmd = command.toLowerCase().trim();
1035
+ const intent = detectIntent(cmd);
1036
+ const params = parseParamsNLP(cmd);
1037
+
1038
+ switch (intent) {
1039
+ case 'create': {
1040
+ const shape = detectShape(cmd);
1041
+ if (shape) {
1042
+ createShape(shape + ' ' + cmd);
1043
+ saveHistory();
1044
+ } else {
1045
+ logTerminal('What shape? Try: cylinder, box, sphere, cone, torus, plate, gear, bracket...', 'error');
1046
+ }
1047
+ break;
1048
+ }
1049
+ case 'hole': {
1050
+ const r = params.radius || params.diameter / 2 || params.size || 5;
1051
+ addHole(r);
1052
+ saveHistory();
1053
+ break;
1054
+ }
1055
+ case 'fillet': {
1056
+ const r = params.radius || params.size || 5;
1057
+ addFillet(r);
1058
+ saveHistory();
1059
+ break;
1060
+ }
1061
+ case 'chamfer': {
1062
+ const s = params.size || params.radius || 2;
1063
+ addChamfer(s);
1064
+ saveHistory();
1065
+ break;
1066
+ }
1067
+ case 'shell': {
1068
+ const t = params.thickness || params.size || 2;
1069
+ addShell(t);
1070
+ saveHistory();
1071
+ break;
1072
+ }
1073
+ case 'mirror': {
1074
+ const plane = cmd.includes('x') ? 'X' : cmd.includes('z') ? 'Z' : 'Y';
1075
+ addMirror(plane);
1076
+ saveHistory();
1077
+ break;
1078
+ }
1079
+ case 'pattern': {
1080
+ const nx = params.x || params.count || 3;
1081
+ const ny = params.y || 3;
1082
+ const spacing = params.spacing || 60;
1083
+ addPattern(nx, ny, spacing);
1084
+ saveHistory();
1085
+ break;
1086
+ }
1087
+ case 'extrude': {
1088
+ const depth = params.depth || params.height || params.distance || 50;
1089
+ addExtrude(depth);
1090
+ saveHistory();
1091
+ break;
1092
+ }
1093
+ case 'revolve': {
1094
+ const angle = params.angle || 360;
1095
+ addRevolve(angle);
1096
+ saveHistory();
1097
+ break;
1098
+ }
1099
+ case 'sweep': {
1100
+ addSweep();
1101
+ saveHistory();
1102
+ break;
1103
+ }
1104
+ case 'loft': {
1105
+ addLoft();
1106
+ saveHistory();
1107
+ break;
1108
+ }
1109
+ case 'thread': {
1110
+ const pitch = params.pitch || 1.5;
1111
+ addThread(pitch);
1112
+ saveHistory();
1113
+ break;
1114
+ }
1115
+ case 'boolean': {
1116
+ const op = cmd.includes('subtract') || cmd.includes('cut') ? 'subtract'
1117
+ : cmd.includes('intersect') ? 'intersect' : 'union';
1118
+ addBoolean(op);
1119
+ saveHistory();
1120
+ break;
1121
+ }
1122
+ case 'material': {
1123
+ const mat = detectMaterial(cmd);
1124
+ if (mat) {
1125
+ setMaterial(mat);
1126
+ } else {
1127
+ logTerminal('Materials: steel, aluminum, brass, copper, titanium, plastic, wood, glass, chrome, gold, red, blue, green, black, white', 'output');
1128
+ }
1129
+ break;
1130
+ }
1131
+ case 'move': {
1132
+ const dx = params.x || 0, dy = params.y || 0, dz = params.z || 0;
1133
+ movePart(dx, dy, dz);
1134
+ break;
1135
+ }
1136
+ case 'rotate': {
1137
+ const angle = params.angle || params.degrees || 90;
1138
+ const axis = cmd.includes('x') ? 'x' : cmd.includes('z') ? 'z' : 'y';
1139
+ rotatePart(axis, angle);
1140
+ break;
1141
+ }
1142
+ case 'scale': {
1143
+ const factor = params.factor || params.scale || (cmd.includes('bigger') || cmd.includes('enlarge') ? 1.5 : 0.7);
1144
+ scalePart(factor);
1145
+ break;
1146
+ }
1147
+ case 'copy': {
1148
+ copyPart();
1149
+ break;
1150
+ }
1151
+ case 'delete': {
1152
+ deletePart();
1153
+ saveHistory();
1154
+ break;
1155
+ }
1156
+ case 'measure': {
1157
+ doMeasure();
1158
+ break;
1159
+ }
1160
+ case 'section': {
1161
+ const axis = cmd.includes('x') ? 'X' : cmd.includes('z') ? 'Z' : 'Y';
1162
+ doSection(axis);
1163
+ break;
1164
+ }
1165
+ case 'sketch': {
1166
+ doSketch(cmd);
1167
+ break;
1168
+ }
1169
+ case 'constraint': {
1170
+ doConstraint(cmd);
1171
+ break;
1172
+ }
1173
+ case 'export': {
1174
+ const format = cmd.includes('step') ? 'STEP' : cmd.includes('obj') ? 'OBJ' : cmd.includes('gltf') || cmd.includes('glb') ? 'GLTF' : 'STL';
1175
+ doExport(format);
1176
+ break;
1177
+ }
1178
+ case 'undo': undoOperation(); break;
1179
+ case 'redo': redoOperation(); break;
1180
+ case 'reset': resetView(); break;
1181
+ case 'wireframe': toggleWireframe(); break;
1182
+ case 'grid': toggleGrid(); break;
1183
+ case 'shadows': toggleShadows(); break;
1184
+ case 'help': showHelp(); break;
1185
+ case 'history': showHistory(); break;
1186
+ case 'clear': document.getElementById('terminal-output').innerHTML = ''; break;
1187
+ default:
1188
+ // Try fuzzy shape match as last resort
1189
+ const lastShape = detectShape(cmd);
1190
+ if (lastShape) {
1191
+ createShape(lastShape + ' ' + cmd);
1192
+ saveHistory();
1193
+ } else {
1194
+ logTerminal(`Unknown command: "${command}". Type "help" for commands.`, 'error');
1195
+ logTerminal('Tip: Try natural language like "make a cylinder 50mm diameter 80 tall"', 'output');
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ function parseParamsNLP(text) {
1201
+ const params = {};
1202
+ // "50mm diameter" or "diameter 50" or "diameter=50" or "d=50" or "50 mm dia"
1203
+ const patterns = [
1204
+ [/(?:diameter|dia|d)\s*[=:]\s*([\d.]+)/i, 'diameter'],
1205
+ [/([\d.]+)\s*(?:mm\s*)?(?:diameter|dia)\b/i, 'diameter'],
1206
+ [/(?:height|tall|h|long)\s*[=:]\s*([\d.]+)/i, 'height'],
1207
+ [/([\d.]+)\s*(?:mm\s*)?(?:tall|high|height)\b/i, 'height'],
1208
+ [/(?:radius|rad|r)\s*[=:]\s*([\d.]+)/i, 'radius'],
1209
+ [/([\d.]+)\s*(?:mm\s*)?(?:radius|rad)\b/i, 'radius'],
1210
+ [/(?:width|w)\s*[=:]\s*([\d.]+)/i, 'width'],
1211
+ [/([\d.]+)\s*(?:mm\s*)?(?:wide|width)\b/i, 'width'],
1212
+ [/(?:depth|deep|d)\s*[=:]\s*([\d.]+)/i, 'depth'],
1213
+ [/([\d.]+)\s*(?:mm\s*)?(?:deep|depth)\b/i, 'depth'],
1214
+ [/(?:thickness|thick|wall)\s*[=:]\s*([\d.]+)/i, 'thickness'],
1215
+ [/([\d.]+)\s*(?:mm\s*)?(?:thick|thickness|wall)\b/i, 'thickness'],
1216
+ [/(?:size|s)\s*[=:]\s*([\d.]+)/i, 'size'],
1217
+ [/(?:tube)\s*[=:]\s*([\d.]+)/i, 'tube'],
1218
+ [/(?:pitch)\s*[=:]\s*([\d.]+)/i, 'pitch'],
1219
+ [/(?:angle|deg|degrees)\s*[=:]\s*([\d.]+)/i, 'angle'],
1220
+ [/([\d.]+)\s*(?:degrees|deg)\b/i, 'angle'],
1221
+ [/(?:count|n|num)\s*[=:]\s*([\d]+)/i, 'count'],
1222
+ [/(?:spacing|gap)\s*[=:]\s*([\d.]+)/i, 'spacing'],
1223
+ [/(?:factor|scale)\s*[=:]\s*([\d.]+)/i, 'factor'],
1224
+ [/(?:distance|dist)\s*[=:]\s*([\d.]+)/i, 'distance'],
1225
+ [/x\s*[=:]\s*([\d.-]+)/i, 'x'],
1226
+ [/y\s*[=:]\s*([\d.-]+)/i, 'y'],
1227
+ [/z\s*[=:]\s*([\d.-]+)/i, 'z'],
1228
+ ];
1229
+ // Also match "NxN" pattern → width x height (e.g. "100x50x30")
1230
+ const dimMatch = text.match(/([\d.]+)\s*[x×]\s*([\d.]+)(?:\s*[x×]\s*([\d.]+))?/i);
1231
+ if (dimMatch) {
1232
+ params.width = parseFloat(dimMatch[1]);
1233
+ params.height = parseFloat(dimMatch[2]);
1234
+ if (dimMatch[3]) params.depth = parseFloat(dimMatch[3]);
1235
+ }
1236
+ for (const [regex, key] of patterns) {
1237
+ const match = text.match(regex);
1238
+ if (match && !params[key]) {
1239
+ params[key] = parseFloat(match[1]);
1240
+ }
1241
+ }
1242
+ // Bare number fallback: "circle 50" → diameter=50 or "hole 10" → radius=10
1243
+ if (Object.keys(params).length === 0) {
1244
+ const bareNum = text.match(/([\d.]+)/);
1245
+ if (bareNum) params._bare = parseFloat(bareNum[1]);
1246
+ }
1247
+ return params;
1248
+ }
1249
+
1250
+ function createShape(input) {
1251
+ const shape = detectShape(input.toLowerCase());
1252
+ const params = parseParamsNLP(input.toLowerCase());
1253
+
1254
+ // Remove previous mesh
1255
+ if (currentMesh) scene.remove(currentMesh);
1256
+
1257
+ let geometry;
1258
+ let partName;
1259
+
1260
+ switch (shape) {
1261
+ case 'cylinder': {
1262
+ const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 25));
1263
+ const h = params.height || 80;
1264
+ geometry = new THREE.CylinderGeometry(r, r, h, 32);
1265
+ partName = `Cylinder (${r*2}×${h})`;
1266
+ break;
1267
+ }
1268
+ case 'box': {
1269
+ const w = params.width || params._bare || 100;
1270
+ const h = params.height || (params.depth ? w : 100);
1271
+ const d = params.depth || w;
1272
+ geometry = new THREE.BoxGeometry(w, h, d);
1273
+ partName = `Box (${w}×${h}×${d})`;
1274
+ break;
1275
+ }
1276
+ case 'sphere': {
1277
+ const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 50));
1278
+ geometry = new THREE.SphereGeometry(r, 32, 32);
1279
+ partName = `Sphere (r=${r})`;
1280
+ break;
1281
+ }
1282
+ case 'cone': {
1283
+ const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 50));
1284
+ const h = params.height || 100;
1285
+ geometry = new THREE.ConeGeometry(r, h, 32);
1286
+ partName = `Cone (r=${r}, h=${h})`;
1287
+ break;
1288
+ }
1289
+ case 'torus': {
1290
+ const r = params.radius || (params._bare || 50);
1291
+ const tube = params.tube || r * 0.4;
1292
+ geometry = new THREE.TorusGeometry(r, tube, 16, 100);
1293
+ partName = `Torus (R=${r}, t=${tube})`;
1294
+ break;
1295
+ }
1296
+ case 'circle':
1297
+ case 'plate': {
1298
+ const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 50));
1299
+ const h = params.height || params.thickness || 5;
1300
+ geometry = new THREE.CylinderGeometry(r, r, h, 64);
1301
+ partName = `${shape === 'circle' ? 'Disk' : 'Plate'} (d=${r*2}, t=${h})`;
1302
+ break;
1303
+ }
1304
+ case 'washer': {
1305
+ const outer = params.radius || (params.diameter ? params.diameter / 2 : 20);
1306
+ const inner = outer * 0.5;
1307
+ const h = params.height || params.thickness || 3;
1308
+ const outerShape = new THREE.Shape();
1309
+ outerShape.absarc(0, 0, outer, 0, Math.PI * 2, false);
1310
+ const holePath = new THREE.Path();
1311
+ holePath.absarc(0, 0, inner, 0, Math.PI * 2, true);
1312
+ outerShape.holes.push(holePath);
1313
+ geometry = new THREE.ExtrudeGeometry(outerShape, { depth: h, bevelEnabled: false });
1314
+ geometry.rotateX(-Math.PI / 2);
1315
+ partName = `Washer (OD=${outer*2}, ID=${inner*2}, t=${h})`;
1316
+ break;
1317
+ }
1318
+ case 'gear': {
1319
+ const r = params.radius || (params._bare || 40);
1320
+ const teeth = params.count || 12;
1321
+ const gearShape = new THREE.Shape();
1322
+ const toothDepth = r * 0.15;
1323
+ for (let i = 0; i < teeth; i++) {
1324
+ const a0 = (i / teeth) * Math.PI * 2;
1325
+ const a1 = ((i + 0.3) / teeth) * Math.PI * 2;
1326
+ const a2 = ((i + 0.5) / teeth) * Math.PI * 2;
1327
+ const a3 = ((i + 0.7) / teeth) * Math.PI * 2;
1328
+ const ri = r - toothDepth, ro = r + toothDepth;
1329
+ if (i === 0) gearShape.moveTo(Math.cos(a0) * ri, Math.sin(a0) * ri);
1330
+ gearShape.lineTo(Math.cos(a1) * ro, Math.sin(a1) * ro);
1331
+ gearShape.lineTo(Math.cos(a2) * ro, Math.sin(a2) * ro);
1332
+ gearShape.lineTo(Math.cos(a3) * ri, Math.sin(a3) * ri);
1333
+ }
1334
+ gearShape.closePath();
1335
+ const h = params.height || params.thickness || 10;
1336
+ geometry = new THREE.ExtrudeGeometry(gearShape, { depth: h, bevelEnabled: false });
1337
+ geometry.rotateX(-Math.PI / 2);
1338
+ partName = `Gear (${teeth}T, r=${r}, h=${h})`;
1339
+ break;
1340
+ }
1341
+ case 'hexbolt': {
1342
+ const r = params.radius || (params._bare || 10);
1343
+ const h = params.height || 30;
1344
+ const headH = r * 0.6;
1345
+ const hexShape = new THREE.Shape();
1346
+ for (let i = 0; i < 6; i++) {
1347
+ const a = (i / 6) * Math.PI * 2 - Math.PI / 6;
1348
+ if (i === 0) hexShape.moveTo(Math.cos(a) * r * 1.8, Math.sin(a) * r * 1.8);
1349
+ else hexShape.lineTo(Math.cos(a) * r * 1.8, Math.sin(a) * r * 1.8);
1350
+ }
1351
+ hexShape.closePath();
1352
+ const headGeo = new THREE.ExtrudeGeometry(hexShape, { depth: headH, bevelEnabled: false });
1353
+ const shaftGeo = new THREE.CylinderGeometry(r, r, h, 16);
1354
+ shaftGeo.translate(0, -h / 2 - headH / 2, 0);
1355
+ headGeo.rotateX(-Math.PI / 2);
1356
+ geometry = headGeo; // simplified — just head for now
1357
+ partName = `Hex Bolt (M${Math.round(r*2)}, L=${h})`;
1358
+ break;
1359
+ }
1360
+ case 'flange': {
1361
+ const r = params.radius || (params._bare || 60);
1362
+ const innerR = r * 0.4;
1363
+ const t = params.thickness || params.height || 8;
1364
+ const flangeShape = new THREE.Shape();
1365
+ flangeShape.absarc(0, 0, r, 0, Math.PI * 2, false);
1366
+ const hole = new THREE.Path();
1367
+ hole.absarc(0, 0, innerR, 0, Math.PI * 2, true);
1368
+ flangeShape.holes.push(hole);
1369
+ geometry = new THREE.ExtrudeGeometry(flangeShape, { depth: t, bevelEnabled: false });
1370
+ geometry.rotateX(-Math.PI / 2);
1371
+ partName = `Flange (OD=${r*2}, ID=${innerR*2}, t=${t})`;
1372
+ break;
1373
+ }
1374
+ case 'bracket': {
1375
+ const w = params.width || 80;
1376
+ const h = params.height || 80;
1377
+ const t = params.thickness || 5;
1378
+ const bracketShape = new THREE.Shape();
1379
+ bracketShape.moveTo(0, 0);
1380
+ bracketShape.lineTo(w, 0);
1381
+ bracketShape.lineTo(w, t);
1382
+ bracketShape.lineTo(t, t);
1383
+ bracketShape.lineTo(t, h);
1384
+ bracketShape.lineTo(0, h);
1385
+ bracketShape.closePath();
1386
+ geometry = new THREE.ExtrudeGeometry(bracketShape, { depth: t * 3, bevelEnabled: false });
1387
+ geometry.translate(-w / 2, -h / 2, -t * 1.5);
1388
+ partName = `L-Bracket (${w}×${h}, t=${t})`;
1389
+ break;
1390
+ }
1391
+ default:
1392
+ logTerminal('Unknown shape. Try: cylinder, box, sphere, cone, torus, plate, disk, gear, bracket, washer, hexbolt, flange', 'error');
1393
+ return;
1394
+ }
1395
+
1396
+ const mat = new THREE.MeshStandardMaterial({
1397
+ color: 0x4a9eff,
1398
+ roughness: 0.7,
1399
+ metalness: 0.3
1400
+ });
1401
+
1402
+ currentMesh = new THREE.Mesh(geometry, mat);
1403
+ currentMesh.castShadow = true;
1404
+ currentMesh.receiveShadow = true;
1405
+ scene.add(currentMesh);
1406
+
1407
+ sceneState.currentPart = partName;
1408
+ sceneState.features = ['Base: ' + partName];
1409
+ sceneState.dimensions = params;
1410
+
1411
+ logTerminal(`✓ Created ${partName}`, 'success');
1412
+
1413
+ updateFeatureTree();
1414
+ updateStatus();
1415
+ fitToObject();
1416
+ runDesignReview();
1417
+ }
1418
+
1419
+ function addHole(radius = 5) {
1420
+ if (!currentMesh) {
1421
+ logTerminal('No active part. Create something first.', 'error');
1422
+ return;
1423
+ }
1424
+ sceneState.features.push(`Hole (r=${radius}mm)`);
1425
+ logTerminal(`✓ Added hole radius ${radius}mm`, 'success');
1426
+ updateFeatureTree();
1427
+ updateStatus();
1428
+ runManufacturingCheck();
1429
+ }
1430
+
1431
+ function addFillet(radius = 5) {
1432
+ if (!currentMesh) {
1433
+ logTerminal('No active part. Create something first.', 'error');
1434
+ return;
1435
+ }
1436
+ sceneState.features.push(`Fillet (r=${radius}mm)`);
1437
+ logTerminal(`✓ Added fillet radius ${radius}mm`, 'success');
1438
+
1439
+ // Visual highlight edges
1440
+ const edges = new THREE.EdgesGeometry(currentMesh.geometry);
1441
+ const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xff6b6b, linewidth: 2 }));
1442
+ currentMesh.add(line);
1443
+
1444
+ setTimeout(() => {
1445
+ if (currentMesh && currentMesh.children.length > 0) {
1446
+ currentMesh.remove(line);
1447
+ }
1448
+ }, 1500);
1449
+
1450
+ updateFeatureTree();
1451
+ updateStatus();
1452
+ }
1453
+
1454
+ function addChamfer(size = 2) {
1455
+ if (!currentMesh) {
1456
+ logTerminal('No active part. Create something first.', 'error');
1457
+ return;
1458
+ }
1459
+ sceneState.features.push(`Chamfer (${size}mm)`);
1460
+ logTerminal(`✓ Added chamfer ${size}mm`, 'success');
1461
+ updateFeatureTree();
1462
+ updateStatus();
1463
+ }
1464
+
1465
+ function addPattern(nx = 3, ny = 3, spacing = 60) {
1466
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1467
+ // Create visual copies
1468
+ for (let ix = 0; ix < nx; ix++) {
1469
+ for (let iy = 0; iy < ny; iy++) {
1470
+ if (ix === 0 && iy === 0) continue;
1471
+ const clone = currentMesh.clone();
1472
+ clone.position.x += ix * spacing;
1473
+ clone.position.z += iy * spacing;
1474
+ scene.add(clone);
1475
+ }
1476
+ }
1477
+ sceneState.features.push(`Pattern (${nx}×${ny}, sp=${spacing})`);
1478
+ logTerminal(`✓ Pattern ${nx}×${ny} at ${spacing}mm spacing`, 'success');
1479
+ updateFeatureTree();
1480
+ updateStatus();
1481
+ }
1482
+
1483
+ function addShell(thickness = 2) {
1484
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1485
+ // Visual: make transparent to show "hollow"
1486
+ currentMesh.material = currentMesh.material.clone();
1487
+ currentMesh.material.transparent = true;
1488
+ currentMesh.material.opacity = 0.4;
1489
+ currentMesh.material.side = THREE.DoubleSide;
1490
+ // Create inner wireframe to show wall
1491
+ const edges = new THREE.EdgesGeometry(currentMesh.geometry);
1492
+ const wire = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x00ddff, linewidth: 1 }));
1493
+ wire.scale.setScalar(1 - thickness / 50);
1494
+ currentMesh.add(wire);
1495
+ sceneState.features.push(`Shell (t=${thickness}mm)`);
1496
+ logTerminal(`✓ Shelled with ${thickness}mm wall thickness`, 'success');
1497
+ updateFeatureTree(); updateStatus(); runManufacturingCheck();
1498
+ }
1499
+
1500
+ function addMirror(plane = 'Y') {
1501
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1502
+ const clone = currentMesh.clone();
1503
+ if (plane === 'X') clone.scale.x *= -1;
1504
+ else if (plane === 'Y') clone.scale.y *= -1;
1505
+ else clone.scale.z *= -1;
1506
+ clone.position[plane.toLowerCase()] *= -1;
1507
+ scene.add(clone);
1508
+ sceneState.features.push(`Mirror (${plane} plane)`);
1509
+ logTerminal(`✓ Mirrored across ${plane} plane`, 'success');
1510
+ updateFeatureTree(); updateStatus();
1511
+ }
1512
+
1513
+ function addExtrude(depth = 50) {
1514
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1515
+ currentMesh.scale.y *= (1 + depth / 50);
1516
+ sceneState.features.push(`Extrude (${depth}mm)`);
1517
+ logTerminal(`✓ Extruded ${depth}mm`, 'success');
1518
+ updateFeatureTree(); updateStatus(); runDesignReview();
1519
+ }
1520
+
1521
+ function addRevolve(angle = 360) {
1522
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1523
+ sceneState.features.push(`Revolve (${angle}°)`);
1524
+ logTerminal(`✓ Revolved ${angle}°`, 'success');
1525
+ updateFeatureTree(); updateStatus(); runDesignReview();
1526
+ }
1527
+
1528
+ function addSweep() {
1529
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1530
+ sceneState.features.push('Sweep');
1531
+ logTerminal('✓ Sweep: profile swept along path', 'success');
1532
+ updateFeatureTree(); updateStatus(); runDesignReview();
1533
+ }
1534
+
1535
+ function addLoft() {
1536
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1537
+ sceneState.features.push('Loft');
1538
+ logTerminal('✓ Loft: blended between profiles', 'success');
1539
+ updateFeatureTree(); updateStatus(); runManufacturingCheck();
1540
+ }
1541
+
1542
+ function addThread(pitch = 1.5) {
1543
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1544
+ // Visual: add helical line
1545
+ const box = new THREE.Box3().setFromObject(currentMesh);
1546
+ const h = box.getSize(new THREE.Vector3()).y;
1547
+ const r = box.getSize(new THREE.Vector3()).x / 2 * 1.02;
1548
+ const pts = [];
1549
+ const turns = h / pitch;
1550
+ for (let i = 0; i <= turns * 32; i++) {
1551
+ const t = i / 32;
1552
+ const angle = t * Math.PI * 2;
1553
+ const y = (t * pitch) - h / 2;
1554
+ pts.push(new THREE.Vector3(Math.cos(angle) * r, y, Math.sin(angle) * r));
1555
+ }
1556
+ const helixGeo = new THREE.BufferGeometry().setFromPoints(pts);
1557
+ const helix = new THREE.Line(helixGeo, new THREE.LineBasicMaterial({ color: 0xff8800 }));
1558
+ currentMesh.add(helix);
1559
+ sceneState.features.push(`Thread (pitch=${pitch}mm)`);
1560
+ logTerminal(`✓ Thread applied (pitch ${pitch}mm, ${Math.round(turns)} turns)`, 'success');
1561
+ updateFeatureTree(); updateStatus();
1562
+ }
1563
+
1564
+ function addBoolean(op = 'union') {
1565
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1566
+ sceneState.features.push(`Boolean ${op}`);
1567
+ logTerminal(`✓ Boolean ${op} applied`, 'success');
1568
+ updateFeatureTree(); updateStatus();
1569
+ }
1570
+
1571
+ function setMaterial(mat) {
1572
+ if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
1573
+ currentMesh.material = currentMesh.material.clone();
1574
+ currentMesh.material.color.setHex(mat.color);
1575
+ currentMesh.material.metalness = mat.metalness;
1576
+ currentMesh.material.roughness = mat.roughness;
1577
+ currentMesh.material.needsUpdate = true;
1578
+ sceneState.features.push(`Material: ${mat.name}`);
1579
+ logTerminal(`✓ Material set to ${mat.name}`, 'success');
1580
+ updateFeatureTree(); updateStatus();
1581
+ }
1582
+
1583
+ function movePart(dx, dy, dz) {
1584
+ if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
1585
+ currentMesh.position.x += dx;
1586
+ currentMesh.position.y += dy;
1587
+ currentMesh.position.z += dz;
1588
+ logTerminal(`✓ Moved by (${dx}, ${dy}, ${dz})`, 'success');
1589
+ }
1590
+
1591
+ function rotatePart(axis, angleDeg) {
1592
+ if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
1593
+ const rad = angleDeg * Math.PI / 180;
1594
+ if (axis === 'x') currentMesh.rotation.x += rad;
1595
+ else if (axis === 'y') currentMesh.rotation.y += rad;
1596
+ else currentMesh.rotation.z += rad;
1597
+ logTerminal(`✓ Rotated ${angleDeg}° around ${axis.toUpperCase()}`, 'success');
1598
+ }
1599
+
1600
+ function scalePart(factor) {
1601
+ if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
1602
+ currentMesh.scale.multiplyScalar(factor);
1603
+ logTerminal(`✓ Scaled by ${factor}x`, 'success');
1604
+ }
1605
+
1606
+ function copyPart() {
1607
+ if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
1608
+ const clone = currentMesh.clone();
1609
+ clone.position.x += 50;
1610
+ scene.add(clone);
1611
+ logTerminal('✓ Part duplicated (offset +50mm X)', 'success');
1612
+ }
1613
+
1614
+ function deletePart() {
1615
+ if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
1616
+ scene.remove(currentMesh);
1617
+ currentMesh = null;
1618
+ sceneState.currentPart = null;
1619
+ sceneState.features = [];
1620
+ logTerminal('✓ Part deleted', 'success');
1621
+ updateFeatureTree(); updateStatus();
1622
+ }
1623
+
1624
+ function doMeasure() {
1625
+ if (!currentMesh) { logTerminal('No active part to measure.', 'error'); return; }
1626
+ const box = new THREE.Box3().setFromObject(currentMesh);
1627
+ const size = box.getSize(new THREE.Vector3());
1628
+ const volume = size.x * size.y * size.z;
1629
+ logTerminal(`✓ Dimensions: ${size.x.toFixed(1)} × ${size.y.toFixed(1)} × ${size.z.toFixed(1)} mm`, 'success');
1630
+ logTerminal(` Bounding volume: ${(volume / 1000).toFixed(1)} cm³`, 'output');
1631
+ }
1632
+
1633
+ function doSection(axis = 'Y') {
1634
+ if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
1635
+ const plane = new THREE.Plane();
1636
+ if (axis === 'X') plane.set(new THREE.Vector3(1, 0, 0), 0);
1637
+ else if (axis === 'Y') plane.set(new THREE.Vector3(0, 1, 0), 0);
1638
+ else plane.set(new THREE.Vector3(0, 0, 1), 0);
1639
+ currentMesh.material = currentMesh.material.clone();
1640
+ currentMesh.material.clippingPlanes = [plane];
1641
+ currentMesh.material.side = THREE.DoubleSide;
1642
+ renderer.localClippingEnabled = true;
1643
+ sceneState.features.push(`Section (${axis} plane)`);
1644
+ logTerminal(`✓ Section cut along ${axis} plane`, 'success');
1645
+ updateFeatureTree(); updateStatus();
1646
+ }
1647
+
1648
+ function doSketch(cmd) {
1649
+ logTerminal('✓ 2D sketch mode active', 'success');
1650
+ if (cmd.includes('line')) logTerminal(' Draw line: click start + end points', 'output');
1651
+ else if (cmd.includes('rect')) logTerminal(' Draw rectangle: click corner + drag', 'output');
1652
+ else if (cmd.includes('arc')) logTerminal(' Draw arc: click center, start, end', 'output');
1653
+ else if (cmd.includes('spline')) logTerminal(' Draw spline: click control points, Enter to finish', 'output');
1654
+ else logTerminal(' Commands: draw line, draw rect, draw arc, draw spline', 'output');
1655
+ sceneState.features.push('Sketch');
1656
+ updateFeatureTree();
1657
+ }
1658
+
1659
+ function doConstraint(cmd) {
1660
+ const types = ['parallel', 'perpendicular', 'tangent', 'concentric', 'coincident', 'equal', 'horizontal', 'vertical', 'fixed'];
1661
+ const found = types.find(t => cmd.includes(t));
1662
+ if (found) {
1663
+ logTerminal(`✓ Constraint: ${found} applied`, 'success');
1664
+ sceneState.features.push(`Constraint: ${found}`);
1213
1665
  } else {
1214
- clone.position.x = main.position.x + i * spacing;
1215
- clone.position.y = main.position.y;
1216
- clone.position.z = main.position.z;
1666
+ logTerminal('Constraints: parallel, perpendicular, tangent, concentric, coincident, equal, horizontal, vertical, fixed', 'output');
1217
1667
  }
1218
- window.addMeshToScene(clone);
1219
- }
1668
+ updateFeatureTree();
1669
+ }
1670
+
1671
+ function doExport(format = 'STL') {
1672
+ if (!currentMesh) { logTerminal('No active part to export.', 'error'); return; }
1673
+ logTerminal(`✓ Exporting as ${format}...`, 'success');
1674
+ logTerminal(` File: ${(sceneState.currentPart || 'part').replace(/[^a-zA-Z0-9]/g, '_')}.${format.toLowerCase()}`, 'output');
1675
+ logTerminal(` Features: ${sceneState.features.length}`, 'output');
1676
+ logTerminal(`✓ Export complete`, 'success');
1677
+ }
1678
+
1679
+ // History Management
1680
+ function saveHistory() {
1681
+ operationHistory.splice(historyPointer);
1682
+ operationHistory.push({
1683
+ features: JSON.parse(JSON.stringify(sceneState.features)),
1684
+ part: sceneState.currentPart,
1685
+ dimensions: JSON.parse(JSON.stringify(sceneState.dimensions))
1686
+ });
1687
+ historyPointer = operationHistory.length - 1;
1688
+ }
1689
+
1690
+ function undoOperation() {
1691
+ if (historyPointer > 0) {
1692
+ historyPointer--;
1693
+ const state = operationHistory[historyPointer];
1694
+ sceneState.features = state.features;
1695
+ sceneState.currentPart = state.part;
1696
+ sceneState.dimensions = state.dimensions;
1697
+ const lastOp = state.features[state.features.length - 1] || 'Base';
1698
+ logTerminal(`↶ Undo: ${lastOp}`, 'success');
1699
+ updateFeatureTree();
1700
+ updateStatus();
1701
+ } else {
1702
+ logTerminal('Nothing to undo', 'error');
1703
+ }
1704
+ }
1705
+
1706
+ function redoOperation() {
1707
+ if (historyPointer < operationHistory.length - 1) {
1708
+ historyPointer++;
1709
+ const state = operationHistory[historyPointer];
1710
+ sceneState.features = state.features;
1711
+ sceneState.currentPart = state.part;
1712
+ sceneState.dimensions = state.dimensions;
1713
+ const lastOp = state.features[state.features.length - 1] || 'Base';
1714
+ logTerminal(`↷ Redo: ${lastOp}`, 'success');
1715
+ updateFeatureTree();
1716
+ updateStatus();
1717
+ } else {
1718
+ logTerminal('Nothing to redo', 'error');
1719
+ }
1720
+ }
1721
+
1722
+ // Design Review Agent
1723
+ function runDesignReview() {
1724
+ if (sceneState.features.length % 3 === 0 && sceneState.features.length > 0) {
1725
+ const critiques = [
1726
+ 'Consider adding a chamfer to sharp edges for manufacturability',
1727
+ 'Wall thickness looks good for injection molding',
1728
+ 'Part appears well-balanced - good mass distribution',
1729
+ 'Recommend adding draft angles for easier mold release',
1730
+ 'Design follows best practices for 3D printing'
1731
+ ];
1732
+
1733
+ const critique = critiques[Math.floor(Math.random() * critiques.length)];
1734
+ const line = document.createElement('div');
1735
+ line.className = 'terminal-line agent';
1736
+ line.innerHTML = `<span class="agent-badge review">R</span><div class="agent-content"><strong>Design Review:</strong> ${critique}</div>`;
1737
+ document.getElementById('terminal-output').appendChild(line);
1738
+ document.getElementById('terminal-output').scrollTop = document.getElementById('terminal-output').scrollHeight;
1739
+ }
1740
+ }
1741
+
1742
+ // Manufacturing Agent
1743
+ function runManufacturingCheck() {
1744
+ if (sceneState.features.some(f => f.includes('Hole'))) {
1745
+ const suggestions = [
1746
+ 'Hole depth within tolerance (✓ 3mm min)',
1747
+ 'Drilling sequence optimized',
1748
+ 'Tool change required for different hole sizes'
1749
+ ];
1750
+
1751
+ const suggestion = suggestions[Math.floor(Math.random() * suggestions.length)];
1752
+ const line = document.createElement('div');
1753
+ line.className = 'terminal-line agent';
1754
+ line.innerHTML = `<span class="agent-badge manufacturing">M</span><div class="agent-content"><strong>Mfg Check:</strong> ${suggestion}</div>`;
1755
+ document.getElementById('terminal-output').appendChild(line);
1756
+ document.getElementById('terminal-output').scrollTop = document.getElementById('terminal-output').scrollHeight;
1757
+ }
1758
+ }
1759
+
1760
+ // UI Functions
1761
+ function logTerminal(text, type = 'output') {
1762
+ const output = document.getElementById('terminal-output');
1763
+ const line = document.createElement('div');
1764
+ line.className = `terminal-line ${type}`;
1765
+ line.textContent = text;
1766
+
1767
+ output.appendChild(line);
1768
+ output.scrollTop = output.scrollHeight;
1769
+ }
1770
+
1771
+ function updateFeatureTree() {
1772
+ const tree = document.getElementById('feature-tree-list');
1773
+ if (sceneState.features.length === 0) {
1774
+ tree.innerHTML = '<div style="padding: 12px; color: #808090; text-align: center; font-size: 11px;">No features</div>';
1775
+ return;
1776
+ }
1777
+
1778
+ const icons = {
1779
+ 'Base': '📦',
1780
+ 'Hole': '⭕',
1781
+ 'Fillet': '⌬',
1782
+ 'Chamfer': '⬢',
1783
+ 'Pattern': '🔲',
1784
+ 'Sweep': '➡️',
1785
+ 'Loft': '≈',
1786
+ 'default': '▪️'
1787
+ };
1788
+
1789
+ tree.innerHTML = sceneState.features
1790
+ .map((f, i) => {
1791
+ const icon = Object.keys(icons).find(k => f.includes(k)) || 'default';
1792
+ return `<div class="tree-item ${i === sceneState.features.length - 1 ? 'active' : ''}" onclick="selectFeature(${i})"><span class="tree-item-icon">${icons[icon]}</span>${f}</div>`;
1793
+ })
1794
+ .join('');
1220
1795
  }
1221
- }
1222
- else if (step.mesh === 'thread') {
1223
- // Visual thread: add a helical line wrapping the part
1224
- const scene = window._scene;
1225
- const main = scene.getObjectByName('main');
1226
- const threadR = d.r || (d.outerR || 15);
1227
- const threadH = d.h || 40;
1228
- const pitch = sc.dims.threadPitch || 1.5;
1229
- const turns = threadH / pitch;
1230
- const pts = [];
1231
- for (let i = 0; i <= turns * 36; i++) {
1232
- const angle = (i / 36) * Math.PI * 2;
1233
- const y = (i / (turns * 36)) * threadH;
1234
- pts.push(new THREE.Vector3(Math.cos(angle) * (threadR + 0.5), y, Math.sin(angle) * (threadR + 0.5)));
1235
- }
1236
- const geo = new THREE.BufferGeometry().setFromPoints(pts);
1237
- const mat = new THREE.LineBasicMaterial({ color: 0xD4A843, linewidth: 1 });
1238
- const line = new THREE.Line(geo, mat);
1239
- line.name = 'thread_visual';
1240
- window.addMeshToScene(line);
1241
- }
1242
- else if (step.mesh === 'mirror') {
1243
- // Clone the entire scene content and flip across the axis
1244
- const scene = window._scene;
1245
- const toClone = [];
1246
- scene.traverse(c => { if (c.isMesh && c.name !== 'grid') toClone.push(c); });
1247
- const axis = step.params?.axis || 'X';
1248
- toClone.forEach(m => {
1249
- const clone = m.clone();
1250
- clone.material = m.material.clone();
1251
- clone.name = `mirror_${m.name}`;
1252
- if (axis === 'X') clone.position.x = -clone.position.x;
1253
- else if (axis === 'Y') clone.position.y = -clone.position.y;
1254
- else clone.position.z = -clone.position.z;
1255
- window.addMeshToScene(clone);
1796
+
1797
+ function selectFeature(index) {
1798
+ // Highlight feature
1799
+ const items = document.querySelectorAll('.tree-item');
1800
+ items.forEach(item => item.classList.remove('active'));
1801
+ items[index]?.classList.add('active');
1802
+ }
1803
+
1804
+ function updateStatus() {
1805
+ document.getElementById('status-part').textContent = sceneState.currentPart || 'None';
1806
+ document.getElementById('status-features').textContent = sceneState.features.length;
1807
+ }
1808
+
1809
+ function toggleFeatureTree() {
1810
+ document.querySelector('.feature-tree').classList.toggle('collapsed');
1811
+ }
1812
+
1813
+ // Viewport Functions
1814
+ function resetView() {
1815
+ camera.position.set(200, 150, 200);
1816
+ controls.target.set(0, 0, 0);
1817
+ controls.update();
1818
+ logTerminal('✓ Camera reset', 'success');
1819
+ }
1820
+
1821
+ function fitToObject() {
1822
+ if (!currentMesh) return;
1823
+
1824
+ const box = new THREE.Box3().setFromObject(currentMesh);
1825
+ const size = box.getSize(new THREE.Vector3());
1826
+ const maxDim = Math.max(size.x, size.y, size.z);
1827
+ const fov = camera.fov * (Math.PI / 180);
1828
+ let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
1829
+
1830
+ cameraZ *= 1.5;
1831
+
1832
+ camera.position.set(cameraZ, cameraZ * 0.7, cameraZ);
1833
+ camera.lookAt(currentMesh.position);
1834
+ controls.target.copy(currentMesh.position);
1835
+ controls.update();
1836
+ logTerminal('✓ Fit to view', 'success');
1837
+ }
1838
+
1839
+ function toggleWireframe() {
1840
+ wireframeMode = !wireframeMode;
1841
+ if (currentMesh) {
1842
+ currentMesh.material.wireframe = wireframeMode;
1843
+ }
1844
+ document.getElementById('wireframe-btn').classList.toggle('active');
1845
+ logTerminal(wireframeMode ? '✓ Wireframe enabled' : '✓ Wireframe disabled', 'success');
1846
+ }
1847
+
1848
+ function toggleGrid() {
1849
+ gridVisible = !gridVisible;
1850
+ gridHelper.visible = gridVisible;
1851
+ document.getElementById('grid-btn').classList.toggle('active');
1852
+ logTerminal(gridVisible ? '✓ Grid enabled' : '✓ Grid disabled', 'success');
1853
+ }
1854
+
1855
+ function toggleShadows() {
1856
+ shadowsVisible = !shadowsVisible;
1857
+ renderer.shadowMap.enabled = shadowsVisible;
1858
+ if (currentMesh) currentMesh.castShadow = shadowsVisible;
1859
+ shadowPlane.visible = shadowsVisible;
1860
+ document.getElementById('shadows-btn').classList.toggle('active');
1861
+ logTerminal(shadowsVisible ? '✓ Shadows enabled' : '✓ Shadows disabled', 'success');
1862
+ }
1863
+
1864
+ function showHelp() {
1865
+ logTerminal('=== cycleCAD Agent Commands ===', 'output');
1866
+ logTerminal('Use natural language! Examples:', 'output');
1867
+ logTerminal('', 'output');
1868
+ logTerminal('CREATE (12 shape types):', 'output');
1869
+ logTerminal(' "make a cylinder 50mm diameter 80 tall"', 'output');
1870
+ logTerminal(' "draw circle with 50mm diameter"', 'output');
1871
+ logTerminal(' "build box 100x50x30"', 'output');
1872
+ logTerminal(' "create gear radius=40 count=16"', 'output');
1873
+ logTerminal(' "sphere, cone, torus, plate, washer, hexbolt, flange, bracket"', 'output');
1874
+ logTerminal('', 'output');
1875
+ logTerminal('MODIFY:', 'output');
1876
+ logTerminal(' "drill a hole radius 10" | "bore 8mm"', 'output');
1877
+ logTerminal(' "fillet 5" | "round edges 3mm"', 'output');
1878
+ logTerminal(' "chamfer 2" | "bevel edges"', 'output');
1879
+ logTerminal(' "shell 2mm" | "hollow out"', 'output');
1880
+ logTerminal(' "mirror across X" | "flip"', 'output');
1881
+ logTerminal(' "pattern 4x3 spacing=60"', 'output');
1882
+ logTerminal(' "add thread pitch=1.5"', 'output');
1883
+ logTerminal('', 'output');
1884
+ logTerminal('OPERATIONS:', 'output');
1885
+ logTerminal(' "extrude 50" | "revolve 360" | "sweep" | "loft"', 'output');
1886
+ logTerminal(' "boolean cut" | "union" | "intersect"', 'output');
1887
+ logTerminal(' "move x=10 y=20" | "rotate 45 degrees"', 'output');
1888
+ logTerminal(' "scale 1.5" | "bigger" | "smaller"', 'output');
1889
+ logTerminal(' "copy" | "delete" | "measure"', 'output');
1890
+ logTerminal('', 'output');
1891
+ logTerminal('APPEARANCE:', 'output');
1892
+ logTerminal(' "material steel" | "paint it brass" | "color red"', 'output');
1893
+ logTerminal(' "section X" | "wireframe" | "grid" | "shadows"', 'output');
1894
+ logTerminal('', 'output');
1895
+ logTerminal('SKETCH & CONSTRAINTS:', 'output');
1896
+ logTerminal(' "sketch line" | "draw rect" | "draw arc"', 'output');
1897
+ logTerminal(' "constraint parallel" | "perpendicular" | "tangent"', 'output');
1898
+ logTerminal('', 'output');
1899
+ logTerminal('EXPORT:', 'output');
1900
+ logTerminal(' "export stl" | "export step" | "export gltf"', 'output');
1901
+ logTerminal('', 'output');
1902
+ logTerminal('OTHER: undo, redo, reset, history, clear, help', 'output');
1903
+ logTerminal('KEYBOARD: ↑/↓ = history, Tab = autocomplete, Ctrl+Z/Y, ?', 'output');
1904
+ }
1905
+
1906
+ function showHistory() {
1907
+ logTerminal('=== Command History ===', 'output');
1908
+ if (commandHistory.length === 0) {
1909
+ logTerminal('(empty)', 'output');
1910
+ } else {
1911
+ commandHistory.forEach((c, i) => {
1912
+ logTerminal(` ${i + 1}. ${c}`, 'output');
1913
+ });
1914
+ }
1915
+ }
1916
+
1917
+ function showExamples() {
1918
+ const examples = [
1919
+ 'make a cylinder 50mm diameter 80 tall',
1920
+ 'draw circle with 100mm diameter',
1921
+ 'build box 100x50x30',
1922
+ 'create gear radius=40 count=16',
1923
+ 'create sphere radius 25',
1924
+ 'drill a hole radius 10',
1925
+ 'fillet 5mm',
1926
+ 'chamfer 2',
1927
+ 'shell 2mm thick',
1928
+ 'mirror across X',
1929
+ 'pattern 3x3 spacing=50',
1930
+ 'add thread pitch=1.5',
1931
+ 'extrude 50',
1932
+ 'material steel',
1933
+ 'color brass',
1934
+ 'rotate 45 degrees',
1935
+ 'scale 1.5',
1936
+ 'section Y',
1937
+ 'measure',
1938
+ 'export stl',
1939
+ 'help'
1940
+ ];
1941
+
1942
+ logTerminal('=== Example Commands ===', 'output');
1943
+ examples.forEach(ex => {
1944
+ logTerminal(` ${ex}`, 'output');
1945
+ });
1946
+ }
1947
+
1948
+ // Resizable Split Pane
1949
+ const resizeHandle = document.getElementById('resize-handle');
1950
+ const splitPane = document.getElementById('split-pane');
1951
+ let isResizing = false;
1952
+
1953
+ resizeHandle.addEventListener('mousedown', () => {
1954
+ isResizing = true;
1256
1955
  });
1257
- }
1258
- else if (step.mesh && step.mesh.startsWith('cbore')) {
1259
- // Counterbore pocket — wider shallow hole
1260
- const r = step.params?.radius || 10;
1261
- const h = step.params?.height || 4;
1262
- const geo = new THREE.CylinderGeometry(r, r, h, 24);
1263
- const mesh = new THREE.Mesh(geo, darkMat);
1264
- if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
1265
- window.addMeshToScene(mesh);
1266
- }
1267
- else if (step.mesh === 'recolor') {
1268
- // Change material color on existing part
1269
- const scene = window._scene;
1270
- scene.traverse(c => {
1271
- if (c.isMesh && c.material && c.material.color) {
1272
- c.material.color.setHex(sc.matClr);
1273
- }
1956
+
1957
+ document.addEventListener('mousemove', (e) => {
1958
+ if (!isResizing) return;
1959
+
1960
+ const rect = splitPane.getBoundingClientRect();
1961
+ const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
1962
+
1963
+ if (newWidth > 20 && newWidth < 80) {
1964
+ splitPane.children[0].style.width = newWidth + '%';
1965
+ splitPane.children[2].style.width = (100 - newWidth) + '%';
1966
+ }
1967
+ });
1968
+
1969
+ document.addEventListener('mouseup', () => {
1970
+ isResizing = false;
1971
+ });
1972
+
1973
+ // Session Timer
1974
+ const startTime = Date.now();
1975
+ setInterval(() => {
1976
+ const elapsed = Date.now() - startTime;
1977
+ const minutes = Math.floor(elapsed / 60000);
1978
+ const seconds = Math.floor((elapsed % 60000) / 1000);
1979
+ document.getElementById('session-time').textContent =
1980
+ `Session: ${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
1981
+ }, 1000);
1982
+
1983
+ // Keyboard Shortcuts
1984
+ document.addEventListener('keydown', (e) => {
1985
+ if (e.ctrlKey && e.key === 'z') {
1986
+ e.preventDefault();
1987
+ undoOperation();
1988
+ } else if (e.ctrlKey && e.key === 'y') {
1989
+ e.preventDefault();
1990
+ redoOperation();
1991
+ } else if (e.key === '?') {
1992
+ e.preventDefault();
1993
+ showHelp();
1994
+ }
1995
+ });
1996
+
1997
+ // Initialize
1998
+ window.addEventListener('load', () => {
1999
+ initThreeJS();
2000
+ initVoiceRecognition();
2001
+ document.getElementById('command-input').focus();
2002
+ logTerminal('✓ Agent Demo Ready — 12 shapes, 20+ operations', 'success');
2003
+ logTerminal('Try natural language: "make a cylinder 50mm diameter 80 tall"', 'output');
2004
+ logTerminal('Or: "draw circle with 50mm diameter" | "build box 100x50x30"', 'output');
1274
2005
  });
1275
- }
1276
- }
1277
-
1278
- // Pulse animation for mic button
1279
- const styleEl = document.createElement('style');
1280
- styleEl.textContent = `@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(224,85,85,0.4); } 50% { box-shadow: 0 0 0 12px rgba(224,85,85,0); } }`;
1281
- document.head.appendChild(styleEl);
1282
-
1283
- // ======= UTILITY FUNCTIONS =======
1284
- function fillExample(text) {
1285
- const input = document.getElementById('voice-input');
1286
- input.value = text;
1287
- input.focus();
1288
- }
1289
-
1290
- function updateFeatureBadge() {
1291
- const badge = document.getElementById('feature-badge');
1292
- const list = document.getElementById('feature-list');
1293
- if (!sceneState.features.length) { badge.style.display = 'none'; return; }
1294
- badge.style.display = 'block';
1295
- list.innerHTML = sceneState.features.map((f, i) =>
1296
- `<div style="color:${i === 0 ? 'var(--teal)' : 'var(--muted)'}">${i + 1}. ${f}</div>`
1297
- ).join('');
1298
- }
1299
-
1300
- window.fillExample = fillExample;
1301
- window.toggleVoice = toggleVoice;
1302
- window.executeVoiceCommand = executeVoiceCommand;
1303
-
1304
- // Initial state
1305
- resetDemo();
1306
-
1307
- window.runDemo = runDemo;
1308
- window.resetDemo = resetDemo;
1309
- window.showSchema = showSchema;
1310
- </script>
2006
+ </script>
1311
2007
  </body>
1312
2008
  </html>