cyclecad 3.10.3 → 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
@@ -511,6 +511,86 @@
511
511
  plan.push({method:'view.fit', params:{}});
512
512
  return plan;
513
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
+ }
514
594
  return null;
515
595
  }
516
596
  async function run(){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyclecad",
3
- "version": "3.10.3",
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": {