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