cyclecad 0.1.8 → 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.
Files changed (2) hide show
  1. package/app/agent-demo.html +268 -5
  2. package/package.json +1 -1
@@ -98,14 +98,42 @@
98
98
  </div>
99
99
  </div>
100
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
+
101
123
  <!-- 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;">
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;">
103
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>
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>
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>
106
128
  <div id="voice-status" style="position:absolute;top:-22px;left:16px;font-size:11px;color:var(--dim);"></div>
107
129
  </div>
108
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
+
109
137
  <div class="controls">
110
138
  <button class="btn-run" id="btn-run" onclick="runDemo()">▶ Run Agent Demo</button>
111
139
  <button class="btn-reset" onclick="resetDemo()">↺ Reset</button>
@@ -571,6 +599,11 @@
571
599
  if (/chamfer/.test(t)) return 'chamfer';
572
600
  if (/add\s*(a\s*)?(slot|groove|channel)/.test(t)) return 'slot';
573
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';
574
607
  // If a known shape word is present → create new
575
608
  if (detectShape(t)) return 'create';
576
609
  // Default: try to create
@@ -598,6 +631,11 @@
598
631
  holeOffset: parseNum(t, /(\d+)\s*mm?\s*from\s*(?:the\s*)?edge/) || 10,
599
632
  posX: parseNum(t, /(?:at|x)\s*(-?\d+(?:\.\d+)?)/) || 0,
600
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,
601
639
  };
602
640
  }
603
641
 
@@ -823,6 +861,105 @@
823
861
  return { steps, clearScene: false };
824
862
  }
825
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
+
826
963
  // ======= EXECUTE =======
827
964
  async function executeVoiceCommand() {
828
965
  const input = document.getElementById('voice-input');
@@ -848,12 +985,17 @@
848
985
  if (intent === 'create') result = buildCreateSteps(text, t, ex);
849
986
  else if (intent === 'hole') result = buildHoleSteps(text, t, ex);
850
987
  else if (intent === 'fillet') result = buildFilletSteps(text, t, ex);
851
- else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex); // reuse fillet visual
988
+ else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex);
852
989
  else if (intent === 'export') result = buildExportSteps(text, t);
853
990
  else if (intent === 'validate') result = buildValidateSteps(text);
854
991
  else if (intent === 'material') result = buildMaterialSteps(text, t);
855
992
  else if (intent === 'boss') result = buildBossSteps(text, t, ex);
856
- else result = buildCreateSteps(text, t, ex); // fallback
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);
857
999
 
858
1000
  const voiceSteps = result.steps;
859
1001
  if (result.clearScene) {
@@ -900,6 +1042,7 @@
900
1042
  }
901
1043
  }
902
1044
  running = false;
1045
+ updateFeatureBadge();
903
1046
  }
904
1047
 
905
1048
  function simulateResult(step) {
@@ -913,6 +1056,10 @@
913
1056
  if (m === 'ops.fillet') return { ok: true, radius: step.params.radius, applied: true };
914
1057
  if (m === 'ops.material') return { ok: true, material: step.params.material };
915
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 };
916
1063
  if (m === 'validate.dimensions') return { ok: true, size: sceneState.dims.sizeLabel };
917
1064
  if (m === 'validate.printability') return { ok: true, printable: true, issues: [] };
918
1065
  if (m === 'validate.cost') return { ok: true, unitCost: costMap[sceneState.material] || 12.40 };
@@ -1019,6 +1166,104 @@
1019
1166
  if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
1020
1167
  window.addMeshToScene(mesh);
1021
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
+ }
1022
1267
  else if (step.mesh === 'recolor') {
1023
1268
  // Change material color on existing part
1024
1269
  const scene = window._scene;
@@ -1035,6 +1280,24 @@
1035
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); } }`;
1036
1281
  document.head.appendChild(styleEl);
1037
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;
1038
1301
  window.toggleVoice = toggleVoice;
1039
1302
  window.executeVoiceCommand = executeVoiceCommand;
1040
1303
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyclecad",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Browser-based parametric 3D CAD modeler with AI-powered tools, native Inventor file parsing, and smart assembly management. No install required.",
5
5
  "main": "index.html",
6
6
  "scripts": {