cyclecad 3.10.2 → 3.10.4

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 CHANGED
@@ -1119,3 +1119,88 @@ import { startSketch, endSketch, setTool, getEntities, clearSketch, entitiesToGe
1119
1119
 
1120
1120
  # currentDate
1121
1121
  Today's date is 2026-04-01.
1122
+
1123
+ ## Session 2026-04-23 — AI Copilot Template Library + Real CSG
1124
+
1125
+ ### What shipped
1126
+ - **cyclecad@3.10.2** (live on npm): AI Copilot v1.1 with template library, real CSG booleans, draggable dialogs, click-pin menus
1127
+ - **cyclecad@3.10.3** (pending/retry): template expansion with 5 more shapes (washer, flange, threaded rod, mounting plate, box)
1128
+
1129
+ ### Key files
1130
+ - `app/js/modules/ai-copilot.js` (~900 lines) — multi-step CAD copilot. IIFE that sets `window.CycleCAD.AICopilot`. Key internals: `matchTemplate()`, `miniExecute()` (async), `subtractFromBody()` (CSG), `loadCSG()` (lazy import vendored lib), `run()` (orchestrator), `buildUI()`
1131
+ - `app/js/vendor/three-bvh-csg.js` (3892 lines) — vendored three-bvh-csg@0.0.17 with bare imports rewritten: `'three'` and `'three-mesh-bvh'` → CDN URLs (`cdn.jsdelivr.net/npm/three@0.170.0/...` and `three-mesh-bvh@0.7.8`)
1132
+ - `app/index.html` — has `<script src="/app/js/modules/ai-copilot.js?v=HASH">` (cache-busted) and menu entry `data-action="tools-ai-copilot"`
1133
+
1134
+ ### Template library (8 shapes) — `matchTemplate(prompt)` in ai-copilot.js
1135
+ All templates bypass LLM and output a fixed JSON plan with correct DIN/ISO coordinates:
1136
+ | Prompt pattern | Shape | Notes |
1137
+ |---|---|---|
1138
+ | `raspberry pi \| rpi \| pi 4` + `case` | Pi case | Case body 89×14×60, 4 mounting posts at ±38.75, y=14, ±26. Optional USB/HDMI/Ethernet ops.hole cutouts |
1139
+ | `M(n) nut` | Hex nut (DIN 934) | Cylinder with correct across-flats + thickness by M-size |
1140
+ | `L-bracket` + `Nmm` + `N holes` + `Nmm centers` | L-bracket | Rect plate + N CSG holes |
1141
+ | `M(n) washer` or `DIN 125 M(n)` | Washer | Disk + center hole, DIN 125 spec |
1142
+ | `flange` + `Nmm` + `N bolt holes` + `PCD N` | Flange | Disk + bolt circle + center bore |
1143
+ | `threaded rod` + `M(n)` + `Nmm` | Threaded rod | Cylinder by M-size + length |
1144
+ | `mounting plate` + `NxN` + `N holes` | Mounting plate | Rect + N holes (4-hole = corners, else evenly spaced) |
1145
+ | `box NxNxN` | Generic box | Box extrude |
1146
+
1147
+ ### Coordinate system (for mini-executor)
1148
+ - X = left/right, Y = up, Z = front/back
1149
+ - All solids centered at origin: rect W×H → [-W/2,W/2] × [-H/2,H/2]
1150
+ - ops.extrude depth=D → Y spans [0, D]
1151
+ - Params `position:[x,y,z]` places the centerpoint of the extruded solid
1152
+
1153
+ ### Mini-executor API (handles these methods)
1154
+ - `sketch.start/rect/circle/line/end`
1155
+ - `ops.extrude {depth, position, subtract}` — subtract:true does CSG cut
1156
+ - `ops.hole {position, depth, radius OR width+height}` — CSG subtraction from body
1157
+ - `ops.revolve/fillet/chamfer/shell/pattern` (fillet/chamfer/shell are visual-approx only)
1158
+ - `view.set/fit`, `query.*`, `validate.*`
1159
+
1160
+ ### State (IIFE-local `miniState`)
1161
+ - `miniState.group` — THREE.Group with name 'AICopilotBuild' (wiped on every `run()` via `miniReset()`)
1162
+ - `miniState.body` — first solid mesh; ALL `ops.hole` subtractions target this (not lastMesh)
1163
+ - `miniState.lastMesh` — last added mesh; used by `ops.pattern` for cloning
1164
+ - `miniState.currentSketch` — `{shape, width, height, radius, origin}` consumed by next `ops.extrude`
1165
+
1166
+ ### Critical bug patterns fixed this session
1167
+ 1. **Single-quote apostrophe in SYSTEM_PROMPT** — `isn't` closed the JS string. Use `is not` or escape.
1168
+ 2. **Module not loading** — `window.CycleCAD.AICopilot` was undefined because wrapped import had syntax error. Always `node --check` the file before push.
1169
+ 3. **Base64 2-chunk delivery corrupts file** — large files clipboard-split and concatenated via bash can lose the first half. Prefer single `cat > file << 'EOF'` heredoc.
1170
+ 4. **Scene accumulates between runs** — `miniReset()` must be called at start of `run()`. This call was dropped twice in earlier patches.
1171
+ 5. **ops.hole eating posts instead of body** — `subtractFromLast` targeted whatever was most recently added. Fixed by tracking `miniState.body` separately and calling `subtractFromBody`.
1172
+ 6. **Posts landing at origin** — `ops.extrude` was ignoring `params.position` and only reading `sketch.origin`. Fixed to read params first.
1173
+ 7. **Bare imports in vendored library** — `'three-mesh-bvh'` also needed rewriting, not just `'three'`.
1174
+ 8. **Cmd+C hijacking Copy** — killer-features.js shortcuts had no Shift modifier. Changed to Cmd+Shift+C/K/P/G/T.
1175
+
1176
+ ### Click-pin menu (in index.html)
1177
+ CSS: `.menu-item.open .menu-dropdown { display: flex !important; }`. JS initMenuPin() wires click-to-toggle + click-outside-to-close. File still has the `:hover` fallback for quick hover.
1178
+
1179
+ ### Dialog drag (in index.html)
1180
+ Pointer events on `#dialog-title` drag the whole dialog container via `position: fixed; left/top` — set once on pointerdown, updated on pointermove.
1181
+
1182
+ ### UX niceties
1183
+ - Banner: `Ready: <model>` (green) or `No <provider> key — click the key icon` (red)
1184
+ - Low-credit errors auto-offer "Switch to Gemini (free)" button
1185
+ - Multi-goal prompts: detect `N` quoted strings, run first, log others to "paste one at a time"
1186
+ - Gemini 404 fallback: `gemini-2.0-flash` → `gemini-1.5-flash` → `gemini-1.5-flash-latest`
1187
+ - Cache-bust: `?v=HASH` query param on script tags, auto-bumped by MD5 of mtime
1188
+
1189
+ ### Supported models
1190
+ - Claude Sonnet 4.6 (paid, default when anthropic key present)
1191
+ - Claude Haiku 4.5 / Opus 4.6 (paid)
1192
+ - Gemini 2.0 Flash (free, 10 RPM)
1193
+ - Groq Llama 3.3 70B (free, 30 RPM — better for repeat prompts)
1194
+
1195
+ ### Pending
1196
+ - Retry `npm publish cyclecad@3.10.3` (earlier attempt interrupted; registry still shows 3.10.2)
1197
+ - Add gear/pulley/shaft templates (Task #7)
1198
+ - Verify ExplodeView redirects
1199
+ - The `block-only-in-chrome` user report — needs repro with actual prompt
1200
+
1201
+ ### Collaboration notes
1202
+ - User on Mac, tests in Chrome/Safari private
1203
+ - Git user is `sachin@sachins-MacBook-Air.fritz.box` (not configured globally; harmless warning)
1204
+ - Clipboard delivery via `write_clipboard` — user pastes in Terminal
1205
+ - Verification via Claude-in-Chrome MCP (navigate → execute → screenshot → dom inspect)
1206
+ - Terminal is tier-click, so clipboard write is BLOCKED while Terminal is frontmost — switch to Chrome first
package/app/index.html CHANGED
@@ -1538,7 +1538,7 @@ window._dismissSplash = function(action) {
1538
1538
 
1539
1539
  <!-- Killer Feature Modules -->
1540
1540
  <script src="/app/js/agent-api.js?v=a41b98a5"></script>
1541
- <script src="/app/js/modules/ai-copilot.js?v=407987b9"></script>
1541
+ <script src="/app/js/modules/ai-copilot.js?v=14e5d5d7"></script>
1542
1542
  <script src="/app/js/modules/text-to-cad.js"></script>
1543
1543
  <script src="/app/js/modules/photo-to-cad.js"></script>
1544
1544
  <script src="/app/js/modules/manufacturability.js"></script>
@@ -390,7 +390,105 @@
390
390
  {method:'view.fit', params:{}}
391
391
  ];
392
392
  }
393
- // L-bracket with holes
393
+ // DIN 125 washer (M3-M12)
394
+ const washerM = p.match(/\bm(\d+)\s*washer|washer\s+m(\d+)|din\s*125\s*m?(\d+)/);
395
+ if (washerM) {
396
+ const size = parseInt(washerM[1]||washerM[2]||washerM[3]);
397
+ const outerR = ({3:3.5, 4:4.5, 5:5.3, 6:6.4, 8:8.4, 10:10.5, 12:13})[size] || size*1.2;
398
+ const thick = ({3:0.5, 4:0.8, 5:1, 6:1.6, 8:1.6, 10:2, 12:2.5})[size] || size*0.2;
399
+ const holeR = (size + 0.4) / 2;
400
+ return [
401
+ {method:'sketch.start', params:{plane:'XY'}},
402
+ {method:'sketch.circle', params:{radius: outerR}},
403
+ {method:'ops.extrude', params:{depth: thick, position:[0,0,0]}, note:'DIN 125 M'+size+' washer'},
404
+ {method:'ops.hole', params:{position:[0, thick, 0], radius: holeR, depth: thick+2}, note:'M'+size+' hole'},
405
+ {method:'view.set', params:{view:'iso'}},
406
+ {method:'view.fit', params:{}}
407
+ ];
408
+ }
409
+ // Flange with bolt circle
410
+ const flangeM = p.match(/flange/);
411
+ if (flangeM) {
412
+ const odM = p.match(/(\d+)\s*mm/);
413
+ const od = odM ? parseInt(odM[1]) : 80;
414
+ const nM = p.match(/(\d+)\s*(?:bolt\s*)?holes?/);
415
+ const nHoles = nM ? parseInt(nM[1]) : 4;
416
+ const pcdM = p.match(/pcd\s*(\d+)|bolt\s*circle\s*(\d+)/);
417
+ const pcd = pcdM ? parseInt(pcdM[1]||pcdM[2]) : Math.round(od*0.7);
418
+ const thick = 8;
419
+ const plan = [
420
+ {method:'sketch.start', params:{plane:'XY'}},
421
+ {method:'sketch.circle', params:{radius: od/2}},
422
+ {method:'ops.extrude', params:{depth: thick, position:[0,0,0]}, note:'flange body Ø'+od},
423
+ {method:'ops.hole', params:{position:[0, thick, 0], radius: Math.max(5, od/8), depth: thick+2}, note:'center bore'}
424
+ ];
425
+ for (let i = 0; i < nHoles; i++) {
426
+ const a = (i / nHoles) * Math.PI * 2;
427
+ const x = Math.cos(a) * pcd/2, z = Math.sin(a) * pcd/2;
428
+ plan.push({method:'ops.hole', params:{position:[x, thick/2, z], radius: 3, depth: thick+2}, note:'bolt hole '+(i+1)+'/'+nHoles});
429
+ }
430
+ plan.push({method:'view.set', params:{view:'iso'}});
431
+ plan.push({method:'view.fit', params:{}});
432
+ return plan;
433
+ }
434
+ // Threaded rod / stud
435
+ const rodM = p.match(/threaded\s*rod|m(\d+)\s*rod|m(\d+)\s*stud|studding/);
436
+ if (rodM) {
437
+ const sM = p.match(/m(\d+)/);
438
+ const size = sM ? parseInt(sM[1]) : 8;
439
+ const lM = p.match(/(\d+)\s*mm/);
440
+ const len = lM ? parseInt(lM[1]) : 100;
441
+ return [
442
+ {method:'sketch.start', params:{plane:'XY'}},
443
+ {method:'sketch.circle', params:{radius: size/2}},
444
+ {method:'ops.extrude', params:{depth: len, position:[0,0,0]}, note:'M'+size+' threaded rod, '+len+'mm long'},
445
+ {method:'view.set', params:{view:'iso'}},
446
+ {method:'view.fit', params:{}}
447
+ ];
448
+ }
449
+ // Mounting plate
450
+ const plateM = p.match(/mounting\s*plate|base\s*plate|flat\s*plate/);
451
+ if (plateM) {
452
+ const dimM = p.match(/(\d+)\s*x\s*(\d+)/);
453
+ const w = dimM ? parseInt(dimM[1]) : 120;
454
+ const h = dimM ? parseInt(dimM[2]) : 80;
455
+ const thick = 6;
456
+ const nM = p.match(/(\d+)\s*holes?/);
457
+ const nHoles = nM ? parseInt(nM[1]) : 4;
458
+ const plan = [
459
+ {method:'sketch.start', params:{plane:'XY'}},
460
+ {method:'sketch.rect', params:{width: w, height: h}},
461
+ {method:'ops.extrude', params:{depth: thick, position:[0,0,0]}, note: w+'x'+h+'x'+thick+' mounting plate'}
462
+ ];
463
+ if (nHoles === 4) {
464
+ const mx = w/2 - 10, mz = h/2 - 10;
465
+ [[-mx,-mz],[mx,-mz],[-mx,mz],[mx,mz]].forEach((pp,i) => {
466
+ plan.push({method:'ops.hole', params:{position:[pp[0], thick/2, pp[1]], radius:3, depth:thick+2}, note:'corner hole '+(i+1)});
467
+ });
468
+ } else {
469
+ for (let i = 0; i < nHoles; i++) {
470
+ const x = -w/2 + 10 + (i/(nHoles-1)) * (w-20);
471
+ plan.push({method:'ops.hole', params:{position:[x, thick/2, 0], radius:3, depth:thick+2}, note:'hole '+(i+1)});
472
+ }
473
+ }
474
+ plan.push({method:'view.set', params:{view:'iso'}});
475
+ plan.push({method:'view.fit', params:{}});
476
+ return plan;
477
+ }
478
+ // Generic box NxNxN with optional fillet
479
+ const boxM = p.match(/\bbox\b|\bblock\b|\bcube\b|\bcuboid\b/);
480
+ const boxDim = p.match(/(\d+)\s*[x×]\s*(\d+)\s*[x×]\s*(\d+)/);
481
+ if (boxM && boxDim) {
482
+ const w = parseInt(boxDim[1]), h = parseInt(boxDim[2]), d = parseInt(boxDim[3]);
483
+ return [
484
+ {method:'sketch.start', params:{plane:'XY'}},
485
+ {method:'sketch.rect', params:{width: w, height: d}},
486
+ {method:'ops.extrude', params:{depth: h, position:[0,0,0]}, note: w+'x'+h+'x'+d+' box'},
487
+ {method:'view.set', params:{view:'iso'}},
488
+ {method:'view.fit', params:{}}
489
+ ];
490
+ }
491
+ // L-bracket with holes
394
492
  if (/l-?bracket|mounting\s*bracket|angle\s*bracket/.test(p)) {
395
493
  const lenM = p.match(/(\d+)\s*mm/);
396
494
  const length = lenM ? parseInt(lenM[1]) : 100;
@@ -413,6 +511,86 @@
413
511
  plan.push({method:'view.fit', params:{}});
414
512
  return plan;
415
513
  }
514
+ // Spur gear (simplified — cylinder disc at OD with center bore)
515
+ const gearM = p.match(/\bspur\s*gear\b|\bgear\b/);
516
+ if (gearM) {
517
+ const modM = p.match(/module\s*(\d+(?:\.\d+)?)|\bm\s*=?\s*(\d+(?:\.\d+)?)\b/);
518
+ const teethM = p.match(/(\d+)\s*(?:teeth|tooth)|z\s*=?\s*(\d+)/);
519
+ const mod = modM ? parseFloat(modM[1]||modM[2]) : 2;
520
+ const teeth = teethM ? parseInt(teethM[1]||teethM[2]) : 20;
521
+ const widthM = p.match(/(\d+)\s*mm\s*(?:wide|width|face)/);
522
+ const width = widthM ? parseInt(widthM[1]) : 10;
523
+ const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s*(\d+)/);
524
+ const bore = boreM ? parseInt(boreM[1]||boreM[2]) : 8;
525
+ const pitchDia = mod * teeth;
526
+ const outsideDia = mod * (teeth + 2);
527
+ return [
528
+ {method:'sketch.start', params:{plane:'XY'}},
529
+ {method:'sketch.circle', params:{radius: outsideDia/2}},
530
+ {method:'ops.extrude', params:{depth: width, position:[0,0,0]}, note:'spur gear blank — m='+mod+', Z='+teeth+', pitch Ø'+pitchDia+', OD Ø'+outsideDia+' (add involute teeth via Sketch tab polyline)'},
531
+ {method:'ops.hole', params:{position:[0, width/2, 0], radius: bore/2, depth: width+2}, note:'center bore Ø'+bore},
532
+ {method:'view.set', params:{view:'iso'}},
533
+ {method:'view.fit', params:{}}
534
+ ];
535
+ }
536
+ // Pulley (V-belt, simplified — disc with center bore, groove noted)
537
+ const pulleyM = p.match(/\bpulley\b|\bv-?belt\s*pulley\b|\btiming\s*pulley\b/);
538
+ if (pulleyM) {
539
+ const odM = p.match(/(\d+)\s*mm/);
540
+ const od = odM ? parseInt(odM[1]) : 80;
541
+ const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s*(\d+)/);
542
+ const bore = boreM ? parseInt(boreM[1]||boreM[2]) : 12;
543
+ const widthM = p.match(/(\d+)\s*mm\s*(?:wide|width)/);
544
+ const width = widthM ? parseInt(widthM[1]) : 20;
545
+ return [
546
+ {method:'sketch.start', params:{plane:'XY'}},
547
+ {method:'sketch.circle', params:{radius: od/2}},
548
+ {method:'ops.extrude', params:{depth: width, position:[0,0,0]}, note:'pulley blank Ø'+od+'x'+width+' (add V-groove via revolve in Solid tab)'},
549
+ {method:'ops.hole', params:{position:[0, width/2, 0], radius: bore/2, depth: width+2}, note:'center bore Ø'+bore},
550
+ {method:'view.set', params:{view:'iso'}},
551
+ {method:'view.fit', params:{}}
552
+ ];
553
+ }
554
+ // Shaft (simple cylinder or stepped if "stepped" keyword)
555
+ const shaftM = p.match(/\bshaft\b|\baxle\b|\bspindle\b/);
556
+ if (shaftM) {
557
+ // Explicit diameter keywords first
558
+ const diaM = p.match(/(\d+)\s*mm\s*(?:dia|diameter)|Ø\s*(\d+)|ø\s*(\d+)/);
559
+ // Explicit length keywords
560
+ const lenM = p.match(/(\d+)\s*mm\s*(?:long|length|tall)/);
561
+ // Fallback: if both missing, heuristic from bare Nmm values (smaller=dia, larger=length)
562
+ const allMm = (p.match(/(\d+)\s*mm/g) || []).map(s => parseInt(s)).filter(n => n > 0);
563
+ let dia, len;
564
+ if (diaM) dia = parseInt(diaM[1]||diaM[2]||diaM[3]);
565
+ if (lenM) len = parseInt(lenM[1]);
566
+ if (!dia && !len && allMm.length === 2) { dia = Math.min(allMm[0], allMm[1]); len = Math.max(allMm[0], allMm[1]); }
567
+ else if (!dia && allMm.length >= 1) dia = allMm[0];
568
+ else if (!len && allMm.length >= 2) len = allMm[1];
569
+ dia = dia || 20; len = len || 100;
570
+ const stepped = /\bstepped\b|\bstep\b|\b2[- ]?step\b/.test(p);
571
+ if (stepped) {
572
+ const dia2 = Math.max(6, dia - 4);
573
+ const len1 = Math.round(len * 0.6);
574
+ const len2 = len - len1;
575
+ return [
576
+ {method:'sketch.start', params:{plane:'XY'}},
577
+ {method:'sketch.circle', params:{radius: dia/2}},
578
+ {method:'ops.extrude', params:{depth: len1, position:[0,0,0]}, note:'stepped shaft main Ø'+dia+' x '+len1+'mm'},
579
+ {method:'sketch.start', params:{plane:'XY'}},
580
+ {method:'sketch.circle', params:{radius: dia2/2}},
581
+ {method:'ops.extrude', params:{depth: len2, position:[0,len1,0]}, note:'stepped shaft reduced Ø'+dia2+' x '+len2+'mm'},
582
+ {method:'view.set', params:{view:'iso'}},
583
+ {method:'view.fit', params:{}}
584
+ ];
585
+ }
586
+ return [
587
+ {method:'sketch.start', params:{plane:'XY'}},
588
+ {method:'sketch.circle', params:{radius: dia/2}},
589
+ {method:'ops.extrude', params:{depth: len, position:[0,0,0]}, note:'shaft Ø'+dia+' x '+len+'mm'},
590
+ {method:'view.set', params:{view:'iso'}},
591
+ {method:'view.fit', params:{}}
592
+ ];
593
+ }
416
594
  return null;
417
595
  }
418
596
  async function run(){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyclecad",
3
- "version": "3.10.2",
3
+ "version": "3.10.4",
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
  "bin": {