main
John Lauer Publish 0.2.50 01ab14c
use aiflow_board::Board;
use aiflow_router::{Router, Rules};

fn fixture() -> Router {
    let board = Board::from_text("regression.kicad_pcb", r#"(kicad_pcb
      (layers (0 "F.Cu" signal) (31 "B.Cu" signal))
      (net 1 "SIG")
      (gr_rect (start 0 0) (end 12 12) (layer "Edge.Cuts"))
      (footprint "test" (layer "F.Cu") (at 5 5) (property "Reference" "U1")
        (pad "1" smd rect (at -2 0) (size 0.25 1) (layers "F.Cu") (net 1 "SIG"))
        (pad "2" smd rect (at 0 0) (size 1 1) (layers "F.Cu"))
        (pad "3" smd rect (at 2 0) (size 0.25 1) (layers "F.Cu") (net 1 "SIG"))))"#.into()).unwrap();
    Router::new(board, Rules::default(), false)
}

#[test]
fn no_net_copper_blocks_tracks_and_vias_without_inventing_a_net() {
    let mut r = fixture();
    assert_eq!(r.nets.len(), 1);
    assert_eq!(r.board.pad_by_key("U1.2").unwrap().net, "");
    let (i,j) = r.grid.to_cell(5.,5.);
    let win = (0,0,r.grid.w as i64-1,r.grid.h as i64-1);
    let (orth, diagonal, _, via) = r.obstacle_maps(0,0.25,win,false);
    assert!(orth[0].get(i as usize,j as usize));
    assert!(diagonal[0].get(i as usize,j as usize));
    assert!(!via.get(i as usize,j as usize));
}

#[test]
fn released_clear_escape_becomes_searchable_again() {
    let mut r=fixture();
    let c=r.nets[0].clusters.iter().position(|c| !c.escapes.is_empty()).expect("narrow-pad escape");
    let t=*r.nets[0].clusters[c].escapes.keys().next().unwrap();
    r.nets[0].clusters[c].terminals.retain(|x| *x!=t);
    r.nets[0].clusters[c].released.insert(t);
    r.reserve_escapes(0,c);
    assert!(r.nets[0].clusters[c].terminals.contains(&t));
    assert!(!r.nets[0].clusters[c].released.contains(&t));
    r.reserve_escapes(0,c);
    assert_eq!(r.nets[0].clusters[c].terminals.iter().filter(|x| **x==t).count(),1);
}

/// Issue #36: an unnumbered, no-net NPTH mounting hole (KiCad 7 "*.Cu" and KiCad 8+ "F&B.Cu"
/// layer spellings) stays a router obstacle: no via on or beside it, no track through it.
#[test]
fn unnumbered_npth_mounting_hole_blocks_vias_and_tracks() {
    for layers in ["\"*.Cu\" \"*.Mask\"", "\"F&B.Cu\" \"*.Mask\""] {
        let board = Board::from_text("npth.kicad_pcb", format!(r#"(kicad_pcb
          (layers (0 "F.Cu" signal) (31 "B.Cu" signal))
          (net 1 "SIG")
          (gr_rect (start 0 0) (end 20 12) (layer "Edge.Cuts"))
          (footprint "test" (layer "F.Cu") (at 3 6) (property "Reference" "J1")
            (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "SIG")))
          (footprint "test" (layer "F.Cu") (at 17 6) (property "Reference" "J2")
            (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "SIG")))
          (footprint "MountingHole" (layer "F.Cu") (at 10 6) (property "Reference" "H1")
            (pad "" np_thru_hole circle (at 0 0) (size 3.2 3.2) (drill 3.2) (layers {layers}))))"#)).unwrap();
        let hole = board.pads.iter().find(|p| p.reference == "H1").expect("the unnumbered hole is kept");
        assert!(hole.net.is_empty() && hole.thru);
        let mut r = Router::new(board, Rules::default(), false);
        let win = (0, 0, r.grid.w as i64 - 1, r.grid.h as i64 - 1);
        let (orth, _, _, via_ok) = r.obstacle_maps(0, 0.25, win, false);
        // the hole's centre, and a point just outside its edge (a via there would overlap the drill)
        for x in [10.0, 10.0 + 1.6 + 0.2] {
            let (i, j) = r.grid.to_cell(x, 6.0);
            assert!(!via_ok.get(i as usize, j as usize), "{layers}: via allowed at x={x}");
        }
        let (i, j) = r.grid.to_cell(10.0, 6.0);
        assert!(orth[0].get(i as usize, j as usize) && orth[1].get(i as usize, j as usize), "{layers}: track allowed through the hole");
    }
}

#[test]
fn edge_cuts_bore_is_outside_the_board_and_keeps_edge_clearance() {
    // issue #77: an outline drawn in lines with a round bore (gr_circle) on Edge.Cuts. The outline is
    // the outer loop; the bore is a cutout the router may not route through or hug.
    let board = Board::from_text("bore.kicad_pcb", r#"(kicad_pcb
      (layers (0 "F.Cu" signal) (31 "B.Cu" signal))
      (net 1 "SIG")
      (gr_line (start 0 0) (end 20 0) (layer "Edge.Cuts"))
      (gr_line (start 20 0) (end 20 12) (layer "Edge.Cuts"))
      (gr_line (start 20 12) (end 0 12) (layer "Edge.Cuts"))
      (gr_line (start 0 12) (end 0 0) (layer "Edge.Cuts"))
      (gr_circle (center 10 6) (end 12 6) (layer "Edge.Cuts"))
      (footprint "test" (layer "F.Cu") (at 3 6) (property "Reference" "J1")
        (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "SIG"))))"#.into()).unwrap();
    assert_eq!(board.outline_cutouts.len(), 1);
    assert!(aiflow_board::geom::polygon_area(&board.outline).abs() > 200.0, "the outline is the outer rectangle");
    let r = Router::new(board, Rules::default(), false);
    let (i, j) = r.grid.to_cell(10.0, 6.0);
    assert!(!r.inside.get(i as usize, j as usize), "the bore's centre is not board");
    let (i, j) = r.grid.to_cell(12.3, 6.0);
    assert!(r.inside.get(i as usize, j as usize), "just outside the bore is board");
    assert!(r.edge_dist[j as usize * r.grid.w + i as usize] < 0.5, "and it is close to an edge");
    let (i, j) = r.grid.to_cell(3.0, 3.0);
    assert!(r.inside.get(i as usize, j as usize));
}

/// Issue #140, the reported panic (lib.rs:842 in 0.2.49, "index out of bounds" in obstacle_maps): a plane
/// net's pad on a part parked outside the outline made a via-search window wholly past the grid. Now the
/// part's pads are set aside and reported, with the part named.
#[test]
fn plane_pad_on_a_part_off_the_board_does_not_panic() {
    let t = include_str!("../../../tests/fixtures/bottom-side.kicad_pcb").replace("(footprint \"T:R3\"\n\t\t(layer \"F.Cu\")\n\t\t(at 21 15)", "(footprint \"T:R3\"\n\t\t(layer \"F.Cu\")\n\t\t(at 61 46)");
    let b = aiflow_board::Board::from_text("x.kicad_pcb", t).unwrap();
    let mut rules = aiflow_router::Rules::default();
    rules.planes.insert("GND".into(), "B.Cu".into());
    let mut r = aiflow_router::Router::new(b, rules, false);
    let (_, report, failed) = aiflow_router::drive::route_board(&mut r, 2, 4, 40);
    let un = report["unrouted"].as_array().unwrap();
    for key in ["R3.1", "R3.2"] {
        assert!(un.iter().any(|u| u[1][0] == key && u[1][1].as_str().unwrap().starts_with("R3 is outside the board outline")), "{key}: {un:?}");
    }
    assert!(failed >= 2);
}