cyclecad 0.1.7 → 0.1.9
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/CLAUDE.md +164 -34
- package/app/agent-demo.html +1312 -0
- package/app/index.html +15 -0
- package/app/js/agent-api.js +1048 -0
- package/brand-identity.html +918 -0
- package/competitive-analysis.html +629 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/index.html +746 -776
- package/logo-concepts.html +167 -0
- package/package.json +1 -1
- package/~$cycleCAD-Investor-Deck.pptx +0 -0
|
@@ -0,0 +1,1312 @@
|
|
|
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
|
+
<!-- Example chips -->
|
|
102
|
+
<div id="example-chips" style="position:absolute;bottom:118px;left:50%;transform:translateX(-50%);z-index:11;display:flex;gap:6px;flex-wrap:wrap;justify-content:center;max-width:720px;">
|
|
103
|
+
<button class="chip" onclick="fillExample('build a cylinder 50mm diameter 80mm tall in steel')">cylinder</button>
|
|
104
|
+
<button class="chip" onclick="fillExample('gear 24 teeth module 2.5 in brass')">gear</button>
|
|
105
|
+
<button class="chip" onclick="fillExample('bracket 120x60 4 M8 holes 15mm from edge fillet 8')">bracket</button>
|
|
106
|
+
<button class="chip" onclick="fillExample('hex bolt M12 40mm long')">hex bolt</button>
|
|
107
|
+
<button class="chip" onclick="fillExample('flange 100mm diameter inner 40 6 M6 holes')">flange</button>
|
|
108
|
+
<button class="chip" onclick="fillExample('tube outer 50 inner 40 height 80 in aluminum')">tube</button>
|
|
109
|
+
<button class="chip" onclick="fillExample('sphere radius 30 in titanium')">sphere</button>
|
|
110
|
+
<button class="chip" onclick="fillExample('add a hole radius 8')">+ hole</button>
|
|
111
|
+
<button class="chip" onclick="fillExample('fillet 5')">+ fillet</button>
|
|
112
|
+
<button class="chip" onclick="fillExample('shell thickness 2')">+ shell</button>
|
|
113
|
+
<button class="chip" onclick="fillExample('pattern circular 6 copies')">+ pattern</button>
|
|
114
|
+
<button class="chip" onclick="fillExample('change material to steel')">material</button>
|
|
115
|
+
<button class="chip" onclick="fillExample('validate')">validate</button>
|
|
116
|
+
<button class="chip" onclick="fillExample('export stl')">export</button>
|
|
117
|
+
</div>
|
|
118
|
+
<style>
|
|
119
|
+
.chip { padding:5px 12px;border:1px solid rgba(255,255,255,0.12);border-radius:16px;background:rgba(18,34,64,0.8);color:var(--muted);font-size:11px;cursor:pointer;transition:all 0.15s;font-family:inherit; }
|
|
120
|
+
.chip:hover { background:rgba(46,134,222,0.2);color:var(--text);border-color:rgba(46,134,222,0.4); }
|
|
121
|
+
</style>
|
|
122
|
+
|
|
123
|
+
<!-- Voice input bar -->
|
|
124
|
+
<div id="voice-bar" style="position:absolute;bottom:70px;left:50%;transform:translateX(-50%);z-index:11;display:flex;align-items:center;gap:10px;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:8px 16px;min-width:500px;max-width:720px;">
|
|
125
|
+
<button id="btn-mic" onclick="toggleVoice()" style="width:40px;height:40px;border-radius:50%;border:2px solid var(--coral);background:transparent;color:var(--coral);font-size:18px;cursor:pointer;transition:all 0.2s;flex-shrink:0;">🎤</button>
|
|
126
|
+
<input id="voice-input" type="text" placeholder="Describe what to build or modify..." style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;font-family:inherit;outline:none;" onkeydown="if(event.key==='Enter')executeVoiceCommand()">
|
|
127
|
+
<button onclick="executeVoiceCommand()" style="padding:8px 16px;border:none;border-radius:8px;background:linear-gradient(135deg,var(--gold),var(--blue));color:#000;font-size:13px;font-weight:700;cursor:pointer;flex-shrink:0;">Go</button>
|
|
128
|
+
<div id="voice-status" style="position:absolute;top:-22px;left:16px;font-size:11px;color:var(--dim);"></div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Feature count badge -->
|
|
132
|
+
<div id="feature-badge" style="position:absolute;bottom:70px;right:20px;z-index:11;display:none;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;font-size:11px;color:var(--muted);max-width:180px;">
|
|
133
|
+
<div style="color:var(--gold);font-weight:600;margin-bottom:4px;">FEATURES</div>
|
|
134
|
+
<div id="feature-list" style="line-height:1.6;"></div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="controls">
|
|
138
|
+
<button class="btn-run" id="btn-run" onclick="runDemo()">▶ Run Agent Demo</button>
|
|
139
|
+
<button class="btn-reset" onclick="resetDemo()">↺ Reset</button>
|
|
140
|
+
<button class="btn-schema" onclick="showSchema()">{ } API Schema</button>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<script type="importmap">
|
|
144
|
+
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js" } }
|
|
145
|
+
</script>
|
|
146
|
+
<script type="module">
|
|
147
|
+
import * as THREE from 'three';
|
|
148
|
+
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/controls/OrbitControls.js';
|
|
149
|
+
|
|
150
|
+
// ========== Mini 3D viewport ==========
|
|
151
|
+
const canvas = document.getElementById('viewport3d');
|
|
152
|
+
const scene = new THREE.Scene();
|
|
153
|
+
scene.background = new THREE.Color(0x0A1628);
|
|
154
|
+
const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
|
|
155
|
+
camera.position.set(60, 50, 80);
|
|
156
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
157
|
+
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
|
158
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
159
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
160
|
+
controls.enableDamping = true;
|
|
161
|
+
controls.dampingFactor = 0.05;
|
|
162
|
+
|
|
163
|
+
// Lights
|
|
164
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
|
|
165
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
166
|
+
dirLight.position.set(30, 50, 40);
|
|
167
|
+
scene.add(dirLight);
|
|
168
|
+
|
|
169
|
+
// Grid
|
|
170
|
+
const grid = new THREE.GridHelper(200, 40, 0x1E3A5F, 0x122240);
|
|
171
|
+
scene.add(grid);
|
|
172
|
+
|
|
173
|
+
// Animate
|
|
174
|
+
function animate() {
|
|
175
|
+
requestAnimationFrame(animate);
|
|
176
|
+
controls.update();
|
|
177
|
+
renderer.render(scene, camera);
|
|
178
|
+
}
|
|
179
|
+
animate();
|
|
180
|
+
window.addEventListener('resize', () => {
|
|
181
|
+
camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
|
182
|
+
camera.updateProjectionMatrix();
|
|
183
|
+
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ========== Expose for demo script ==========
|
|
187
|
+
window._scene = scene;
|
|
188
|
+
window._camera = camera;
|
|
189
|
+
window._renderer = renderer;
|
|
190
|
+
|
|
191
|
+
window.addMeshToScene = (mesh) => { scene.add(mesh); };
|
|
192
|
+
window.clearScene = () => {
|
|
193
|
+
const toRemove = [];
|
|
194
|
+
scene.traverse(c => { if (c.isMesh) toRemove.push(c); });
|
|
195
|
+
toRemove.forEach(m => { scene.remove(m); m.geometry?.dispose(); m.material?.dispose(); });
|
|
196
|
+
};
|
|
197
|
+
window.THREE = THREE;
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<script>
|
|
201
|
+
const term = document.getElementById('term');
|
|
202
|
+
const progress = document.getElementById('progress');
|
|
203
|
+
let running = false;
|
|
204
|
+
let cmdCount = 0;
|
|
205
|
+
let startTime = 0;
|
|
206
|
+
|
|
207
|
+
function addLine(html, cls = '') {
|
|
208
|
+
const div = document.createElement('div');
|
|
209
|
+
div.className = cls;
|
|
210
|
+
div.innerHTML = html;
|
|
211
|
+
term.appendChild(div);
|
|
212
|
+
term.scrollTop = term.scrollHeight;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function addDivider() {
|
|
216
|
+
const div = document.createElement('div');
|
|
217
|
+
div.className = 'divider';
|
|
218
|
+
term.appendChild(div);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
222
|
+
|
|
223
|
+
function updateStats(data) {
|
|
224
|
+
document.getElementById('stats-overlay').style.display = 'block';
|
|
225
|
+
if (data.size) document.getElementById('stat-size').textContent = data.size;
|
|
226
|
+
if (data.material) document.getElementById('stat-mat').textContent = data.material;
|
|
227
|
+
if (data.printable !== undefined) document.getElementById('stat-print').textContent = data.printable ? '✅ Yes' : '❌ No';
|
|
228
|
+
if (data.cost) document.getElementById('stat-cost').textContent = data.cost;
|
|
229
|
+
document.getElementById('stat-cmds').textContent = cmdCount;
|
|
230
|
+
document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ========== Demo Script ==========
|
|
234
|
+
// This simulates what an AI agent does when it calls cycleCAD's API
|
|
235
|
+
|
|
236
|
+
const DEMO_STEPS = [
|
|
237
|
+
// Agent thinks
|
|
238
|
+
{ type: 'agent', text: '🤖 Agent received task: "Design a mounting bracket for the DUO cycleWASH with 4 M6 bolt holes"' },
|
|
239
|
+
{ type: 'comment', text: '// Agent analyzes requirements: 80×40mm base plate, 5mm thick, aluminum, 4 corner holes' },
|
|
240
|
+
{ type: 'delay', ms: 600 },
|
|
241
|
+
|
|
242
|
+
// Phase 1: Sketch
|
|
243
|
+
{ type: 'divider' },
|
|
244
|
+
{ type: 'agent', text: '📐 Phase 1: Sketch the base profile' },
|
|
245
|
+
{ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } },
|
|
246
|
+
{ type: 'cmd', method: 'sketch.rect', params: { x: -40, y: -20, width: 80, height: 40 } },
|
|
247
|
+
{ type: 'delay', ms: 300 },
|
|
248
|
+
|
|
249
|
+
// Phase 2: Extrude
|
|
250
|
+
{ type: 'divider' },
|
|
251
|
+
{ type: 'agent', text: '📦 Phase 2: Extrude to 3D solid' },
|
|
252
|
+
{ type: 'cmd', method: 'ops.extrude', params: { height: 5, material: 'aluminum' }, mesh: 'bracket' },
|
|
253
|
+
{ type: 'delay', ms: 400 },
|
|
254
|
+
|
|
255
|
+
// Phase 3: Add bolt holes (primitives positioned at corners)
|
|
256
|
+
{ type: 'divider' },
|
|
257
|
+
{ type: 'agent', text: '🔩 Phase 3: Add 4 M6 bolt holes at corners' },
|
|
258
|
+
{ type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole1', pos: [-30, 2.5, -12] },
|
|
259
|
+
{ type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole2', pos: [30, 2.5, -12] },
|
|
260
|
+
{ type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole3', pos: [-30, 2.5, 12] },
|
|
261
|
+
{ type: 'cmd', method: 'ops.primitive', params: { shape: 'cylinder', radius: 3.2, height: 6 }, mesh: 'hole4', pos: [30, 2.5, 12] },
|
|
262
|
+
{ type: 'delay', ms: 300 },
|
|
263
|
+
|
|
264
|
+
// Phase 4: Fillet edges
|
|
265
|
+
{ type: 'divider' },
|
|
266
|
+
{ type: 'agent', text: '✨ Phase 4: Fillet edges for stress relief' },
|
|
267
|
+
{ type: 'cmd', method: 'ops.fillet', params: { target: 'bracket', radius: 15 }, mesh: 'fillet' },
|
|
268
|
+
{ type: 'delay', ms: 300 },
|
|
269
|
+
|
|
270
|
+
// Phase 5: Validate
|
|
271
|
+
{ type: 'divider' },
|
|
272
|
+
{ type: 'agent', text: '🔍 Phase 5: Validate for manufacturing' },
|
|
273
|
+
{ type: 'cmd', method: 'validate.dimensions', params: { target: 'bracket' }, stat: 'size' },
|
|
274
|
+
{ type: 'cmd', method: 'validate.printability', params: { target: 'bracket', process: 'CNC' }, stat: 'printable' },
|
|
275
|
+
{ type: 'cmd', method: 'validate.cost', params: { target: 'bracket', process: 'CNC', material: 'aluminum' }, stat: 'cost' },
|
|
276
|
+
{ type: 'delay', ms: 400 },
|
|
277
|
+
|
|
278
|
+
// Phase 6: Export
|
|
279
|
+
{ type: 'divider' },
|
|
280
|
+
{ type: 'agent', text: '📤 Phase 6: Export for manufacturing' },
|
|
281
|
+
{ type: 'cmd', method: 'export.stl', params: { filename: 'duo-bracket.stl', binary: true } },
|
|
282
|
+
{ type: 'delay', ms: 200 },
|
|
283
|
+
|
|
284
|
+
// Done
|
|
285
|
+
{ type: 'divider' },
|
|
286
|
+
{ type: 'agent', text: '✅ Task complete. Bracket designed, validated, and exported in {TIME}s using {CMDS} API calls.' },
|
|
287
|
+
{ type: 'comment', text: '// No human touched a mouse. No GUI was opened. The agent designed through cycleCAD.' },
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
async function runDemo() {
|
|
291
|
+
if (running) return;
|
|
292
|
+
running = true;
|
|
293
|
+
cmdCount = 0;
|
|
294
|
+
startTime = Date.now();
|
|
295
|
+
document.getElementById('btn-run').disabled = true;
|
|
296
|
+
document.getElementById('btn-run').textContent = '⏳ Running...';
|
|
297
|
+
term.innerHTML = '';
|
|
298
|
+
window.clearScene();
|
|
299
|
+
document.getElementById('stats-overlay').style.display = 'none';
|
|
300
|
+
|
|
301
|
+
const totalSteps = DEMO_STEPS.filter(s => s.type === 'cmd').length;
|
|
302
|
+
let cmdIdx = 0;
|
|
303
|
+
|
|
304
|
+
for (const step of DEMO_STEPS) {
|
|
305
|
+
if (!running) break;
|
|
306
|
+
|
|
307
|
+
if (step.type === 'agent') {
|
|
308
|
+
let text = step.text
|
|
309
|
+
.replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1))
|
|
310
|
+
.replace('{CMDS}', cmdCount);
|
|
311
|
+
addLine(text, 'agent');
|
|
312
|
+
await delay(200);
|
|
313
|
+
}
|
|
314
|
+
else if (step.type === 'comment') {
|
|
315
|
+
addLine(step.text, 'comment');
|
|
316
|
+
await delay(100);
|
|
317
|
+
}
|
|
318
|
+
else if (step.type === 'divider') {
|
|
319
|
+
addDivider();
|
|
320
|
+
}
|
|
321
|
+
else if (step.type === 'delay') {
|
|
322
|
+
await delay(step.ms);
|
|
323
|
+
}
|
|
324
|
+
else if (step.type === 'cmd') {
|
|
325
|
+
cmdCount++;
|
|
326
|
+
cmdIdx++;
|
|
327
|
+
progress.style.width = (cmdIdx / totalSteps * 100) + '%';
|
|
328
|
+
|
|
329
|
+
const cmdStr = `cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`;
|
|
330
|
+
addLine(cmdStr, 'cmd');
|
|
331
|
+
await delay(150);
|
|
332
|
+
|
|
333
|
+
// Simulate the result
|
|
334
|
+
const result = simulateCommand(step);
|
|
335
|
+
const resStr = JSON.stringify(result, null, 0);
|
|
336
|
+
addLine(`→ ${resStr}`, 'res ok');
|
|
337
|
+
|
|
338
|
+
// Update stats
|
|
339
|
+
if (step.stat === 'size') updateStats({ size: '80 × 40 × 5 mm' });
|
|
340
|
+
if (step.stat === 'printable') updateStats({ printable: true });
|
|
341
|
+
if (step.stat === 'cost') updateStats({ cost: '$12.40 (CNC)' });
|
|
342
|
+
updateStats({ material: 'Aluminum' });
|
|
343
|
+
|
|
344
|
+
// Add 3D geometry
|
|
345
|
+
if (step.mesh) {
|
|
346
|
+
createMesh(step);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await delay(250);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
running = false;
|
|
354
|
+
document.getElementById('btn-run').disabled = false;
|
|
355
|
+
document.getElementById('btn-run').textContent = '▶ Run Again';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function simulateCommand(step) {
|
|
359
|
+
const m = step.method;
|
|
360
|
+
if (m === 'sketch.start') return { ok: true, result: { plane: 'XY', status: 'active' } };
|
|
361
|
+
if (m === 'sketch.rect') return { ok: true, result: { id: 'rect_1', type: 'rect', width: step.params.width, height: step.params.height } };
|
|
362
|
+
if (m === 'ops.extrude') return { ok: true, result: { id: 'extrude_1', type: 'extrude', height: step.params.height, material: step.params.material, bbox: { width: 80, height: 5, depth: 40 } } };
|
|
363
|
+
if (m === 'ops.primitive') return { ok: true, result: { id: step.mesh, type: 'cylinder', shape: 'cylinder' } };
|
|
364
|
+
if (m === 'ops.fillet') return { ok: true, result: { target: 'bracket', radius: step.params.radius, applied: true } };
|
|
365
|
+
if (m === 'validate.dimensions') return { ok: true, result: { width: 80, height: 5, depth: 40, volume: 16000, fitsInPrintBed: true } };
|
|
366
|
+
if (m === 'validate.printability') return { ok: true, result: { printable: true, process: 'CNC', issues: [] } };
|
|
367
|
+
if (m === 'validate.cost') return { ok: true, result: { process: 'CNC', unitCost: 12.40, batchOf100: 892 } };
|
|
368
|
+
if (m === 'export.stl') return { ok: true, result: { format: 'stl', filename: step.params.filename, featureCount: 5 } };
|
|
369
|
+
return { ok: true };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function createMesh(step) {
|
|
373
|
+
const THREE = window.THREE;
|
|
374
|
+
if (step.mesh === 'bracket') {
|
|
375
|
+
// Main bracket body — sharp box (will be replaced by fillet step)
|
|
376
|
+
const geo = new THREE.BoxGeometry(80, 5, 40);
|
|
377
|
+
const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
|
|
378
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
379
|
+
mesh.position.y = 2.5;
|
|
380
|
+
mesh.name = 'bracket';
|
|
381
|
+
window.addMeshToScene(mesh);
|
|
382
|
+
}
|
|
383
|
+
else if (step.mesh === 'fillet') {
|
|
384
|
+
// Replace sharp bracket with rounded version — visible fillet r=15
|
|
385
|
+
const scene = window._scene;
|
|
386
|
+
// Remove old bracket
|
|
387
|
+
const old = scene.getObjectByName('bracket');
|
|
388
|
+
if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
|
|
389
|
+
|
|
390
|
+
const r = step.params?.radius || 15; // fillet radius from command
|
|
391
|
+
const w = 80, h = 5, d = 40;
|
|
392
|
+
const shape = new THREE.Shape();
|
|
393
|
+
// Rounded rectangle in XZ plane (we'll extrude along Y)
|
|
394
|
+
const hw = w / 2, hd = d / 2;
|
|
395
|
+
shape.moveTo(-hw + r, -hd);
|
|
396
|
+
shape.lineTo(hw - r, -hd);
|
|
397
|
+
shape.quadraticCurveTo(hw, -hd, hw, -hd + r);
|
|
398
|
+
shape.lineTo(hw, hd - r);
|
|
399
|
+
shape.quadraticCurveTo(hw, hd, hw - r, hd);
|
|
400
|
+
shape.lineTo(-hw + r, hd);
|
|
401
|
+
shape.quadraticCurveTo(-hw, hd, -hw, hd - r);
|
|
402
|
+
shape.lineTo(-hw, -hd + r);
|
|
403
|
+
shape.quadraticCurveTo(-hw, -hd, -hw + r, -hd);
|
|
404
|
+
|
|
405
|
+
const extrudeSettings = { depth: h, bevelEnabled: false, curveSegments: 16 };
|
|
406
|
+
const geo = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
|
407
|
+
// Rotate so extrude goes along Y axis
|
|
408
|
+
geo.rotateX(-Math.PI / 2);
|
|
409
|
+
const mat = new THREE.MeshStandardMaterial({ color: 0xccccdd, metalness: 0.7, roughness: 0.3 });
|
|
410
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
411
|
+
mesh.position.y = 5; // align with bolt holes
|
|
412
|
+
mesh.name = 'bracket';
|
|
413
|
+
window.addMeshToScene(mesh);
|
|
414
|
+
}
|
|
415
|
+
else if (step.mesh && step.mesh.startsWith('hole')) {
|
|
416
|
+
// Bolt holes — dark cylinders subtracted visually
|
|
417
|
+
const geo = new THREE.CylinderGeometry(3.2, 3.2, 6, 24);
|
|
418
|
+
const mat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
|
|
419
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
420
|
+
if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
|
|
421
|
+
window.addMeshToScene(mesh);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function resetDemo() {
|
|
426
|
+
running = false;
|
|
427
|
+
cmdCount = 0;
|
|
428
|
+
startTime = 0;
|
|
429
|
+
sceneState = { shape: null, material: 'aluminum', matClr: 0xccccdd, dims: {}, features: [], holeIdx: 0 };
|
|
430
|
+
term.innerHTML = '<div class="comment">// Ready. Type a command like "build a cylinder 50mm diameter 80 tall in steel"</div><div class="comment">// Then: "add a hole radius 10" → "fillet 5" → "export stl"</div><div class="comment">// Each command builds on the last. Say "start over" to reset.</div>';
|
|
431
|
+
window.clearScene();
|
|
432
|
+
document.getElementById('stats-overlay').style.display = 'none';
|
|
433
|
+
progress.style.width = '0%';
|
|
434
|
+
document.getElementById('btn-run').disabled = false;
|
|
435
|
+
document.getElementById('btn-run').textContent = '▶ Run Agent Demo';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function showSchema() {
|
|
439
|
+
term.innerHTML = '';
|
|
440
|
+
const schema = {
|
|
441
|
+
sketch: ['sketch.start', 'sketch.end', 'sketch.line', 'sketch.rect', 'sketch.circle', 'sketch.arc', 'sketch.clear'],
|
|
442
|
+
ops: ['ops.extrude', 'ops.revolve', 'ops.primitive', 'ops.fillet', 'ops.chamfer', 'ops.boolean', 'ops.shell', 'ops.pattern', 'ops.material', 'ops.sweep', 'ops.loft', 'ops.spring', 'ops.thread', 'ops.bend'],
|
|
443
|
+
transform: ['transform.move', 'transform.rotate', 'transform.scale'],
|
|
444
|
+
view: ['view.set', 'view.fit', 'view.wireframe', 'view.grid'],
|
|
445
|
+
export: ['export.stl', 'export.obj', 'export.gltf', 'export.json'],
|
|
446
|
+
validate: ['validate.dimensions', 'validate.wallThickness', 'validate.printability', 'validate.cost'],
|
|
447
|
+
query: ['query.features', 'query.bbox', 'query.materials', 'query.session', 'query.log'],
|
|
448
|
+
scene: ['scene.clear', 'scene.snapshot'],
|
|
449
|
+
meta: ['meta.ping', 'meta.version', 'meta.schema']
|
|
450
|
+
};
|
|
451
|
+
addLine('📋 cycleCAD Agent API — 46 commands across 9 namespaces', 'agent');
|
|
452
|
+
addDivider();
|
|
453
|
+
for (const [ns, methods] of Object.entries(schema)) {
|
|
454
|
+
addLine(`<span style="color:var(--gold)">${ns}</span> (${methods.length})`, '');
|
|
455
|
+
methods.forEach(m => addLine(` ${m}`, 'res'));
|
|
456
|
+
}
|
|
457
|
+
addDivider();
|
|
458
|
+
addLine('Usage: window.cycleCAD.execute({ method: "ops.extrude", params: { height: 10 } })', 'comment');
|
|
459
|
+
addLine('Full schema: window.cycleCAD.getSchema()', 'comment');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ========== Voice Command System ==========
|
|
463
|
+
let recognition = null;
|
|
464
|
+
let isListening = false;
|
|
465
|
+
|
|
466
|
+
function toggleVoice() {
|
|
467
|
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
468
|
+
document.getElementById('voice-status').textContent = 'Speech recognition not supported — type your command instead';
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (isListening) { stopVoice(); return; }
|
|
472
|
+
|
|
473
|
+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
474
|
+
recognition = new SR();
|
|
475
|
+
recognition.continuous = false;
|
|
476
|
+
recognition.interimResults = true;
|
|
477
|
+
recognition.lang = 'en-US';
|
|
478
|
+
|
|
479
|
+
const btn = document.getElementById('btn-mic');
|
|
480
|
+
const status = document.getElementById('voice-status');
|
|
481
|
+
const input = document.getElementById('voice-input');
|
|
482
|
+
|
|
483
|
+
recognition.onstart = () => {
|
|
484
|
+
isListening = true;
|
|
485
|
+
btn.style.background = 'var(--coral)';
|
|
486
|
+
btn.style.color = '#fff';
|
|
487
|
+
btn.style.animation = 'pulse 1s ease-in-out infinite';
|
|
488
|
+
status.textContent = 'Listening...';
|
|
489
|
+
};
|
|
490
|
+
recognition.onresult = (e) => {
|
|
491
|
+
let transcript = '';
|
|
492
|
+
for (let i = 0; i < e.results.length; i++) transcript = e.results[i][0].transcript;
|
|
493
|
+
input.value = transcript;
|
|
494
|
+
if (e.results[0].isFinal) status.textContent = 'Got it! Press "Build It" or Enter.';
|
|
495
|
+
};
|
|
496
|
+
recognition.onerror = (e) => {
|
|
497
|
+
if (e.error === 'not-allowed') {
|
|
498
|
+
status.textContent = 'Mic needs HTTPS (works on cyclecad.com) — type your command below';
|
|
499
|
+
} else {
|
|
500
|
+
status.textContent = 'Mic unavailable — type your command below';
|
|
501
|
+
}
|
|
502
|
+
stopVoice();
|
|
503
|
+
};
|
|
504
|
+
recognition.onend = () => { stopVoice(); };
|
|
505
|
+
try { recognition.start(); } catch(err) {
|
|
506
|
+
document.getElementById('voice-status').textContent = 'Mic blocked — type your command instead';
|
|
507
|
+
stopVoice();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function stopVoice() {
|
|
512
|
+
isListening = false;
|
|
513
|
+
if (recognition) try { recognition.stop(); } catch(e) {}
|
|
514
|
+
const btn = document.getElementById('btn-mic');
|
|
515
|
+
btn.style.background = 'transparent';
|
|
516
|
+
btn.style.color = 'var(--coral)';
|
|
517
|
+
btn.style.animation = 'none';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ========== STATEFUL NLP COMMAND SYSTEM v3 ==========
|
|
521
|
+
// Iterative: each command builds on the previous.
|
|
522
|
+
// "build cylinder 50mm diameter 80 tall" → creates cylinder (clears scene)
|
|
523
|
+
// "add a hole radius 10" → cuts hole into existing part
|
|
524
|
+
// "fillet 5" → rounds edges
|
|
525
|
+
// "export stl" → exports (only when asked)
|
|
526
|
+
// "validate" → checks manufacturing (only when asked)
|
|
527
|
+
// "change material to steel" → updates material
|
|
528
|
+
// "start over" / "reset" / "new" → clears everything
|
|
529
|
+
|
|
530
|
+
// ---- Persistent scene state ----
|
|
531
|
+
let sceneState = {
|
|
532
|
+
shape: null, // 'cylinder', 'bracket', 'sphere', etc.
|
|
533
|
+
material: 'aluminum',
|
|
534
|
+
matClr: 0xccccdd,
|
|
535
|
+
dims: {}, // shape-specific dimensions
|
|
536
|
+
features: [], // log of operations applied
|
|
537
|
+
holeIdx: 0, // counter for unique hole names
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
function parseNum(t, ...patterns) {
|
|
541
|
+
for (const p of patterns) {
|
|
542
|
+
const m = t.match(p);
|
|
543
|
+
if (m) return parseFloat(m[1]);
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function parseMaterial(t) {
|
|
549
|
+
if (/steel|stainless/.test(t)) return 'steel';
|
|
550
|
+
if (/brass/.test(t)) return 'brass';
|
|
551
|
+
if (/titanium/.test(t)) return 'titanium';
|
|
552
|
+
if (/copper/.test(t)) return 'copper';
|
|
553
|
+
if (/abs|plastic/.test(t)) return 'ABS';
|
|
554
|
+
if (/nylon/.test(t)) return 'nylon';
|
|
555
|
+
if (/wood/.test(t)) return 'wood';
|
|
556
|
+
if (/carbon/.test(t)) return 'carbon fiber';
|
|
557
|
+
return null; // null = don't change
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function materialColor(mat) {
|
|
561
|
+
const colors = {
|
|
562
|
+
aluminum: 0xccccdd, steel: 0x888899, brass: 0xd4a843, titanium: 0xaabbcc,
|
|
563
|
+
copper: 0xb87333, ABS: 0xe8e8e0, nylon: 0xf0f0e0, wood: 0x8B6914, 'carbon fiber': 0x333333
|
|
564
|
+
};
|
|
565
|
+
return colors[mat] || 0xccccdd;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const costMap = { aluminum: 12.40, steel: 8.90, brass: 18.50, titanium: 45.00, ABS: 3.20, nylon: 4.80, copper: 22.10, wood: 5.50, 'carbon fiber': 65.00 };
|
|
569
|
+
|
|
570
|
+
function detectShape(t) {
|
|
571
|
+
if (/hollow\s*cylinder|tube|pipe/.test(t)) return 'tube';
|
|
572
|
+
if (/cylinder|cylind|cylindar/.test(t)) return 'cylinder';
|
|
573
|
+
if (/disk|disc|puck/.test(t)) return 'disk';
|
|
574
|
+
if (/sphere|ball/.test(t)) return 'sphere';
|
|
575
|
+
if (/cone|taper/.test(t)) return 'cone';
|
|
576
|
+
if (/gear|sprocket/.test(t)) return 'gear';
|
|
577
|
+
if (/hex\s*bolt|bolt/.test(t)) return 'hexbolt';
|
|
578
|
+
if (/washer/.test(t)) return 'washer';
|
|
579
|
+
if (/\bring\b/.test(t)) return 'ring';
|
|
580
|
+
if (/flange/.test(t)) return 'flange';
|
|
581
|
+
if (/plate/.test(t)) return 'plate';
|
|
582
|
+
if (/block|cube/.test(t)) return 'box';
|
|
583
|
+
if (/bracket|mount/.test(t)) return 'bracket';
|
|
584
|
+
if (/\d+\s*x\s*\d+/.test(t)) return 'bracket';
|
|
585
|
+
if (/diameter|radius/.test(t)) return 'cylinder';
|
|
586
|
+
return null; // unknown — might be a modify command
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---- Detect what KIND of command this is ----
|
|
590
|
+
function detectIntent(t) {
|
|
591
|
+
if (/^(start\s*over|reset|clear|new\s*part|new\s*design)/.test(t)) return 'reset';
|
|
592
|
+
if (/export|save|download/.test(t)) return 'export';
|
|
593
|
+
if (/validate|check|verify|inspect|analyze/.test(t)) return 'validate';
|
|
594
|
+
if (/change\s*material|set\s*material|make\s*it\s*(steel|brass|aluminum|titanium|nylon|abs|plastic|copper|wood|carbon)/.test(t)) return 'material';
|
|
595
|
+
if (/undo/.test(t)) return 'undo';
|
|
596
|
+
// Modify operations on existing part
|
|
597
|
+
if (/add\s*(a\s*)?hole|cut\s*(a\s*)?hole|drill|bore|extrude\s*(a\s*)?hole|punch/.test(t)) return 'hole';
|
|
598
|
+
if (/fillet|round\s*(the\s*)?edge/.test(t)) return 'fillet';
|
|
599
|
+
if (/chamfer/.test(t)) return 'chamfer';
|
|
600
|
+
if (/add\s*(a\s*)?(slot|groove|channel)/.test(t)) return 'slot';
|
|
601
|
+
if (/add\s*(a\s*)?(boss|peg|pin|post)/.test(t)) return 'boss';
|
|
602
|
+
if (/shell|hollow\s*out|thin\s*wall/.test(t)) return 'shell';
|
|
603
|
+
if (/pattern|array|copies|repeat/.test(t)) return 'pattern';
|
|
604
|
+
if (/counterbore|counter\s*bore|c'?bore/.test(t)) return 'counterbore';
|
|
605
|
+
if (/thread|tap/.test(t)) return 'thread';
|
|
606
|
+
if (/mirror|symmetr/.test(t)) return 'mirror';
|
|
607
|
+
// If a known shape word is present → create new
|
|
608
|
+
if (detectShape(t)) return 'create';
|
|
609
|
+
// Default: try to create
|
|
610
|
+
return 'create';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ---- Parse dimensions from text ----
|
|
614
|
+
function extractDims(t) {
|
|
615
|
+
return {
|
|
616
|
+
diameter: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*(?:dia|diameter)/, /dia(?:meter)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/, /[øo]\s*(\d+(?:\.\d+)?)/) || 0,
|
|
617
|
+
radius: parseNum(t, /radius\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*radius/) || 0,
|
|
618
|
+
height: parseNum(t, /height\s*(?:of\s*)?(\d+(?:\.\d+)?)(?:\s*mm)?/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:tall|high|height|long|deep)/) || 0,
|
|
619
|
+
thick: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*thick/, /thick(?:ness)?\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
|
|
620
|
+
outerD: parseNum(t, /outer\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /od\s*(\d+(?:\.\d+)?)/) || 0,
|
|
621
|
+
innerD: parseNum(t, /inner\s*(?:diameter\s*)?(\d+(?:\.\d+)?)/, /id\s*(\d+(?:\.\d+)?)/, /bore\s*(\d+(?:\.\d+)?)/) || 0,
|
|
622
|
+
topD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*top/, /top\s*(\d+(?:\.\d+)?)/) || 0,
|
|
623
|
+
baseD: parseNum(t, /(\d+(?:\.\d+)?)\s*mm?\s*base/, /base\s*(\d+(?:\.\d+)?)/) || 0,
|
|
624
|
+
teeth: parseNum(t, /(\d+)\s*teeth/, /teeth\s*(\d+)/) || 0,
|
|
625
|
+
modl: parseNum(t, /module\s*(\d+(?:\.\d+)?)/) || 0,
|
|
626
|
+
mBolt: parseNum(t, /\bm(\d+)\b/) || 0,
|
|
627
|
+
filletR: parseNum(t, /fillet\s*(?:radius\s*(?:of\s*)?)?(\d+(?:\.\d+)?)/, /round\s*(\d+(?:\.\d+)?)/) || 0,
|
|
628
|
+
chamferS: parseNum(t, /chamfer\s*(?:of\s*)?(\d+(?:\.\d+)?)/) || 0,
|
|
629
|
+
dimMatch: t.match(/(\d+)\s*(?:x|by)\s*(\d+)/),
|
|
630
|
+
holeCountMatch: t.match(/(\d+)\s*(?:m\d+\s*)?(?:bolt\s*)?holes?/),
|
|
631
|
+
holeOffset: parseNum(t, /(\d+)\s*mm?\s*from\s*(?:the\s*)?edge/) || 10,
|
|
632
|
+
posX: parseNum(t, /(?:at|x)\s*(-?\d+(?:\.\d+)?)/) || 0,
|
|
633
|
+
posZ: parseNum(t, /(?:,\s*|y\s*)(-?\d+(?:\.\d+)?)/) || 0,
|
|
634
|
+
shellThick: parseNum(t, /shell\s*(?:thickness\s*)?(\d+(?:\.\d+)?)/, /thick(?:ness)?\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:shell|wall)/) || 0,
|
|
635
|
+
copies: parseNum(t, /(\d+)\s*cop(?:y|ies)/, /(\d+)\s*times/, /pattern\s*(?:of\s*)?(\d+)/) || 0,
|
|
636
|
+
patternType: /circular|radial|around/.test(t) ? 'circular' : (/linear|row|line/.test(t) ? 'linear' : 'circular'),
|
|
637
|
+
spacing: parseNum(t, /spacing\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:apart|spacing)/) || 0,
|
|
638
|
+
threadPitch: parseNum(t, /pitch\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*pitch/) || 0,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ======= STEP BUILDERS per intent =======
|
|
643
|
+
|
|
644
|
+
function buildCreateSteps(text, t, ex) {
|
|
645
|
+
const steps = [];
|
|
646
|
+
const shape = detectShape(t) || 'bracket';
|
|
647
|
+
const mat = parseMaterial(t) || 'aluminum';
|
|
648
|
+
const matClr = materialColor(mat);
|
|
649
|
+
|
|
650
|
+
// Update scene state — this is a NEW part
|
|
651
|
+
sceneState = { shape, material: mat, matClr, dims: {}, features: [], holeIdx: 0 };
|
|
652
|
+
const d = sceneState.dims;
|
|
653
|
+
|
|
654
|
+
if (shape === 'cylinder' || shape === 'disk') {
|
|
655
|
+
d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 25);
|
|
656
|
+
d.h = ex.height || ex.thick || (shape === 'disk' ? 5 : 60);
|
|
657
|
+
d.sizeLabel = `ø${d.r * 2} × ${d.h}mm`;
|
|
658
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
659
|
+
steps.push({ type: 'comment', text: `// New ${shape}: ø${d.r * 2}mm × ${d.h}mm in ${mat}` });
|
|
660
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
661
|
+
steps.push({ type: 'divider' });
|
|
662
|
+
steps.push({ type: 'agent', text: `📐 Sketch circle ø${d.r * 2}mm` });
|
|
663
|
+
steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
|
|
664
|
+
steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.r } });
|
|
665
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
666
|
+
steps.push({ type: 'divider' });
|
|
667
|
+
steps.push({ type: 'agent', text: `📦 Extrude ${d.h}mm in ${mat}` });
|
|
668
|
+
steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
|
|
669
|
+
}
|
|
670
|
+
else if (shape === 'tube' || shape === 'ring') {
|
|
671
|
+
d.outerR = ex.outerD ? ex.outerD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
|
|
672
|
+
d.innerR = ex.innerD ? ex.innerD / 2 : d.outerR * 0.7;
|
|
673
|
+
d.h = ex.height || ex.thick || (shape === 'ring' ? 10 : 60);
|
|
674
|
+
d.sizeLabel = `OD${d.outerR * 2} × ID${d.innerR * 2} × ${d.h}mm`;
|
|
675
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
676
|
+
steps.push({ type: 'comment', text: `// New ${shape}: OD${d.outerR * 2} ID${d.innerR * 2} × ${d.h}mm` });
|
|
677
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
678
|
+
steps.push({ type: 'divider' });
|
|
679
|
+
steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
|
|
680
|
+
steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.outerR } });
|
|
681
|
+
steps.push({ type: 'cmd', method: 'sketch.circle', params: { x: 0, y: 0, radius: d.innerR, type: 'inner' } });
|
|
682
|
+
steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, hollow: true, material: mat }, mesh: 'main' });
|
|
683
|
+
}
|
|
684
|
+
else if (shape === 'sphere') {
|
|
685
|
+
d.r = ex.diameter ? ex.diameter / 2 : (ex.radius || 30);
|
|
686
|
+
d.h = d.r * 2;
|
|
687
|
+
d.sizeLabel = `ø${d.r * 2}mm sphere`;
|
|
688
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
689
|
+
steps.push({ type: 'comment', text: `// New sphere ø${d.r * 2}mm in ${mat}` });
|
|
690
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
691
|
+
steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'sphere', radius: d.r, material: mat }, mesh: 'main' });
|
|
692
|
+
}
|
|
693
|
+
else if (shape === 'cone') {
|
|
694
|
+
d.baseR = ex.baseD ? ex.baseD / 2 : (ex.diameter ? ex.diameter / 2 : 25);
|
|
695
|
+
d.topR = ex.topD ? ex.topD / 2 : 5;
|
|
696
|
+
d.h = ex.height || 50;
|
|
697
|
+
d.sizeLabel = `ø${d.baseR * 2}→ø${d.topR * 2} × ${d.h}mm`;
|
|
698
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
699
|
+
steps.push({ type: 'comment', text: `// New cone: base ø${d.baseR * 2} → top ø${d.topR * 2} × ${d.h}mm` });
|
|
700
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
701
|
+
steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
|
|
702
|
+
steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, taper: d.topR, material: mat }, mesh: 'main' });
|
|
703
|
+
}
|
|
704
|
+
else if (shape === 'gear') {
|
|
705
|
+
d.teeth = ex.teeth || 20;
|
|
706
|
+
d.module = ex.modl || 3;
|
|
707
|
+
d.r = (d.teeth * d.module) / 2;
|
|
708
|
+
d.h = ex.thick || ex.height || 10;
|
|
709
|
+
d.sizeLabel = `${d.teeth}T m${d.module} × ${d.h}mm`;
|
|
710
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
711
|
+
steps.push({ type: 'comment', text: `// Gear: ${d.teeth} teeth, module ${d.module}, ø${d.r * 2}mm` });
|
|
712
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
713
|
+
steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'gear', teeth: d.teeth, module: d.module, material: mat }, mesh: 'main' });
|
|
714
|
+
}
|
|
715
|
+
else if (shape === 'hexbolt') {
|
|
716
|
+
d.m = ex.mBolt || 10;
|
|
717
|
+
d.headR = d.m * 0.9; d.headH = d.m * 0.65;
|
|
718
|
+
d.shankR = d.m / 2; d.shankH = ex.height || d.m * 2;
|
|
719
|
+
d.h = d.headH + d.shankH;
|
|
720
|
+
d.sizeLabel = `M${d.m} × ${d.shankH}mm bolt`;
|
|
721
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
722
|
+
steps.push({ type: 'comment', text: `// Hex bolt M${d.m} × ${d.shankH}mm` });
|
|
723
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
724
|
+
steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'hexbolt', size: d.m, length: d.shankH, material: mat }, mesh: 'main' });
|
|
725
|
+
}
|
|
726
|
+
else if (shape === 'washer') {
|
|
727
|
+
d.m = ex.mBolt || 10;
|
|
728
|
+
d.outerR = d.m * 1.1; d.innerR = d.m / 2 + 0.5;
|
|
729
|
+
d.h = ex.thick || 2;
|
|
730
|
+
d.sizeLabel = `M${d.m} washer`;
|
|
731
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
732
|
+
steps.push({ type: 'cmd', method: 'ops.primitive', params: { shape: 'washer', size: d.m, material: mat }, mesh: 'main' });
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// bracket / plate / box
|
|
736
|
+
d.w = ex.dimMatch ? parseInt(ex.dimMatch[1]) : 80;
|
|
737
|
+
d.d = ex.dimMatch ? parseInt(ex.dimMatch[2]) : 40;
|
|
738
|
+
d.h = ex.thick || ex.height || 5;
|
|
739
|
+
d.sizeLabel = `${d.w} × ${d.d} × ${d.h}mm`;
|
|
740
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
741
|
+
steps.push({ type: 'comment', text: `// New ${shape}: ${d.w}×${d.d}×${d.h}mm in ${mat}` });
|
|
742
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
743
|
+
steps.push({ type: 'divider' });
|
|
744
|
+
steps.push({ type: 'cmd', method: 'sketch.start', params: { plane: 'XY' } });
|
|
745
|
+
steps.push({ type: 'cmd', method: 'sketch.rect', params: { x: -d.w / 2, y: -d.d / 2, width: d.w, height: d.d } });
|
|
746
|
+
steps.push({ type: 'cmd', method: 'ops.extrude', params: { height: d.h, material: mat }, mesh: 'main' });
|
|
747
|
+
}
|
|
748
|
+
sceneState.features.push(`Created ${shape}`);
|
|
749
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
750
|
+
steps.push({ type: 'agent', text: `✅ ${shape} created. Keep going — add holes, fillets, or say "export".` });
|
|
751
|
+
return { steps, clearScene: true };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function buildHoleSteps(text, t, ex) {
|
|
755
|
+
const steps = [];
|
|
756
|
+
if (!sceneState.shape) {
|
|
757
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first — e.g. "build a cylinder 50mm diameter 80 tall"' });
|
|
758
|
+
return { steps, clearScene: false };
|
|
759
|
+
}
|
|
760
|
+
const hr = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
|
|
761
|
+
const hh = ex.height || sceneState.dims.h || 20;
|
|
762
|
+
const px = ex.posX || 0;
|
|
763
|
+
const pz = ex.posZ || 0;
|
|
764
|
+
const py = (sceneState.dims.h || hh) / 2;
|
|
765
|
+
sceneState.holeIdx++;
|
|
766
|
+
const holeName = `hole${sceneState.holeIdx}`;
|
|
767
|
+
|
|
768
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
769
|
+
steps.push({ type: 'comment', text: `// Adding hole ø${hr * 2}mm through part at (${px}, ${pz})` });
|
|
770
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
771
|
+
steps.push({ type: 'divider' });
|
|
772
|
+
steps.push({ type: 'agent', text: `🕳️ Extruding cut: ø${hr * 2}mm hole` });
|
|
773
|
+
steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: hr, height: hh + 2, x: px, z: pz }, mesh: holeName, pos: [px, py, pz] });
|
|
774
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
775
|
+
sceneState.features.push(`Hole ø${hr * 2} at (${px},${pz})`);
|
|
776
|
+
steps.push({ type: 'agent', text: `✅ Hole added. ${sceneState.features.length} features total. Keep going!` });
|
|
777
|
+
return { steps, clearScene: false };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function buildFilletSteps(text, t, ex) {
|
|
781
|
+
const steps = [];
|
|
782
|
+
if (!sceneState.shape) {
|
|
783
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
784
|
+
return { steps, clearScene: false };
|
|
785
|
+
}
|
|
786
|
+
const fr = ex.filletR || parseNum(t, /(\d+(?:\.\d+)?)/) || 3;
|
|
787
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
788
|
+
steps.push({ type: 'divider' });
|
|
789
|
+
steps.push({ type: 'agent', text: `✨ Fillet edges r=${fr}mm` });
|
|
790
|
+
steps.push({ type: 'cmd', method: 'ops.fillet', params: { target: 'main', radius: fr }, mesh: 'fillet' });
|
|
791
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
792
|
+
sceneState.dims.filletR = fr;
|
|
793
|
+
sceneState.features.push(`Fillet r=${fr}`);
|
|
794
|
+
steps.push({ type: 'agent', text: `✅ Filleted. ${sceneState.features.length} features. Keep going!` });
|
|
795
|
+
return { steps, clearScene: false };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function buildExportSteps(text, t) {
|
|
799
|
+
const steps = [];
|
|
800
|
+
if (!sceneState.shape) {
|
|
801
|
+
steps.push({ type: 'agent', text: '⚠️ No part to export. Build something first.' });
|
|
802
|
+
return { steps, clearScene: false };
|
|
803
|
+
}
|
|
804
|
+
const fmt = /obj/.test(t) ? 'obj' : (/gltf|glb/.test(t) ? 'gltf' : 'stl');
|
|
805
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
806
|
+
steps.push({ type: 'divider' });
|
|
807
|
+
steps.push({ type: 'agent', text: `📤 Exporting as ${fmt.toUpperCase()}` });
|
|
808
|
+
steps.push({ type: 'cmd', method: `export.${fmt}`, params: { filename: `${sceneState.shape}.${fmt}`, binary: true } });
|
|
809
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
810
|
+
steps.push({ type: 'agent', text: `✅ Exported ${sceneState.shape}.${fmt}` });
|
|
811
|
+
return { steps, clearScene: false };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function buildValidateSteps(text) {
|
|
815
|
+
const steps = [];
|
|
816
|
+
if (!sceneState.shape) {
|
|
817
|
+
steps.push({ type: 'agent', text: '⚠️ No part to validate. Build something first.' });
|
|
818
|
+
return { steps, clearScene: false };
|
|
819
|
+
}
|
|
820
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
821
|
+
steps.push({ type: 'divider' });
|
|
822
|
+
steps.push({ type: 'agent', text: '🔍 Validating for manufacturing' });
|
|
823
|
+
steps.push({ type: 'cmd', method: 'validate.dimensions', params: { target: 'main' }, stat: 'size' });
|
|
824
|
+
steps.push({ type: 'cmd', method: 'validate.printability', params: { target: 'main', process: 'CNC' }, stat: 'printable' });
|
|
825
|
+
steps.push({ type: 'cmd', method: 'validate.cost', params: { target: 'main', process: 'CNC', material: sceneState.material }, stat: 'cost' });
|
|
826
|
+
steps.push({ type: 'delay', ms: 300 });
|
|
827
|
+
steps.push({ type: 'agent', text: '✅ Validation complete.' });
|
|
828
|
+
return { steps, clearScene: false };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function buildMaterialSteps(text, t) {
|
|
832
|
+
const steps = [];
|
|
833
|
+
const newMat = parseMaterial(t) || 'aluminum';
|
|
834
|
+
sceneState.material = newMat;
|
|
835
|
+
sceneState.matClr = materialColor(newMat);
|
|
836
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
837
|
+
steps.push({ type: 'cmd', method: 'ops.material', params: { material: newMat }, mesh: 'recolor' });
|
|
838
|
+
steps.push({ type: 'agent', text: `✅ Material changed to ${newMat}. Keep going!` });
|
|
839
|
+
return { steps, clearScene: false };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function buildBossSteps(text, t, ex) {
|
|
843
|
+
const steps = [];
|
|
844
|
+
if (!sceneState.shape) {
|
|
845
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet.' });
|
|
846
|
+
return { steps, clearScene: false };
|
|
847
|
+
}
|
|
848
|
+
const br = ex.diameter ? ex.diameter / 2 : (ex.radius || 5);
|
|
849
|
+
const bh = ex.height || 15;
|
|
850
|
+
const px = ex.posX || 0;
|
|
851
|
+
const pz = ex.posZ || 0;
|
|
852
|
+
const py = (sceneState.dims.h || 5) + bh / 2;
|
|
853
|
+
sceneState.holeIdx++;
|
|
854
|
+
const bossName = `boss${sceneState.holeIdx}`;
|
|
855
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
856
|
+
steps.push({ type: 'divider' });
|
|
857
|
+
steps.push({ type: 'agent', text: `📌 Adding boss ø${br * 2} × ${bh}mm` });
|
|
858
|
+
steps.push({ type: 'cmd', method: 'ops.boss', params: { radius: br, height: bh, x: px, z: pz }, mesh: bossName, pos: [px, py, pz] });
|
|
859
|
+
sceneState.features.push(`Boss ø${br * 2} at (${px},${pz})`);
|
|
860
|
+
steps.push({ type: 'agent', text: `✅ Boss added. Keep going!` });
|
|
861
|
+
return { steps, clearScene: false };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function buildShellSteps(text, t, ex) {
|
|
865
|
+
const steps = [];
|
|
866
|
+
if (!sceneState.shape) {
|
|
867
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
868
|
+
return { steps, clearScene: false };
|
|
869
|
+
}
|
|
870
|
+
const thick = ex.shellThick || ex.thick || 2;
|
|
871
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
872
|
+
steps.push({ type: 'divider' });
|
|
873
|
+
steps.push({ type: 'agent', text: `🥚 Shell — hollow out with ${thick}mm wall thickness` });
|
|
874
|
+
steps.push({ type: 'cmd', method: 'ops.shell', params: { target: 'main', thickness: thick }, mesh: 'shell' });
|
|
875
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
876
|
+
sceneState.dims.shellThick = thick;
|
|
877
|
+
sceneState.features.push(`Shell t=${thick}`);
|
|
878
|
+
steps.push({ type: 'agent', text: `✅ Shelled. ${sceneState.features.length} features. Keep going!` });
|
|
879
|
+
return { steps, clearScene: false };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function buildPatternSteps(text, t, ex) {
|
|
883
|
+
const steps = [];
|
|
884
|
+
if (!sceneState.shape) {
|
|
885
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
886
|
+
return { steps, clearScene: false };
|
|
887
|
+
}
|
|
888
|
+
const copies = ex.copies || 6;
|
|
889
|
+
const pType = ex.patternType || 'circular';
|
|
890
|
+
const spacing = ex.spacing || 30;
|
|
891
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
892
|
+
steps.push({ type: 'divider' });
|
|
893
|
+
steps.push({ type: 'agent', text: `🔄 ${pType.charAt(0).toUpperCase() + pType.slice(1)} pattern — ${copies} copies` });
|
|
894
|
+
steps.push({ type: 'cmd', method: 'ops.pattern', params: { target: 'main', type: pType, copies, spacing }, mesh: 'pattern' });
|
|
895
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
896
|
+
sceneState.dims.patternCopies = copies;
|
|
897
|
+
sceneState.dims.patternType = pType;
|
|
898
|
+
sceneState.features.push(`${pType} pattern ×${copies}`);
|
|
899
|
+
steps.push({ type: 'agent', text: `✅ Pattern applied. ${sceneState.features.length} features. Keep going!` });
|
|
900
|
+
return { steps, clearScene: false };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function buildCounterboreSteps(text, t, ex) {
|
|
904
|
+
const steps = [];
|
|
905
|
+
if (!sceneState.shape) {
|
|
906
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
907
|
+
return { steps, clearScene: false };
|
|
908
|
+
}
|
|
909
|
+
const boreR = ex.diameter ? ex.diameter / 2 : (ex.radius || 6);
|
|
910
|
+
const headR = boreR * 1.6;
|
|
911
|
+
const headDepth = ex.thick || 4;
|
|
912
|
+
const px = ex.posX || 0, pz = ex.posZ || 0;
|
|
913
|
+
const py = (sceneState.dims.h || 10) / 2;
|
|
914
|
+
sceneState.holeIdx++;
|
|
915
|
+
const name = `cbore${sceneState.holeIdx}`;
|
|
916
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
917
|
+
steps.push({ type: 'divider' });
|
|
918
|
+
steps.push({ type: 'agent', text: `🔩 Counterbore: ø${boreR * 2}mm through + ø${headR * 2}mm × ${headDepth}mm pocket` });
|
|
919
|
+
steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: boreR, height: (sceneState.dims.h || 10) + 2, x: px, z: pz }, mesh: `hole${sceneState.holeIdx}`, pos: [px, py, pz] });
|
|
920
|
+
steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: headR, height: headDepth, x: px, z: pz, counterbore: true }, mesh: name, pos: [px, (sceneState.dims.h || 10) - headDepth / 2, pz] });
|
|
921
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
922
|
+
sceneState.features.push(`Counterbore ø${boreR * 2} at (${px},${pz})`);
|
|
923
|
+
steps.push({ type: 'agent', text: `✅ Counterbore added. ${sceneState.features.length} features. Keep going!` });
|
|
924
|
+
return { steps, clearScene: false };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function buildThreadSteps(text, t, ex) {
|
|
928
|
+
const steps = [];
|
|
929
|
+
if (!sceneState.shape) {
|
|
930
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
931
|
+
return { steps, clearScene: false };
|
|
932
|
+
}
|
|
933
|
+
const pitch = ex.threadPitch || 1.5;
|
|
934
|
+
const threadR = ex.diameter ? ex.diameter / 2 : (ex.radius || (sceneState.dims.r || 10));
|
|
935
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
936
|
+
steps.push({ type: 'divider' });
|
|
937
|
+
steps.push({ type: 'agent', text: `🔩 Thread: ø${threadR * 2}mm, pitch ${pitch}mm` });
|
|
938
|
+
steps.push({ type: 'cmd', method: 'ops.thread', params: { target: 'main', radius: threadR, pitch, external: true }, mesh: 'thread' });
|
|
939
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
940
|
+
sceneState.dims.threadPitch = pitch;
|
|
941
|
+
sceneState.features.push(`Thread p=${pitch} ø${threadR * 2}`);
|
|
942
|
+
steps.push({ type: 'agent', text: `✅ Thread applied. ${sceneState.features.length} features. Keep going!` });
|
|
943
|
+
return { steps, clearScene: false };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function buildMirrorSteps(text, t, ex) {
|
|
947
|
+
const steps = [];
|
|
948
|
+
if (!sceneState.shape) {
|
|
949
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
950
|
+
return { steps, clearScene: false };
|
|
951
|
+
}
|
|
952
|
+
const axis = /[yz]/i.test(t) ? (t.match(/([yz])/i)[1].toUpperCase()) : 'X';
|
|
953
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
954
|
+
steps.push({ type: 'divider' });
|
|
955
|
+
steps.push({ type: 'agent', text: `🪞 Mirror across ${axis} axis` });
|
|
956
|
+
steps.push({ type: 'cmd', method: 'ops.mirror', params: { target: 'main', axis }, mesh: 'mirror' });
|
|
957
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
958
|
+
sceneState.features.push(`Mirror ${axis}`);
|
|
959
|
+
steps.push({ type: 'agent', text: `✅ Mirrored. ${sceneState.features.length} features. Keep going!` });
|
|
960
|
+
return { steps, clearScene: false };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// ======= EXECUTE =======
|
|
964
|
+
async function executeVoiceCommand() {
|
|
965
|
+
const input = document.getElementById('voice-input');
|
|
966
|
+
const text = input.value.trim();
|
|
967
|
+
if (!text || running) return;
|
|
968
|
+
stopVoice();
|
|
969
|
+
input.value = '';
|
|
970
|
+
|
|
971
|
+
const t = text.toLowerCase();
|
|
972
|
+
const intent = detectIntent(t);
|
|
973
|
+
const ex = extractDims(t);
|
|
974
|
+
|
|
975
|
+
console.log('[cycleCAD NLP]', { intent, text, extracted: ex, sceneState: sceneState.shape });
|
|
976
|
+
|
|
977
|
+
// Handle reset
|
|
978
|
+
if (intent === 'reset') {
|
|
979
|
+
resetDemo();
|
|
980
|
+
addLine('🔄 Scene cleared. Start fresh!', 'agent');
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let result;
|
|
985
|
+
if (intent === 'create') result = buildCreateSteps(text, t, ex);
|
|
986
|
+
else if (intent === 'hole') result = buildHoleSteps(text, t, ex);
|
|
987
|
+
else if (intent === 'fillet') result = buildFilletSteps(text, t, ex);
|
|
988
|
+
else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex);
|
|
989
|
+
else if (intent === 'export') result = buildExportSteps(text, t);
|
|
990
|
+
else if (intent === 'validate') result = buildValidateSteps(text);
|
|
991
|
+
else if (intent === 'material') result = buildMaterialSteps(text, t);
|
|
992
|
+
else if (intent === 'boss') result = buildBossSteps(text, t, ex);
|
|
993
|
+
else if (intent === 'shell') result = buildShellSteps(text, t, ex);
|
|
994
|
+
else if (intent === 'pattern') result = buildPatternSteps(text, t, ex);
|
|
995
|
+
else if (intent === 'counterbore') result = buildCounterboreSteps(text, t, ex);
|
|
996
|
+
else if (intent === 'thread') result = buildThreadSteps(text, t, ex);
|
|
997
|
+
else if (intent === 'mirror') result = buildMirrorSteps(text, t, ex);
|
|
998
|
+
else result = buildCreateSteps(text, t, ex);
|
|
999
|
+
|
|
1000
|
+
const voiceSteps = result.steps;
|
|
1001
|
+
if (result.clearScene) {
|
|
1002
|
+
window.clearScene();
|
|
1003
|
+
document.getElementById('stats-overlay').style.display = 'none';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Don't clear terminal — append (iterative)
|
|
1007
|
+
addDivider();
|
|
1008
|
+
running = true;
|
|
1009
|
+
if (!startTime) startTime = Date.now();
|
|
1010
|
+
|
|
1011
|
+
const totalSteps = voiceSteps.filter(s => s.type === 'cmd').length;
|
|
1012
|
+
let localCmdIdx = 0;
|
|
1013
|
+
|
|
1014
|
+
for (const step of voiceSteps) {
|
|
1015
|
+
if (!running) break;
|
|
1016
|
+
if (step.type === 'agent') {
|
|
1017
|
+
addLine(step.text.replace('{TIME}', ((Date.now() - startTime) / 1000).toFixed(1)).replace('{CMDS}', cmdCount), 'agent');
|
|
1018
|
+
await delay(150);
|
|
1019
|
+
} else if (step.type === 'comment') {
|
|
1020
|
+
addLine(step.text, 'comment'); await delay(80);
|
|
1021
|
+
} else if (step.type === 'divider') {
|
|
1022
|
+
addDivider();
|
|
1023
|
+
} else if (step.type === 'delay') {
|
|
1024
|
+
await delay(step.ms);
|
|
1025
|
+
} else if (step.type === 'cmd') {
|
|
1026
|
+
cmdCount++; localCmdIdx++;
|
|
1027
|
+
addLine(`cycleCAD.execute({ method: "${step.method}", params: ${JSON.stringify(step.params)} })`, 'cmd');
|
|
1028
|
+
await delay(120);
|
|
1029
|
+
const res = simulateResult(step);
|
|
1030
|
+
addLine(`→ ${JSON.stringify(res, null, 0)}`, 'res ok');
|
|
1031
|
+
// Update stats
|
|
1032
|
+
if (step.stat === 'size') updateStats({ size: sceneState.dims.sizeLabel || 'computed' });
|
|
1033
|
+
if (step.stat === 'printable') updateStats({ printable: true });
|
|
1034
|
+
if (step.stat === 'cost') updateStats({ cost: `$${(costMap[sceneState.material] || 12.40).toFixed(2)} (CNC)` });
|
|
1035
|
+
updateStats({ material: sceneState.material.charAt(0).toUpperCase() + sceneState.material.slice(1) });
|
|
1036
|
+
document.getElementById('stat-cmds').textContent = cmdCount;
|
|
1037
|
+
document.getElementById('stat-time').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
1038
|
+
document.getElementById('stats-overlay').style.display = 'block';
|
|
1039
|
+
// Build 3D
|
|
1040
|
+
if (step.mesh) buildMesh(step);
|
|
1041
|
+
await delay(180);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
running = false;
|
|
1045
|
+
updateFeatureBadge();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function simulateResult(step) {
|
|
1049
|
+
const m = step.method;
|
|
1050
|
+
if (m === 'sketch.start') return { ok: true, plane: 'XY' };
|
|
1051
|
+
if (m === 'sketch.rect') return { ok: true, id: 'rect_1' };
|
|
1052
|
+
if (m === 'sketch.circle') return { ok: true, id: 'circle_1', radius: step.params.radius };
|
|
1053
|
+
if (m === 'ops.extrude') return { ok: true, id: 'extrude_1', height: step.params.height };
|
|
1054
|
+
if (m === 'ops.primitive') return { ok: true, id: step.mesh, shape: step.params.shape };
|
|
1055
|
+
if (m === 'ops.cut') return { ok: true, id: step.mesh, type: 'hole' };
|
|
1056
|
+
if (m === 'ops.fillet') return { ok: true, radius: step.params.radius, applied: true };
|
|
1057
|
+
if (m === 'ops.material') return { ok: true, material: step.params.material };
|
|
1058
|
+
if (m === 'ops.boss') return { ok: true, id: step.mesh };
|
|
1059
|
+
if (m === 'ops.shell') return { ok: true, thickness: step.params.thickness, hollowed: true };
|
|
1060
|
+
if (m === 'ops.pattern') return { ok: true, type: step.params.type, copies: step.params.copies };
|
|
1061
|
+
if (m === 'ops.thread') return { ok: true, pitch: step.params.pitch, external: true };
|
|
1062
|
+
if (m === 'ops.mirror') return { ok: true, axis: step.params.axis, mirrored: true };
|
|
1063
|
+
if (m === 'validate.dimensions') return { ok: true, size: sceneState.dims.sizeLabel };
|
|
1064
|
+
if (m === 'validate.printability') return { ok: true, printable: true, issues: [] };
|
|
1065
|
+
if (m === 'validate.cost') return { ok: true, unitCost: costMap[sceneState.material] || 12.40 };
|
|
1066
|
+
if (m.startsWith('export.')) return { ok: true, filename: step.params.filename };
|
|
1067
|
+
return { ok: true };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ======= 3D MESH BUILDER =======
|
|
1071
|
+
function buildMesh(step) {
|
|
1072
|
+
const THREE = window.THREE;
|
|
1073
|
+
const sc = sceneState;
|
|
1074
|
+
const d = sc.dims;
|
|
1075
|
+
const matOpts = { color: sc.matClr, metalness: 0.7, roughness: 0.3 };
|
|
1076
|
+
const darkMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
|
|
1077
|
+
|
|
1078
|
+
if (step.mesh === 'main' || step.mesh === 'main_ext') {
|
|
1079
|
+
const scene = window._scene;
|
|
1080
|
+
const old = scene.getObjectByName('main');
|
|
1081
|
+
if (old && step.mesh !== 'main_ext') { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
|
|
1082
|
+
|
|
1083
|
+
let geo, mesh;
|
|
1084
|
+
const s = sc.shape;
|
|
1085
|
+
if (s === 'cylinder' || s === 'disk') {
|
|
1086
|
+
geo = new THREE.CylinderGeometry(d.r, d.r, d.h, 48);
|
|
1087
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
|
|
1088
|
+
mesh.position.y = d.h / 2;
|
|
1089
|
+
} else if (s === 'tube' || s === 'ring' || s === 'washer' || s === 'flange') {
|
|
1090
|
+
const profile = [
|
|
1091
|
+
new THREE.Vector2(d.innerR || d.outerR * 0.7, 0),
|
|
1092
|
+
new THREE.Vector2(d.outerR || 25, 0),
|
|
1093
|
+
new THREE.Vector2(d.outerR || 25, d.h),
|
|
1094
|
+
new THREE.Vector2(d.innerR || d.outerR * 0.7, d.h),
|
|
1095
|
+
];
|
|
1096
|
+
geo = new THREE.LatheGeometry(profile, 48);
|
|
1097
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
|
|
1098
|
+
} else if (s === 'sphere') {
|
|
1099
|
+
geo = new THREE.SphereGeometry(d.r, 48, 32);
|
|
1100
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
|
|
1101
|
+
mesh.position.y = d.r;
|
|
1102
|
+
} else if (s === 'cone') {
|
|
1103
|
+
geo = new THREE.CylinderGeometry(d.topR, d.baseR, d.h, 48);
|
|
1104
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
|
|
1105
|
+
mesh.position.y = d.h / 2;
|
|
1106
|
+
} else if (s === 'gear') {
|
|
1107
|
+
const gshape = new THREE.Shape();
|
|
1108
|
+
const nt = d.teeth, pitchR = d.r, add = d.module, ded = d.module * 1.25;
|
|
1109
|
+
const ouR = pitchR + add, roR = pitchR - ded;
|
|
1110
|
+
for (let i = 0; i < nt; i++) {
|
|
1111
|
+
const a0 = (i / nt) * Math.PI * 2, a1 = ((i + 0.15) / nt) * Math.PI * 2;
|
|
1112
|
+
const a2 = ((i + 0.35) / nt) * Math.PI * 2, a3 = ((i + 0.5) / nt) * Math.PI * 2;
|
|
1113
|
+
if (i === 0) gshape.moveTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
|
|
1114
|
+
else gshape.lineTo(Math.cos(a0) * roR, Math.sin(a0) * roR);
|
|
1115
|
+
gshape.lineTo(Math.cos(a1) * ouR, Math.sin(a1) * ouR);
|
|
1116
|
+
gshape.lineTo(Math.cos(a2) * ouR, Math.sin(a2) * ouR);
|
|
1117
|
+
gshape.lineTo(Math.cos(a3) * roR, Math.sin(a3) * roR);
|
|
1118
|
+
}
|
|
1119
|
+
gshape.closePath();
|
|
1120
|
+
geo = new THREE.ExtrudeGeometry(gshape, { depth: d.h, bevelEnabled: false });
|
|
1121
|
+
geo.rotateX(-Math.PI / 2);
|
|
1122
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
|
|
1123
|
+
mesh.position.y = d.h;
|
|
1124
|
+
} else if (s === 'hexbolt') {
|
|
1125
|
+
const group = new THREE.Group();
|
|
1126
|
+
const headGeo = new THREE.CylinderGeometry(d.headR, d.headR, d.headH, 6);
|
|
1127
|
+
const hm = new THREE.Mesh(headGeo, new THREE.MeshStandardMaterial(matOpts));
|
|
1128
|
+
hm.position.y = d.shankH + d.headH / 2; group.add(hm);
|
|
1129
|
+
const sg = new THREE.CylinderGeometry(d.shankR, d.shankR, d.shankH, 24);
|
|
1130
|
+
const sm = new THREE.Mesh(sg, new THREE.MeshStandardMaterial(matOpts));
|
|
1131
|
+
sm.position.y = d.shankH / 2; group.add(sm);
|
|
1132
|
+
group.name = 'main';
|
|
1133
|
+
window.addMeshToScene(group); return;
|
|
1134
|
+
} else {
|
|
1135
|
+
geo = new THREE.BoxGeometry(d.w || 80, d.h || 5, d.d || 40);
|
|
1136
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial(matOpts));
|
|
1137
|
+
mesh.position.y = (d.h || 5) / 2;
|
|
1138
|
+
}
|
|
1139
|
+
if (mesh) { mesh.name = 'main'; window.addMeshToScene(mesh); }
|
|
1140
|
+
}
|
|
1141
|
+
else if (step.mesh === 'fillet' && (sc.shape === 'bracket' || sc.shape === 'plate' || sc.shape === 'box')) {
|
|
1142
|
+
const scene = window._scene;
|
|
1143
|
+
const old = scene.getObjectByName('main');
|
|
1144
|
+
if (old) { scene.remove(old); old.geometry?.dispose(); old.material?.dispose(); }
|
|
1145
|
+
const fr = Math.min(d.filletR || 3, (d.w || 80) / 2, (d.d || 40) / 2);
|
|
1146
|
+
const hw = (d.w || 80) / 2, hd = (d.d || 40) / 2, h = d.h || 5;
|
|
1147
|
+
const shape = new THREE.Shape();
|
|
1148
|
+
shape.moveTo(-hw + fr, -hd); shape.lineTo(hw - fr, -hd);
|
|
1149
|
+
shape.quadraticCurveTo(hw, -hd, hw, -hd + fr); shape.lineTo(hw, hd - fr);
|
|
1150
|
+
shape.quadraticCurveTo(hw, hd, hw - fr, hd); shape.lineTo(-hw + fr, hd);
|
|
1151
|
+
shape.quadraticCurveTo(-hw, hd, -hw, hd - fr); shape.lineTo(-hw, -hd + fr);
|
|
1152
|
+
shape.quadraticCurveTo(-hw, -hd, -hw + fr, -hd);
|
|
1153
|
+
const geo = new THREE.ExtrudeGeometry(shape, { depth: h, bevelEnabled: false, curveSegments: 16 });
|
|
1154
|
+
geo.rotateX(-Math.PI / 2);
|
|
1155
|
+
const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }));
|
|
1156
|
+
mesh.position.y = h; mesh.name = 'main';
|
|
1157
|
+
window.addMeshToScene(mesh);
|
|
1158
|
+
}
|
|
1159
|
+
else if (step.mesh && (step.mesh.startsWith('hole') || step.mesh.startsWith('boss'))) {
|
|
1160
|
+
const isBoss = step.mesh.startsWith('boss');
|
|
1161
|
+
const r = step.params?.radius || 5;
|
|
1162
|
+
const h = step.params?.height || (sc.dims.h || 10) + 2;
|
|
1163
|
+
const geo = new THREE.CylinderGeometry(r, r, h, 24);
|
|
1164
|
+
const mat = isBoss ? new THREE.MeshStandardMaterial({ color: sc.matClr, metalness: 0.7, roughness: 0.3 }) : darkMat;
|
|
1165
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
1166
|
+
if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
|
|
1167
|
+
window.addMeshToScene(mesh);
|
|
1168
|
+
}
|
|
1169
|
+
else if (step.mesh === 'shell') {
|
|
1170
|
+
// Visual shell: make existing part semi-transparent + add inner void
|
|
1171
|
+
const scene = window._scene;
|
|
1172
|
+
const main = scene.getObjectByName('main');
|
|
1173
|
+
if (main && main.isMesh) {
|
|
1174
|
+
main.material.transparent = true;
|
|
1175
|
+
main.material.opacity = 0.35;
|
|
1176
|
+
main.material.depthWrite = false;
|
|
1177
|
+
// Add inner representation (slightly smaller dark shape)
|
|
1178
|
+
const thick = sc.dims.shellThick || 2;
|
|
1179
|
+
let innerGeo;
|
|
1180
|
+
const s = sc.shape;
|
|
1181
|
+
if (s === 'cylinder' || s === 'disk') {
|
|
1182
|
+
innerGeo = new THREE.CylinderGeometry(d.r - thick, d.r - thick, d.h - thick * 2, 48);
|
|
1183
|
+
} else if (s === 'sphere') {
|
|
1184
|
+
innerGeo = new THREE.SphereGeometry(d.r - thick, 48, 32);
|
|
1185
|
+
} else {
|
|
1186
|
+
innerGeo = new THREE.BoxGeometry((d.w || 80) - thick * 2, (d.h || 5) - thick, (d.d || 40) - thick * 2);
|
|
1187
|
+
}
|
|
1188
|
+
const innerMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
|
|
1189
|
+
const inner = new THREE.Mesh(innerGeo, innerMat);
|
|
1190
|
+
inner.position.copy(main.position);
|
|
1191
|
+
inner.name = 'shell_inner';
|
|
1192
|
+
window.addMeshToScene(inner);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
else if (step.mesh === 'pattern') {
|
|
1196
|
+
// Clone the main part in a circular or linear pattern
|
|
1197
|
+
const scene = window._scene;
|
|
1198
|
+
const main = scene.getObjectByName('main');
|
|
1199
|
+
if (main) {
|
|
1200
|
+
const copies = sc.dims.patternCopies || 6;
|
|
1201
|
+
const pType = sc.dims.patternType || 'circular';
|
|
1202
|
+
const spacing = step.params?.spacing || 30;
|
|
1203
|
+
for (let i = 1; i < copies; i++) {
|
|
1204
|
+
const clone = main.clone();
|
|
1205
|
+
clone.material = main.material.clone();
|
|
1206
|
+
clone.name = `pattern_${i}`;
|
|
1207
|
+
if (pType === 'circular') {
|
|
1208
|
+
const angle = (i / copies) * Math.PI * 2;
|
|
1209
|
+
const rad = Math.max(d.r || 30, (d.w || 80) / 2) * 2.5;
|
|
1210
|
+
clone.position.x = Math.cos(angle) * rad;
|
|
1211
|
+
clone.position.z = Math.sin(angle) * rad;
|
|
1212
|
+
clone.position.y = main.position.y;
|
|
1213
|
+
} else {
|
|
1214
|
+
clone.position.x = main.position.x + i * spacing;
|
|
1215
|
+
clone.position.y = main.position.y;
|
|
1216
|
+
clone.position.z = main.position.z;
|
|
1217
|
+
}
|
|
1218
|
+
window.addMeshToScene(clone);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
else if (step.mesh === 'thread') {
|
|
1223
|
+
// Visual thread: add a helical line wrapping the part
|
|
1224
|
+
const scene = window._scene;
|
|
1225
|
+
const main = scene.getObjectByName('main');
|
|
1226
|
+
const threadR = d.r || (d.outerR || 15);
|
|
1227
|
+
const threadH = d.h || 40;
|
|
1228
|
+
const pitch = sc.dims.threadPitch || 1.5;
|
|
1229
|
+
const turns = threadH / pitch;
|
|
1230
|
+
const pts = [];
|
|
1231
|
+
for (let i = 0; i <= turns * 36; i++) {
|
|
1232
|
+
const angle = (i / 36) * Math.PI * 2;
|
|
1233
|
+
const y = (i / (turns * 36)) * threadH;
|
|
1234
|
+
pts.push(new THREE.Vector3(Math.cos(angle) * (threadR + 0.5), y, Math.sin(angle) * (threadR + 0.5)));
|
|
1235
|
+
}
|
|
1236
|
+
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
|
1237
|
+
const mat = new THREE.LineBasicMaterial({ color: 0xD4A843, linewidth: 1 });
|
|
1238
|
+
const line = new THREE.Line(geo, mat);
|
|
1239
|
+
line.name = 'thread_visual';
|
|
1240
|
+
window.addMeshToScene(line);
|
|
1241
|
+
}
|
|
1242
|
+
else if (step.mesh === 'mirror') {
|
|
1243
|
+
// Clone the entire scene content and flip across the axis
|
|
1244
|
+
const scene = window._scene;
|
|
1245
|
+
const toClone = [];
|
|
1246
|
+
scene.traverse(c => { if (c.isMesh && c.name !== 'grid') toClone.push(c); });
|
|
1247
|
+
const axis = step.params?.axis || 'X';
|
|
1248
|
+
toClone.forEach(m => {
|
|
1249
|
+
const clone = m.clone();
|
|
1250
|
+
clone.material = m.material.clone();
|
|
1251
|
+
clone.name = `mirror_${m.name}`;
|
|
1252
|
+
if (axis === 'X') clone.position.x = -clone.position.x;
|
|
1253
|
+
else if (axis === 'Y') clone.position.y = -clone.position.y;
|
|
1254
|
+
else clone.position.z = -clone.position.z;
|
|
1255
|
+
window.addMeshToScene(clone);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
else if (step.mesh && step.mesh.startsWith('cbore')) {
|
|
1259
|
+
// Counterbore pocket — wider shallow hole
|
|
1260
|
+
const r = step.params?.radius || 10;
|
|
1261
|
+
const h = step.params?.height || 4;
|
|
1262
|
+
const geo = new THREE.CylinderGeometry(r, r, h, 24);
|
|
1263
|
+
const mesh = new THREE.Mesh(geo, darkMat);
|
|
1264
|
+
if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
|
|
1265
|
+
window.addMeshToScene(mesh);
|
|
1266
|
+
}
|
|
1267
|
+
else if (step.mesh === 'recolor') {
|
|
1268
|
+
// Change material color on existing part
|
|
1269
|
+
const scene = window._scene;
|
|
1270
|
+
scene.traverse(c => {
|
|
1271
|
+
if (c.isMesh && c.material && c.material.color) {
|
|
1272
|
+
c.material.color.setHex(sc.matClr);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
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>
|
|
1311
|
+
</body>
|
|
1312
|
+
</html>
|