cyclecad 3.10.3 → 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.
- package/.github/workflows/pages.yml +34 -0
- package/.nojekyll +0 -0
- package/CLAUDE.md +430 -0
- package/HANDOFF-2026-04-24-session-2.md +239 -0
- package/HANDOFF-2026-04-24.md +90 -0
- package/app/index.html +9 -0
- package/app/js/modules/ai-copilot.js +275 -2
- package/app/js/modules/ai-engineer.js +613 -0
- package/app/js/modules/pentacad-bridge.js +216 -0
- package/app/js/modules/pentacad-cam.js +184 -0
- package/app/js/modules/pentacad-sim.js +215 -0
- package/app/js/modules/pentacad.js +233 -0
- package/app/pentacad.html +240 -0
- package/index-agent-first.html.bak +1306 -0
- package/machines/v2-50-chb/kinematics.json +51 -0
- package/mockups/cyclecad-suite-mockup.html +1746 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* AI Copilot v1.
|
|
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});
|
|
@@ -511,6 +534,256 @@
|
|
|
511
534
|
plan.push({method:'view.fit', params:{}});
|
|
512
535
|
return plan;
|
|
513
536
|
}
|
|
537
|
+
// Spur gear (simplified — cylinder disc at OD with center bore)
|
|
538
|
+
const gearM = p.match(/\bspur\s*gear\b|\bgear\b/);
|
|
539
|
+
if (gearM) {
|
|
540
|
+
const modM = p.match(/module\s*(\d+(?:\.\d+)?)|\bm\s*=?\s*(\d+(?:\.\d+)?)\b/);
|
|
541
|
+
const teethM = p.match(/(\d+)\s*(?:teeth|tooth)|z\s*=?\s*(\d+)/);
|
|
542
|
+
const mod = modM ? parseFloat(modM[1]||modM[2]) : 2;
|
|
543
|
+
const teeth = teethM ? parseInt(teethM[1]||teethM[2]) : 20;
|
|
544
|
+
const widthM = p.match(/(\d+)\s*mm\s*(?:wide|width|face)/);
|
|
545
|
+
const width = widthM ? parseInt(widthM[1]) : 10;
|
|
546
|
+
const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s*(\d+)/);
|
|
547
|
+
const bore = boreM ? parseInt(boreM[1]||boreM[2]) : 8;
|
|
548
|
+
const pitchDia = mod * teeth;
|
|
549
|
+
const outsideDia = mod * (teeth + 2);
|
|
550
|
+
return [
|
|
551
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
552
|
+
{method:'sketch.circle', params:{radius: outsideDia/2}},
|
|
553
|
+
{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)'},
|
|
554
|
+
{method:'ops.hole', params:{position:[0, width/2, 0], radius: bore/2, depth: width+2}, note:'center bore Ø'+bore},
|
|
555
|
+
{method:'view.set', params:{view:'iso'}},
|
|
556
|
+
{method:'view.fit', params:{}}
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
// Pulley (V-belt, simplified — disc with center bore, groove noted)
|
|
560
|
+
const pulleyM = p.match(/\bpulley\b|\bv-?belt\s*pulley\b|\btiming\s*pulley\b/);
|
|
561
|
+
if (pulleyM) {
|
|
562
|
+
const odM = p.match(/(\d+)\s*mm/);
|
|
563
|
+
const od = odM ? parseInt(odM[1]) : 80;
|
|
564
|
+
const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s*(\d+)/);
|
|
565
|
+
const bore = boreM ? parseInt(boreM[1]||boreM[2]) : 12;
|
|
566
|
+
const widthM = p.match(/(\d+)\s*mm\s*(?:wide|width)/);
|
|
567
|
+
const width = widthM ? parseInt(widthM[1]) : 20;
|
|
568
|
+
return [
|
|
569
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
570
|
+
{method:'sketch.circle', params:{radius: od/2}},
|
|
571
|
+
{method:'ops.extrude', params:{depth: width, position:[0,0,0]}, note:'pulley blank Ø'+od+'x'+width+' (add V-groove via revolve in Solid tab)'},
|
|
572
|
+
{method:'ops.hole', params:{position:[0, width/2, 0], radius: bore/2, depth: width+2}, note:'center bore Ø'+bore},
|
|
573
|
+
{method:'view.set', params:{view:'iso'}},
|
|
574
|
+
{method:'view.fit', params:{}}
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
// Shaft (simple cylinder or stepped if "stepped" keyword)
|
|
578
|
+
const shaftM = p.match(/\bshaft\b|\baxle\b|\bspindle\b/);
|
|
579
|
+
if (shaftM) {
|
|
580
|
+
// Explicit diameter keywords first
|
|
581
|
+
const diaM = p.match(/(\d+)\s*mm\s*(?:dia|diameter)|Ø\s*(\d+)|ø\s*(\d+)/);
|
|
582
|
+
// Explicit length keywords
|
|
583
|
+
const lenM = p.match(/(\d+)\s*mm\s*(?:long|length|tall)/);
|
|
584
|
+
// Fallback: if both missing, heuristic from bare Nmm values (smaller=dia, larger=length)
|
|
585
|
+
const allMm = (p.match(/(\d+)\s*mm/g) || []).map(s => parseInt(s)).filter(n => n > 0);
|
|
586
|
+
let dia, len;
|
|
587
|
+
if (diaM) dia = parseInt(diaM[1]||diaM[2]||diaM[3]);
|
|
588
|
+
if (lenM) len = parseInt(lenM[1]);
|
|
589
|
+
if (!dia && !len && allMm.length === 2) { dia = Math.min(allMm[0], allMm[1]); len = Math.max(allMm[0], allMm[1]); }
|
|
590
|
+
else if (!dia && allMm.length >= 1) dia = allMm[0];
|
|
591
|
+
else if (!len && allMm.length >= 2) len = allMm[1];
|
|
592
|
+
dia = dia || 20; len = len || 100;
|
|
593
|
+
const stepped = /\bstepped\b|\bstep\b|\b2[- ]?step\b/.test(p);
|
|
594
|
+
if (stepped) {
|
|
595
|
+
const dia2 = Math.max(6, dia - 4);
|
|
596
|
+
const len1 = Math.round(len * 0.6);
|
|
597
|
+
const len2 = len - len1;
|
|
598
|
+
return [
|
|
599
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
600
|
+
{method:'sketch.circle', params:{radius: dia/2}},
|
|
601
|
+
{method:'ops.extrude', params:{depth: len1, position:[0,0,0]}, note:'stepped shaft main Ø'+dia+' x '+len1+'mm'},
|
|
602
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
603
|
+
{method:'sketch.circle', params:{radius: dia2/2}},
|
|
604
|
+
{method:'ops.extrude', params:{depth: len2, position:[0,len1,0]}, note:'stepped shaft reduced Ø'+dia2+' x '+len2+'mm'},
|
|
605
|
+
{method:'view.set', params:{view:'iso'}},
|
|
606
|
+
{method:'view.fit', params:{}}
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
return [
|
|
610
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
611
|
+
{method:'sketch.circle', params:{radius: dia/2}},
|
|
612
|
+
{method:'ops.extrude', params:{depth: len, position:[0,0,0]}, note:'shaft Ø'+dia+' x '+len+'mm'},
|
|
613
|
+
{method:'view.set', params:{view:'iso'}},
|
|
614
|
+
{method:'view.fit', params:{}}
|
|
615
|
+
];
|
|
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
|
+
}
|
|
514
787
|
return null;
|
|
515
788
|
}
|
|
516
789
|
async function run(){
|
|
@@ -643,5 +916,5 @@
|
|
|
643
916
|
abort: () => abort(),
|
|
644
917
|
getState: () => ({ running:S.running, stepIndex:S.stepIndex, results:S.results.length, errors:S.errors.length, model:S.els.model?.value })
|
|
645
918
|
};
|
|
646
|
-
console.log('AI Copilot v1.
|
|
919
|
+
console.log('AI Copilot v1.2 module loaded');
|
|
647
920
|
})();
|