cyclecad 0.1.5 → 0.1.8

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.
@@ -0,0 +1,1049 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>cycleCAD Agent Demo — Watch an AI design a part</title>
7
+ <style>
8
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
9
+ :root {
10
+ --bg: #0A1628; --bg2: #122240; --bg3: #1A2D50;
11
+ --text: #F0F0E8; --muted: #8B9AB5; --dim: #5A6B85;
12
+ --gold: #D4A843; --blue: #2E86DE; --teal: #3AAFA9;
13
+ --purple: #8B6FC0; --warn: #E8963A; --coral: #E05555;
14
+ --green: #2ECC71;
15
+ }
16
+ body { background: var(--bg); color: var(--text); font-family: 'Inter', -apple-system, sans-serif; min-height: 100vh; }
17
+
18
+ .header { padding: 20px 40px; display: flex; align-items: center; gap: 16px; border-bottom: 1px solid rgba(255,255,255,0.06); }
19
+ .header .logo { font-size: 20px; font-weight: 800; }
20
+ .header .logo .cy { color: var(--gold); }
21
+ .header .logo .ca { color: var(--blue); }
22
+ .header .tag { color: var(--muted); font-size: 13px; margin-left: auto; }
23
+
24
+ .main { display: grid; grid-template-columns: 1fr 1fr; height: calc(100vh - 65px); }
25
+
26
+ /* LEFT: Agent terminal */
27
+ .terminal { background: #0D1117; border-right: 1px solid rgba(255,255,255,0.06); display: flex; flex-direction: column; }
28
+ .term-header { padding: 12px 20px; border-bottom: 1px solid rgba(255,255,255,0.06); display: flex; align-items: center; gap: 10px; }
29
+ .term-header .dot { width: 10px; height: 10px; border-radius: 50%; }
30
+ .dot-r { background: var(--coral); } .dot-y { background: var(--warn); } .dot-g { background: var(--green); }
31
+ .term-header span { color: var(--muted); font-size: 12px; margin-left: 8px; }
32
+ .term-body { flex: 1; overflow-y: auto; padding: 20px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 13px; line-height: 1.8; }
33
+ .term-body .cmd { color: var(--green); }
34
+ .term-body .cmd::before { content: '> '; color: var(--dim); }
35
+ .term-body .res { color: var(--muted); padding-left: 16px; }
36
+ .term-body .res.ok { color: var(--teal); }
37
+ .term-body .res.err { color: var(--coral); }
38
+ .term-body .comment { color: var(--dim); font-style: italic; }
39
+ .term-body .agent { color: var(--purple); font-weight: 600; }
40
+ .term-body .divider { border-top: 1px solid rgba(255,255,255,0.06); margin: 12px 0; }
41
+ .cursor { display: inline-block; width: 8px; height: 16px; background: var(--green); animation: blink 1s step-end infinite; vertical-align: middle; margin-left: 2px; }
42
+ @keyframes blink { 50% { opacity: 0; } }
43
+
44
+ /* RIGHT: 3D viewport */
45
+ .viewport { position: relative; background: var(--bg); }
46
+ .viewport iframe { width: 100%; height: 100%; border: none; }
47
+ .viewport .overlay { position: absolute; top: 12px; right: 12px; background: rgba(10,22,40,0.85); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 12px 16px; }
48
+ .viewport .overlay h4 { font-size: 11px; color: var(--gold); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
49
+ .viewport .overlay .stat { font-size: 12px; color: var(--muted); margin: 3px 0; }
50
+ .viewport .overlay .stat b { color: var(--text); }
51
+
52
+ /* Controls */
53
+ .controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px; z-index: 10; }
54
+ .controls button {
55
+ padding: 10px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;
56
+ }
57
+ .btn-run { background: linear-gradient(135deg, var(--gold), var(--blue)); color: #000; }
58
+ .btn-run:hover { opacity: 0.9; transform: translateY(-1px); }
59
+ .btn-reset { background: var(--bg2); color: var(--muted); border: 1px solid rgba(255,255,255,0.1) !important; }
60
+ .btn-reset:hover { color: var(--text); }
61
+ .btn-schema { background: var(--bg2); color: var(--purple); border: 1px solid rgba(139,111,192,0.3) !important; }
62
+
63
+ /* Progress bar */
64
+ .progress { position: absolute; bottom: 0; left: 0; width: 100%; height: 3px; background: var(--bg2); }
65
+ .progress-bar { height: 100%; width: 0%; background: linear-gradient(90deg, var(--gold), var(--blue), var(--teal)); transition: width 0.3s; }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <div class="header">
70
+ <div class="logo"><span class="cy">cycle</span><span class="ca">CAD</span></div>
71
+ <div style="color:var(--dim);font-size:13px;">Agent Demo</div>
72
+ <div class="tag">The Agent-First OS for Manufacturing</div>
73
+ </div>
74
+
75
+ <div class="main">
76
+ <!-- LEFT: Terminal showing agent commands -->
77
+ <div class="terminal">
78
+ <div class="term-header">
79
+ <div class="dot dot-r"></div><div class="dot dot-y"></div><div class="dot dot-g"></div>
80
+ <span>AI Agent — Bracket Design Task</span>
81
+ </div>
82
+ <div class="term-body" id="term"></div>
83
+ <div class="progress"><div class="progress-bar" id="progress"></div></div>
84
+ </div>
85
+
86
+ <!-- RIGHT: 3D viewport (placeholder) -->
87
+ <div class="viewport">
88
+ <canvas id="viewport3d" style="width:100%;height:100%;"></canvas>
89
+ <div class="overlay" id="stats-overlay" style="display:none;">
90
+ <h4>Part Stats</h4>
91
+ <div class="stat">Size: <b id="stat-size">—</b></div>
92
+ <div class="stat">Material: <b id="stat-mat">—</b></div>
93
+ <div class="stat">Printable: <b id="stat-print">—</b></div>
94
+ <div class="stat">Cost: <b id="stat-cost">—</b></div>
95
+ <div class="stat">Commands: <b id="stat-cmds">0</b></div>
96
+ <div class="stat">Time: <b id="stat-time">—</b></div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Voice input bar -->
102
+ <div id="voice-bar" style="position:absolute;bottom:70px;left:50%;transform:translateX(-50%);z-index:11;display:flex;align-items:center;gap:10px;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:8px 16px;min-width:500px;max-width:700px;">
103
+ <button id="btn-mic" onclick="toggleVoice()" style="width:40px;height:40px;border-radius:50%;border:2px solid var(--coral);background:transparent;color:var(--coral);font-size:18px;cursor:pointer;transition:all 0.2s;flex-shrink:0;">🎤</button>
104
+ <input id="voice-input" type="text" placeholder="Step 1: &quot;build a cylinder 50mm diameter 80 tall&quot; → Step 2: &quot;add a hole radius 10&quot; → Step 3: &quot;export stl&quot;" style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;font-family:inherit;outline:none;" onkeydown="if(event.key==='Enter')executeVoiceCommand()">
105
+ <button onclick="executeVoiceCommand()" style="padding:8px 16px;border:none;border-radius:8px;background:linear-gradient(135deg,var(--gold),var(--blue));color:#000;font-size:13px;font-weight:700;cursor:pointer;flex-shrink:0;">Build It</button>
106
+ <div id="voice-status" style="position:absolute;top:-22px;left:16px;font-size:11px;color:var(--dim);"></div>
107
+ </div>
108
+
109
+ <div class="controls">
110
+ <button class="btn-run" id="btn-run" onclick="runDemo()">▶ Run Agent Demo</button>
111
+ <button class="btn-reset" onclick="resetDemo()">↺ Reset</button>
112
+ <button class="btn-schema" onclick="showSchema()">{ } API Schema</button>
113
+ </div>
114
+
115
+ <script type="importmap">
116
+ { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js" } }
117
+ </script>
118
+ <script type="module">
119
+ import * as THREE from 'three';
120
+ import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/controls/OrbitControls.js';
121
+
122
+ // ========== Mini 3D viewport ==========
123
+ const canvas = document.getElementById('viewport3d');
124
+ const scene = new THREE.Scene();
125
+ scene.background = new THREE.Color(0x0A1628);
126
+ const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
127
+ camera.position.set(60, 50, 80);
128
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
129
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
130
+ renderer.setPixelRatio(window.devicePixelRatio);
131
+ const controls = new OrbitControls(camera, renderer.domElement);
132
+ controls.enableDamping = true;
133
+ controls.dampingFactor = 0.05;
134
+
135
+ // Lights
136
+ scene.add(new THREE.AmbientLight(0xffffff, 0.4));
137
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
138
+ dirLight.position.set(30, 50, 40);
139
+ scene.add(dirLight);
140
+
141
+ // Grid
142
+ const grid = new THREE.GridHelper(200, 40, 0x1E3A5F, 0x122240);
143
+ scene.add(grid);
144
+
145
+ // Animate
146
+ function animate() {
147
+ requestAnimationFrame(animate);
148
+ controls.update();
149
+ renderer.render(scene, camera);
150
+ }
151
+ animate();
152
+ window.addEventListener('resize', () => {
153
+ camera.aspect = canvas.clientWidth / canvas.clientHeight;
154
+ camera.updateProjectionMatrix();
155
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
156
+ });
157
+
158
+ // ========== Expose for demo script ==========
159
+ window._scene = scene;
160
+ window._camera = camera;
161
+ window._renderer = renderer;
162
+
163
+ window.addMeshToScene = (mesh) => { scene.add(mesh); };
164
+ window.clearScene = () => {
165
+ const toRemove = [];
166
+ scene.traverse(c => { if (c.isMesh) toRemove.push(c); });
167
+ toRemove.forEach(m => { scene.remove(m); m.geometry?.dispose(); m.material?.dispose(); });
168
+ };
169
+ window.THREE = THREE;
170
+ </script>
171
+
172
+ <script>
173
+ const term = document.getElementById('term');
174
+ const progress = document.getElementById('progress');
175
+ let running = false;
176
+ let cmdCount = 0;
177
+ let startTime = 0;
178
+
179
+ function addLine(html, cls = '') {
180
+ const div = document.createElement('div');
181
+ div.className = cls;
182
+ div.innerHTML = html;
183
+ term.appendChild(div);
184
+ term.scrollTop = term.scrollHeight;
185
+ }
186
+
187
+ function addDivider() {
188
+ const div = document.createElement('div');
189
+ div.className = 'divider';
190
+ term.appendChild(div);
191
+ }
192
+
193
+ function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
194
+
195
+ function updateStats(data) {
196
+ document.getElementById('stats-overlay').style.display = 'block';
197
+ if (data.size) document.getElementById('stat-size').textContent = data.size;
198
+ if (data.material) document.getElementById('stat-mat').textContent = data.material;
199
+ if (data.printable !== undefined) document.getElementById('stat-print').textContent = data.printable ? '✅ Yes' : '❌ No';
200
+ if (data.cost) document.getElementById('stat-cost').textContent = data.cost;
201
+ document.getElementById('stat-cmds').textContent = cmdCount;
202
+ document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
203
+ }
204
+
205
+ // ========== Demo Script ==========
206
+ // This simulates what an AI agent does when it calls cycleCAD's API
207
+
208
+ const DEMO_STEPS = [
209
+ // Agent thinks
210
+ { type: 'agent', text: '🤖 Agent received task: "Design a mounting bracket for the DUO cycleWASH with 4 M6 bolt holes"' },
211
+ { type: 'comment', text: '// Agent analyzes requirements: 80×40mm base plate, 5mm thick, aluminum, 4 corner holes' },
212
+ { type: 'delay', ms: 600 },
213
+
214
+ // Phase 1: Sketch
215
+ { type: 'divider' },
216
+ { type: 'agent', text: '📐 Phase 1: Sketch the base profile' },
217
+ { type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } },
218
+ { type: 'cmd', method: 'sketch.rect', params: { x: -40, y: -20, width: 80, height: 40 } },
219
+ { type: 'delay', ms: 300 },
220
+
221
+ // Phase 2: Extrude
222
+ { type: 'divider' },
223
+ { type: 'agent', text: '📦 Phase 2: Extrude to 3D solid' },
224
+ { type: 'cmd', method: 'ops.extrude', params: { height: 5, material: 'aluminum' }, mesh: 'bracket' },
225
+ { type: 'delay', ms: 400 },
226
+
227
+ // Phase 3: Add bolt holes (primitives positioned at corners)
228
+ { type: 'divider' },
229
+ { type: 'agent', text: '🔩 Phase 3: Add 4 M6 bolt holes at corners' },
230
+ { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole1', pos: [-30, 2.5, -12] },
231
+ { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole2', pos: [30, 2.5, -12] },
232
+ { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole3', pos: [-30, 2.5, 12] },
233
+ { type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole4', pos: [30, 2.5, 12] },
234
+ { type: 'delay', ms: 300 },
235
+
236
+ // Phase 4: Fillet edges
237
+ { type: 'divider' },
238
+ { type: 'agent', text: '✨ Phase 4: Fillet edges for stress relief' },
239
+ { type: 'cmd', method: 'ops.fillet', params: { target: 'bracket', radius: 15 }, mesh: 'fillet' },
240
+ { type: 'delay', ms: 300 },
241
+
242
+ // Phase 5: Validate
243
+ { type: 'divider' },
244
+ { type: 'agent', text: '🔍 Phase 5: Validate for manufacturing' },
245
+ { type: 'cmd', method: 'validate.dimensions', params: { target: 'bracket' }, stat: 'size' },
246
+ { type: 'cmd', method: 'validate.printability', params: { target: 'bracket', process: 'CNC' }, stat: 'printable' },
247
+ { type: 'cmd', method: 'validate.cost', params: { target: 'bracket', process: 'CNC', material: 'aluminum' }, stat: 'cost' },
248
+ { type: 'delay', ms: 400 },
249
+
250
+ // Phase 6: Export
251
+ { type: 'divider' },
252
+ { type: 'agent', text: '📤 Phase 6: Export for manufacturing' },
253
+ { type: 'cmd', method: 'export.stl', params: { filename: 'duo-bracket.stl', binary: true } },
254
+ { type: 'delay', ms: 200 },
255
+
256
+ // Done
257
+ { type: 'divider' },
258
+ { type: 'agent', text: '✅ Task complete. Bracket designed, validated, and exported in {TIME}s using {CMDS} API calls.' },
259
+ { type: 'comment', text: '// No human touched a mouse. No GUI was opened. The agent designed through cycleCAD.' },
260
+ ];
261
+
262
+ async function runDemo() {
263
+ if (running) return;
264
+ running = true;
265
+ cmdCount = 0;
266
+ startTime = Date.now();
267
+ document.getElementById('btn-run').disabled = true;
268
+ document.getElementById('btn-run').textContent = '⏳ Running...';
269
+ term.innerHTML = '';
270
+ window.clearScene();
271
+ document.getElementById('stats-overlay').style.display = 'none';
272
+
273
+ const totalSteps = DEMO_STEPS.filter(s => s.type === 'cmd').length;
274
+ let cmdIdx = 0;
275
+
276
+ for (const step of DEMO_STEPS) {
277
+ if (!running) break;
278
+
279
+ if (step.type === 'agent') {
280
+ let text = step.text
281
+ .replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1))
282
+ .replace('{CMDS}', cmdCount);
283
+ addLine(text, 'agent');
284
+ await delay(200);
285
+ }
286
+ else if (step.type === 'comment') {
287
+ addLine(step.text, 'comment');
288
+ await delay(100);
289
+ }
290
+ else if (step.type === 'divider') {
291
+ addDivider();
292
+ }
293
+ else if (step.type === 'delay') {
294
+ await delay(step.ms);
295
+ }
296
+ else if (step.type === 'cmd') {
297
+ cmdCount++;
298
+ cmdIdx++;
299
+ progress.style.width = (cmdIdx / totalSteps * 100) + '%';
300
+
301
+ const cmdStr = `cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`;
302
+ addLine(cmdStr, 'cmd');
303
+ await delay(150);
304
+
305
+ // Simulate the result
306
+ const result = simulateCommand(step);
307
+ const resStr = JSON.stringify(result, null, 0);
308
+ addLine(`→ ${resStr}`, 'res ok');
309
+
310
+ // Update stats
311
+ if (step.stat === 'size') updateStats({ size: '80 × 40 × 5 mm' });
312
+ if (step.stat === 'printable') updateStats({ printable: true });
313
+ if (step.stat === 'cost') updateStats({ cost: '$12.40 (CNC)' });
314
+ updateStats({ material: 'Aluminum' });
315
+
316
+ // Add 3D geometry
317
+ if (step.mesh) {
318
+ createMesh(step);
319
+ }
320
+
321
+ await delay(250);
322
+ }
323
+ }
324
+
325
+ running = false;
326
+ document.getElementById('btn-run').disabled = false;
327
+ document.getElementById('btn-run').textContent = '▶ Run Again';
328
+ }
329
+
330
+ function simulateCommand(step) {
331
+ const m = step.method;
332
+ if (m === 'sketch.start') return { ok: true, result: { plane: 'XY', status: 'active' } };
333
+ if (m === 'sketch.rect') return { ok: true, result: { id: 'rect_1', type: 'rect', width: step.params.width, height: step.params.height } };
334
+ if (m === 'ops.extrude') return { ok: true, result: { id: 'extrude_1', type: 'extrude', height: step.params.height, material: step.params.material, bbox: { width: 80, height: 5, depth: 40 } } };
335
+ if (m === 'ops.primitive') return { ok: true, result: { id: step.mesh, type: 'cylinder', shape: 'cylinder' } };
336
+ if (m === 'ops.fillet') return { ok: true, result: { target: 'bracket', radius: step.params.radius, applied: true } };
337
+ if (m === 'validate.dimensions') return { ok: true, result: { width: 80, height: 5, depth: 40, volume: 16000, fitsInPrintBed: true } };
338
+ if (m === 'validate.printability') return { ok: true, result: { printable: true, process: 'CNC', issues: [] } };
339
+ if (m === 'validate.cost') return { ok: true, result: { process: 'CNC', unitCost: 12.40, batchOf100: 892 } };
340
+ if (m === 'export.stl') return { ok: true, result: { format: 'stl', filename: step.params.filename, featureCount: 5 } };
341
+ return { ok: true };
342
+ }
343
+
344
+ function createMesh(step) {
345
+ const THREE = window.THREE;
346
+ if (step.mesh === 'bracket') {
347
+ // Main bracket body — sharp box (will be replaced by fillet step)
348
+ const geo = new THREE.BoxGeometry(80, 5, 40);
349
+ const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
350
+ const mesh = new THREE.Mesh(geo, mat);
351
+ mesh.position.y = 2.5;
352
+ mesh.name = 'bracket';
353
+ window.addMeshToScene(mesh);
354
+ }
355
+ else if (step.mesh === 'fillet') {
356
+ // Replace sharp bracket with rounded version — visible fillet r=15
357
+ const scene = window._scene;
358
+ // Remove old bracket
359
+ const old = scene.getObjectByName('bracket');
360
+ if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
361
+
362
+ const r = step.params?.radius || 15; // fillet radius from command
363
+ const w = 80, h = 5, d = 40;
364
+ const shape = new THREE.Shape();
365
+ // Rounded rectangle in XZ plane (we'll extrude along Y)
366
+ const hw = w / 2, hd = d / 2;
367
+ shape.moveTo(-hw + r, -hd);
368
+ shape.lineTo(hw - r, -hd);
369
+ shape.quadraticCurveTo(hw, -hd, hw, -hd + r);
370
+ shape.lineTo(hw, hd - r);
371
+ shape.quadraticCurveTo(hw, hd, hw - r, hd);
372
+ shape.lineTo(-hw + r, hd);
373
+ shape.quadraticCurveTo(-hw, hd, -hw, hd - r);
374
+ shape.lineTo(-hw, -hd + r);
375
+ shape.quadraticCurveTo(-hw, -hd, -hw + r, -hd);
376
+
377
+ const extrudeSettings = { depth: h, bevelEnabled: false, curveSegments: 16 };
378
+ const geo = new THREE.ExtrudeGeometry(shape, extrudeSettings);
379
+ // Rotate so extrude goes along Y axis
380
+ geo.rotateX(-Math.PI / 2);
381
+ const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
382
+ const mesh = new THREE.Mesh(geo, mat);
383
+ mesh.position.y = 5; // align with bolt holes
384
+ mesh.name = 'bracket';
385
+ window.addMeshToScene(mesh);
386
+ }
387
+ else if (step.mesh && step.mesh.startsWith('hole')) {
388
+ // Bolt holes — dark cylinders subtracted visually
389
+ const geo = new THREE.CylinderGeometry(3.2, 3.2, 6, 24);
390
+ const mat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
391
+ const mesh = new THREE.Mesh(geo, mat);
392
+ if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
393
+ window.addMeshToScene(mesh);
394
+ }
395
+ }
396
+
397
+ function resetDemo() {
398
+ running = false;
399
+ cmdCount = 0;
400
+ startTime = 0;
401
+ sceneState = { shape: null, material: 'aluminum', matClr: 0xccccdd, dims: {}, features: [], holeIdx: 0 };
402
+ term.innerHTML = '<div class="comment">// Ready. Type a command like "build a cylinder 50mm diameter 80 tall in steel"</div><div class="comment">// Then: "add a hole radius 10" → "fillet 5" → "export stl"</div><div class="comment">// Each command builds on the last. Say "start over" to reset.</div>';
403
+ window.clearScene();
404
+ document.getElementById('stats-overlay').style.display = 'none';
405
+ progress.style.width = '0%';
406
+ document.getElementById('btn-run').disabled = false;
407
+ document.getElementById('btn-run').textContent = '▶ Run Agent Demo';
408
+ }
409
+
410
+ function showSchema() {
411
+ term.innerHTML = '';
412
+ const schema = {
413
+ sketch: ['sketch.start', 'sketch.end', 'sketch.line', 'sketch.rect', 'sketch.circle', 'sketch.arc', 'sketch.clear'],
414
+ ops: ['ops.extrude', 'ops.revolve', 'ops.primitive', 'ops.fillet', 'ops.chamfer', 'ops.boolean', 'ops.shell', 'ops.pattern', 'ops.material', 'ops.sweep', 'ops.loft', 'ops.spring', 'ops.thread', 'ops.bend'],
415
+ transform: ['transform.move', 'transform.rotate', 'transform.scale'],
416
+ view: ['view.set', 'view.fit', 'view.wireframe', 'view.grid'],
417
+ export: ['export.stl', 'export.obj', 'export.gltf', 'export.json'],
418
+ validate: ['validate.dimensions', 'validate.wallThickness', 'validate.printability', 'validate.cost'],
419
+ query: ['query.features', 'query.bbox', 'query.materials', 'query.session', 'query.log'],
420
+ scene: ['scene.clear', 'scene.snapshot'],
421
+ meta: ['meta.ping', 'meta.version', 'meta.schema']
422
+ };
423
+ addLine('📋 cycleCAD Agent API — 46 commands across 9 namespaces', 'agent');
424
+ addDivider();
425
+ for (const [ns, methods] of Object.entries(schema)) {
426
+ addLine(`<span style="color:var(--gold)">${ns}</span> (${methods.length})`, '');
427
+ methods.forEach(m => addLine(` ${m}`, 'res'));
428
+ }
429
+ addDivider();
430
+ addLine('Usage: window.cycleCAD.execute({ method: "ops.extrude", params: { height: 10 } })', 'comment');
431
+ addLine('Full schema: window.cycleCAD.getSchema()', 'comment');
432
+ }
433
+
434
+ // ========== Voice Command System ==========
435
+ let recognition = null;
436
+ let isListening = false;
437
+
438
+ function toggleVoice() {
439
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
440
+ document.getElementById('voice-status').textContent = 'Speech recognition not supported — type your command instead';
441
+ return;
442
+ }
443
+ if (isListening) { stopVoice(); return; }
444
+
445
+ const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
446
+ recognition = new SR();
447
+ recognition.continuous = false;
448
+ recognition.interimResults = true;
449
+ recognition.lang = 'en-US';
450
+
451
+ const btn = document.getElementById('btn-mic');
452
+ const status = document.getElementById('voice-status');
453
+ const input = document.getElementById('voice-input');
454
+
455
+ recognition.onstart = () => {
456
+ isListening = true;
457
+ btn.style.background = 'var(--coral)';
458
+ btn.style.color = '#fff';
459
+ btn.style.animation = 'pulse 1s ease-in-out infinite';
460
+ status.textContent = 'Listening...';
461
+ };
462
+ recognition.onresult = (e) => {
463
+ let transcript = '';
464
+ for (let i = 0; i < e.results.length; i++) transcript = e.results[i][0].transcript;
465
+ input.value = transcript;
466
+ if (e.results[0].isFinal) status.textContent = 'Got it! Press "Build It" or Enter.';
467
+ };
468
+ recognition.onerror = (e) => {
469
+ if (e.error === 'not-allowed') {
470
+ status.textContent = 'Mic needs HTTPS (works on cyclecad.com) — type your command below';
471
+ } else {
472
+ status.textContent = 'Mic unavailable — type your command below';
473
+ }
474
+ stopVoice();
475
+ };
476
+ recognition.onend = () => { stopVoice(); };
477
+ try { recognition.start(); } catch(err) {
478
+ document.getElementById('voice-status').textContent = 'Mic blocked — type your command instead';
479
+ stopVoice();
480
+ }
481
+ }
482
+
483
+ function stopVoice() {
484
+ isListening = false;
485
+ if (recognition) try { recognition.stop(); } catch(e) {}
486
+ const btn = document.getElementById('btn-mic');
487
+ btn.style.background = 'transparent';
488
+ btn.style.color = 'var(--coral)';
489
+ btn.style.animation = 'none';
490
+ }
491
+
492
+ // ========== STATEFUL NLP COMMAND SYSTEM v3 ==========
493
+ // Iterative: each command builds on the previous.
494
+ // "build cylinder 50mm diameter 80 tall" → creates cylinder (clears scene)
495
+ // "add a hole radius 10" → cuts hole into existing part
496
+ // "fillet 5" → rounds edges
497
+ // "export stl" → exports (only when asked)
498
+ // "validate" → checks manufacturing (only when asked)
499
+ // "change material to steel" → updates material
500
+ // "start over" / "reset" / "new" → clears everything
501
+
502
+ // ---- Persistent scene state ----
503
+ let sceneState = {
504
+ shape: null, // 'cylinder', 'bracket', 'sphere', etc.
505
+ material: 'aluminum',
506
+ matClr: 0xccccdd,
507
+ dims: {}, // shape-specific dimensions
508
+ features: [], // log of operations applied
509
+ holeIdx: 0, // counter for unique hole names
510
+ };
511
+
512
+ function parseNum(t, ...patterns) {
513
+ for (const p of patterns) {
514
+ const m = t.match(p);
515
+ if (m) return parseFloat(m[1]);
516
+ }
517
+ return null;
518
+ }
519
+
520
+ function parseMaterial(t) {
521
+ if (/steel|stainless/.test(t)) return 'steel';
522
+ if (/brass/.test(t)) return 'brass';
523
+ if (/titanium/.test(t)) return 'titanium';
524
+ if (/copper/.test(t)) return 'copper';
525
+ if (/abs|plastic/.test(t)) return 'ABS';
526
+ if (/nylon/.test(t)) return 'nylon';
527
+ if (/wood/.test(t)) return 'wood';
528
+ if (/carbon/.test(t)) return 'carbon fiber';
529
+ return null; // null = don't change
530
+ }
531
+
532
+ function materialColor(mat) {
533
+ const colors = {
534
+ aluminum: 0xccccdd, steel: 0x888899, brass: 0xd4a843, titanium: 0xaabbcc,
535
+ copper: 0xb87333, ABS: 0xe8e8e0, nylon: 0xf0f0e0, wood: 0x8B6914, 'carbon fiber': 0x333333
536
+ };
537
+ return colors[mat] || 0xccccdd;
538
+ }
539
+
540
+ const costMap = { aluminum: 12.40, steel: 8.90, brass: 18.50, titanium: 45.00, ABS: 3.20, nylon: 4.80, copper: 22.10, wood: 5.50, 'carbon fiber': 65.00 };
541
+
542
+ function detectShape(t) {
543
+ if (/hollow\s*cylinder|tube|pipe/.test(t)) return 'tube';
544
+ if (/cylinder|cylind|cylindar/.test(t)) return 'cylinder';
545
+ if (/disk|disc|puck/.test(t)) return 'disk';
546
+ if (/sphere|ball/.test(t)) return 'sphere';
547
+ if (/cone|taper/.test(t)) return 'cone';
548
+ if (/gear|sprocket/.test(t)) return 'gear';
549
+ if (/hex\s*bolt|bolt/.test(t)) return 'hexbolt';
550
+ if (/washer/.test(t)) return 'washer';
551
+ if (/\bring\b/.test(t)) return 'ring';
552
+ if (/flange/.test(t)) return 'flange';
553
+ if (/plate/.test(t)) return 'plate';
554
+ if (/block|cube/.test(t)) return 'box';
555
+ if (/bracket|mount/.test(t)) return 'bracket';
556
+ if (/\d+\s*x\s*\d+/.test(t)) return 'bracket';
557
+ if (/diameter|radius/.test(t)) return 'cylinder';
558
+ return null; // unknown — might be a modify command
559
+ }
560
+
561
+ // ---- Detect what KIND of command this is ----
562
+ function detectIntent(t) {
563
+ if (/^(start\s*over|reset|clear|new\s*part|new\s*design)/.test(t)) return 'reset';
564
+ if (/export|save|download/.test(t)) return 'export';
565
+ if (/validate|check|verify|inspect|analyze/.test(t)) return 'validate';
566
+ if (/change\s*material|set\s*material|make\s*it\s*(steel|brass|aluminum|titanium|nylon|abs|plastic|copper|wood|carbon)/.test(t)) return 'material';
567
+ if (/undo/.test(t)) return 'undo';
568
+ // Modify operations on existing part
569
+ if (/add\s*(a\s*)?hole|cut\s*(a\s*)?hole|drill|bore|extrude\s*(a\s*)?hole|punch/.test(t)) return 'hole';
570
+ if (/fillet|round\s*(the\s*)?edge/.test(t)) return 'fillet';
571
+ if (/chamfer/.test(t)) return 'chamfer';
572
+ if (/add\s*(a\s*)?(slot|groove|channel)/.test(t)) return 'slot';
573
+ if (/add\s*(a\s*)?(boss|peg|pin|post)/.test(t)) return 'boss';
574
+ // If a known shape word is present → create new
575
+ if (detectShape(t)) return 'create';
576
+ // Default: try to create
577
+ return 'create';
578
+ }
579
+
580
+ // ---- Parse dimensions from text ----
581
+ function extractDims(t) {
582
+ return {
583
+ diameter: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*(?:dia|diameter)/, /dia(?:meter)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/, /[øo]\s*(\d+(?:\.\d+)?)/) || 0,
584
+ radius: parseNum(t, /radius\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*radius/) || 0,
585
+ height: parseNum(t, /height\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:tall|high|height|long|deep)/) || 0,
586
+ thick: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*thick/, /thick(?:ness)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
587
+ outerD: parseNum(t, /outer\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /od\s*(\d+(?:\.\d+)?)/) || 0,
588
+ innerD: parseNum(t, /inner\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /id\s*(\d+(?:\.\d+)?)/, /bore\s*(\d+(?:\.\d+)?)/) || 0,
589
+ topD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*top/, /top\s*(\d+(?:\.\d+)?)/) || 0,
590
+ baseD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*base/, /base\s*(\d+(?:\.\d+)?)/) || 0,
591
+ teeth: parseNum(t, /(\d+)\s*teeth/, /teeth\s*(\d+)/) || 0,
592
+ modl: parseNum(t, /module\s*(\d+(?:\.\d+)?)/) || 0,
593
+ mBolt: parseNum(t, /\bm(\d+)\b/) || 0,
594
+ filletR: parseNum(t, /fillet\s*(?:radius\s*(?:of\s*)?)?(\d+(?:\.\d+)?)/, /round\s*(\d+(?:\.\d+)?)/) || 0,
595
+ chamferS: parseNum(t, /chamfer\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
596
+ dimMatch: t.match(/(\d+)\s*(?:x|by)\s*(\d+)/),
597
+ holeCountMatch: t.match(/(\d+)\s*(?:m\d+\s*)?(?:bolt\s*)?holes?/),
598
+ holeOffset: parseNum(t, /(\d+)\s*mm?\s*from\s*(?:the\s*)?edge/) || 10,
599
+ posX: parseNum(t, /(?:at|x)\s*(-?\d+(?:\.\d+)?)/) || 0,
600
+ posZ: parseNum(t, /(?:,\s*|y\s*)(-?\d+(?:\.\d+)?)/) || 0,
601
+ };
602
+ }
603
+
604
+ // ======= STEP BUILDERS per intent =======
605
+
606
+ function buildCreateSteps(text, t, ex) {
607
+ const steps = [];
608
+ const shape = detectShape(t) || 'bracket';
609
+ const mat = parseMaterial(t) || 'aluminum';
610
+ const matClr = materialColor(mat);
611
+
612
+ // Update scene state — this is a NEW part
613
+ sceneState = { shape, material: mat, matClr, dims: {}, features: [], holeIdx: 0 };
614
+ const d = sceneState.dims;
615
+
616
+ if (shape === 'cylinder' || shape === 'disk') {
617
+ d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 25);
618
+ d.h = ex.height || ex.thick || (shape === 'disk' ? 5 : 60);
619
+ d.sizeLabel = `ø${d.r * 2} × ${d.h}mm`;
620
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
621
+ steps.push({ type: 'comment', text: `// New ${shape}: ø${d.r * 2}mm × ${d.h}mm in ${mat}` });
622
+ steps.push({ type: 'delay', ms: 300 });
623
+ steps.push({ type: 'divider' });
624
+ steps.push({ type: 'agent', text: `📐 Sketch circle ø${d.r * 2}mm` });
625
+ steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
626
+ steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.r } });
627
+ steps.push({ type: 'delay', ms: 200 });
628
+ steps.push({ type: 'divider' });
629
+ steps.push({ type: 'agent', text: `📦 Extrude ${d.h}mm in ${mat}` });
630
+ steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
631
+ }
632
+ else if (shape === 'tube' || shape === 'ring') {
633
+ d.outerR = ex.outerD ? ex.outerD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
634
+ d.innerR = ex.innerD ? ex.innerD / 2 : d.outerR * 0.7;
635
+ d.h = ex.height || ex.thick || (shape === 'ring' ? 10 : 60);
636
+ d.sizeLabel = `OD${d.outerR * 2} × ID${d.innerR * 2} × ${d.h}mm`;
637
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
638
+ steps.push({ type: 'comment', text: `// New ${shape}: OD${d.outerR * 2} ID${d.innerR * 2} × ${d.h}mm` });
639
+ steps.push({ type: 'delay', ms: 300 });
640
+ steps.push({ type: 'divider' });
641
+ steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
642
+ steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.outerR } });
643
+ steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.innerR, type: 'inner' } });
644
+ steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, hollow: true, material: mat }, mesh: 'main' });
645
+ }
646
+ else if (shape === 'sphere') {
647
+ d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 30);
648
+ d.h = d.r * 2;
649
+ d.sizeLabel = `ø${d.r * 2}mm sphere`;
650
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
651
+ steps.push({ type: 'comment', text: `// New sphere ø${d.r * 2}mm in ${mat}` });
652
+ steps.push({ type: 'delay', ms: 300 });
653
+ steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'sphere', radius: d.r, material: mat }, mesh: 'main' });
654
+ }
655
+ else if (shape === 'cone') {
656
+ d.baseR = ex.baseD ? ex.baseD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
657
+ d.topR = ex.topD ? ex.topD / 2 : 5;
658
+ d.h = ex.height || 50;
659
+ d.sizeLabel = `ø${d.baseR * 2}→ø${d.topR * 2} × ${d.h}mm`;
660
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
661
+ steps.push({ type: 'comment', text: `// New cone: base ø${d.baseR * 2} → top ø${d.topR * 2} × ${d.h}mm` });
662
+ steps.push({ type: 'delay', ms: 300 });
663
+ steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
664
+ steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, taper: d.topR, material: mat }, mesh: 'main' });
665
+ }
666
+ else if (shape === 'gear') {
667
+ d.teeth = ex.teeth || 20;
668
+ d.module = ex.modl || 3;
669
+ d.r = (d.teeth * d.module) / 2;
670
+ d.h = ex.thick || ex.height || 10;
671
+ d.sizeLabel = `${d.teeth}T m${d.module} × ${d.h}mm`;
672
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
673
+ steps.push({ type: 'comment', text: `// Gear: ${d.teeth} teeth, module ${d.module}, ø${d.r * 2}mm` });
674
+ steps.push({ type: 'delay', ms: 300 });
675
+ steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'gear', teeth: d.teeth, module: d.module, material: mat }, mesh: 'main' });
676
+ }
677
+ else if (shape === 'hexbolt') {
678
+ d.m = ex.mBolt || 10;
679
+ d.headR = d.m * 0.9; d.headH = d.m * 0.65;
680
+ d.shankR = d.m / 2; d.shankH = ex.height || d.m * 2;
681
+ d.h = d.headH + d.shankH;
682
+ d.sizeLabel = `M${d.m} × ${d.shankH}mm bolt`;
683
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
684
+ steps.push({ type: 'comment', text: `// Hex bolt M${d.m} × ${d.shankH}mm` });
685
+ steps.push({ type: 'delay', ms: 300 });
686
+ steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'hexbolt', size: d.m, length: d.shankH, material: mat }, mesh: 'main' });
687
+ }
688
+ else if (shape === 'washer') {
689
+ d.m = ex.mBolt || 10;
690
+ d.outerR = d.m * 1.1; d.innerR = d.m / 2 + 0.5;
691
+ d.h = ex.thick || 2;
692
+ d.sizeLabel = `M${d.m} washer`;
693
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
694
+ steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'washer', size: d.m, material: mat }, mesh: 'main' });
695
+ }
696
+ else {
697
+ // bracket / plate / box
698
+ d.w = ex.dimMatch ? parseInt(ex.dimMatch[1]) : 80;
699
+ d.d = ex.dimMatch ? parseInt(ex.dimMatch[2]) : 40;
700
+ d.h = ex.thick || ex.height || 5;
701
+ d.sizeLabel = `${d.w} × ${d.d} × ${d.h}mm`;
702
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
703
+ steps.push({ type: 'comment', text: `// New ${shape}: ${d.w}×${d.d}×${d.h}mm in ${mat}` });
704
+ steps.push({ type: 'delay', ms: 300 });
705
+ steps.push({ type: 'divider' });
706
+ steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
707
+ steps.push({ type: 'cmd', method: 'sketch.rect', params: { x: -d.w / 2, y: -d.d / 2, width: d.w, height: d.d } });
708
+ steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
709
+ }
710
+ sceneState.features.push(`Created ${shape}`);
711
+ steps.push({ type: 'delay', ms: 300 });
712
+ steps.push({ type: 'agent', text: `✅ ${shape} created. Keep going — add holes, fillets, or say "export".` });
713
+ return { steps, clearScene: true };
714
+ }
715
+
716
+ function buildHoleSteps(text, t, ex) {
717
+ const steps = [];
718
+ if (!sceneState.shape) {
719
+ steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first — e.g. "build a cylinder 50mm diameter 80 tall"' });
720
+ return { steps, clearScene: false };
721
+ }
722
+ const hr = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
723
+ const hh = ex.height || sceneState.dims.h || 20;
724
+ const px = ex.posX || 0;
725
+ const pz = ex.posZ || 0;
726
+ const py = (sceneState.dims.h || hh) / 2;
727
+ sceneState.holeIdx++;
728
+ const holeName = `hole${sceneState.holeIdx}`;
729
+
730
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
731
+ steps.push({ type: 'comment', text: `// Adding hole ø${hr * 2}mm through part at (${px}, ${pz})` });
732
+ steps.push({ type: 'delay', ms: 200 });
733
+ steps.push({ type: 'divider' });
734
+ steps.push({ type: 'agent', text: `🕳️ Extruding cut: ø${hr * 2}mm hole` });
735
+ steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: hr, height: hh + 2, x: px, z: pz }, mesh: holeName, pos: [px, py, pz] });
736
+ steps.push({ type: 'delay', ms: 200 });
737
+ sceneState.features.push(`Hole ø${hr * 2} at (${px},${pz})`);
738
+ steps.push({ type: 'agent', text: `✅ Hole added. ${sceneState.features.length} features total. Keep going!` });
739
+ return { steps, clearScene: false };
740
+ }
741
+
742
+ function buildFilletSteps(text, t, ex) {
743
+ const steps = [];
744
+ if (!sceneState.shape) {
745
+ steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
746
+ return { steps, clearScene: false };
747
+ }
748
+ const fr = ex.filletR || parseNum(t, /(\d+(?:\.\d+)?)/) || 3;
749
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
750
+ steps.push({ type: 'divider' });
751
+ steps.push({ type: 'agent', text: `✨ Fillet edges r=${fr}mm` });
752
+ steps.push({ type: 'cmd', method: 'ops.fillet', params: { target: 'main', radius: fr }, mesh: 'fillet' });
753
+ steps.push({ type: 'delay', ms: 200 });
754
+ sceneState.dims.filletR = fr;
755
+ sceneState.features.push(`Fillet r=${fr}`);
756
+ steps.push({ type: 'agent', text: `✅ Filleted. ${sceneState.features.length} features. Keep going!` });
757
+ return { steps, clearScene: false };
758
+ }
759
+
760
+ function buildExportSteps(text, t) {
761
+ const steps = [];
762
+ if (!sceneState.shape) {
763
+ steps.push({ type: 'agent', text: '⚠️ No part to export. Build something first.' });
764
+ return { steps, clearScene: false };
765
+ }
766
+ const fmt = /obj/.test(t) ? 'obj' : (/gltf|glb/.test(t) ? 'gltf' : 'stl');
767
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
768
+ steps.push({ type: 'divider' });
769
+ steps.push({ type: 'agent', text: `📤 Exporting as ${fmt.toUpperCase()}` });
770
+ steps.push({ type: 'cmd', method: `export.${fmt}`, params: { filename: `${sceneState.shape}.${fmt}`, binary: true } });
771
+ steps.push({ type: 'delay', ms: 200 });
772
+ steps.push({ type: 'agent', text: `✅ Exported ${sceneState.shape}.${fmt}` });
773
+ return { steps, clearScene: false };
774
+ }
775
+
776
+ function buildValidateSteps(text) {
777
+ const steps = [];
778
+ if (!sceneState.shape) {
779
+ steps.push({ type: 'agent', text: '⚠️ No part to validate. Build something first.' });
780
+ return { steps, clearScene: false };
781
+ }
782
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
783
+ steps.push({ type: 'divider' });
784
+ steps.push({ type: 'agent', text: '🔍 Validating for manufacturing' });
785
+ steps.push({ type: 'cmd', method: 'validate.dimensions', params: { target: 'main' }, stat: 'size' });
786
+ steps.push({ type: 'cmd', method: 'validate.printability', params: { target: 'main', process: 'CNC' }, stat: 'printable' });
787
+ steps.push({ type: 'cmd', method: 'validate.cost', params: { target: 'main', process: 'CNC', material: sceneState.material }, stat: 'cost' });
788
+ steps.push({ type: 'delay', ms: 300 });
789
+ steps.push({ type: 'agent', text: '✅ Validation complete.' });
790
+ return { steps, clearScene: false };
791
+ }
792
+
793
+ function buildMaterialSteps(text, t) {
794
+ const steps = [];
795
+ const newMat = parseMaterial(t) || 'aluminum';
796
+ sceneState.material = newMat;
797
+ sceneState.matClr = materialColor(newMat);
798
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
799
+ steps.push({ type: 'cmd', method: 'ops.material', params: { material: newMat }, mesh: 'recolor' });
800
+ steps.push({ type: 'agent', text: `✅ Material changed to ${newMat}. Keep going!` });
801
+ return { steps, clearScene: false };
802
+ }
803
+
804
+ function buildBossSteps(text, t, ex) {
805
+ const steps = [];
806
+ if (!sceneState.shape) {
807
+ steps.push({ type: 'agent', text: '⚠️ No part yet.' });
808
+ return { steps, clearScene: false };
809
+ }
810
+ const br = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
811
+ const bh = ex.height || 15;
812
+ const px = ex.posX || 0;
813
+ const pz = ex.posZ || 0;
814
+ const py = (sceneState.dims.h || 5) + bh / 2;
815
+ sceneState.holeIdx++;
816
+ const bossName = `boss${sceneState.holeIdx}`;
817
+ steps.push({ type: 'agent', text: `🎤 "${text}"` });
818
+ steps.push({ type: 'divider' });
819
+ steps.push({ type: 'agent', text: `📌 Adding boss ø${br * 2} × ${bh}mm` });
820
+ steps.push({ type: 'cmd', method: 'ops.boss', params: { radius: br, height: bh, x: px, z: pz }, mesh: bossName, pos: [px, py, pz] });
821
+ sceneState.features.push(`Boss ø${br * 2} at (${px},${pz})`);
822
+ steps.push({ type: 'agent', text: `✅ Boss added. Keep going!` });
823
+ return { steps, clearScene: false };
824
+ }
825
+
826
+ // ======= EXECUTE =======
827
+ async function executeVoiceCommand() {
828
+ const input = document.getElementById('voice-input');
829
+ const text = input.value.trim();
830
+ if (!text || running) return;
831
+ stopVoice();
832
+ input.value = '';
833
+
834
+ const t = text.toLowerCase();
835
+ const intent = detectIntent(t);
836
+ const ex = extractDims(t);
837
+
838
+ console.log('[cycleCAD NLP]', { intent, text, extracted: ex, sceneState: sceneState.shape });
839
+
840
+ // Handle reset
841
+ if (intent === 'reset') {
842
+ resetDemo();
843
+ addLine('🔄 Scene cleared. Start fresh!', 'agent');
844
+ return;
845
+ }
846
+
847
+ let result;
848
+ if (intent === 'create') result = buildCreateSteps(text, t, ex);
849
+ else if (intent === 'hole') result = buildHoleSteps(text, t, ex);
850
+ else if (intent === 'fillet') result = buildFilletSteps(text, t, ex);
851
+ else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex); // reuse fillet visual
852
+ else if (intent === 'export') result = buildExportSteps(text, t);
853
+ else if (intent === 'validate') result = buildValidateSteps(text);
854
+ else if (intent === 'material') result = buildMaterialSteps(text, t);
855
+ else if (intent === 'boss') result = buildBossSteps(text, t, ex);
856
+ else result = buildCreateSteps(text, t, ex); // fallback
857
+
858
+ const voiceSteps = result.steps;
859
+ if (result.clearScene) {
860
+ window.clearScene();
861
+ document.getElementById('stats-overlay').style.display = 'none';
862
+ }
863
+
864
+ // Don't clear terminal — append (iterative)
865
+ addDivider();
866
+ running = true;
867
+ if (!startTime) startTime = Date.now();
868
+
869
+ const totalSteps = voiceSteps.filter(s => s.type === 'cmd').length;
870
+ let localCmdIdx = 0;
871
+
872
+ for (const step of voiceSteps) {
873
+ if (!running) break;
874
+ if (step.type === 'agent') {
875
+ addLine(step.text.replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1)).replace('{CMDS}', cmdCount), 'agent');
876
+ await delay(150);
877
+ } else if (step.type === 'comment') {
878
+ addLine(step.text, 'comment'); await delay(80);
879
+ } else if (step.type === 'divider') {
880
+ addDivider();
881
+ } else if (step.type === 'delay') {
882
+ await delay(step.ms);
883
+ } else if (step.type === 'cmd') {
884
+ cmdCount++; localCmdIdx++;
885
+ addLine(`cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`, 'cmd');
886
+ await delay(120);
887
+ const res = simulateResult(step);
888
+ addLine(`→ ${JSON.stringify(res, null, 0)}`, 'res ok');
889
+ // Update stats
890
+ if (step.stat === 'size') updateStats({ size: sceneState.dims.sizeLabel || 'computed' });
891
+ if (step.stat === 'printable') updateStats({ printable: true });
892
+ if (step.stat === 'cost') updateStats({ cost: `$${(costMap[sceneState.material] || 12.40).toFixed(2)} (CNC)` });
893
+ updateStats({ material: sceneState.material.charAt(0).toUpperCase() + sceneState.material.slice(1) });
894
+ document.getElementById('stat-cmds').textContent = cmdCount;
895
+ document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
896
+ document.getElementById('stats-overlay').style.display = 'block';
897
+ // Build 3D
898
+ if (step.mesh) buildMesh(step);
899
+ await delay(180);
900
+ }
901
+ }
902
+ running = false;
903
+ }
904
+
905
+ function simulateResult(step) {
906
+ const m = step.method;
907
+ if (m === 'sketch.start') return { ok: true, plane: 'XY' };
908
+ if (m === 'sketch.rect') return { ok: true, id: 'rect_1' };
909
+ if (m === 'sketch.circle') return { ok: true, id: 'circle_1', radius: step.params.radius };
910
+ if (m === 'ops.extrude') return { ok: true, id: 'extrude_1', height: step.params.height };
911
+ if (m === 'ops.primitive') return { ok: true, id: step.mesh, shape: step.params.shape };
912
+ if (m === 'ops.cut') return { ok: true, id: step.mesh, type: 'hole' };
913
+ if (m === 'ops.fillet') return { ok: true, radius: step.params.radius, applied: true };
914
+ if (m === 'ops.material') return { ok: true, material: step.params.material };
915
+ if (m === 'ops.boss') return { ok: true, id: step.mesh };
916
+ if (m === 'validate.dimensions') return { ok: true, size: sceneState.dims.sizeLabel };
917
+ if (m === 'validate.printability') return { ok: true, printable: true, issues: [] };
918
+ if (m === 'validate.cost') return { ok: true, unitCost: costMap[sceneState.material] || 12.40 };
919
+ if (m.startsWith('export.')) return { ok: true, filename: step.params.filename };
920
+ return { ok: true };
921
+ }
922
+
923
+ // ======= 3D MESH BUILDER =======
924
+ function buildMesh(step) {
925
+ const THREE = window.THREE;
926
+ const sc = sceneState;
927
+ const d = sc.dims;
928
+ const matOpts = { color: sc.matClr, metalness: 0.7, roughness: 0.3 };
929
+ const darkMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
930
+
931
+ if (step.mesh === 'main' || step.mesh === 'main_ext') {
932
+ const scene = window._scene;
933
+ const old = scene.getObjectByName('main');
934
+ if (old && step.mesh !== 'main_ext') { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
935
+
936
+ let geo, mesh;
937
+ const s = sc.shape;
938
+ if (s === 'cylinder' || s === 'disk') {
939
+ geo = new THREE.CylinderGeometry(d.r, d.r, d.h, 48);
940
+ mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
941
+ mesh.position.y = d.h / 2;
942
+ } else if (s === 'tube' || s === 'ring' || s === 'washer' || s === 'flange') {
943
+ const profile = [
944
+ new THREE.Vector2(d.innerR || d.outerR * 0.7, 0),
945
+ new THREE.Vector2(d.outerR || 25, 0),
946
+ new THREE.Vector2(d.outerR || 25, d.h),
947
+ new THREE.Vector2(d.innerR || d.outerR * 0.7, d.h),
948
+ ];
949
+ geo = new THREE.LatheGeometry(profile, 48);
950
+ mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
951
+ } else if (s === 'sphere') {
952
+ geo = new THREE.SphereGeometry(d.r, 48, 32);
953
+ mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
954
+ mesh.position.y = d.r;
955
+ } else if (s === 'cone') {
956
+ geo = new THREE.CylinderGeometry(d.topR, d.baseR, d.h, 48);
957
+ mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
958
+ mesh.position.y = d.h / 2;
959
+ } else if (s === 'gear') {
960
+ const gshape = new THREE.Shape();
961
+ const nt = d.teeth, pitchR = d.r, add = d.module, ded = d.module * 1.25;
962
+ const ouR = pitchR + add, roR = pitchR - ded;
963
+ for (let i = 0; i < nt; i++) {
964
+ const a0 = (i / nt) * Math.PI * 2, a1 = ((i + 0.15) / nt) * Math.PI * 2;
965
+ const a2 = ((i + 0.35) / nt) * Math.PI * 2, a3 = ((i + 0.5) / nt) * Math.PI * 2;
966
+ if (i === 0) gshape.moveTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
967
+ else gshape.lineTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
968
+ gshape.lineTo(Math.cos(a1) * ouR, Math.sin(a1) * ouR);
969
+ gshape.lineTo(Math.cos(a2) * ouR, Math.sin(a2) * ouR);
970
+ gshape.lineTo(Math.cos(a3) * roR, Math.sin(a3) * roR);
971
+ }
972
+ gshape.closePath();
973
+ geo = new THREE.ExtrudeGeometry(gshape, { depth: d.h, bevelEnabled: false });
974
+ geo.rotateX(-Math.PI / 2);
975
+ mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
976
+ mesh.position.y = d.h;
977
+ } else if (s === 'hexbolt') {
978
+ const group = new THREE.Group();
979
+ const headGeo = new THREE.CylinderGeometry(d.headR, d.headR, d.headH, 6);
980
+ const hm = new THREE.Mesh(headGeo, new THREE.MeshStandardMaterial(matOpts));
981
+ hm.position.y = d.shankH + d.headH / 2; group.add(hm);
982
+ const sg = new THREE.CylinderGeometry(d.shankR, d.shankR, d.shankH, 24);
983
+ const sm = new THREE.Mesh(sg, new THREE.MeshStandardMaterial(matOpts));
984
+ sm.position.y = d.shankH / 2; group.add(sm);
985
+ group.name = 'main';
986
+ window.addMeshToScene(group); return;
987
+ } else {
988
+ geo = new THREE.BoxGeometry(d.w || 80, d.h || 5, d.d || 40);
989
+ mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
990
+ mesh.position.y = (d.h || 5) / 2;
991
+ }
992
+ if (mesh) { mesh.name = 'main'; window.addMeshToScene(mesh); }
993
+ }
994
+ else if (step.mesh === 'fillet' && (sc.shape === 'bracket' || sc.shape === 'plate' || sc.shape === 'box')) {
995
+ const scene = window._scene;
996
+ const old = scene.getObjectByName('main');
997
+ if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
998
+ const fr = Math.min(d.filletR || 3, (d.w || 80) / 2, (d.d || 40) / 2);
999
+ const hw = (d.w || 80) / 2, hd = (d.d || 40) / 2, h = d.h || 5;
1000
+ const shape = new THREE.Shape();
1001
+ shape.moveTo(-hw + fr, -hd); shape.lineTo(hw - fr, -hd);
1002
+ shape.quadraticCurveTo(hw, -hd, hw, -hd + fr); shape.lineTo(hw, hd - fr);
1003
+ shape.quadraticCurveTo(hw, hd, hw - fr, hd); shape.lineTo(-hw + fr, hd);
1004
+ shape.quadraticCurveTo(-hw, hd, -hw, hd - fr); shape.lineTo(-hw, -hd + fr);
1005
+ shape.quadraticCurveTo(-hw, -hd, -hw + fr, -hd);
1006
+ const geo = new THREE.ExtrudeGeometry(shape, { depth: h, bevelEnabled: false, curveSegments: 16 });
1007
+ geo.rotateX(-Math.PI / 2);
1008
+ const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }));
1009
+ mesh.position.y = h; mesh.name = 'main';
1010
+ window.addMeshToScene(mesh);
1011
+ }
1012
+ else if (step.mesh && (step.mesh.startsWith('hole') || step.mesh.startsWith('boss'))) {
1013
+ const isBoss = step.mesh.startsWith('boss');
1014
+ const r = step.params?.radius || 5;
1015
+ const h = step.params?.height || (sc.dims.h || 10) + 2;
1016
+ const geo = new THREE.CylinderGeometry(r, r, h, 24);
1017
+ const mat = isBoss ? new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }) : darkMat;
1018
+ const mesh = new THREE.Mesh(geo, mat);
1019
+ if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
1020
+ window.addMeshToScene(mesh);
1021
+ }
1022
+ else if (step.mesh === 'recolor') {
1023
+ // Change material color on existing part
1024
+ const scene = window._scene;
1025
+ scene.traverse(c => {
1026
+ if (c.isMesh && c.material && c.material.color) {
1027
+ c.material.color.setHex(sc.matClr);
1028
+ }
1029
+ });
1030
+ }
1031
+ }
1032
+
1033
+ // Pulse animation for mic button
1034
+ const styleEl = document.createElement('style');
1035
+ styleEl.textContent = `@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(224,85,85,0.4); } 50% { box-shadow: 0 0 0 12px rgba(224,85,85,0); } }`;
1036
+ document.head.appendChild(styleEl);
1037
+
1038
+ window.toggleVoice = toggleVoice;
1039
+ window.executeVoiceCommand = executeVoiceCommand;
1040
+
1041
+ // Initial state
1042
+ resetDemo();
1043
+
1044
+ window.runDemo = runDemo;
1045
+ window.resetDemo = resetDemo;
1046
+ window.showSchema = showSchema;
1047
+ </script>
1048
+ </body>
1049
+ </html>