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
standard 0.7.1, engine 0.4.1: placement_check lists a part under a raised one as stacked, not a courtyard overlap, when their pads stay apart and no bodies meet in 3D (fusion-lane #19); Altium's lane and sentinel answers unchanged
8 files changed
+63−8
README.md+2@@ -157,6 +157,8 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.7.1: the engine (0.4.1)'s `placement_check` lists a part under a raised one (an Adom molecule on pins) as+`stacked`, not as a courtyard overlap, when their pads stay apart and no bodies meet in 3D (fusion-lane #19). Version 0.7.0: the engine (0.4.0) adds `height_check` and `placement_check` from the Altium bridge, reading the 3D bodies companion; both match Altium's answers on its test boards, and Fusion gets them for the first time. Version 0.6.0: the engine (0.3.0) adds `thermal_vias` and `via_fence` from the Altium bridge, as plans in `place_vias`
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.4.0"+version = "0.4.1" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.4.0"+version = "0.4.1" edition = "2021" description = "The EDA Bridge Standard's checks and board-file writers, computed from the board model JSON: no bridge calls, no tool." license = "MIT"
engine/README.md+2−2@@ -17,7 +17,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `impedance` | stack | the track width (and pair gap) for a target impedance on each copper layer; `apply` answers `add_rule` arguments | | `current_capacity` | board, stack? | per net and layer, the narrowest copper against the width the current needs (IPC-2221), and the vias a layer change needs | | `height_check` | bodies, board? | each part's height on its side, parts over `maxTopMm`/`maxBottomMm` or under a lid region (`regions`), body collisions and near misses (`minGapMm`); `noBody` and `complete: false` when a part has no 3D body |-| `placement_check` | board, bodies? | courtyard overlaps (pads and bodies, grown by `marginMm`), parts off or over the edge, pads within `edgeMm` of the edge, parts on mounting holes; `bodiesUsed: false` when no bodies were given |+| `placement_check` | board, bodies? | courtyard overlaps (pads and bodies, grown by `marginMm`), parts off or over the edge, pads within `edgeMm` of the edge, parts on mounting holes; `stacked` lists plan-view overlaps where one part sits wholly above the other (a part under a raised molecule: pads apart, no bodies meeting in 3D); `bodiesUsed: false` when no bodies were given | | `thermal_vias` | board, rules? | a grid of vias in a part's exposed pad (`ref`, `pad?`), clear of vias already there; `apply` is `place_vias` arguments | | `via_fence` | board, rules? | vias of `viaNet` both sides of the named nets' tracks and/or round the board edge (`edge`), at `pitchMm` or lambda/20 at `freqGHz`, clear of other copper; `apply` is `place_vias` arguments | @@ -88,7 +88,7 @@ adom-eda-checks import_svg --svg logo.svg --args '{"at": [10, 10], "widthMm": 20 Replies keep the standard envelope (`success`, `errorCode`, `error`, `_hint`) and name verbs without a prefix (`apply_pours`, `add_rule`, `place_vias`): a bridge adds its own. Every reply also carries `engine`-(`"adom-eda-checks 0.4.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.4.1"`), so a bridge reports the engine it runs without writing the version by hand. ## Tests
engine/src/placement.rs+54−1@@ -116,7 +116,36 @@ pub fn placement_check(input: &Value) -> Value { let holes: Vec<(f64, f64, f64)> = pads.iter().filter(|p| f(&p["drill"]) > 0.0 && p["ref"].as_str().map(|r| r.is_empty()).unwrap_or(true)) .map(|p| (f(&p["x"]), f(&p["y"]), f(&p["sizeX"]).max(f(&p["drill"])) / 2.0)).collect(); let num = |k: &str, d: f64| args.get(k).and_then(|v| v.as_f64()).unwrap_or(d);- let mut out = review(&boxes, &pads, &ring, &holes, num("marginMm", 0.1), num("edgeMm", 0.5));+ let margin = num("marginMm", 0.1);+ let mut out = review(&boxes, &pads, &ring, &holes, margin, num("edgeMm", 0.5));+ // a part under a raised one (an Adom molecule on pins, a connector's overhang) overlaps it in plan view only: when+ // their pads stay clear and no body of one meets a body of the other in 3D, it is a stack, listed apart, not an+ // overlap (fusion-lane #19). A part with no body cannot be shown stacked, so its overlaps stand.+ if let Some(bs) = &given {+ // each pad's own box, turned with it: a molecule's pins span the part under it, so the union of its pads would too+ let pad_rects = |r: &str| -> Vec<Rect> { pads.iter().filter(|p| p["ref"].as_str() == Some(r)).map(|p| {+ let (c, s) = (f(&p["rotation"]).to_radians().cos().abs(), f(&p["rotation"]).to_radians().sin().abs());+ let (hx, hy) = ((f(&p["sizeX"]) * c + f(&p["sizeY"]) * s) / 2.0, (f(&p["sizeX"]) * s + f(&p["sizeY"]) * c) / 2.0);+ (f(&p["x"]) - hx, f(&p["y"]) - hy, f(&p["x"]) + hx, f(&p["y"]) + hy)+ }).collect() };+ let meets = |a: &str, b: &str| {+ let (ba, bb): (Vec<_>, Vec<_>) = (bs.iter().filter(|x| x.part == a).collect(), bs.iter().filter(|x| x.part == b).collect());+ if ba.is_empty() || bb.is_empty() { return true; }+ ba.iter().any(|p| bb.iter().any(|q| hit(grow((p.x0, p.y0, p.x1, p.y1), margin), grow((q.x0, q.y0, q.x1, q.y1), margin)) && p.z0.max(q.z0) < p.z1.min(q.z1)))+ };+ let (mut real, mut stacked) = (Vec::new(), Vec::new());+ for o in out["courtyardOverlaps"].as_array().cloned().unwrap_or_default() {+ let (a, b) = (o["parts"][0].as_str().unwrap_or(""), o["parts"][1].as_str().unwrap_or(""));+ let (pa, pb) = (pad_rects(a), pad_rects(b));+ let pads_meet = pa.is_empty() || pb.is_empty() || pa.iter().any(|x| pb.iter().any(|y| hit(grow(*x, margin), grow(*y, margin))));+ if pads_meet || meets(a, b) { real.push(o) } else { stacked.push(o) }+ }+ if !stacked.is_empty() {+ out["ok"] = json!(real.is_empty() && out["offOrOverEdge"].as_array().map(|a| a.is_empty()).unwrap_or(true) && out["onMountingHoles"].as_array().map(|a| a.is_empty()).unwrap_or(true));+ }+ out["courtyardOverlaps"] = json!(real);+ out["stacked"] = json!(stacked);+ } out["success"] = json!(true); out["bodiesUsed"] = json!(given.is_some()); out["_hint"] = json!("courtyardOverlaps: parts closer than twice marginMm (their pads and bodies): move them apart (move_parts, find_space). offOrOverEdge parts must come onto the board; nearEdge pads risk the router or the V-cut (JLCPCB: 0.4 mm from a V-cut, 0.2 from a routed edge). onMountingHoles: a screw head lands on the part. Without bodies (bodiesUsed false) a part's box is its pads only: pass the bodies companion to count overhanging bodies.");@@ -148,4 +177,28 @@ mod tests { assert_eq!(r["nearEdge"][0]["ref"], "U1"); assert_eq!(r["ok"], json!(false)); }++ #[test]+ fn a_part_under_a_raised_one_is_stacked_not_overlapping() {+ let pad = |r: &str, x: f64, y: f64| json!({"ref": r, "x": x, "y": y, "sizeX": 0.6, "sizeY": 0.6, "rotation": 0.0});+ // M1: a molecule on two pins at x 0 and 10, its board raised 3 to 4 mm; C1 sits under it, between the pins+ let st = json!({"outline": [[-5, -5], [15, -5], [15, 5], [-5, 5]],+ "outlineEdges": [{"kind": "line", "start": [-5.0, -5.0]}, {"kind": "line", "start": [15.0, -5.0]}, {"kind": "line", "start": [15.0, 5.0]}, {"kind": "line", "start": [-5.0, 5.0]}],+ "components": [{"ref": "M1", "side": "top"}, {"ref": "C1", "side": "top"}],+ "pads": [pad("M1", 0.0, 0.0), pad("M1", 10.0, 0.0), pad("C1", 5.0, 0.0), pad("C1", 6.0, 0.0)]});+ let body = |r: &str, x0: f64, x1: f64, z0: f64, z1: f64| json!({"ref": r, "side": "top", "x0": x0, "y0": -2.0, "x1": x1, "y1": 2.0, "z0": z0, "z1": z1});+ let stacked = placement_check(&json!({"board": st, "bodies": {"bodies": [body("M1", -1.0, 11.0, 3.0, 4.0), body("C1", 4.5, 6.5, 0.0, 1.0)]}, "args": {}}));+ assert_eq!(stacked["courtyardOverlaps"].as_array().unwrap().len(), 0, "{stacked}");+ assert_eq!(stacked["stacked"][0]["parts"], json!(["C1", "M1"]));+ assert_eq!(stacked["ok"], json!(true));+ // the same plan, but C1 tall enough to reach the raised board: a real overlap+ let real = placement_check(&json!({"board": st, "bodies": {"bodies": [body("M1", -1.0, 11.0, 3.0, 4.0), body("C1", 4.5, 6.5, 0.0, 3.5)]}, "args": {}}));+ assert_eq!(real["courtyardOverlaps"].as_array().unwrap().len(), 1);+ assert_eq!(real["ok"], json!(false));+ // no bodies: pads only, no stack possible to show, nothing hidden+ let none = placement_check(&json!({"board": st, "args": {}}));+ assert_eq!(none["bodiesUsed"], json!(false));+ assert!(none.get("stacked").is_none());+ } }+
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.7.0",+ "version": "0.7.1", "type": "skill", "title": "EDA Bridge Standard: one way for an AI to drive every PCB tool", "description": "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.",
verbs/coverage.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.7.0",+ "standard": "0.7.1", "reference": { "bridge": "altium", "version": "0.2.78"
verbs/standard.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.7.0",+ "standard": "0.7.1", "reference": { "bridge": "altium", "version": "0.2.78"