skill
EDA Bridge Standard: one way for an AI to drive every PCB tool
Public Made by Adomby adom
The standard every Adom EDA bridge follows so one AI can drive Altium, KiCad, Fusion 360 and any future PCB tool the same way: shared verb names in tiers, the board model JSON, reply and refusal conventions, and a coverage table of each bridge against the Altium reference.
← Commit history
Merge PR #323: altium-lane: plan_route takes the board's width and via rules (#21)
2 files changed
+53−5
engine/src/features.rs+29@@ -188,6 +188,26 @@ pub fn via_style(rules: &[Value]) -> Option<(f64, f64)> { } +/// The board's preferred track width in mm. A standard rule gives it as `{kind: "width", preferredMm}` (or `widthMm`);+/// Altium's Width rule descriptor is read too, e.g. "Width Constraint (Min=10mil) (Max=10mil) (Preferred=10mil) (All)":+/// the board-wide one (scope All), highest priority first, its Preferred else its Min. Pure.+pub fn width_style(rules: &[Value]) -> Option<f64> {+ if let Some(r) = rules.iter().filter(|r| r["enabled"] != json!(false))+ .find(|r| r["kind"].as_str().map(|k| k.eq_ignore_ascii_case("width")).unwrap_or(false)) {+ if let Some(v) = r["preferredMm"].as_f64().or_else(|| r["widthMm"].as_f64()) { return Some(v); }+ }+ let num = |d: &str, key: &str| -> Option<f64> {+ let i = d.find(&format!("({key}="))? + key.len() + 2;+ let s = &d[i..d[i..].find(')')? + i];+ let v: f64 = s.trim_end_matches(char::is_alphabetic).parse().ok()?;+ Some(if s.ends_with("mil") { v * 0.0254 } else { v })+ };+ let mut rs: Vec<&Value> = rules.iter().filter(|r| r["enabled"] != json!(false) && r["descriptor"].as_str().map(|d| d.starts_with("Width")).unwrap_or(false)).collect();+ rs.sort_by_key(|r| (r["scope1"].as_str() != Some("All"), r["priority"].as_i64().unwrap_or(99)));+ let d = rs.first()?["descriptor"].as_str()?;+ num(d, "Preferred").or_else(|| num(d, "Min")).map(|v| (v * 10000.0).round() / 10000.0)+}+ /// Thermal via grid points inside a pad (x, y, sx, sy, rotation mm): `pitch` apart, `inset` from the pad's edge plus /// the via's radius, centred. Pure. pub fn thermal_grid(x: f64, y: f64, sx: f64, sy: f64, rot: f64, pitch: f64, size: f64, inset: f64) -> Vec<(f64, f64)> {@@ -408,4 +428,13 @@ mod tests { assert_eq!(via_fence(&json!({"board": st, "args": {"viaNet": "NOPE", "nets": ["RF"]}}))["errorCode"], "no_such_net"); assert_eq!(thermal_vias(&json!({"board": st, "args": {"ref": "U9"}}))["errorCode"], "part_not_found"); }+ #[test]+ fn width_style_reads_the_standard_rule_and_altiums_descriptor() {+ assert_eq!(width_style(&[json!({"kind": "width", "preferredMm": 0.2})]), Some(0.2));+ let alt = vec![json!({"enabled": true, "scope1": "InNet('VIN')", "priority": 1, "descriptor": "Width Constraint (Min=20mil) (Max=40mil) (Preferred=30mil) (InNet('VIN'))"}),+ json!({"enabled": true, "scope1": "All", "priority": 2, "descriptor": "Width Constraint (Min=10mil) (Max=10mil) (Preferred=10mil) (All)"})];+ assert_eq!(width_style(&alt), Some(0.254), "the board-wide rule, not a net's");+ assert_eq!(width_style(&[json!({"descriptor": "Width Constraint (Min=0.15mm) (Max=1mm) (All)", "scope1": "All"})]), Some(0.15));+ assert_eq!(width_style(&[]), None);+ } }
engine/src/route.rs+24−5@@ -83,7 +83,8 @@ fn pad_layers(p: &Value, all: &[String]) -> Vec<String> { } /// plan_route: input {board, rules?, args: {net, from, to, widthMm?, layers?, allowVias?, viaSizeMm?, viaDrillMm?, gridMm?,-/// viaCost?, clearanceMm?, edgeClearanceMm?, maxNodes?}}. `from` and `to` are pads as Ref.Pad.+/// viaCost?, clearanceMm?, edgeClearanceMm?, maxNodes?}}. `from` and `to` are pads as Ref.Pad. The width and via come from+/// the args, else the board's width and via rules (`rules`), else 0.25 mm and 0.6/0.3; `planner` says which. pub fn plan_route(input: &Value) -> Value { let st = match crate::input::board(input) { Ok(b) => b, Err(e) => return e }; let args = &input["args"];@@ -106,8 +107,14 @@ pub fn plan_route(input: &Value) -> Value { if key(&ends[0]).eq_ignore_ascii_case(&key(&ends[1])) { return crate::input::refuse("bad_args", "from and to are the same pad"); } let layers: Vec<String> = args["layers"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect()).unwrap_or_else(|| vec!["F.Cu".into(), "B.Cu".into()]); if layers.is_empty() || layers.iter().any(|l| !l.ends_with(".Cu") || l == "*.Cu") { return crate::input::refuse("bad_args", "layers are copper layers by name: [\"F.Cu\", \"In1.Cu\", \"B.Cu\"]"); }- let width = num("widthMm", 0.25);- let (via_size, via_drill) = (num("viaSizeMm", 0.6), num("viaDrillMm", 0.3));+ // the board's own width and via rules, so the route passes its DRC (a 0.25 mm default broke a 10 mil Width rule on+ // Altium's lane board, 2026-10-28); args still win+ let rules = crate::input::rules(input);+ let (width_rule, via_rule) = (crate::features::width_style(&rules), crate::features::via_style(&rules));+ let width = num("widthMm", width_rule.unwrap_or(0.25));+ let width_from = if args.get("widthMm").is_some() { "widthMm" } else if width_rule.is_some() { "the board's width rule" } else { "default 0.25" };+ let (via_size, via_drill) = (num("viaSizeMm", via_rule.map(|v| v.0).unwrap_or(0.6)), num("viaDrillMm", via_rule.map(|v| v.1).unwrap_or(0.3)));+ let via_from = if args.get("viaSizeMm").is_some() { "viaSizeMm/viaDrillMm" } else if via_rule.is_some() { "the board's via rule" } else { "default 0.6/0.3" }; if via_drill >= via_size { return crate::input::refuse("bad_args", "viaDrillMm must be smaller than viaSizeMm"); } let grid = num("gridMm", 0.5); if !(0.05..=5.0).contains(&grid) { return crate::input::refuse("bad_args", "gridMm is 0.05 to 5"); }@@ -115,7 +122,7 @@ pub fn plan_route(input: &Value) -> Value { let via_cost = num("viaCost", 5.0); let max_nodes = args["maxNodes"].as_u64().unwrap_or(50_000).clamp(1, 500_000) as usize; // the board's own clearance decides; a caller's clearanceMm can only raise it- let rule = crate::fab::min_clearance(&crate::input::rules(input));+ let rule = crate::fab::min_clearance(&rules); let clearance = num("clearanceMm", 0.0).max(rule.unwrap_or(0.2)); let edge = num("edgeClearanceMm", 0.3); let ring = crate::coppermap::outline_ring(&st);@@ -252,7 +259,7 @@ pub fn plan_route(input: &Value) -> Value { way.push(json!({"pad": key(&ends[1])})); json!({"success": true, "found": true, "lengthMm": r3(length), "vias": vias.len(), "viaAt": vias, "routeNet": {"net": net, "layer": layers[path[0].2], "width": width, "viaSize": via_size, "viaDrill": via_drill, "paths": [way]},- "planner": {"algorithm": "bounded orthogonal A*", "gridMm": grid, "layers": layers, "clearanceMm": clearance, "edgeClearanceMm": edge, "expandedNodes": seen.len()},+ "planner": {"algorithm": "bounded orthogonal A*", "widthFrom": width_from, "viaFrom": via_from, "gridMm": grid, "layers": layers, "clearanceMm": clearance, "edgeClearanceMm": edge, "expandedNodes": seen.len()}, "revision": revision, "_hint": "A plan only: pass routeNet to route_net unchanged (with expectedRevision: revision). A layer change is two waypoints at the same point, where route_net places a via. Pours (and a hatched pour's net-less pieces) are not obstacles: repour after placing the route. The path is orthogonal on the grid: a finer gridMm finds tighter paths and searches longer."}) }@@ -320,4 +327,16 @@ mod tests { assert_eq!(three["found"], json!(true), "{three}"); assert!(three["routeNet"]["paths"][0].as_array().unwrap().iter().any(|w| w["layer"] == "In1.Cu")); }+ #[test]+ fn the_track_and_via_follow_the_boards_rules() {+ // Altium's lane board: a 10 mil Width rule and a 0.5/0.3 Routing Via rule+ let rules = json!([{"enabled": true, "scope1": "All", "priority": 1, "descriptor": "Width Constraint (Min=10mil) (Max=10mil) (Preferred=10mil) (All)"},+ {"enabled": true, "scope1": "All", "priority": 1, "descriptor": "Routing Via (PreferredHoleWidth=0.3mm) (PreferedWidth=0.5mm) (All)"}]);+ let r = plan_route(&json!({"board": board(json!([])), "rules": rules, "args": {"net": "SIG", "from": "U1.1", "to": "J1.1"}}));+ assert_eq!((r["routeNet"]["width"].clone(), r["routeNet"]["viaSize"].clone()), (json!(0.254), json!(0.5)), "{r}");+ assert_eq!(r["planner"]["widthFrom"], "the board's width rule");+ let given = plan_route(&json!({"board": board(json!([])), "rules": rules, "args": {"net": "SIG", "from": "U1.1", "to": "J1.1", "widthMm": 0.4}}));+ assert_eq!(given["routeNet"]["width"], json!(0.4));+ assert_eq!(plan_route(&json!({"board": board(json!([])), "args": {"net": "SIG", "from": "U1.1", "to": "J1.1"}}))["planner"]["widthFrom"], "default 0.25");+ } }