← Commit history

standard 0.22.2: engine 0.19.1, land pattern SOT-23-5 pins and courtyard (#25)

Colby Knox ·1bbf323d2e ·12d ago ·parent 13b33ab
8 files changed +25−13
README.md+2
@@ -158,6 +158,8 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue  ## Status +Version 0.22.2: the engine (0.19.1)'s `footprint_from_land_pattern` lines SOT-23-5's pins 4 and 5 up with 3 and 1, and draws the+courtyard round the pads and body only, not the silkscreen pin-1 dot (kicad-lane #25). Version 0.22.1: regenerated with fusion-bridge 1.13.15 (Altium-only verbs marked unsupported; swap_footprint, set_models, add_models). Version 0.22.0: the engine (0.19.0) adds `panelize` (the Altium bridge's panel plan: V-score or mouse bites, rails, tooling and JLCPCB's rules) and `teardrops` (a planner of its own: Altium uses its built-in command), as plans every bridge can apply
engine/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-eda-checks"-version = "0.19.0"+version = "0.19.1" dependencies = [  "roxmltree",  "serde_json",
engine/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.19.0"+version = "0.19.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
@@ -23,7 +23,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `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 |-| `footprint_from_land_pattern` | args only | a footprint laid out from a datasheet's recommended land pattern (`family` chip, dual, sot, quad or bga, with pitch, span, pad size, `exposedPad`, `body`): pads numbered the standard way (pin 1 top-left, counterclockwise; BGA rows without I, O, Q, S, X, Z), silkscreen (`overlay`) kept `silkClearance` from copper with a pin-1 dot, a `fab` outline with a pin-1 chamfer and the `courtyard` grown by its margin, in mm, Y up, origin at the centre. A missing number is refused by name. Each bridge writes it in its own library format. From the Altium bridge's footprint.rs |+| `footprint_from_land_pattern` | args only | a footprint laid out from a datasheet's recommended land pattern (`family` chip, dual, sot, quad or bga, with pitch, span, pad size, `exposedPad`, `body`): pads numbered the standard way (pin 1 top-left, counterclockwise; BGA rows without I, O, Q, S, X, Z), a shorter column spread over the longer one's span (SOT-23-5's pins 4 and 5 level with 3 and 1), silkscreen (`overlay`) kept `silkClearance` from copper with a pin-1 dot, a `fab` outline with a pin-1 chamfer and the `courtyard` round the pads and body grown by its margin, in mm, Y up, origin at the centre. A missing number is refused by name. Each bridge writes it in its own library format. From the Altium bridge's footprint.rs | | `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 | | `panelize` | board, stack? | a panel plan in panel mm from its lower-left corner: `rows` x `cols` copies (each with its `copyShift`), rails (`railMm`) with three tooling holes and fiducials, and `separation` none, `vscore` (V-CUT lines edge to edge with their labels) or `tabs` (mouse-bite holes and the rout path), with JLCPCB's V-cut or mouse-bite rules checked and copper near the edge or outside the outline named. Refuses tabs when the board draws its outline on a mechanical layer. From the Altium bridge's panel.rs | | `pin_pad_check` | board, sheetParts | each symbol's pins against its footprint's pads: pins with no pad (a net that can never reach copper), pads with no pin (mounting and exposed pads excepted), parts only on the sheets or only on the board. From the Altium bridge's schreview.rs |@@ -115,7 +115,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.19.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.19.1"`), so a bridge reports the engine it runs without writing the version by hand.  ## Tests 
engine/src/landpattern.rs+16−6
@@ -41,9 +41,14 @@ pub fn pads(a: &Value) -> Result<Vec<Pad>, String> {                 if n % 2 != 0 { return Err(format!("dual needs an even pin count, got {n}")); }                 (n / 2, n / 2)             };-            // left column top to bottom, then right column bottom to top (counter-clockwise from pin 1 top-left)-            for i in 0..left { push((i + 1).to_string(), -span / 2.0, ((left as f64 - 1.0) / 2.0 - i as f64) * pitch, pl, pw); }-            for j in 0..right { push((left + j + 1).to_string(), span / 2.0, (j as f64 - (right as f64 - 1.0) / 2.0) * pitch, pl, pw); }+            // left column top to bottom, then right column bottom to top (counter-clockwise from pin 1 top-left). A shorter+            // column spreads over the longer one's span, its middle left empty: SOT-23-5's pins 4 and 5 line up with 3 and+            // 1 (kicad-lane #25); a single pin sits in the middle (SOT-23)+            let n = left.max(right);+            let step = |count: usize| if count > 1 && count < n { (n as f64 - 1.0) * pitch / (count as f64 - 1.0) } else { pitch };+            let (ls, rs) = (step(left), step(right));+            for i in 0..left { push((i + 1).to_string(), -span / 2.0, ((left as f64 - 1.0) / 2.0 - i as f64) * ls, pl, pw); }+            for j in 0..right { push((left + j + 1).to_string(), span / 2.0, (j as f64 - (right as f64 - 1.0) / 2.0) * rs, pl, pw); }         }         "quad" => {             let (pitch, pl, pw) = (need("pitch")?, need("padLength")?, need("padWidth")?);@@ -148,10 +153,9 @@ pub fn layout(a: &Value) -> Result<Fp, String> {         let r = 0.15;         let (dx, dy) = if family == "bga" { (x0 - clear - r, y1 + clear + r) } else { (p1.x - p1.w / 2.0 - clear - r - silk_w, p1.y) };         fp.discs.push(("overlay", r3(dx), r3(dy), r));-        x0 = x0.min(dx - r);-        y1 = y1.max(dy + r);     }-    // courtyard: everything grown by the margin, on a 0.01 mm grid+    // courtyard: the pads and the body grown by the margin, on a 0.01 mm grid. The silkscreen pin-1 dot may sit outside it+    // (IPC-7351; kicad-lane #25: holding the dot pushed a 7 mm BGA's courtyard 0.5 mm out on two sides)     let g = |v: f64, up: bool| if up { (v * 100.0).ceil() / 100.0 } else { (v * 100.0).floor() / 100.0 };     let c = (g(x0 - margin, false), g(y0 - margin, false), g(x1 + margin, true), g(y1 + margin, true));     fp.courtyard = c;@@ -219,6 +223,9 @@ mod tests {     fn sot23_has_two_pins_left_and_one_right() {         let p = pads(&json!({"family": "sot", "leftPins": 2, "rightPins": 1, "pitch": 1.9, "span": 2.2, "padLength": 1.0, "padWidth": 0.6})).unwrap();         assert_eq!(p.iter().map(|q| (q.name.clone(), q.x, q.y)).collect::<Vec<_>>(), vec![("1".into(), -1.1, 0.95), ("2".into(), -1.1, -0.95), ("3".into(), 1.1, 0.0)]);+        // SOT-23-5: pins 4 and 5 line up with 3 and 1 (kicad-lane #25)+        let q = pads(&json!({"family": "sot", "leftPins": 3, "rightPins": 2, "pitch": 0.95, "span": 2.6, "padLength": 1.1, "padWidth": 0.6})).unwrap();+        assert_eq!(q.iter().map(|p| (p.name.clone(), p.y)).collect::<Vec<_>>(), vec![("1".into(), 0.95), ("2".into(), 0.0), ("3".into(), -0.95), ("4".into(), -0.95), ("5".into(), 0.95)]);     }      #[test]@@ -249,6 +256,9 @@ mod tests {         let c = fp.courtyard;         assert!(fp.pads.iter().all(|p| { let (a0, b0, a1, b1) = ext(p); a0 >= c.0 && b0 >= c.1 && a1 <= c.2 && b1 <= c.3 }));         assert_eq!(fp.discs.len(), 1);+        // the courtyard holds the pads and the body, not the pin-1 dot (kicad-lane #25): a 7 mm BGA is symmetric+        let bga = layout(&json!({"family": "bga", "rows": 8, "cols": 8, "pitch": 0.8, "ballPad": 0.4, "body": {"w": 7, "l": 7}})).unwrap();+        assert_eq!(bga.courtyard, (-3.75, -3.75, 3.75, 3.75));         assert!(fp.discs[0].1 < -2.7 - 0.775);      }
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "eda-bridge-standard",-  "version": "0.22.1",+  "version": "0.22.2",   "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.22.1",+ "standard": "0.22.2",  "reference": {   "bridge": "altium",   "version": "0.2.81"
verbs/standard.json+1−1
@@ -1,5 +1,5 @@ {- "standard": "0.22.1",+ "standard": "0.22.2",  "reference": {   "bridge": "altium",   "version": "0.2.81"