← Commit history

1.0.55: 1.0.55 REVERTS the 1.0.54 pad change for wiki #119, which was wrong. KiCad's own DRC on KiCad 10.0.2 and 10.0.5 shows that a footprint on B.Cu whose SMD pads are on F.Cu keeps those pads on the FRONT copper at their stored positions (its connectivity report names them 'on F.Cu'; back-layer tracks reach them neither at the stored nor at the mirrored spot). The bridge's literal reading, as in 1.0.53, matches KiCad; 1.0.54 moved such pads to the back and mirrored them, disagreeing with KiCad. New instead: kicad_board_pads and kicad_routing_state list backFootprintsWithFrontPads with a hint, because such a footprint is a part placed on the back without being flipped the way KiCad flips it (KiCad's flip moves the pads to B.*). 1.0.54's other changes stay: the product name out of hints, examples and docs (wiki #118), and the silk hints (wiki #104).

John Lauer ·22ffdca42a ·6d ago ·parent a11021b
1 file changed +20−29
rust/crates/kicad-core/src/pcb.rs+20−29
@@ -831,11 +831,8 @@ pub fn pads(board: &Board) -> Vec<Pad> {         let (fx, fy, rot) = fp.position.as_ref().map(|p| (p.x, p.y, p.rotation.unwrap_or(0.0))).unwrap_or((0.0, 0.0, 0.0));         let ang = rot.to_radians();         let (sin_a, cos_a) = ang.sin_cos();-        let unflipped = stored_unflipped(fp);         for pad in &fp.pads {             let (Some(px), Some(py)) = (pad.x, pad.y) else { continue };-            // wiki #119: a back-side footprint stored in the FRONT frame is mirrored by KiCad at load.-            let px = if unflipped { -px } else { px };             let net_number = pad.net_number.flatten();             let net_name = pad.net_name.clone().unwrap_or_default();             out.push(Pad {@@ -848,7 +845,7 @@ pub fn pads(board: &Board) -> Vec<Pad> {                 net_key: if pad.has_net() { net_key(board, net_number, Some(&net_name)) } else { net_key(board, None, None) },                 net_name,                 kind: pad.kind.clone(),-                layers: if unflipped { flip_layers(&pad.layers.clone().unwrap_or_default()) } else { pad.layers.clone().unwrap_or_default() },+                layers: pad.layers.clone().unwrap_or_default(),                 side: fp.layer.clone().unwrap_or_default(),                 footprint: fp.footprint.clone(),             });@@ -857,15 +854,14 @@ pub fn pads(board: &Board) -> Vec<Pad> {     out } -/// Wiki #119. KiCad saves a footprint flipped in KiCad with its pads already on the back-/// layers (`B.Cu`, `B.Mask`, ...): read literally, those positions are right (verified against-/// KiCad's own StickHub demo, KiCad 10 format: 123 of 123 back-side pads have their own net's-/// copper ending exactly where the literal transform puts them, 0 at the mirrored spot).-/// A footprint WRITTEN by another tool can sit on `B.Cu` while its SMD pads still say `F.Cu`, in the-/// front frame; KiCad mirrors those on load (the reporter's native DRC proved it). That combination-/// is unambiguous, because a back-side part's SMD pads cannot really be on the front copper, so only-/// then are the pads mirrored (local x negated) and moved to the back layers.-pub fn stored_unflipped(fp: &Footprint) -> bool {+/// Wiki #119, measured with KiCad's own DRC on KiCad 10.0.2 and 10.0.5: a footprint on `B.Cu` whose+/// SMD pads say `F.Cu` keeps those pads on the FRONT copper at their literal positions ("Pad 1 [N1] of R1+/// on F.Cu" in KiCad's connectivity report; back-layer tracks connect to them neither at the literal nor+/// at the mirrored spot). The bridge reads them the same way, which is right. Such a footprint is+/// almost always a part placed on the back by a tool that did not flip it the way KiCad does (KiCad's+/// own flip moves the pads to `B.*`: its StickHub demo, 123 of 123 back-side pads). This finds them so+/// replies can say so, instead of letting routing fail on copper that is really on the other side.+pub fn back_footprint_with_front_pads(fp: &Footprint) -> bool {     if fp.layer.as_deref() != Some("B.Cu") {         return false;     }@@ -875,15 +871,13 @@ pub fn stored_unflipped(fp: &Footprint) -> bool {     }) } -/// F.* <-> B.* for a pad's layer list (inner layers and wildcards unchanged).-pub fn flip_layers(layers: &[String]) -> Vec<String> {-    layers.iter().map(|l| {-        if let Some(rest) = l.strip_prefix("F.") { format!("B.{rest}") }-        else if let Some(rest) = l.strip_prefix("B.") { format!("F.{rest}") }-        else { l.clone() }-    }).collect()+/// References of every footprint `back_footprint_with_front_pads` finds, for a reply warning.+pub fn back_footprints_with_front_pads(board: &Board) -> Vec<String> {+    board.footprints.iter().filter(|f| back_footprint_with_front_pads(f)).map(|f| f.reference.clone()).collect() } +pub const FRONT_PADS_HINT: &str = "These footprints sit on B.Cu but their SMD pads are on F.Cu. KiCad keeps such pads on the FRONT copper at their stored positions (checked with KiCad's own DRC on 10.0.2 and 10.0.5), so back-layer copper cannot reach them and a via is checked against them where they really are. A footprint flipped in KiCad has its pads on B.Cu; one written by another tool must flip it the same way: move every pad layer F.* to B.* and negate each pad's local x (and mirror its angle), or flip the part in KiCad (select, F).";+ pub fn copper_layers(board: &Board) -> Vec<String> {     board.layers.iter().filter(|l| l.name.ends_with(".Cu")).map(|l| l.name.clone()).collect() }@@ -1348,18 +1342,14 @@ pub(crate) mod tests {     }      #[test]-    fn a_back_side_footprint_stored_in_the_front_frame_is_mirrored_like_kicad_does() {-        // wiki #119's QFN on B.Cu at (52,153) rot 0, written by another tool with F.Cu pads. KiCad-        // mirrors local x: pad 19 (local -1.00) lands at 52 - (-1) = 53.0, pad 24 (local +1.00) at 51.0.-        // That is why the bridge, reading literally, said a via on pad 19 "would touch pad U1.24".+    fn a_back_side_footprint_with_front_pads_is_read_as_kicad_reads_it_and_flagged() {+        // KiCad 10.0.2 and 10.0.5 keep these pads on F.Cu at the literal position (wiki #119).         let b = qfn_board("\"F.Cu\" \"F.Mask\" \"F.Paste\"");         let p = pads(&b);         let p19 = p.iter().find(|x| x.pad == "19").unwrap();-        let p24 = p.iter().find(|x| x.pad == "24").unwrap();-        assert_eq!((p19.x, p19.y), (53.0, 156.44));-        assert_eq!((p24.x, p24.y), (51.0, 156.44));-        assert!(p19.layers.contains(&"B.Cu".to_string()) && !p19.layers.contains(&"F.Cu".to_string()));-        assert!(pad_reaches(Some(p19), "B.Cu") && !pad_reaches(Some(p19), "F.Cu"));+        assert_eq!((p19.x, p19.y), (51.0, 156.44));+        assert!(pad_reaches(Some(p19), "F.Cu") && !pad_reaches(Some(p19), "B.Cu"));+        assert_eq!(back_footprints_with_front_pads(&b), vec!["U1".to_string()]);     }      #[test]@@ -1373,6 +1363,7 @@ pub(crate) mod tests {         assert_eq!((p19.x, p19.y), (51.0, 156.44));         assert_eq!((p24.x, p24.y), (53.0, 156.44));         assert!(pad_reaches(Some(p19), "B.Cu"));+        assert!(back_footprints_with_front_pads(&b).is_empty());     }      fn repo_root() -> PathBuf {