master
Colby Knox 1.0.59: EDA Bridge Standard board model (routing_state model:standard), layer_stack, pour_state, bodies, netlist, seven… a0caf33 15d ago
//! The standard board model on the boards this page ships, checked against the parser the routing
//! verbs already use (`pcb::pads`, `segments`, `vias`): same pads at the same place, same copper.
use std::path::PathBuf;

use kicad_core::board_model::{build, revision_of_text, Opts};
use kicad_core::pcb;

fn repo(rel: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join(rel)
}

const BOARDS: &[&str] = &[
    "demo/routing/esc/esc-g431-fable-placed-planes.kicad_pcb",
    "demo/placement/esc-g431-unrouted.kicad_pcb",
    "tour-pack-rp2040/template/rp2040-breakout.kicad_pcb",
    "docs/autorouting/live-routing.kicad_pcb",
];

#[test]
fn pads_and_copper_agree_with_the_routing_parser() {
    for rel in BOARDS {
        let path = repo(rel);
        let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{rel}: {e}"));
        let legacy = pcb::parse_pcb_text(&text, &path.to_string_lossy()).unwrap();
        let k = build(&text, None, &Opts { y_up: false }).unwrap();
        let s = build(&text, None, &Opts::default()).unwrap();
        assert_eq!(k["revision"], s["revision"], "{rel}: the revision does not depend on the frame");
        let old = pcb::pads(&legacy);
        let new = k["pads"].as_array().unwrap();
        let mut checked = 0;
        for p in &old {
            if p.layers.iter().all(|l| !l.ends_with(".Cu")) {
                continue;
            }
            let hit = new.iter().find(|n| n["ref"] == p.reference.as_str() && n["name"] == p.pad.as_str()
                && (n["x"].as_f64().unwrap() - p.x).abs() < 1e-5 && (n["y"].as_f64().unwrap() - p.y).abs() < 1e-5);
            let hit = hit.or_else(|| new.iter().find(|n| n["ref"] == p.reference.as_str() && n["name"] == p.pad.as_str() && n.get("anchor").is_some()
                && (n["anchor"]["x"].as_f64().unwrap() - p.x).abs() < 1e-5 && (n["anchor"]["y"].as_f64().unwrap() - p.y).abs() < 1e-5));
            let hit = hit.unwrap_or_else(|| panic!("{rel}: pad {} at ({}, {}) not in the model", p.name, p.x, p.y));
            assert_eq!(hit["net"], p.net_name.as_str(), "{rel}: {}", p.name);
            // y-up is the same pad mirrored
            let i = new.iter().position(|n| std::ptr::eq(n, hit)).unwrap();
            let up = &s["pads"][i];
            assert!((up["y"].as_f64().unwrap() + hit["y"].as_f64().unwrap()).abs() < 1e-9);
            checked += 1;
        }
        assert!(checked > 0, "{rel}");
        assert_eq!(k["counts"]["tracks"].as_u64().unwrap() as usize, legacy.segments.len(), "{rel}");
        assert_eq!(k["counts"]["vias"].as_u64().unwrap() as usize, legacy.vias.len(), "{rel}");
        assert_eq!(k["counts"]["components"].as_u64().unwrap() as usize, legacy.footprints.len(), "{rel}");
        assert!(k["outlineEdges"].as_array().unwrap().len() >= 4, "{rel}: a closed outline");
        assert!(k.get("outlineOpenEdges").is_none(), "{rel}");
        assert!(k["pads"].as_array().unwrap().iter().all(|p| p["sizeX"].as_f64().unwrap() > 0.0 && p["sizeY"].as_f64().unwrap() > 0.0), "{rel}: no 0 x 0 pad");
    }
}

#[test]
fn revision_ignores_silkscreen_and_follows_copper() {
    let text = std::fs::read_to_string(repo("docs/autorouting/live-routing.kicad_pcb")).unwrap();
    let r0 = revision_of_text(&text).unwrap();
    let at = text.rfind(')').unwrap();
    let silk = format!("{}(gr_text \"note\" (at 1 1) (layer \"F.SilkS\") (effects (font (size 1 1))))\n)", &text[..at]);
    assert_eq!(revision_of_text(&silk).unwrap(), r0);
    let copper = format!("{}(segment (start 1 1) (end 2 1) (width 0.2) (layer \"F.Cu\") (net 0))\n)", &text[..at]);
    assert_ne!(revision_of_text(&copper).unwrap(), r0);
}