cyclecad 0.1.8 → 0.1.10

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,1049 +1,1463 @@
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
- <!-- Voice input bar -->
102
- <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:700px;">
103
- <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>
104
- <input id="voice-input" type="text" placeholder="Step 1: &quot;build a cylinder 50mm diameter 80 tall&quot; → Step 2: &quot;add a hole radius 10&quot; → Step 3: &quot;export stl&quot;" style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;font-family:inherit;outline:none;" onkeydown="if(event.key==='Enter')executeVoiceCommand()">
105
- <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;">Build It</button>
106
- <div id="voice-status" style="position:absolute;top:-22px;left:16px;font-size:11px;color:var(--dim);"></div>
107
- </div>
108
-
109
- <div class="controls">
110
- <button class="btn-run" id="btn-run" onclick="runDemo()">▶ Run Agent Demo</button>
111
- <button class="btn-reset" onclick="resetDemo()">↺ Reset</button>
112
- <button class="btn-schema" onclick="showSchema()">{ } API Schema</button>
113
- </div>
114
-
115
- <script type="importmap">
116
- { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js" } }
117
- </script>
118
- <script type="module">
119
- import * as THREE from 'three';
120
- import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/controls/OrbitControls.js';
121
-
122
- // ========== Mini 3D viewport ==========
123
- const canvas = document.getElementById('viewport3d');
124
- const scene = new THREE.Scene();
125
- scene.background = new THREE.Color(0x0A1628);
126
- const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
127
- camera.position.set(60, 50, 80);
128
- const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
129
- renderer.setSize(canvas.clientWidth, canvas.clientHeight);
130
- renderer.setPixelRatio(window.devicePixelRatio);
131
- const controls = new OrbitControls(camera, renderer.domElement);
132
- controls.enableDamping = true;
133
- controls.dampingFactor = 0.05;
134
-
135
- // Lights
136
- scene.add(new THREE.AmbientLight(0xffffff, 0.4));
137
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
138
- dirLight.position.set(30, 50, 40);
139
- scene.add(dirLight);
140
-
141
- // Grid
142
- const grid = new THREE.GridHelper(200, 40, 0x1E3A5F, 0x122240);
143
- scene.add(grid);
144
-
145
- // Animate
146
- function animate() {
147
- requestAnimationFrame(animate);
148
- controls.update();
149
- renderer.render(scene, camera);
150
- }
151
- animate();
152
- window.addEventListener('resize', () => {
153
- camera.aspect = canvas.clientWidth / canvas.clientHeight;
154
- camera.updateProjectionMatrix();
155
- renderer.setSize(canvas.clientWidth, canvas.clientHeight);
156
- });
157
-
158
- // ========== Expose for demo script ==========
159
- window._scene = scene;
160
- window._camera = camera;
161
- window._renderer = renderer;
162
-
163
- window.addMeshToScene = (mesh) => { scene.add(mesh); };
164
- window.clearScene = () => {
165
- const toRemove = [];
166
- scene.traverse(c => { if (c.isMesh) toRemove.push(c); });
167
- toRemove.forEach(m => { scene.remove(m); m.geometry?.dispose(); m.material?.dispose(); });
168
- };
169
- window.THREE = THREE;
170
- </script>
171
-
172
- <script>
173
- const term = document.getElementById('term');
174
- const progress = document.getElementById('progress');
175
- let running = false;
176
- let cmdCount = 0;
177
- let startTime = 0;
178
-
179
- function addLine(html, cls = '') {
180
- const div = document.createElement('div');
181
- div.className = cls;
182
- div.innerHTML = html;
183
- term.appendChild(div);
184
- term.scrollTop = term.scrollHeight;
185
- }
186
-
187
- function addDivider() {
188
- const div = document.createElement('div');
189
- div.className = 'divider';
190
- term.appendChild(div);
191
- }
192
-
193
- function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
194
-
195
- function updateStats(data) {
196
- document.getElementById('stats-overlay').style.display = 'block';
197
- if (data.size) document.getElementById('stat-size').textContent = data.size;
198
- if (data.material) document.getElementById('stat-mat').textContent = data.material;
199
- if (data.printable !== undefined) document.getElementById('stat-print').textContent = data.printable ? '✅ Yes' : '❌ No';
200
- if (data.cost) document.getElementById('stat-cost').textContent = data.cost;
201
- document.getElementById('stat-cmds').textContent = cmdCount;
202
- document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
203
- }
204
-
205
- // ========== Demo Script ==========
206
- // This simulates what an AI agent does when it calls cycleCAD's API
207
-
208
- const DEMO_STEPS = [
209
- // Agent thinks
210
- { type: 'agent', text: '🤖 Agent received task: "Design a mounting bracket for the DUO cycleWASH with 4 M6 bolt holes"' },
211
- { type: 'comment', text: '// Agent analyzes requirements: 80×40mm base plate, 5mm thick, aluminum, 4 corner holes' },
212
- { type: 'delay', ms: 600 },
213
-
214
- // Phase 1: Sketch
215
- { type: 'divider' },
216
- { type: 'agent', text: '📐 Phase 1: Sketch the base profile' },
217
- { type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } },
218
- { type: 'cmd', method: 'sketch.rect', params: { x: -40, y: -20, width: 80, height: 40 } },
219
- { type: 'delay', ms: 300 },
220
-
221
- // Phase 2: Extrude
222
- { type: 'divider' },
223
- { type: 'agent', text: '📦 Phase 2: Extrude to 3D solid' },
224
- { type: 'cmd', method: 'ops.extrude', params: { height: 5, material: 'aluminum' }, mesh: 'bracket' },
225
- { type: 'delay', ms: 400 },
226
-
227
- // Phase 3: Add bolt holes (primitives positioned at corners)
228
- { type: 'divider' },
229
- { type: 'agent', text: '🔩 Phase 3: Add 4 M6 bolt holes at corners' },
230
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole1', pos: [-30, 2.5, -12] },
231
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole2', pos: [30, 2.5, -12] },
232
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole3', pos: [-30, 2.5, 12] },
233
- { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole4', pos: [30, 2.5, 12] },
234
- { type: 'delay', ms: 300 },
235
-
236
- // Phase 4: Fillet edges
237
- { type: 'divider' },
238
- { type: 'agent', text: '✨ Phase 4: Fillet edges for stress relief' },
239
- { type: 'cmd', method: 'ops.fillet', params: { target: 'bracket', radius: 15 }, mesh: 'fillet' },
240
- { type: 'delay', ms: 300 },
241
-
242
- // Phase 5: Validate
243
- { type: 'divider' },
244
- { type: 'agent', text: '🔍 Phase 5: Validate for manufacturing' },
245
- { type: 'cmd', method: 'validate.dimensions', params: { target: 'bracket' }, stat: 'size' },
246
- { type: 'cmd', method: 'validate.printability', params: { target: 'bracket', process: 'CNC' }, stat: 'printable' },
247
- { type: 'cmd', method: 'validate.cost', params: { target: 'bracket', process: 'CNC', material: 'aluminum' }, stat: 'cost' },
248
- { type: 'delay', ms: 400 },
249
-
250
- // Phase 6: Export
251
- { type: 'divider' },
252
- { type: 'agent', text: '📤 Phase 6: Export for manufacturing' },
253
- { type: 'cmd', method: 'export.stl', params: { filename: 'duo-bracket.stl', binary: true } },
254
- { type: 'delay', ms: 200 },
255
-
256
- // Done
257
- { type: 'divider' },
258
- { type: 'agent', text: '✅ Task complete. Bracket designed, validated, and exported in {TIME}s using {CMDS} API calls.' },
259
- { type: 'comment', text: '// No human touched a mouse. No GUI was opened. The agent designed through cycleCAD.' },
260
- ];
261
-
262
- async function runDemo() {
263
- if (running) return;
264
- running = true;
265
- cmdCount = 0;
266
- startTime = Date.now();
267
- document.getElementById('btn-run').disabled = true;
268
- document.getElementById('btn-run').textContent = '⏳ Running...';
269
- term.innerHTML = '';
270
- window.clearScene();
271
- document.getElementById('stats-overlay').style.display = 'none';
272
-
273
- const totalSteps = DEMO_STEPS.filter(s => s.type === 'cmd').length;
274
- let cmdIdx = 0;
275
-
276
- for (const step of DEMO_STEPS) {
277
- if (!running) break;
278
-
279
- if (step.type === 'agent') {
280
- let text = step.text
281
- .replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1))
282
- .replace('{CMDS}', cmdCount);
283
- addLine(text, 'agent');
284
- await delay(200);
285
- }
286
- else if (step.type === 'comment') {
287
- addLine(step.text, 'comment');
288
- await delay(100);
289
- }
290
- else if (step.type === 'divider') {
291
- addDivider();
292
- }
293
- else if (step.type === 'delay') {
294
- await delay(step.ms);
295
- }
296
- else if (step.type === 'cmd') {
297
- cmdCount++;
298
- cmdIdx++;
299
- progress.style.width = (cmdIdx / totalSteps * 100) + '%';
300
-
301
- const cmdStr = `cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`;
302
- addLine(cmdStr, 'cmd');
303
- await delay(150);
304
-
305
- // Simulate the result
306
- const result = simulateCommand(step);
307
- const resStr = JSON.stringify(result, null, 0);
308
- addLine(`→ ${resStr}`, 'res ok');
309
-
310
- // Update stats
311
- if (step.stat === 'size') updateStats({ size: '80 × 40 × 5 mm' });
312
- if (step.stat === 'printable') updateStats({ printable: true });
313
- if (step.stat === 'cost') updateStats({ cost: '$12.40 (CNC)' });
314
- updateStats({ material: 'Aluminum' });
315
-
316
- // Add 3D geometry
317
- if (step.mesh) {
318
- createMesh(step);
319
- }
320
-
321
- await delay(250);
322
- }
323
- }
324
-
325
- running = false;
326
- document.getElementById('btn-run').disabled = false;
327
- document.getElementById('btn-run').textContent = '▶ Run Again';
328
- }
329
-
330
- function simulateCommand(step) {
331
- const m = step.method;
332
- if (m === 'sketch.start') return { ok: true, result: { plane: 'XY', status: 'active' } };
333
- if (m === 'sketch.rect') return { ok: true, result: { id: 'rect_1', type: 'rect', width: step.params.width, height: step.params.height } };
334
- 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 } } };
335
- if (m === 'ops.primitive') return { ok: true, result: { id: step.mesh, type: 'cylinder', shape: 'cylinder' } };
336
- if (m === 'ops.fillet') return { ok: true, result: { target: 'bracket', radius: step.params.radius, applied: true } };
337
- if (m === 'validate.dimensions') return { ok: true, result: { width: 80, height: 5, depth: 40, volume: 16000, fitsInPrintBed: true } };
338
- if (m === 'validate.printability') return { ok: true, result: { printable: true, process: 'CNC', issues: [] } };
339
- if (m === 'validate.cost') return { ok: true, result: { process: 'CNC', unitCost: 12.40, batchOf100: 892 } };
340
- if (m === 'export.stl') return { ok: true, result: { format: 'stl', filename: step.params.filename, featureCount: 5 } };
341
- return { ok: true };
342
- }
343
-
344
- function createMesh(step) {
345
- const THREE = window.THREE;
346
- if (step.mesh === 'bracket') {
347
- // Main bracket body — sharp box (will be replaced by fillet step)
348
- const geo = new THREE.BoxGeometry(80, 5, 40);
349
- const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
350
- const mesh = new THREE.Mesh(geo, mat);
351
- mesh.position.y = 2.5;
352
- mesh.name = 'bracket';
353
- window.addMeshToScene(mesh);
354
- }
355
- else if (step.mesh === 'fillet') {
356
- // Replace sharp bracket with rounded version — visible fillet r=15
357
- const scene = window._scene;
358
- // Remove old bracket
359
- const old = scene.getObjectByName('bracket');
360
- if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
361
-
362
- const r = step.params?.radius || 15; // fillet radius from command
363
- const w = 80, h = 5, d = 40;
364
- const shape = new THREE.Shape();
365
- // Rounded rectangle in XZ plane (we'll extrude along Y)
366
- const hw = w / 2, hd = d / 2;
367
- shape.moveTo(-hw + r, -hd);
368
- shape.lineTo(hw - r, -hd);
369
- shape.quadraticCurveTo(hw, -hd, hw, -hd + r);
370
- shape.lineTo(hw, hd - r);
371
- shape.quadraticCurveTo(hw, hd, hw - r, hd);
372
- shape.lineTo(-hw + r, hd);
373
- shape.quadraticCurveTo(-hw, hd, -hw, hd - r);
374
- shape.lineTo(-hw, -hd + r);
375
- shape.quadraticCurveTo(-hw, -hd, -hw + r, -hd);
376
-
377
- const extrudeSettings = { depth: h, bevelEnabled: false, curveSegments: 16 };
378
- const geo = new THREE.ExtrudeGeometry(shape, extrudeSettings);
379
- // Rotate so extrude goes along Y axis
380
- geo.rotateX(-Math.PI / 2);
381
- const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
382
- const mesh = new THREE.Mesh(geo, mat);
383
- mesh.position.y = 5; // align with bolt holes
384
- mesh.name = 'bracket';
385
- window.addMeshToScene(mesh);
386
- }
387
- else if (step.mesh && step.mesh.startsWith('hole')) {
388
- // Bolt holes — dark cylinders subtracted visually
389
- const geo = new THREE.CylinderGeometry(3.2, 3.2, 6, 24);
390
- const mat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
391
- const mesh = new THREE.Mesh(geo, mat);
392
- if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
393
- window.addMeshToScene(mesh);
394
- }
395
- }
396
-
397
- function resetDemo() {
398
- running = false;
399
- cmdCount = 0;
400
- startTime = 0;
401
- sceneState = { shape: null, material: 'aluminum', matClr: 0xccccdd, dims: {}, features: [], holeIdx: 0 };
402
- 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>';
403
- window.clearScene();
404
- document.getElementById('stats-overlay').style.display = 'none';
405
- progress.style.width = '0%';
406
- document.getElementById('btn-run').disabled = false;
407
- document.getElementById('btn-run').textContent = '▶ Run Agent Demo';
408
- }
409
-
410
- function showSchema() {
411
- term.innerHTML = '';
412
- const schema = {
413
- sketch: ['sketch.start', 'sketch.end', 'sketch.line', 'sketch.rect', 'sketch.circle', 'sketch.arc', 'sketch.clear'],
414
- 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'],
415
- transform: ['transform.move', 'transform.rotate', 'transform.scale'],
416
- view: ['view.set', 'view.fit', 'view.wireframe', 'view.grid'],
417
- export: ['export.stl', 'export.obj', 'export.gltf', 'export.json'],
418
- validate: ['validate.dimensions', 'validate.wallThickness', 'validate.printability', 'validate.cost'],
419
- query: ['query.features', 'query.bbox', 'query.materials', 'query.session', 'query.log'],
420
- scene: ['scene.clear', 'scene.snapshot'],
421
- meta: ['meta.ping', 'meta.version', 'meta.schema']
422
- };
423
- addLine('📋 cycleCAD Agent API — 46 commands across 9 namespaces', 'agent');
424
- addDivider();
425
- for (const [ns, methods] of Object.entries(schema)) {
426
- addLine(`<span style="color:var(--gold)">${ns}</span> (${methods.length})`, '');
427
- methods.forEach(m => addLine(` ${m}`, 'res'));
428
- }
429
- addDivider();
430
- addLine('Usage: window.cycleCAD.execute({ method: "ops.extrude", params: { height: 10 } })', 'comment');
431
- addLine('Full schema: window.cycleCAD.getSchema()', 'comment');
432
- }
433
-
434
- // ========== Voice Command System ==========
435
- let recognition = null;
436
- let isListening = false;
437
-
438
- function toggleVoice() {
439
- if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
440
- document.getElementById('voice-status').textContent = 'Speech recognition not supported — type your command instead';
441
- return;
442
- }
443
- if (isListening) { stopVoice(); return; }
444
-
445
- const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
446
- recognition = new SR();
447
- recognition.continuous = false;
448
- recognition.interimResults = true;
449
- recognition.lang = 'en-US';
450
-
451
- const btn = document.getElementById('btn-mic');
452
- const status = document.getElementById('voice-status');
453
- const input = document.getElementById('voice-input');
454
-
455
- recognition.onstart = () => {
456
- isListening = true;
457
- btn.style.background = 'var(--coral)';
458
- btn.style.color = '#fff';
459
- btn.style.animation = 'pulse 1s ease-in-out infinite';
460
- status.textContent = 'Listening...';
461
- };
462
- recognition.onresult = (e) => {
463
- let transcript = '';
464
- for (let i = 0; i < e.results.length; i++) transcript = e.results[i][0].transcript;
465
- input.value = transcript;
466
- if (e.results[0].isFinal) status.textContent = 'Got it! Press "Build It" or Enter.';
467
- };
468
- recognition.onerror = (e) => {
469
- if (e.error === 'not-allowed') {
470
- status.textContent = 'Mic needs HTTPS (works on cyclecad.com) — type your command below';
471
- } else {
472
- status.textContent = 'Mic unavailable — type your command below';
473
- }
474
- stopVoice();
475
- };
476
- recognition.onend = () => { stopVoice(); };
477
- try { recognition.start(); } catch(err) {
478
- document.getElementById('voice-status').textContent = 'Mic blocked type your command instead';
479
- stopVoice();
480
- }
481
- }
482
-
483
- function stopVoice() {
484
- isListening = false;
485
- if (recognition) try { recognition.stop(); } catch(e) {}
486
- const btn = document.getElementById('btn-mic');
487
- btn.style.background = 'transparent';
488
- btn.style.color = 'var(--coral)';
489
- btn.style.animation = 'none';
490
- }
491
-
492
- // ========== STATEFUL NLP COMMAND SYSTEM v3 ==========
493
- // Iterative: each command builds on the previous.
494
- // "build cylinder 50mm diameter 80 tall" → creates cylinder (clears scene)
495
- // "add a hole radius 10" → cuts hole into existing part
496
- // "fillet 5" → rounds edges
497
- // "export stl" → exports (only when asked)
498
- // "validate" → checks manufacturing (only when asked)
499
- // "change material to steel" → updates material
500
- // "start over" / "reset" / "new" → clears everything
501
-
502
- // ---- Persistent scene state ----
503
- let sceneState = {
504
- shape: null, // 'cylinder', 'bracket', 'sphere', etc.
505
- material: 'aluminum',
506
- matClr: 0xccccdd,
507
- dims: {}, // shape-specific dimensions
508
- features: [], // log of operations applied
509
- holeIdx: 0, // counter for unique hole names
510
- };
511
-
512
- function parseNum(t, ...patterns) {
513
- for (const p of patterns) {
514
- const m = t.match(p);
515
- if (m) return parseFloat(m[1]);
516
- }
517
- return null;
518
- }
519
-
520
- function parseMaterial(t) {
521
- if (/steel|stainless/.test(t)) return 'steel';
522
- if (/brass/.test(t)) return 'brass';
523
- if (/titanium/.test(t)) return 'titanium';
524
- if (/copper/.test(t)) return 'copper';
525
- if (/abs|plastic/.test(t)) return 'ABS';
526
- if (/nylon/.test(t)) return 'nylon';
527
- if (/wood/.test(t)) return 'wood';
528
- if (/carbon/.test(t)) return 'carbon fiber';
529
- return null; // null = don't change
530
- }
531
-
532
- function materialColor(mat) {
533
- const colors = {
534
- aluminum: 0xccccdd, steel: 0x888899, brass: 0xd4a843, titanium: 0xaabbcc,
535
- copper: 0xb87333, ABS: 0xe8e8e0, nylon: 0xf0f0e0, wood: 0x8B6914, 'carbon fiber': 0x333333
536
- };
537
- return colors[mat] || 0xccccdd;
538
- }
539
-
540
- 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 };
541
-
542
- function detectShape(t) {
543
- if (/hollow\s*cylinder|tube|pipe/.test(t)) return 'tube';
544
- if (/cylinder|cylind|cylindar/.test(t)) return 'cylinder';
545
- if (/disk|disc|puck/.test(t)) return 'disk';
546
- if (/sphere|ball/.test(t)) return 'sphere';
547
- if (/cone|taper/.test(t)) return 'cone';
548
- if (/gear|sprocket/.test(t)) return 'gear';
549
- if (/hex\s*bolt|bolt/.test(t)) return 'hexbolt';
550
- if (/washer/.test(t)) return 'washer';
551
- if (/\bring\b/.test(t)) return 'ring';
552
- if (/flange/.test(t)) return 'flange';
553
- if (/plate/.test(t)) return 'plate';
554
- if (/block|cube/.test(t)) return 'box';
555
- if (/bracket|mount/.test(t)) return 'bracket';
556
- if (/\d+\s*x\s*\d+/.test(t)) return 'bracket';
557
- if (/diameter|radius/.test(t)) return 'cylinder';
558
- return null; // unknown — might be a modify command
559
- }
560
-
561
- // ---- Detect what KIND of command this is ----
562
- function detectIntent(t) {
563
- if (/^(start\s*over|reset|clear|new\s*part|new\s*design)/.test(t)) return 'reset';
564
- if (/export|save|download/.test(t)) return 'export';
565
- if (/validate|check|verify|inspect|analyze/.test(t)) return 'validate';
566
- 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';
567
- if (/undo/.test(t)) return 'undo';
568
- // Modify operations on existing part
569
- if (/add\s*(a\s*)?hole|cut\s*(a\s*)?hole|drill|bore|extrude\s*(a\s*)?hole|punch/.test(t)) return 'hole';
570
- if (/fillet|round\s*(the\s*)?edge/.test(t)) return 'fillet';
571
- if (/chamfer/.test(t)) return 'chamfer';
572
- if (/add\s*(a\s*)?(slot|groove|channel)/.test(t)) return 'slot';
573
- if (/add\s*(a\s*)?(boss|peg|pin|post)/.test(t)) return 'boss';
574
- // If a known shape word is present → create new
575
- if (detectShape(t)) return 'create';
576
- // Default: try to create
577
- return 'create';
578
- }
579
-
580
- // ---- Parse dimensions from text ----
581
- function extractDims(t) {
582
- return {
583
- diameter: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*(?:dia|diameter)/, /dia(?:meter)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/, /[øo]\s*(\d+(?:\.\d+)?)/) || 0,
584
- radius: parseNum(t, /radius\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*radius/) || 0,
585
- height: parseNum(t, /height\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:tall|high|height|long|deep)/) || 0,
586
- thick: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*thick/, /thick(?:ness)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
587
- outerD: parseNum(t, /outer\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /od\s*(\d+(?:\.\d+)?)/) || 0,
588
- innerD: parseNum(t, /inner\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /id\s*(\d+(?:\.\d+)?)/, /bore\s*(\d+(?:\.\d+)?)/) || 0,
589
- topD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*top/, /top\s*(\d+(?:\.\d+)?)/) || 0,
590
- baseD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*base/, /base\s*(\d+(?:\.\d+)?)/) || 0,
591
- teeth: parseNum(t, /(\d+)\s*teeth/, /teeth\s*(\d+)/) || 0,
592
- modl: parseNum(t, /module\s*(\d+(?:\.\d+)?)/) || 0,
593
- mBolt: parseNum(t, /\bm(\d+)\b/) || 0,
594
- filletR: parseNum(t, /fillet\s*(?:radius\s*(?:of\s*)?)?(\d+(?:\.\d+)?)/, /round\s*(\d+(?:\.\d+)?)/) || 0,
595
- chamferS: parseNum(t, /chamfer\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
596
- dimMatch: t.match(/(\d+)\s*(?:x|by)\s*(\d+)/),
597
- holeCountMatch: t.match(/(\d+)\s*(?:m\d+\s*)?(?:bolt\s*)?holes?/),
598
- holeOffset: parseNum(t, /(\d+)\s*mm?\s*from\s*(?:the\s*)?edge/) || 10,
599
- posX: parseNum(t, /(?:at|x)\s*(-?\d+(?:\.\d+)?)/) || 0,
600
- posZ: parseNum(t, /(?:,\s*|y\s*)(-?\d+(?:\.\d+)?)/) || 0,
601
- };
602
- }
603
-
604
- // ======= STEP BUILDERS per intent =======
605
-
606
- function buildCreateSteps(text, t, ex) {
607
- const steps = [];
608
- const shape = detectShape(t) || 'bracket';
609
- const mat = parseMaterial(t) || 'aluminum';
610
- const matClr = materialColor(mat);
611
-
612
- // Update scene state — this is a NEW part
613
- sceneState = { shape, material: mat, matClr, dims: {}, features: [], holeIdx: 0 };
614
- const d = sceneState.dims;
615
-
616
- if (shape === 'cylinder' || shape === 'disk') {
617
- d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 25);
618
- d.h = ex.height || ex.thick || (shape === 'disk' ? 5 : 60);
619
- d.sizeLabel = `ø${d.r * 2} × ${d.h}mm`;
620
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
621
- steps.push({ type: 'comment', text: `// New ${shape}: ø${d.r * 2}mm × ${d.h}mm in ${mat}` });
622
- steps.push({ type: 'delay', ms: 300 });
623
- steps.push({ type: 'divider' });
624
- steps.push({ type: 'agent', text: `📐 Sketch circle ø${d.r * 2}mm` });
625
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
626
- steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.r } });
627
- steps.push({ type: 'delay', ms: 200 });
628
- steps.push({ type: 'divider' });
629
- steps.push({ type: 'agent', text: `📦 Extrude ${d.h}mm in ${mat}` });
630
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
631
- }
632
- else if (shape === 'tube' || shape === 'ring') {
633
- d.outerR = ex.outerD ? ex.outerD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
634
- d.innerR = ex.innerD ? ex.innerD / 2 : d.outerR * 0.7;
635
- d.h = ex.height || ex.thick || (shape === 'ring' ? 10 : 60);
636
- d.sizeLabel = `OD${d.outerR * 2} × ID${d.innerR * 2} × ${d.h}mm`;
637
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
638
- steps.push({ type: 'comment', text: `// New ${shape}: OD${d.outerR * 2} ID${d.innerR * 2} × ${d.h}mm` });
639
- steps.push({ type: 'delay', ms: 300 });
640
- steps.push({ type: 'divider' });
641
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
642
- steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.outerR } });
643
- steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.innerR, type: 'inner' } });
644
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, hollow: true, material: mat }, mesh: 'main' });
645
- }
646
- else if (shape === 'sphere') {
647
- d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 30);
648
- d.h = d.r * 2;
649
- d.sizeLabel = `ø${d.r * 2}mm sphere`;
650
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
651
- steps.push({ type: 'comment', text: `// New sphere ø${d.r * 2}mm in ${mat}` });
652
- steps.push({ type: 'delay', ms: 300 });
653
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'sphere', radius: d.r, material: mat }, mesh: 'main' });
654
- }
655
- else if (shape === 'cone') {
656
- d.baseR = ex.baseD ? ex.baseD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
657
- d.topR = ex.topD ? ex.topD / 2 : 5;
658
- d.h = ex.height || 50;
659
- d.sizeLabel = `ø${d.baseR * 2}→ø${d.topR * 2} × ${d.h}mm`;
660
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
661
- steps.push({ type: 'comment', text: `// New cone: base ø${d.baseR * 2} → top ø${d.topR * 2} × ${d.h}mm` });
662
- steps.push({ type: 'delay', ms: 300 });
663
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
664
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, taper: d.topR, material: mat }, mesh: 'main' });
665
- }
666
- else if (shape === 'gear') {
667
- d.teeth = ex.teeth || 20;
668
- d.module = ex.modl || 3;
669
- d.r = (d.teeth * d.module) / 2;
670
- d.h = ex.thick || ex.height || 10;
671
- d.sizeLabel = `${d.teeth}T m${d.module} × ${d.h}mm`;
672
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
673
- steps.push({ type: 'comment', text: `// Gear: ${d.teeth} teeth, module ${d.module}, ø${d.r * 2}mm` });
674
- steps.push({ type: 'delay', ms: 300 });
675
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'gear', teeth: d.teeth, module: d.module, material: mat }, mesh: 'main' });
676
- }
677
- else if (shape === 'hexbolt') {
678
- d.m = ex.mBolt || 10;
679
- d.headR = d.m * 0.9; d.headH = d.m * 0.65;
680
- d.shankR = d.m / 2; d.shankH = ex.height || d.m * 2;
681
- d.h = d.headH + d.shankH;
682
- d.sizeLabel = `M${d.m} × ${d.shankH}mm bolt`;
683
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
684
- steps.push({ type: 'comment', text: `// Hex bolt M${d.m} × ${d.shankH}mm` });
685
- steps.push({ type: 'delay', ms: 300 });
686
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'hexbolt', size: d.m, length: d.shankH, material: mat }, mesh: 'main' });
687
- }
688
- else if (shape === 'washer') {
689
- d.m = ex.mBolt || 10;
690
- d.outerR = d.m * 1.1; d.innerR = d.m / 2 + 0.5;
691
- d.h = ex.thick || 2;
692
- d.sizeLabel = `M${d.m} washer`;
693
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
694
- steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'washer', size: d.m, material: mat }, mesh: 'main' });
695
- }
696
- else {
697
- // bracket / plate / box
698
- d.w = ex.dimMatch ? parseInt(ex.dimMatch[1]) : 80;
699
- d.d = ex.dimMatch ? parseInt(ex.dimMatch[2]) : 40;
700
- d.h = ex.thick || ex.height || 5;
701
- d.sizeLabel = `${d.w} × ${d.d} × ${d.h}mm`;
702
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
703
- steps.push({ type: 'comment', text: `// New ${shape}: ${d.w}×${d.d}×${d.h}mm in ${mat}` });
704
- steps.push({ type: 'delay', ms: 300 });
705
- steps.push({ type: 'divider' });
706
- steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
707
- steps.push({ type: 'cmd', method: 'sketch.rect', params: { x: -d.w / 2, y: -d.d / 2, width: d.w, height: d.d } });
708
- steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
709
- }
710
- sceneState.features.push(`Created ${shape}`);
711
- steps.push({ type: 'delay', ms: 300 });
712
- steps.push({ type: 'agent', text: `✅ ${shape} created. Keep going — add holes, fillets, or say "export".` });
713
- return { steps, clearScene: true };
714
- }
715
-
716
- function buildHoleSteps(text, t, ex) {
717
- const steps = [];
718
- if (!sceneState.shape) {
719
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first — e.g. "build a cylinder 50mm diameter 80 tall"' });
720
- return { steps, clearScene: false };
721
- }
722
- const hr = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
723
- const hh = ex.height || sceneState.dims.h || 20;
724
- const px = ex.posX || 0;
725
- const pz = ex.posZ || 0;
726
- const py = (sceneState.dims.h || hh) / 2;
727
- sceneState.holeIdx++;
728
- const holeName = `hole${sceneState.holeIdx}`;
729
-
730
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
731
- steps.push({ type: 'comment', text: `// Adding hole ø${hr * 2}mm through part at (${px}, ${pz})` });
732
- steps.push({ type: 'delay', ms: 200 });
733
- steps.push({ type: 'divider' });
734
- steps.push({ type: 'agent', text: `🕳️ Extruding cut: ø${hr * 2}mm hole` });
735
- 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] });
736
- steps.push({ type: 'delay', ms: 200 });
737
- sceneState.features.push(`Hole ø${hr * 2} at (${px},${pz})`);
738
- steps.push({ type: 'agent', text: `✅ Hole added. ${sceneState.features.length} features total. Keep going!` });
739
- return { steps, clearScene: false };
740
- }
741
-
742
- function buildFilletSteps(text, t, ex) {
743
- const steps = [];
744
- if (!sceneState.shape) {
745
- steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
746
- return { steps, clearScene: false };
747
- }
748
- const fr = ex.filletR || parseNum(t, /(\d+(?:\.\d+)?)/) || 3;
749
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
750
- steps.push({ type: 'divider' });
751
- steps.push({ type: 'agent', text: `✨ Fillet edges r=${fr}mm` });
752
- steps.push({ type: 'cmd', method: 'ops.fillet', params: { target: 'main', radius: fr }, mesh: 'fillet' });
753
- steps.push({ type: 'delay', ms: 200 });
754
- sceneState.dims.filletR = fr;
755
- sceneState.features.push(`Fillet r=${fr}`);
756
- steps.push({ type: 'agent', text: `✅ Filleted. ${sceneState.features.length} features. Keep going!` });
757
- return { steps, clearScene: false };
758
- }
759
-
760
- function buildExportSteps(text, t) {
761
- const steps = [];
762
- if (!sceneState.shape) {
763
- steps.push({ type: 'agent', text: '⚠️ No part to export. Build something first.' });
764
- return { steps, clearScene: false };
765
- }
766
- const fmt = /obj/.test(t) ? 'obj' : (/gltf|glb/.test(t) ? 'gltf' : 'stl');
767
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
768
- steps.push({ type: 'divider' });
769
- steps.push({ type: 'agent', text: `📤 Exporting as ${fmt.toUpperCase()}` });
770
- steps.push({ type: 'cmd', method: `export.${fmt}`, params: { filename: `${sceneState.shape}.${fmt}`, binary: true } });
771
- steps.push({ type: 'delay', ms: 200 });
772
- steps.push({ type: 'agent', text: `✅ Exported ${sceneState.shape}.${fmt}` });
773
- return { steps, clearScene: false };
774
- }
775
-
776
- function buildValidateSteps(text) {
777
- const steps = [];
778
- if (!sceneState.shape) {
779
- steps.push({ type: 'agent', text: '⚠️ No part to validate. Build something first.' });
780
- return { steps, clearScene: false };
781
- }
782
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
783
- steps.push({ type: 'divider' });
784
- steps.push({ type: 'agent', text: '🔍 Validating for manufacturing' });
785
- steps.push({ type: 'cmd', method: 'validate.dimensions', params: { target: 'main' }, stat: 'size' });
786
- steps.push({ type: 'cmd', method: 'validate.printability', params: { target: 'main', process: 'CNC' }, stat: 'printable' });
787
- steps.push({ type: 'cmd', method: 'validate.cost', params: { target: 'main', process: 'CNC', material: sceneState.material }, stat: 'cost' });
788
- steps.push({ type: 'delay', ms: 300 });
789
- steps.push({ type: 'agent', text: '✅ Validation complete.' });
790
- return { steps, clearScene: false };
791
- }
792
-
793
- function buildMaterialSteps(text, t) {
794
- const steps = [];
795
- const newMat = parseMaterial(t) || 'aluminum';
796
- sceneState.material = newMat;
797
- sceneState.matClr = materialColor(newMat);
798
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
799
- steps.push({ type: 'cmd', method: 'ops.material', params: { material: newMat }, mesh: 'recolor' });
800
- steps.push({ type: 'agent', text: `✅ Material changed to ${newMat}. Keep going!` });
801
- return { steps, clearScene: false };
802
- }
803
-
804
- function buildBossSteps(text, t, ex) {
805
- const steps = [];
806
- if (!sceneState.shape) {
807
- steps.push({ type: 'agent', text: '⚠️ No part yet.' });
808
- return { steps, clearScene: false };
809
- }
810
- const br = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
811
- const bh = ex.height || 15;
812
- const px = ex.posX || 0;
813
- const pz = ex.posZ || 0;
814
- const py = (sceneState.dims.h || 5) + bh / 2;
815
- sceneState.holeIdx++;
816
- const bossName = `boss${sceneState.holeIdx}`;
817
- steps.push({ type: 'agent', text: `🎤 "${text}"` });
818
- steps.push({ type: 'divider' });
819
- steps.push({ type: 'agent', text: `📌 Adding boss ø${br * 2} × ${bh}mm` });
820
- steps.push({ type: 'cmd', method: 'ops.boss', params: { radius: br, height: bh, x: px, z: pz }, mesh: bossName, pos: [px, py, pz] });
821
- sceneState.features.push(`Boss ø${br * 2} at (${px},${pz})`);
822
- steps.push({ type: 'agent', text: `✅ Boss added. Keep going!` });
823
- return { steps, clearScene: false };
824
- }
825
-
826
- // ======= EXECUTE =======
827
- async function executeVoiceCommand() {
828
- const input = document.getElementById('voice-input');
829
- const text = input.value.trim();
830
- if (!text || running) return;
831
- stopVoice();
832
- input.value = '';
833
-
834
- const t = text.toLowerCase();
835
- const intent = detectIntent(t);
836
- const ex = extractDims(t);
837
-
838
- console.log('[cycleCAD NLP]', { intent, text, extracted: ex, sceneState: sceneState.shape });
839
-
840
- // Handle reset
841
- if (intent === 'reset') {
842
- resetDemo();
843
- addLine('🔄 Scene cleared. Start fresh!', 'agent');
844
- return;
845
- }
846
-
847
- let result;
848
- if (intent === 'create') result = buildCreateSteps(text, t, ex);
849
- else if (intent === 'hole') result = buildHoleSteps(text, t, ex);
850
- else if (intent === 'fillet') result = buildFilletSteps(text, t, ex);
851
- else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex); // reuse fillet visual
852
- else if (intent === 'export') result = buildExportSteps(text, t);
853
- else if (intent === 'validate') result = buildValidateSteps(text);
854
- else if (intent === 'material') result = buildMaterialSteps(text, t);
855
- else if (intent === 'boss') result = buildBossSteps(text, t, ex);
856
- else result = buildCreateSteps(text, t, ex); // fallback
857
-
858
- const voiceSteps = result.steps;
859
- if (result.clearScene) {
860
- window.clearScene();
861
- document.getElementById('stats-overlay').style.display = 'none';
862
- }
863
-
864
- // Don't clear terminal — append (iterative)
865
- addDivider();
866
- running = true;
867
- if (!startTime) startTime = Date.now();
868
-
869
- const totalSteps = voiceSteps.filter(s => s.type === 'cmd').length;
870
- let localCmdIdx = 0;
871
-
872
- for (const step of voiceSteps) {
873
- if (!running) break;
874
- if (step.type === 'agent') {
875
- addLine(step.text.replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1)).replace('{CMDS}', cmdCount), 'agent');
876
- await delay(150);
877
- } else if (step.type === 'comment') {
878
- addLine(step.text, 'comment'); await delay(80);
879
- } else if (step.type === 'divider') {
880
- addDivider();
881
- } else if (step.type === 'delay') {
882
- await delay(step.ms);
883
- } else if (step.type === 'cmd') {
884
- cmdCount++; localCmdIdx++;
885
- addLine(`cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`, 'cmd');
886
- await delay(120);
887
- const res = simulateResult(step);
888
- addLine(`→ ${JSON.stringify(res, null, 0)}`, 'res ok');
889
- // Update stats
890
- if (step.stat === 'size') updateStats({ size: sceneState.dims.sizeLabel || 'computed' });
891
- if (step.stat === 'printable') updateStats({ printable: true });
892
- if (step.stat === 'cost') updateStats({ cost: `$${(costMap[sceneState.material] || 12.40).toFixed(2)} (CNC)` });
893
- updateStats({ material: sceneState.material.charAt(0).toUpperCase() + sceneState.material.slice(1) });
894
- document.getElementById('stat-cmds').textContent = cmdCount;
895
- document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
896
- document.getElementById('stats-overlay').style.display = 'block';
897
- // Build 3D
898
- if (step.mesh) buildMesh(step);
899
- await delay(180);
900
- }
901
- }
902
- running = false;
903
- }
904
-
905
- function simulateResult(step) {
906
- const m = step.method;
907
- if (m === 'sketch.start') return { ok: true, plane: 'XY' };
908
- if (m === 'sketch.rect') return { ok: true, id: 'rect_1' };
909
- if (m === 'sketch.circle') return { ok: true, id: 'circle_1', radius: step.params.radius };
910
- if (m === 'ops.extrude') return { ok: true, id: 'extrude_1', height: step.params.height };
911
- if (m === 'ops.primitive') return { ok: true, id: step.mesh, shape: step.params.shape };
912
- if (m === 'ops.cut') return { ok: true, id: step.mesh, type: 'hole' };
913
- if (m === 'ops.fillet') return { ok: true, radius: step.params.radius, applied: true };
914
- if (m === 'ops.material') return { ok: true, material: step.params.material };
915
- if (m === 'ops.boss') return { ok: true, id: step.mesh };
916
- if (m === 'validate.dimensions') return { ok: true, size: sceneState.dims.sizeLabel };
917
- if (m === 'validate.printability') return { ok: true, printable: true, issues: [] };
918
- if (m === 'validate.cost') return { ok: true, unitCost: costMap[sceneState.material] || 12.40 };
919
- if (m.startsWith('export.')) return { ok: true, filename: step.params.filename };
920
- return { ok: true };
921
- }
922
-
923
- // ======= 3D MESH BUILDER =======
924
- function buildMesh(step) {
925
- const THREE = window.THREE;
926
- const sc = sceneState;
927
- const d = sc.dims;
928
- const matOpts = { color: sc.matClr, metalness: 0.7, roughness: 0.3 };
929
- const darkMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
930
-
931
- if (step.mesh === 'main' || step.mesh === 'main_ext') {
932
- const scene = window._scene;
933
- const old = scene.getObjectByName('main');
934
- if (old && step.mesh !== 'main_ext') { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
935
-
936
- let geo, mesh;
937
- const s = sc.shape;
938
- if (s === 'cylinder' || s === 'disk') {
939
- geo = new THREE.CylinderGeometry(d.r, d.r, d.h, 48);
940
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
941
- mesh.position.y = d.h / 2;
942
- } else if (s === 'tube' || s === 'ring' || s === 'washer' || s === 'flange') {
943
- const profile = [
944
- new THREE.Vector2(d.innerR || d.outerR * 0.7, 0),
945
- new THREE.Vector2(d.outerR || 25, 0),
946
- new THREE.Vector2(d.outerR || 25, d.h),
947
- new THREE.Vector2(d.innerR || d.outerR * 0.7, d.h),
948
- ];
949
- geo = new THREE.LatheGeometry(profile, 48);
950
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
951
- } else if (s === 'sphere') {
952
- geo = new THREE.SphereGeometry(d.r, 48, 32);
953
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
954
- mesh.position.y = d.r;
955
- } else if (s === 'cone') {
956
- geo = new THREE.CylinderGeometry(d.topR, d.baseR, d.h, 48);
957
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
958
- mesh.position.y = d.h / 2;
959
- } else if (s === 'gear') {
960
- const gshape = new THREE.Shape();
961
- const nt = d.teeth, pitchR = d.r, add = d.module, ded = d.module * 1.25;
962
- const ouR = pitchR + add, roR = pitchR - ded;
963
- for (let i = 0; i < nt; i++) {
964
- const a0 = (i / nt) * Math.PI * 2, a1 = ((i + 0.15) / nt) * Math.PI * 2;
965
- const a2 = ((i + 0.35) / nt) * Math.PI * 2, a3 = ((i + 0.5) / nt) * Math.PI * 2;
966
- if (i === 0) gshape.moveTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
967
- else gshape.lineTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
968
- gshape.lineTo(Math.cos(a1) * ouR, Math.sin(a1) * ouR);
969
- gshape.lineTo(Math.cos(a2) * ouR, Math.sin(a2) * ouR);
970
- gshape.lineTo(Math.cos(a3) * roR, Math.sin(a3) * roR);
971
- }
972
- gshape.closePath();
973
- geo = new THREE.ExtrudeGeometry(gshape, { depth: d.h, bevelEnabled: false });
974
- geo.rotateX(-Math.PI / 2);
975
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
976
- mesh.position.y = d.h;
977
- } else if (s === 'hexbolt') {
978
- const group = new THREE.Group();
979
- const headGeo = new THREE.CylinderGeometry(d.headR, d.headR, d.headH, 6);
980
- const hm = new THREE.Mesh(headGeo, new THREE.MeshStandardMaterial(matOpts));
981
- hm.position.y = d.shankH + d.headH / 2; group.add(hm);
982
- const sg = new THREE.CylinderGeometry(d.shankR, d.shankR, d.shankH, 24);
983
- const sm = new THREE.Mesh(sg, new THREE.MeshStandardMaterial(matOpts));
984
- sm.position.y = d.shankH / 2; group.add(sm);
985
- group.name = 'main';
986
- window.addMeshToScene(group); return;
987
- } else {
988
- geo = new THREE.BoxGeometry(d.w || 80, d.h || 5, d.d || 40);
989
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
990
- mesh.position.y = (d.h || 5) / 2;
991
- }
992
- if (mesh) { mesh.name = 'main'; window.addMeshToScene(mesh); }
993
- }
994
- else if (step.mesh === 'fillet' && (sc.shape === 'bracket' || sc.shape === 'plate' || sc.shape === 'box')) {
995
- const scene = window._scene;
996
- const old = scene.getObjectByName('main');
997
- if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
998
- const fr = Math.min(d.filletR || 3, (d.w || 80) / 2, (d.d || 40) / 2);
999
- const hw = (d.w || 80) / 2, hd = (d.d || 40) / 2, h = d.h || 5;
1000
- const shape = new THREE.Shape();
1001
- shape.moveTo(-hw + fr, -hd); shape.lineTo(hw - fr, -hd);
1002
- shape.quadraticCurveTo(hw, -hd, hw, -hd + fr); shape.lineTo(hw, hd - fr);
1003
- shape.quadraticCurveTo(hw, hd, hw - fr, hd); shape.lineTo(-hw + fr, hd);
1004
- shape.quadraticCurveTo(-hw, hd, -hw, hd - fr); shape.lineTo(-hw, -hd + fr);
1005
- shape.quadraticCurveTo(-hw, -hd, -hw + fr, -hd);
1006
- const geo = new THREE.ExtrudeGeometry(shape, { depth: h, bevelEnabled: false, curveSegments: 16 });
1007
- geo.rotateX(-Math.PI / 2);
1008
- const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }));
1009
- mesh.position.y = h; mesh.name = 'main';
1010
- window.addMeshToScene(mesh);
1011
- }
1012
- else if (step.mesh && (step.mesh.startsWith('hole') || step.mesh.startsWith('boss'))) {
1013
- const isBoss = step.mesh.startsWith('boss');
1014
- const r = step.params?.radius || 5;
1015
- const h = step.params?.height || (sc.dims.h || 10) + 2;
1016
- const geo = new THREE.CylinderGeometry(r, r, h, 24);
1017
- const mat = isBoss ? new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }) : darkMat;
1018
- const mesh = new THREE.Mesh(geo, mat);
1019
- if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
1020
- window.addMeshToScene(mesh);
1021
- }
1022
- else if (step.mesh === 'recolor') {
1023
- // Change material color on existing part
1024
- const scene = window._scene;
1025
- scene.traverse(c => {
1026
- if (c.isMesh && c.material && c.material.color) {
1027
- c.material.color.setHex(sc.matClr);
1028
- }
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
+ 'add hole', 'add fillet', 'add chamfer', 'add pattern',
880
+ 'sweep', 'loft', 'measure', 'section', 'undo', 'redo',
881
+ 'copy', 'move', 'constraint', 'export stl', 'export step',
882
+ 'help', 'history', 'clear', 'reset view', 'wireframe', 'grid'
883
+ ];
884
+
885
+ const matches = commands.filter(cmd => cmd.includes(value.toLowerCase()));
886
+
887
+ if (matches.length > 0) {
888
+ list.innerHTML = matches.slice(0, 8)
889
+ .map(m => `<div class="autocomplete-item" onclick="selectAutocomplete('${m}')">${m}</div>`)
890
+ .join('');
891
+ list.classList.add('visible');
892
+ } else {
893
+ list.classList.remove('visible');
894
+ }
895
+ }
896
+
897
+ function selectAutocomplete(cmd) {
898
+ document.getElementById('command-input').value = cmd;
899
+ document.getElementById('autocomplete-list').classList.remove('visible');
900
+ }
901
+
902
+ // Command Processing
903
+ function sendCommand() {
904
+ const input = document.getElementById('command-input');
905
+ const command = input.value.trim();
906
+
907
+ if (!command) return;
908
+
909
+ const startTime = performance.now();
910
+ logTerminal(command, 'input');
911
+
912
+ commandHistory.push(command);
913
+ historyIndex = -1;
914
+ input.value = '';
915
+
916
+ processCommand(command);
917
+
918
+ const latency = Math.round(performance.now() - startTime);
919
+ document.getElementById('status-latency').textContent = latency + 'ms';
920
+ }
921
+
922
+ function processCommand(command) {
923
+ const cmd = command.toLowerCase().trim();
924
+
925
+ // Create commands
926
+ if (cmd.startsWith('create ')) {
927
+ const shape = cmd.substring(7).trim();
928
+ createShape(shape);
929
+ saveHistory();
930
+ }
931
+ // Feature commands
932
+ else if (cmd.startsWith('add hole')) {
933
+ addHole(parseFloat(cmd.match(/radius\s+([\d.]+)/)?.[1] || 5));
934
+ saveHistory();
935
+ }
936
+ else if (cmd.startsWith('add fillet')) {
937
+ addFillet(parseFloat(cmd.match(/radius\s+([\d.]+)/)?.[1] || 5));
938
+ saveHistory();
939
+ }
940
+ else if (cmd.startsWith('add chamfer')) {
941
+ addChamfer(parseFloat(cmd.match(/size\s+([\d.]+)/)?.[1] || 2));
942
+ saveHistory();
943
+ }
944
+ else if (cmd.startsWith('add pattern')) {
945
+ addPattern();
946
+ saveHistory();
947
+ }
948
+ else if (cmd.startsWith('sweep')) {
949
+ logTerminal('Sweep operation: select profile and path', 'success');
950
+ sceneState.features.push('Sweep');
951
+ updateFeatureTree();
952
+ runDesignReview();
953
+ saveHistory();
954
+ }
955
+ else if (cmd.startsWith('loft')) {
956
+ logTerminal('Loft operation: select two profiles', 'success');
957
+ sceneState.features.push('Loft');
958
+ updateFeatureTree();
959
+ runManufacturingCheck();
960
+ saveHistory();
961
+ }
962
+ else if (cmd.startsWith('measure')) {
963
+ logTerminal('Measurement tool: click on two points to measure distance', 'success');
964
+ }
965
+ else if (cmd.startsWith('section')) {
966
+ logTerminal('Cross-section view: use arrow keys to adjust plane', 'success');
967
+ }
968
+ else if (cmd.startsWith('copy')) {
969
+ if (currentMesh) {
970
+ logTerminal('Part copied to clipboard', 'success');
971
+ } else {
972
+ logTerminal('No active part to copy', 'error');
973
+ }
974
+ }
975
+ else if (cmd.startsWith('move')) {
976
+ if (currentMesh) {
977
+ logTerminal('Move tool active: drag part in viewport', 'success');
978
+ } else {
979
+ logTerminal('No active part to move', 'error');
980
+ }
981
+ }
982
+ else if (cmd.startsWith('constraint')) {
983
+ if (currentMesh) {
984
+ logTerminal('Constraint tool: add dimensional/geometric constraints', 'success');
985
+ } else {
986
+ logTerminal('No active part for constraints', 'error');
987
+ }
988
+ }
989
+ else if (cmd === 'undo') {
990
+ undoOperation();
991
+ }
992
+ else if (cmd === 'redo') {
993
+ redoOperation();
994
+ }
995
+ else if (cmd === 'help') {
996
+ showHelp();
997
+ }
998
+ else if (cmd === 'history') {
999
+ showHistory();
1000
+ }
1001
+ else if (cmd.startsWith('export')) {
1002
+ const format = cmd.includes('step') ? 'STEP' : 'STL';
1003
+ if (currentMesh) {
1004
+ logTerminal(`Exporting as ${format}...`, 'success');
1005
+ logTerminal(`✓ File saved: part.${format.toLowerCase()}`, 'success');
1006
+ } else {
1007
+ logTerminal('No active part to export', 'error');
1008
+ }
1009
+ }
1010
+ else if (cmd === 'clear') {
1011
+ document.getElementById('terminal-output').innerHTML = '';
1012
+ }
1013
+ else if (cmd.startsWith('reset')) {
1014
+ resetView();
1015
+ }
1016
+ else {
1017
+ logTerminal('Unknown command. Type "help" for assistance.', 'error');
1018
+ }
1019
+ }
1020
+
1021
+ function createShape(shape) {
1022
+ // Remove previous mesh
1023
+ if (currentMesh) scene.remove(currentMesh);
1024
+
1025
+ let geometry;
1026
+ const params = parseParams(shape);
1027
+
1028
+ if (shape.includes('cylinder')) {
1029
+ const radius = params.diameter / 2 || 25;
1030
+ const height = params.height || 80;
1031
+ geometry = new THREE.CylinderGeometry(radius, radius, height, 32);
1032
+ sceneState.currentPart = 'Cylinder';
1033
+ } else if (shape.includes('box') || shape.includes('cube')) {
1034
+ const w = params.width || 100;
1035
+ const h = params.height || 100;
1036
+ const d = params.depth || 100;
1037
+ geometry = new THREE.BoxGeometry(w, h, d);
1038
+ sceneState.currentPart = 'Box';
1039
+ } else if (shape.includes('sphere')) {
1040
+ const radius = params.radius || 50;
1041
+ geometry = new THREE.SphereGeometry(radius, 32, 32);
1042
+ sceneState.currentPart = 'Sphere';
1043
+ } else if (shape.includes('cone')) {
1044
+ const radius = params.radius || 50;
1045
+ const height = params.height || 100;
1046
+ geometry = new THREE.ConeGeometry(radius, height, 32);
1047
+ sceneState.currentPart = 'Cone';
1048
+ } else if (shape.includes('torus')) {
1049
+ const radius = params.radius || 50;
1050
+ const tube = params.tube || 20;
1051
+ geometry = new THREE.TorusGeometry(radius, tube, 16, 100);
1052
+ sceneState.currentPart = 'Torus';
1053
+ } else {
1054
+ logTerminal('Unknown shape type. Try: cylinder, box, sphere, cone, torus', 'error');
1055
+ return;
1056
+ }
1057
+
1058
+ const material = new THREE.MeshStandardMaterial({
1059
+ color: 0x4a9eff,
1060
+ roughness: 0.7,
1061
+ metalness: 0.3
1062
+ });
1063
+
1064
+ currentMesh = new THREE.Mesh(geometry, material);
1065
+ currentMesh.castShadow = true;
1066
+ currentMesh.receiveShadow = true;
1067
+ scene.add(currentMesh);
1068
+
1069
+ // Reset features
1070
+ sceneState.features = ['Base'];
1071
+ sceneState.dimensions = params;
1072
+
1073
+ logTerminal(`✓ Created ${sceneState.currentPart} ${JSON.stringify(params)}`, 'success');
1074
+
1075
+ updateFeatureTree();
1076
+ updateStatus();
1077
+ fitToObject();
1078
+ runDesignReview();
1079
+ }
1080
+
1081
+ function parseParams(text) {
1082
+ const params = {};
1083
+ const patterns = {
1084
+ diameter: /diameter\s+([\d.]+)/,
1085
+ height: /(?:height|tall)\s+([\d.]+)/,
1086
+ radius: /radius\s+([\d.]+)/,
1087
+ width: /width\s+([\d.]+)/,
1088
+ depth: /depth\s+([\d.]+)/,
1089
+ mm: /\d+(?:mm|\.?\d+)?/
1090
+ };
1091
+
1092
+ for (let [key, regex] of Object.entries(patterns)) {
1093
+ const match = text.match(regex);
1094
+ if (match && key !== 'mm') {
1095
+ params[key] = parseFloat(match[1] || match[2]);
1096
+ }
1097
+ }
1098
+
1099
+ return params;
1100
+ }
1101
+
1102
+ function addHole(radius = 5) {
1103
+ if (!currentMesh) {
1104
+ logTerminal('No active part. Create something first.', 'error');
1105
+ return;
1106
+ }
1107
+ sceneState.features.push(`Hole (r=${radius}mm)`);
1108
+ logTerminal(`✓ Added hole radius ${radius}mm`, 'success');
1109
+ updateFeatureTree();
1110
+ updateStatus();
1111
+ runManufacturingCheck();
1112
+ }
1113
+
1114
+ function addFillet(radius = 5) {
1115
+ if (!currentMesh) {
1116
+ logTerminal('No active part. Create something first.', 'error');
1117
+ return;
1118
+ }
1119
+ sceneState.features.push(`Fillet (r=${radius}mm)`);
1120
+ logTerminal(`✓ Added fillet radius ${radius}mm`, 'success');
1121
+
1122
+ // Visual highlight edges
1123
+ const edges = new THREE.EdgesGeometry(currentMesh.geometry);
1124
+ const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xff6b6b, linewidth: 2 }));
1125
+ currentMesh.add(line);
1126
+
1127
+ setTimeout(() => {
1128
+ if (currentMesh && currentMesh.children.length > 0) {
1129
+ currentMesh.remove(line);
1130
+ }
1131
+ }, 1500);
1132
+
1133
+ updateFeatureTree();
1134
+ updateStatus();
1135
+ }
1136
+
1137
+ function addChamfer(size = 2) {
1138
+ if (!currentMesh) {
1139
+ logTerminal('No active part. Create something first.', 'error');
1140
+ return;
1141
+ }
1142
+ sceneState.features.push(`Chamfer (${size}mm)`);
1143
+ logTerminal(`✓ Added chamfer ${size}mm`, 'success');
1144
+ updateFeatureTree();
1145
+ updateStatus();
1146
+ }
1147
+
1148
+ function addPattern() {
1149
+ if (!currentMesh) {
1150
+ logTerminal('No active part. Create something first.', 'error');
1151
+ return;
1152
+ }
1153
+ sceneState.features.push('Pattern (3×3)');
1154
+ logTerminal('✓ Added pattern (3×3 rectangular array)', 'success');
1155
+ updateFeatureTree();
1156
+ updateStatus();
1157
+ }
1158
+
1159
+ // History Management
1160
+ function saveHistory() {
1161
+ operationHistory.splice(historyPointer);
1162
+ operationHistory.push({
1163
+ features: JSON.parse(JSON.stringify(sceneState.features)),
1164
+ part: sceneState.currentPart,
1165
+ dimensions: JSON.parse(JSON.stringify(sceneState.dimensions))
1166
+ });
1167
+ historyPointer = operationHistory.length - 1;
1168
+ }
1169
+
1170
+ function undoOperation() {
1171
+ if (historyPointer > 0) {
1172
+ historyPointer--;
1173
+ const state = operationHistory[historyPointer];
1174
+ sceneState.features = state.features;
1175
+ sceneState.currentPart = state.part;
1176
+ sceneState.dimensions = state.dimensions;
1177
+ const lastOp = state.features[state.features.length - 1] || 'Base';
1178
+ logTerminal(`↶ Undo: ${lastOp}`, 'success');
1179
+ updateFeatureTree();
1180
+ updateStatus();
1181
+ } else {
1182
+ logTerminal('Nothing to undo', 'error');
1183
+ }
1184
+ }
1185
+
1186
+ function redoOperation() {
1187
+ if (historyPointer < operationHistory.length - 1) {
1188
+ historyPointer++;
1189
+ const state = operationHistory[historyPointer];
1190
+ sceneState.features = state.features;
1191
+ sceneState.currentPart = state.part;
1192
+ sceneState.dimensions = state.dimensions;
1193
+ const lastOp = state.features[state.features.length - 1] || 'Base';
1194
+ logTerminal(`↷ Redo: ${lastOp}`, 'success');
1195
+ updateFeatureTree();
1196
+ updateStatus();
1197
+ } else {
1198
+ logTerminal('Nothing to redo', 'error');
1199
+ }
1200
+ }
1201
+
1202
+ // Design Review Agent
1203
+ function runDesignReview() {
1204
+ if (sceneState.features.length % 3 === 0 && sceneState.features.length > 0) {
1205
+ const critiques = [
1206
+ 'Consider adding a chamfer to sharp edges for manufacturability',
1207
+ 'Wall thickness looks good for injection molding',
1208
+ 'Part appears well-balanced - good mass distribution',
1209
+ 'Recommend adding draft angles for easier mold release',
1210
+ 'Design follows best practices for 3D printing'
1211
+ ];
1212
+
1213
+ const critique = critiques[Math.floor(Math.random() * critiques.length)];
1214
+ const line = document.createElement('div');
1215
+ line.className = 'terminal-line agent';
1216
+ line.innerHTML = `<span class="agent-badge review">R</span><div class="agent-content"><strong>Design Review:</strong> ${critique}</div>`;
1217
+ document.getElementById('terminal-output').appendChild(line);
1218
+ document.getElementById('terminal-output').scrollTop = document.getElementById('terminal-output').scrollHeight;
1219
+ }
1220
+ }
1221
+
1222
+ // Manufacturing Agent
1223
+ function runManufacturingCheck() {
1224
+ if (sceneState.features.some(f => f.includes('Hole'))) {
1225
+ const suggestions = [
1226
+ 'Hole depth within tolerance (✓ 3mm min)',
1227
+ 'Drilling sequence optimized',
1228
+ 'Tool change required for different hole sizes'
1229
+ ];
1230
+
1231
+ const suggestion = suggestions[Math.floor(Math.random() * suggestions.length)];
1232
+ const line = document.createElement('div');
1233
+ line.className = 'terminal-line agent';
1234
+ line.innerHTML = `<span class="agent-badge manufacturing">M</span><div class="agent-content"><strong>Mfg Check:</strong> ${suggestion}</div>`;
1235
+ document.getElementById('terminal-output').appendChild(line);
1236
+ document.getElementById('terminal-output').scrollTop = document.getElementById('terminal-output').scrollHeight;
1237
+ }
1238
+ }
1239
+
1240
+ // UI Functions
1241
+ function logTerminal(text, type = 'output') {
1242
+ const output = document.getElementById('terminal-output');
1243
+ const line = document.createElement('div');
1244
+ line.className = `terminal-line ${type}`;
1245
+ line.textContent = text;
1246
+
1247
+ output.appendChild(line);
1248
+ output.scrollTop = output.scrollHeight;
1249
+ }
1250
+
1251
+ function updateFeatureTree() {
1252
+ const tree = document.getElementById('feature-tree-list');
1253
+ if (sceneState.features.length === 0) {
1254
+ tree.innerHTML = '<div style="padding: 12px; color: #808090; text-align: center; font-size: 11px;">No features</div>';
1255
+ return;
1256
+ }
1257
+
1258
+ const icons = {
1259
+ 'Base': '📦',
1260
+ 'Hole': '',
1261
+ 'Fillet': '',
1262
+ 'Chamfer': '',
1263
+ 'Pattern': '🔲',
1264
+ 'Sweep': '➡️',
1265
+ 'Loft': '≈',
1266
+ 'default': '▪️'
1267
+ };
1268
+
1269
+ tree.innerHTML = sceneState.features
1270
+ .map((f, i) => {
1271
+ const icon = Object.keys(icons).find(k => f.includes(k)) || 'default';
1272
+ return `<div class="tree-item ${i === sceneState.features.length - 1 ? 'active' : ''}" onclick="selectFeature(${i})"><span class="tree-item-icon">${icons[icon]}</span>${f}</div>`;
1273
+ })
1274
+ .join('');
1275
+ }
1276
+
1277
+ function selectFeature(index) {
1278
+ // Highlight feature
1279
+ const items = document.querySelectorAll('.tree-item');
1280
+ items.forEach(item => item.classList.remove('active'));
1281
+ items[index]?.classList.add('active');
1282
+ }
1283
+
1284
+ function updateStatus() {
1285
+ document.getElementById('status-part').textContent = sceneState.currentPart || 'None';
1286
+ document.getElementById('status-features').textContent = sceneState.features.length;
1287
+ }
1288
+
1289
+ function toggleFeatureTree() {
1290
+ document.querySelector('.feature-tree').classList.toggle('collapsed');
1291
+ }
1292
+
1293
+ // Viewport Functions
1294
+ function resetView() {
1295
+ camera.position.set(200, 150, 200);
1296
+ controls.target.set(0, 0, 0);
1297
+ controls.update();
1298
+ logTerminal('✓ Camera reset', 'success');
1299
+ }
1300
+
1301
+ function fitToObject() {
1302
+ if (!currentMesh) return;
1303
+
1304
+ const box = new THREE.Box3().setFromObject(currentMesh);
1305
+ const size = box.getSize(new THREE.Vector3());
1306
+ const maxDim = Math.max(size.x, size.y, size.z);
1307
+ const fov = camera.fov * (Math.PI / 180);
1308
+ let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
1309
+
1310
+ cameraZ *= 1.5;
1311
+
1312
+ camera.position.set(cameraZ, cameraZ * 0.7, cameraZ);
1313
+ camera.lookAt(currentMesh.position);
1314
+ controls.target.copy(currentMesh.position);
1315
+ controls.update();
1316
+ logTerminal('✓ Fit to view', 'success');
1317
+ }
1318
+
1319
+ function toggleWireframe() {
1320
+ wireframeMode = !wireframeMode;
1321
+ if (currentMesh) {
1322
+ currentMesh.material.wireframe = wireframeMode;
1323
+ }
1324
+ document.getElementById('wireframe-btn').classList.toggle('active');
1325
+ logTerminal(wireframeMode ? '✓ Wireframe enabled' : '✓ Wireframe disabled', 'success');
1326
+ }
1327
+
1328
+ function toggleGrid() {
1329
+ gridVisible = !gridVisible;
1330
+ gridHelper.visible = gridVisible;
1331
+ document.getElementById('grid-btn').classList.toggle('active');
1332
+ logTerminal(gridVisible ? '✓ Grid enabled' : '✓ Grid disabled', 'success');
1333
+ }
1334
+
1335
+ function toggleShadows() {
1336
+ shadowsVisible = !shadowsVisible;
1337
+ renderer.shadowMap.enabled = shadowsVisible;
1338
+ if (currentMesh) currentMesh.castShadow = shadowsVisible;
1339
+ shadowPlane.visible = shadowsVisible;
1340
+ document.getElementById('shadows-btn').classList.toggle('active');
1341
+ logTerminal(shadowsVisible ? '✓ Shadows enabled' : '✓ Shadows disabled', 'success');
1342
+ }
1343
+
1344
+ function showHelp() {
1345
+ logTerminal('=== cycleCAD Agent Commands ===', 'output');
1346
+ logTerminal('', 'output');
1347
+ logTerminal('CREATE SHAPES:', 'output');
1348
+ logTerminal(' create cylinder diameter=50 height=80', 'output');
1349
+ logTerminal(' create box width=100 height=100 depth=100', 'output');
1350
+ logTerminal(' create sphere radius=50', 'output');
1351
+ logTerminal(' create cone radius=50 height=100', 'output');
1352
+ logTerminal(' create torus radius=50 tube=20', 'output');
1353
+ logTerminal('', 'output');
1354
+ logTerminal('MODIFY FEATURES:', 'output');
1355
+ logTerminal(' add hole radius=10', 'output');
1356
+ logTerminal(' add fillet radius=5', 'output');
1357
+ logTerminal(' add chamfer size=2', 'output');
1358
+ logTerminal(' add pattern', 'output');
1359
+ logTerminal('', 'output');
1360
+ logTerminal('ADVANCED OPERATIONS:', 'output');
1361
+ logTerminal(' sweep | loft | measure | section', 'output');
1362
+ logTerminal(' copy | move | constraint', 'output');
1363
+ logTerminal('', 'output');
1364
+ logTerminal('VIEWPORT CONTROL:', 'output');
1365
+ logTerminal(' reset view | wireframe | grid | shadows', 'output');
1366
+ logTerminal('', 'output');
1367
+ logTerminal('HISTORY & EXPORT:', 'output');
1368
+ logTerminal(' undo | redo | history', 'output');
1369
+ logTerminal(' export stl | export step', 'output');
1370
+ logTerminal('', 'output');
1371
+ logTerminal('KEYBOARD: ↑/↓ = history, Ctrl+Z/Y = undo/redo, ? = help', 'output');
1372
+ }
1373
+
1374
+ function showHistory() {
1375
+ logTerminal('=== Command History ===', 'output');
1376
+ if (commandHistory.length === 0) {
1377
+ logTerminal('(empty)', 'output');
1378
+ } else {
1379
+ commandHistory.forEach((c, i) => {
1380
+ logTerminal(` ${i + 1}. ${c}`, 'output');
1381
+ });
1382
+ }
1383
+ }
1384
+
1385
+ function showExamples() {
1386
+ const examples = [
1387
+ 'create cylinder diameter=50 height=80',
1388
+ 'add hole radius 10',
1389
+ 'add fillet radius 5',
1390
+ 'add chamfer size 2',
1391
+ 'add pattern',
1392
+ 'sweep',
1393
+ 'loft',
1394
+ 'export stl',
1395
+ 'help'
1396
+ ];
1397
+
1398
+ logTerminal('=== Example Commands ===', 'output');
1399
+ examples.forEach(ex => {
1400
+ logTerminal(` ${ex}`, 'output');
1401
+ });
1402
+ }
1403
+
1404
+ // Resizable Split Pane
1405
+ const resizeHandle = document.getElementById('resize-handle');
1406
+ const splitPane = document.getElementById('split-pane');
1407
+ let isResizing = false;
1408
+
1409
+ resizeHandle.addEventListener('mousedown', () => {
1410
+ isResizing = true;
1029
1411
  });
1030
- }
1031
- }
1032
1412
 
1033
- // Pulse animation for mic button
1034
- const styleEl = document.createElement('style');
1035
- 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); } }`;
1036
- document.head.appendChild(styleEl);
1413
+ document.addEventListener('mousemove', (e) => {
1414
+ if (!isResizing) return;
1415
+
1416
+ const rect = splitPane.getBoundingClientRect();
1417
+ const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
1037
1418
 
1038
- window.toggleVoice = toggleVoice;
1039
- window.executeVoiceCommand = executeVoiceCommand;
1419
+ if (newWidth > 20 && newWidth < 80) {
1420
+ splitPane.children[0].style.width = newWidth + '%';
1421
+ splitPane.children[2].style.width = (100 - newWidth) + '%';
1422
+ }
1423
+ });
1040
1424
 
1041
- // Initial state
1042
- resetDemo();
1425
+ document.addEventListener('mouseup', () => {
1426
+ isResizing = false;
1427
+ });
1043
1428
 
1044
- window.runDemo = runDemo;
1045
- window.resetDemo = resetDemo;
1046
- window.showSchema = showSchema;
1047
- </script>
1429
+ // Session Timer
1430
+ const startTime = Date.now();
1431
+ setInterval(() => {
1432
+ const elapsed = Date.now() - startTime;
1433
+ const minutes = Math.floor(elapsed / 60000);
1434
+ const seconds = Math.floor((elapsed % 60000) / 1000);
1435
+ document.getElementById('session-time').textContent =
1436
+ `Session: ${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
1437
+ }, 1000);
1438
+
1439
+ // Keyboard Shortcuts
1440
+ document.addEventListener('keydown', (e) => {
1441
+ if (e.ctrlKey && e.key === 'z') {
1442
+ e.preventDefault();
1443
+ undoOperation();
1444
+ } else if (e.ctrlKey && e.key === 'y') {
1445
+ e.preventDefault();
1446
+ redoOperation();
1447
+ } else if (e.key === '?') {
1448
+ e.preventDefault();
1449
+ showHelp();
1450
+ }
1451
+ });
1452
+
1453
+ // Initialize
1454
+ window.addEventListener('load', () => {
1455
+ initThreeJS();
1456
+ initVoiceRecognition();
1457
+ document.getElementById('command-input').focus();
1458
+ logTerminal('✓ Agent Demo Ready', 'success');
1459
+ logTerminal('Try: "create cylinder diameter=50 height=80"', 'output');
1460
+ });
1461
+ </script>
1048
1462
  </body>
1049
1463
  </html>