← Commit history

standard 0.17.1: engine 0.14.1, find_space check matches placement_check

Colby Knox ·337fb00c55 ·12d ago ·parent a3c42e6
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"