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.17.1: engine 0.14.1, find_space check matches placement_check
9 files changed
+32−12
README.md+2@@ -158,6 +158,8 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.17.1: the engine (0.14.1)'s `find_space {check: true}` judges overlaps and the outline as placement_check does: parts+stacked under a raised molecule apart, off the outline by pad centres (fusion-lane on BQ25792). Version 0.17.0: the engine (0.14.0) adds `find_space` and `check_orientation` from the Altium bridge. On the Bluetooth Sentinel the spots found near U4 keep the 0.25 mm clearance from pads, routed tracks and vias (checked independently). Version 0.16.0: the engine (0.13.0) adds `auto_classes` and `silk_designators` from the Altium bridge. On the Bluetooth Sentinel
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.14.0"+version = "0.14.1" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.14.0"+version = "0.14.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@@ -18,7 +18,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `copper_balance` | board, stack, pours | copper per layer and how evenly mirrored layers match (bow and twist) | | `return_path` | board, stack, pours | signal tracks running over a gap in the neighbouring layer's reference copper, with a pour plan that closes each gap (`apply_pours`) | | `estimate_cost` | board, stack?, rules?, prices? | the numbers a fab quote needs (size, copper layers, thickness from the stack, smallest track, gap from the clearance rule, smallest drill, holes, SMD and through-hole parts and joints, unique parts, assembled sides) and, with a `prices` record, a rough table at `quantities` (default 5, 10, 30, 100), with assembly when asked, every assumption stated and the record named in `source`. The engine carries no rates: without `prices` the reply is the numbers only. From the KiCad bridge's fab.rs |-| `find_space` | board, bodies? | free spots for a part (`part`, or `sizeMm`) near another (`near`) or a point (`at`): clear of other part boxes on that side by `clearanceMm`, of that side's routed tracks and the vias, `edgeMm` inside the outline (arcs sampled), nearest first, as where the part's origin goes; `check: true` lists parts that overlap or leave the outline. Pours do not block (they re-pour). From the Altium bridge's design.rs |+| `find_space` | board, bodies? | free spots for a part (`part`, or `sizeMm`) near another (`near`) or a point (`at`): clear of other part boxes on that side by `clearanceMm`, of that side's routed tracks and the vias, `edgeMm` inside the outline (arcs sampled), nearest first, as where the part's origin goes; `check: true` lists parts that overlap (and, with bodies, those `stacked` under a raised one) or have pads off the outline, as placement_check judges them. Pours do not block (they re-pour). From the Altium bridge's design.rs | | `fab_check` | board, stack?, rules?, silk?, fab? | the board's smallest features, compared with a fab's limits from adom/pcb-design-rules; each finding names the fab's page and the date it was read | | `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 |@@ -102,7 +102,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.14.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.14.1"`), so a bridge reports the engine it runs without writing the version by hand. ## Tests
engine/src/space.rs+22−4@@ -44,12 +44,22 @@ pub fn find_space(input: &Value) -> Value { [(r.0, r.1), (r.2, r.1), (r.2, r.3), (r.0, r.3)].iter().all(|&c| crate::route::inside(c, &ring) && (0..ring.len()).all(|i| seg(c, ring[i], ring[(i + 1) % ring.len()]) >= edge)) }; if args["check"].as_bool() == Some(true) {- let mut overlaps = Vec::new();+ // the same tests placement_check makes (fusion-lane): a part under a raised one (an Adom molecule on pins) whose+ // pads stay apart and whose bodies do not meet in 3D is stacked, not overlapping; a part is off the outline by+ // its pads' centres, not its box (a round board's edge cuts a box corner off a part well inside it)+ let pads = st["pads"].as_array().cloned().unwrap_or_default();+ let bodies = crate::bodies::input_boxes(input);+ let (mut overlaps, mut stacked) = (Vec::new(), Vec::new()); for i in 0..parts.len() { for j in i + 1..parts.len() {- if parts[i].2 == parts[j].2 && hits(parts[i].1, parts[j].1) { overlaps.push(json!([parts[i].0, parts[j].0])); }+ if parts[i].2 != parts[j].2 || !hits(parts[i].1, parts[j].1) { continue; }+ let pair = json!([parts[i].0, parts[j].0]);+ match &bodies { Some(bs) if crate::placement::is_stacked(&pads, bs, &parts[i].0, &parts[j].0, 0.0) => stacked.push(pair), _ => overlaps.push(pair) } } }- let outside: Vec<&String> = parts.iter().filter(|p| !in_outline(p.1)).map(|p| &p.0).collect();- return json!({"success": true, "overlaps": overlaps, "outsideOutline": outside, "parts": parts.len(),+ let outside: Vec<&String> = parts.iter().filter(|p| {+ let own: Vec<&Value> = pads.iter().filter(|q| q["ref"].as_str() == Some(p.0.as_str())).collect();+ ring.len() >= 3 && !own.is_empty() && own.iter().any(|q| !crate::route::inside((f(&q["x"]), f(&q["y"])), &ring))+ }).map(|p| &p.0).collect();+ return json!({"success": true, "overlaps": overlaps, "stacked": stacked, "outsideOutline": outside, "parts": parts.len(), "bodiesUsed": bodies.is_some(), "_hint": "Overlaps compare part boxes (pads, and 3D bodies when given) on the same side; placement_check adds margins, stacked parts and mounting holes. find_space {near, part} finds a free spot; move_parts moves it."}); } let comp = |d: &str| st["components"].as_array().and_then(|a| a.iter().find(|c| c["ref"].as_str().map(|r| r.eq_ignore_ascii_case(d)).unwrap_or(false)).cloned());@@ -216,6 +226,14 @@ mod tests { } let chk = find_space(&json!({"board": board(), "args": {"check": true}})); assert_eq!((chk["overlaps"].as_array().unwrap().len(), chk["outsideOutline"].as_array().unwrap().len()), (0, 0));+ // a part under a raised molecule: pads apart, bodies apart in height, so stacked, not an overlap (fusion-lane)+ let mut m = board();+ m["components"].as_array_mut().unwrap().push(json!({"ref": "M1", "x": 10.0, "y": 10.0, "rotation": 0.0, "side": "top"}));+ m["pads"].as_array_mut().unwrap().extend([pad("M1", "1", 6.0, 10.0, "P"), pad("M1", "2", 14.0, 10.0, "Q")]);+ let bodies = json!({"bodies": [{"ref": "M1", "side": "top", "x0": 5.5, "y0": 8.0, "x1": 14.5, "y1": 12.0, "z0": 3.0, "z1": 4.0},+ {"ref": "R1", "side": "top", "x0": 8.7, "y0": 9.7, "x1": 11.3, "y1": 10.3, "z0": 0.0, "z1": 0.5}]});+ let s = find_space(&json!({"board": m, "bodies": bodies, "args": {"check": true}}));+ assert_eq!((s["overlaps"].as_array().unwrap().len(), s["stacked"].clone(), s["bodiesUsed"].clone()), (0, json!([["M1", "R1"]]), json!(true)), "{s}"); assert_eq!(find_space(&json!({"board": board(), "args": {"near": "Q9", "sizeMm": [1, 1]}}))["errorCode"], "no_such_part"); } }
engine/src/writers/ipc2581.rs+1−1@@ -17,7 +17,7 @@ fn n(v: f64) -> String { fn ascii(t: &str) -> String { t.chars().map(|c| match c { c if (c as u32) < 128 => c.to_string(),- 'Ω' | 'Ω' => "ohm".into(), 'µ' | 'μ' => "u".into(), '±' => "+/-".into(), '°' => "deg".into(), '–' | '—' => "-".into(),+ '\u{03A9}' | '\u{2126}' => "ohm".into(), '\u{00B5}' | '\u{03BC}' => "u".into(), '\u{00B1}' => "+/-".into(), '\u{00B0}' => "deg".into(), '\u{2013}' | '\u{2014}' => "-".into(), _ => "?".into(), }).collect() }
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.17.0",+ "version": "0.17.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.17.0",+ "standard": "0.17.1", "reference": { "bridge": "altium", "version": "0.2.80"
verbs/standard.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.17.0",+ "standard": "0.17.1", "reference": { "bridge": "altium", "version": "0.2.80"