cyclecad 3.10.4 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ # Handoff — 2026-04-24 session → next chat
2
+
3
+ Drop this file into the next chat (or tell Claude to read it). Covers both cycleCAD and ExplodeView.
4
+
5
+ ## Who I am
6
+ - SACHIN (vvlars@googlemail.com, GitHub vvlars-cmd)
7
+ - Two repos: `~/cyclecad` (parametric 3D CAD modeler) + `~/explodeview` (CAD viewer for STEP files)
8
+ - Working style: fast iteration, minimal clarifying questions, terminal-paste commands via clipboard, Safari private window for testing
9
+ - Full memory is in `~/cyclecad/CLAUDE.md` and `~/explodeview/CLAUDE.md` — read those first if you need historical context
10
+
11
+ ## What shipped this session
12
+ - **ExplodeView v1.0.22 / v304** live on npm + GitHub Pages
13
+ - Commit `b47d6a9` (code) + `41471f3` (version bump)
14
+ - Rewrote `buildPrompt()` in `docs/demo/app.js` line 6595 with preservation-first logic
15
+ - Added "No Style" preset (`presetSuffixes.none = ''`) + button in `docs/demo/index.html`
16
+ - Cache bump 303 → 304, version badge updated
17
+ - Fixes the "Nano Banana v2 ignores the reference CAD object" problem from the 2026-04-24 diagnosis
18
+ - **cycleCAD@3.10.4** confirmed live on npm (from earlier in the day — gear/pulley/shaft AI Copilot templates)
19
+ - **cycleCAD/CLAUDE.md** updated with the new AI Engineering Analyst roadmap spec (see below)
20
+
21
+ ## Top pending item — AI Engineering Analyst (the big one)
22
+
23
+ **Source of inspiration**: MecAgent demo screenshots in `/Users/sachin/Desktop/mec` (12 PNGs from 11:49–11:51). The demo runs inside Autodesk Inventor and solves a complete bolted joint problem in natural language — parses "4×M12 10.9 bolts, 18kN shear, 18kN axial, 420Nm moment, μ=0.16, preload 39kN, BCD 96mm, K_s=1.5", walks through slip/tension/combined-stress checks with equations, cites textbook pages ("Analysis and Design of Machine Elements" by Wei Jiang, Wiley p.85), verdict "safe".
24
+
25
+ **Why this matters**: cycleCAD's existing `ai-copilot.js` builds geometry (gears, bolts, flanges), `fusion-simulation.js` has stubbed FEA, `validate.designReview` returns A-F heuristic scores. None of them answer an engineering question with analytical methods and source citations. This is the biggest competitive gap vs MecAgent and the clearest proof of cycleCAD's "agent-first for manufacturing" positioning.
26
+
27
+ **Target file**: `app/js/modules/ai-engineer.js` (~800–1200 lines)
28
+
29
+ **Scope v1 — bolted joint (VDI 2230 / Shigley)**:
30
+ - Analytical core: pure JS math, no LLM — functions like `checkSlipResistance({z, Q_F, mu, F_shear, K_s})`, `computeMomentLoad({M, r_i})`, `vonMises({sigma, tau})`
31
+ - Input form: bolt grade (4.6–12.9 dropdown with proof/tensile strength table), count, thread size (M3–M36), preload, external loads (F_shear/F_axial/M + BCD), μ, safety factor target
32
+ - Output: stepwise LaTeX equations (KaTeX preferred over MathJax — faster load, smaller bundle), pass/fail badges per check, final verdict
33
+ - Reuses Fastener Wizard bolt patterns + selected-assembly context as implicit inputs
34
+
35
+ **Scope v2 — other machine elements**:
36
+ - Spur/helical gear pair (AGMA bending + pitting stress)
37
+ - Shaft fatigue (Goodman/Soderberg, stress concentration at keyways/shoulders)
38
+ - Rolling bearing L10 life (C/P^n relation, dynamic vs static load)
39
+ - Fillet weld sizing (throat stress, weld leg from applied loads)
40
+
41
+ **RAG / citations**:
42
+ - Let user upload their own machine-element PDF → chunk + embed locally with `@xenova/transformers` MiniLM-L6-v2 → store in IndexedDB
43
+ - Bundled fallback corpus: public-domain machine-element references + legally-redistributable DIN/ISO standard excerpts
44
+ - Every equation gets `{source: "X", page: N}` metadata → rendered as footnote with "Open Document" button (mimics MecAgent UX exactly)
45
+
46
+ **LLM layer**:
47
+ - Same provider stack as `ai-copilot.js` (Claude Sonnet 4.6 paid, Gemini 2.0 Flash free, Groq Llama 3.3 70B free — already wired)
48
+ - Tool-use pattern: LLM parses natural-language problem → emits structured params → calls analytical JS functions → gets numeric result → writes narrative around it. LLM never fabricates numbers.
49
+
50
+ **UI**:
51
+ - New "Engineer" tab in right panel alongside existing Properties / Chat / Guide / Parameters
52
+ - Input form at top, stepwise calculation log in middle, citations panel at bottom
53
+
54
+ ## Other pending items (from CLAUDE.md)
55
+
56
+ ### cycleCAD
57
+ - More AI Copilot templates in `app/js/modules/ai-copilot.js` `matchTemplate()` — mounting bracket variants, T-slot extrusions (40/40, 80/40), ball/roller bearings, bearing cutouts. Same pattern as the gear/pulley/shaft block in 3.10.4. Would ship as `3.10.5`.
58
+ - Polyline → geometry support in mini-executor (unblocks true involute gear teeth — today's gear template is a blank cylinder)
59
+ - Dynamic version badge in status bar (currently hardcoded `v0.9.0`, should fetch from package.json)
60
+ - Wire splash buttons (New Sketch / Open-Import / Text-to-CAD / Inventor Project) to actually trigger their actions
61
+ - Run `app/test-agent.html` (113 tests, 15 categories) in Chrome and fix failures
62
+ - Text-to-CAD with live preview
63
+ - Photo-to-CAD reverse engineering
64
+ - 138MB STEP import via server-side `server/converter.py` (safer than opencascade.js v293 path)
65
+ - Docker compose local test
66
+
67
+ ### ExplodeView
68
+ - **Compositing render** for guaranteed CAD preservation: render 3D with transparent background, send only the cropped background region to Nano Banana, composite the 3D foreground over the generated background in canvas. Bypasses "generative ≠ inpainting" completely. Probably the right follow-up to v304.
69
+ - Run `docs/demo/killer-features-test.html` in Chrome, fix failures
70
+ - The `block-only-in-chrome` user report — needs repro
71
+
72
+ ## Critical operational context (don't forget)
73
+ - **Git lock dance**: VM commits leave stale `.git/*.lock` files. Always give ONE combined command: `rm -f ~/REPO/.git/index.lock ~/REPO/.git/HEAD.lock && cd ~/REPO && git add ... && git commit -m "..." && git push origin main`
74
+ - **Clipboard delivery**: Commands go via `mcp__computer-use__write_clipboard` → user pastes in Terminal. Must switch OFF Terminal (make Chrome or other app frontmost) before writing — Terminal is tier-"click" and blocks clipboard writes when it's frontmost.
75
+ - **Egress allowlist**: `explodeview.com` and `cyclecad.com` are NOT on the allowlist. Use `npm registry` (allowed) to verify version bumps. For live-site verification, either ask user to add domain to Settings → Capabilities, or use the Claude-in-Chrome MCP.
76
+ - **Deferred tools**: All tools show as names in the prompt but schemas are loaded on demand via ToolSearch. Load `computer-use` in bulk: `{query: "computer-use", max_results: 30}`. Load `chrome`: `{query: "chrome", max_results: 20}`.
77
+ - **GitHub Pages concurrent-deploy fix** shipped 2026-04-24 in `.github/workflows/pages.yml` with `concurrency: group: pages, cancel-in-progress: true`. If Pages fails with "deployment in progress" again, that's the first check.
78
+
79
+ ## Current npm versions
80
+ - cyclecad: **3.10.4** (next bump should be 3.10.5 if AI Copilot templates added, or 3.11.0 if AI Engineering Analyst ships)
81
+ - explodeview: **1.0.22**
82
+
83
+ ## Latest commits
84
+ - ExplodeView: `41471f3` v1.0.22 tag, `b47d6a9` v304 code
85
+ - cycleCAD: HEAD has CLAUDE.md modified locally (untracked pentacad/mockups/machines files present — leave alone unless asked)
86
+
87
+ ## How to resume
88
+ 1. Read `~/cyclecad/CLAUDE.md` and `~/explodeview/CLAUDE.md` for full history
89
+ 2. If continuing AI Engineering Analyst: start with `app/js/modules/ai-engineer.js` skeleton — define the analytical functions for bolted joint first (pure math, no LLM), unit-test them against MecAgent's numbers from the screenshots (F_friction = 24960 N, F_max_tensile = 6687.5 N, σ_vm = 558 MPa), THEN wire in KaTeX rendering, THEN LLM layer, THEN RAG
90
+ 3. If continuing something else: ask me which of the pending items to prioritize
package/app/index.html CHANGED
@@ -1014,6 +1014,7 @@
1014
1014
  <button class="menu-item-link" data-action="tools-marketplace">Marketplace...</button>
1015
1015
  <div class="menu-separator"></div>
1016
1016
  <button class="menu-item-link" data-action="tools-ai-copilot">✨ AI Copilot (multi-step)</button>
1017
+ <button class="menu-item-link" data-action="tools-ai-engineer">🔩 AI Engineering Analyst</button>
1017
1018
  <button class="menu-item-link" data-action="tools-text-to-cad">Text-to-CAD (AI)</button>
1018
1019
  <button class="menu-item-link" data-action="tools-photo-to-cad">Photo-to-CAD</button>
1019
1020
  <button class="menu-item-link" data-action="tools-dfm">Manufacturability Check</button>
@@ -1539,6 +1540,7 @@ window._dismissSplash = function(action) {
1539
1540
  <!-- Killer Feature Modules -->
1540
1541
  <script src="/app/js/agent-api.js?v=a41b98a5"></script>
1541
1542
  <script src="/app/js/modules/ai-copilot.js?v=14e5d5d7"></script>
1543
+ <script src="/app/js/modules/ai-engineer.js?v=20260424v1"></script>
1542
1544
  <script src="/app/js/modules/text-to-cad.js"></script>
1543
1545
  <script src="/app/js/modules/photo-to-cad.js"></script>
1544
1546
  <script src="/app/js/modules/manufacturability.js"></script>
@@ -1967,6 +1969,13 @@ window._dismissSplash = function(action) {
1967
1969
  if (dbc) { dbc.innerHTML = ''; dbc.appendChild(window.CycleCAD.AICopilot.getUI()); dbc.style.maxHeight = '620px'; dbc.style.overflow = 'auto'; }
1968
1970
  } else { showToast('AI Copilot module not loaded', 'error'); }
1969
1971
  break;
1972
+ case 'tools-ai-engineer':
1973
+ if (window.CycleCAD && window.CycleCAD.AIEngineer) {
1974
+ showDialog('🔩 AI Engineering Analyst — bolted-joint analysis (VDI 2230 / Shigley)', '');
1975
+ const dbe = document.getElementById('dialog-body');
1976
+ if (dbe) { dbe.innerHTML = ''; dbe.appendChild(window.CycleCAD.AIEngineer.getUI()); dbe.style.maxHeight = '680px'; dbe.style.overflow = 'auto'; }
1977
+ } else { showToast('AI Engineering Analyst module not loaded', 'error'); }
1978
+ break;
1970
1979
  case 'tools-text-to-cad':
1971
1980
  if (window.CycleCAD && window.CycleCAD.TextToCAD) {
1972
1981
  showDialog('Text-to-CAD (AI)', '');
@@ -1,4 +1,4 @@
1
- /* AI Copilot v1.1 — multi-step CAD generation from natural language */
1
+ /* AI Copilot v1.2 — multi-step CAD generation from natural language. 3.10.5 adds sketch.polyline support + templates for ball bearings (ISO 15/DIN 625), bearing housings, T-slot aluminum extrusions (2020–8080), and U-brackets. */
2
2
  (function(){
3
3
  'use strict';
4
4
  window.CycleCAD = window.CycleCAD || {};
@@ -93,6 +93,7 @@
93
93
  '- sketch.start {plane:"XY"}',
94
94
  '- sketch.rect {width, height} — rectangle centered at current origin',
95
95
  '- sketch.circle {radius} — OR diameter',
96
+ '- sketch.polyline {points:[[x,z],[x,z],...]} — closed polygon in XZ plane (for custom shapes, gear teeth, T-slot profiles, cams)',
96
97
  '- sketch.end',
97
98
  '- ops.extrude {depth, position:[x,y,z], subtract:bool} — create solid. subtract:true carves from last body.',
98
99
  '- ops.hole {position:[x,y,z], depth, radius OR width+height} — carves cylinder OR rectangular hole through last body.',
@@ -255,6 +256,14 @@
255
256
  if (method === 'sketch.start') { miniState.currentSketch = { plane: params.plane||'XY', origin: getPos(params) }; return {ok:true}; }
256
257
  if (method === 'sketch.rect') { miniState.currentSketch = Object.assign(miniState.currentSketch||{}, { shape:'rect', width: params.width||params.w||50, height: params.height||params.h||30, origin: getPos(params) }); return {ok:true}; }
257
258
  if (method === 'sketch.circle'){ miniState.currentSketch = Object.assign(miniState.currentSketch||{}, { shape:'circle', radius: params.radius||params.r||(params.diameter?params.diameter/2:25), origin: getPos(params) }); return {ok:true}; }
259
+ if (method === 'sketch.polyline' || method === 'sketch.polygon') {
260
+ // Points arrive as [[x,z],[x,z],...] in world XZ plane. Min 3 pts for a valid polygon.
261
+ const raw = Array.isArray(params.points) ? params.points : [];
262
+ const pts = raw.filter(p => Array.isArray(p) && p.length >= 2).map(p => [Number(p[0])||0, Number(p[1])||0]);
263
+ if (pts.length < 3) return {ok:false, note:'polyline needs at least 3 points'};
264
+ miniState.currentSketch = Object.assign(miniState.currentSketch||{}, { shape:'polyline', points: pts, origin: getPos(params) });
265
+ return {ok:true};
266
+ }
258
267
  if (method === 'sketch.line' || method === 'sketch.end') return {ok:true};
259
268
  if (method === 'ops.extrude') {
260
269
  const d = params.depth||params.height||params.distance||20;
@@ -264,6 +273,20 @@
264
273
  let g;
265
274
  if (sk.shape==='rect') g = new THREE.BoxGeometry(sk.width, d, sk.height);
266
275
  else if (sk.shape==='circle') g = new THREE.CylinderGeometry(sk.radius, sk.radius, d, 48);
276
+ else if (sk.shape==='polyline' && Array.isArray(sk.points) && sk.points.length >= 3) {
277
+ // Build Shape from [x,z] points. RotateX(-PI/2) maps:
278
+ // Shape-X -> world X, Shape-Y -> world -Z, extrude Z -> world +Y.
279
+ // To preserve user's intent that points[i][1] is world Z, negate Y into Shape.
280
+ const shape = new THREE.Shape();
281
+ const pts = sk.points;
282
+ shape.moveTo(pts[0][0], -pts[0][1]);
283
+ for (let i = 1; i < pts.length; i++) shape.lineTo(pts[i][0], -pts[i][1]);
284
+ shape.closePath();
285
+ g = new THREE.ExtrudeGeometry(shape, { depth: d, bevelEnabled: false, curveSegments: 24 });
286
+ g.translate(0, 0, -d/2); // center along extrude axis BEFORE rotating
287
+ g.rotateX(-Math.PI/2);
288
+ // After rotation: geometry spans world Y:[-d/2, d/2] centered at origin. Matches BoxGeometry/CylinderGeometry convention.
289
+ }
267
290
  else g = new THREE.BoxGeometry(50, d, 30);
268
291
  const isSubtract = params.subtract === true || params.operation === 'cut' || params.operation === 'subtract';
269
292
  const mat = new THREE.MeshStandardMaterial({color: isSubtract?0x1a1a1a:0x4a90e2, metalness:0.35, roughness:0.45});
@@ -591,6 +614,176 @@
591
614
  {method:'view.fit', params:{}}
592
615
  ];
593
616
  }
617
+ // Ball bearing (ISO 15 / DIN 625 deep-groove) — simplified solid ring.
618
+ // Designations: 60x (light), 62x (medium), 63x (heavy), 6x (small single-digit), 625, 608 etc.
619
+ const bearingDesignations = {
620
+ '608': {d:8, D:22, B:7},
621
+ '625': {d:5, D:16, B:5},
622
+ '6000': {d:10, D:26, B:8},
623
+ '6001': {d:12, D:28, B:8},
624
+ '6002': {d:15, D:32, B:9},
625
+ '6003': {d:17, D:35, B:10},
626
+ '6004': {d:20, D:42, B:12},
627
+ '6005': {d:25, D:47, B:12},
628
+ '6006': {d:30, D:55, B:13},
629
+ '6200': {d:10, D:30, B:9},
630
+ '6201': {d:12, D:32, B:10},
631
+ '6202': {d:15, D:35, B:11},
632
+ '6203': {d:17, D:40, B:12},
633
+ '6204': {d:20, D:47, B:14},
634
+ '6205': {d:25, D:52, B:15},
635
+ '6206': {d:30, D:62, B:16},
636
+ '6300': {d:10, D:35, B:11},
637
+ '6301': {d:12, D:37, B:12},
638
+ '6302': {d:15, D:42, B:13},
639
+ '6303': {d:17, D:47, B:14},
640
+ '6304': {d:20, D:52, B:15}
641
+ };
642
+ const bearingCodeM = p.match(/\b(6[023]0[0-6]|608|625)\b/);
643
+ // Housing/pocket/seat keywords take priority — a "bearing pocket for 6204" is a housing, not a bearing body.
644
+ const isHousingIntent = /bearing\s*(?:housing|pocket|seat|recess|mount)/.test(p);
645
+ if (bearingCodeM && /bearing/.test(p) && !isHousingIntent) {
646
+ const spec = bearingDesignations[bearingCodeM[1]];
647
+ if (spec) {
648
+ const code = bearingCodeM[1];
649
+ return [
650
+ {method:'sketch.start', params:{plane:'XY'}},
651
+ {method:'sketch.circle', params:{radius: spec.D/2}},
652
+ {method:'ops.extrude', params:{depth: spec.B, position:[0,0,0]}, note:'bearing '+code+' OD Ø'+spec.D+' x width '+spec.B},
653
+ {method:'ops.hole', params:{position:[0,0,0], radius: spec.d/2, depth: spec.B + 2}, note:code+' bore Ø'+spec.d+' (ISO 15 — simplified ring, real bearing has balls + races)'},
654
+ {method:'view.set', params:{view:'iso'}},
655
+ {method:'view.fit', params:{}}
656
+ ];
657
+ }
658
+ }
659
+ // Generic ball bearing with explicit bore ("ball bearing 10mm bore")
660
+ const genBearingM = p.match(/(?:ball|deep[- ]?groove|roller)\s*bearing/);
661
+ if (genBearingM) {
662
+ const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s+(\d+)|Ø\s*(\d+)/);
663
+ const bore = boreM ? parseInt(boreM[1]||boreM[2]||boreM[3]) : 10;
664
+ // Rough approximation matching 62xx series: OD ≈ 2.5*bore + 10, width ≈ 0.4*bore + 4
665
+ const OD = Math.round(bore * 2.5 + 10);
666
+ const B = Math.max(5, Math.round(bore * 0.4 + 4));
667
+ return [
668
+ {method:'sketch.start', params:{plane:'XY'}},
669
+ {method:'sketch.circle', params:{radius: OD/2}},
670
+ {method:'ops.extrude', params:{depth: B, position:[0,0,0]}, note:'ball bearing Ø'+OD+' x '+B+'mm (approx 62xx series)'},
671
+ {method:'ops.hole', params:{position:[0,0,0], radius: bore/2, depth: B+2}, note:'bore Ø'+bore+' (look up ISO 15 for exact OD)'},
672
+ {method:'view.set', params:{view:'iso'}},
673
+ {method:'view.fit', params:{}}
674
+ ];
675
+ }
676
+ // Bearing housing / pocket — block with recess and clearance through-hole
677
+ if (/bearing\s*(?:housing|pocket|seat|recess|mount)/.test(p)) {
678
+ const odM = p.match(/(\d+)\s*mm\s*(?:od|outer|outside)|for\s+(?:a\s+)?(\d+)\s*mm|bearing\s+(\d+)\s*mm/);
679
+ // If the user named a bearing designation, look up its OD
680
+ const housingBearingCode = p.match(/\b(6[023]0[0-6]|608|625)\b/);
681
+ const codeSpec = housingBearingCode ? bearingDesignations[housingBearingCode[1]] : null;
682
+ const OD = odM ? parseInt(odM[1]||odM[2]||odM[3]) : (codeSpec ? codeSpec.D : 32);
683
+ const pocketDepth = Math.max(6, Math.round(OD * 0.3));
684
+ const wall = 8;
685
+ const blockW = OD + 2*wall;
686
+ const blockH = OD + 2*wall;
687
+ const blockD = pocketDepth + 5;
688
+ const throughR = Math.max(3, OD/2 - 4);
689
+ return [
690
+ {method:'sketch.start', params:{plane:'XY'}},
691
+ {method:'sketch.rect', params:{width: blockW, height: blockH}},
692
+ {method:'ops.extrude', params:{depth: blockD, position:[0,0,0]}, note:'housing block '+blockW+'×'+blockH+'×'+blockD+' for Ø'+OD+' bearing'},
693
+ {method:'ops.hole', params:{position:[0, blockD - pocketDepth, 0], radius: OD/2, depth: pocketDepth + 0.5}, note:'bearing pocket Ø'+OD+' × '+pocketDepth+'mm deep'},
694
+ {method:'ops.hole', params:{position:[0,0,0], radius: throughR, depth: blockD + 2}, note:'shaft clearance bore Ø'+(throughR*2).toFixed(0)},
695
+ // 4 mounting holes at corners
696
+ {method:'ops.hole', params:{position:[-blockW/2 + 6, 0, -blockH/2 + 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 1/4'},
697
+ {method:'ops.hole', params:{position:[ blockW/2 - 6, 0, -blockH/2 + 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 2/4'},
698
+ {method:'ops.hole', params:{position:[-blockW/2 + 6, 0, blockH/2 - 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 3/4'},
699
+ {method:'ops.hole', params:{position:[ blockW/2 - 6, 0, blockH/2 - 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 4/4'},
700
+ {method:'view.set', params:{view:'iso'}},
701
+ {method:'view.fit', params:{}}
702
+ ];
703
+ }
704
+ // T-slot aluminum extrusion (2020/3030/4040/4080 profiles — Bosch/Item/80/20 style)
705
+ const tSlotKw = /\bt-?slot|aluminum\s*(?:extrusion|profile)|\b80[- ]?20\b|\bitem\s*profile|\bmisumi\s*extrusion|\bbosch\s*extrusion|structural\s*extrusion/.test(p);
706
+ const codeM = p.match(/\b(20|25|30|40|45|50|60|80|100)\s*(?:x|×)\s*(20|25|30|40|45|50|60|80|100)\b/);
707
+ const fourDigitM = p.match(/\b(2020|2040|2080|3030|3060|4040|4080|4545|6060|8080)\b/);
708
+ if (tSlotKw || codeM || (fourDigitM && /extrusion|profile|t-?slot/.test(p))) {
709
+ let W = 40, H = 40;
710
+ if (codeM) { W = parseInt(codeM[1]); H = parseInt(codeM[2]); }
711
+ else if (fourDigitM) {
712
+ const c = fourDigitM[1];
713
+ W = parseInt(c.slice(0, c.length/2));
714
+ H = parseInt(c.slice(c.length/2));
715
+ }
716
+ const lenM = p.match(/(\d{2,5})\s*mm\s*(?:long|length)|length\s+(\d{2,5})|\b(\d{3,4})\s*mm\s+(?:rail|bar|stick|piece)/);
717
+ let length = lenM ? parseInt(lenM[1]||lenM[2]||lenM[3]) : 0;
718
+ if (!length) {
719
+ // Fallback: take the largest \d+mm value that isn't the profile code itself (e.g. "4040 extrusion 1000mm")
720
+ const allMm = [...p.matchAll(/\b(\d{2,5})\s*mm\b/g)].map(m => parseInt(m[1]))
721
+ .filter(n => n !== W && n !== H && n !== W*100 + H && n !== W*10 + H);
722
+ length = allMm.length ? Math.max(...allMm) : 500;
723
+ }
724
+ // Slot geometry scales with profile size — approximation of real T-slot
725
+ const slotW = W <= 25 ? 6 : W <= 40 ? 8 : 10; // slot opening (across inward axis)
726
+ const slotD = Math.max(6, W/3); // slot depth
727
+ const yMargin = 2; // covers Y range [0, length]
728
+ const plan = [
729
+ {method:'sketch.start', params:{plane:'XY'}},
730
+ {method:'sketch.rect', params:{width: W, height: H}},
731
+ {method:'ops.extrude', params:{depth: length, position:[0,0,0]}, note:W+'×'+H+' T-slot extrusion, '+length+'mm long (simplified — real profile has T-grooves + hollow center)'},
732
+ // +X face slot
733
+ {method:'ops.hole', params:{position:[W/2 - slotD/2, 0, 0], width: slotD, height: slotW, depth: length + yMargin}, note:'+X T-slot'},
734
+ // -X face slot
735
+ {method:'ops.hole', params:{position:[-W/2 + slotD/2, 0, 0], width: slotD, height: slotW, depth: length + yMargin}, note:'-X T-slot'},
736
+ // +Z face slot
737
+ {method:'ops.hole', params:{position:[0, 0, H/2 - slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'+Z T-slot'},
738
+ // -Z face slot
739
+ {method:'ops.hole', params:{position:[0, 0, -H/2 + slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'-Z T-slot'},
740
+ {method:'view.set', params:{view:'iso'}},
741
+ {method:'view.fit', params:{}}
742
+ ];
743
+ // For double-wide profiles (e.g. 4080), the longer face gets two T-slots per side
744
+ if (Math.max(W, H) >= 2 * Math.min(W, H)) {
745
+ const longSide = W > H ? 'x' : 'z';
746
+ const longDim = Math.max(W, H);
747
+ const offset = longDim / 4;
748
+ if (longSide === 'x') {
749
+ // Add extra slots on top/bottom (Z faces) since X-axis is longer
750
+ plan.splice(-2, 0,
751
+ {method:'ops.hole', params:{position:[ offset, 0, H/2 - slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'+Z T-slot (extra)'},
752
+ {method:'ops.hole', params:{position:[-offset, 0, H/2 - slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'+Z T-slot (extra)'},
753
+ {method:'ops.hole', params:{position:[ offset, 0, -H/2 + slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'-Z T-slot (extra)'},
754
+ {method:'ops.hole', params:{position:[-offset, 0, -H/2 + slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'-Z T-slot (extra)'}
755
+ );
756
+ }
757
+ }
758
+ return plan;
759
+ }
760
+ // U-bracket / channel bracket
761
+ if (/u-?bracket|u-?channel|channel\s*bracket|mounting\s*channel/.test(p)) {
762
+ const wM = p.match(/(\d+)\s*mm\s*(?:wide|width)|width\s+(\d+)/);
763
+ const width = wM ? parseInt(wM[1]||wM[2]) : 60;
764
+ const hM = p.match(/(\d+)\s*mm\s*(?:high|height|tall)|height\s+(\d+)/);
765
+ const height = hM ? parseInt(hM[1]||hM[2]) : 40;
766
+ const lM = p.match(/(\d+)\s*mm\s*(?:long|length)|length\s+(\d+)/);
767
+ const length = lM ? parseInt(lM[1]||lM[2]) : 100;
768
+ const wall = Math.max(3, Math.round(Math.min(width, height) / 12));
769
+ const holesM = p.match(/(\d+)\s*holes?/);
770
+ const nHoles = holesM ? parseInt(holesM[1]) : 2;
771
+ const plan = [
772
+ {method:'sketch.start', params:{plane:'XY'}},
773
+ {method:'sketch.rect', params:{width: width, height: length}},
774
+ {method:'ops.extrude', params:{depth: height, position:[0,0,0]}, note:'U-bracket blank '+width+'×'+height+'×'+length+'mm'},
775
+ // Pocket cut from top down to wall thickness above base. Final Y span: [wall-0.5, height+1.5], intersects body at [wall, height].
776
+ {method:'ops.hole', params:{position:[0, wall, 0], width: width - 2*wall, height: length + 2, depth: height - wall + 2}, note:'U-bracket pocket ('+(width - 2*wall)+'×'+(height - wall)+' leaves base + 2 side walls)'}
777
+ ];
778
+ // Mounting holes through the base plate
779
+ for (let i = 0; i < nHoles; i++) {
780
+ const y = (nHoles === 1) ? 0 : -length/2 + 12 + i * ((length - 24) / Math.max(1, nHoles - 1));
781
+ plan.push({method:'ops.hole', params:{position:[0, 0, y], radius: 3, depth: wall + 2}, note:'base mounting hole '+(i+1)+'/'+nHoles});
782
+ }
783
+ plan.push({method:'view.set', params:{view:'iso'}});
784
+ plan.push({method:'view.fit', params:{}});
785
+ return plan;
786
+ }
594
787
  return null;
595
788
  }
596
789
  async function run(){
@@ -723,5 +916,5 @@
723
916
  abort: () => abort(),
724
917
  getState: () => ({ running:S.running, stepIndex:S.stepIndex, results:S.results.length, errors:S.errors.length, model:S.els.model?.value })
725
918
  };
726
- console.log('AI Copilot v1.1 module loaded');
919
+ console.log('AI Copilot v1.2 module loaded');
727
920
  })();