master
John Lauer 1.0.3: 1.0.3: placement. kicad_placement_state, kicad_move_footprint (a batch of moves as one native undo step, refused… 2569119 1mo ago
//! The placement demo fixture (`demo/placement/`, built by `tools/make_placement_fixture.py`
//! from the public Adom ESC G431 board) read back through the bridge's own parser: both
//! copies keep every footprint, carry zero segments and zero vias, and the unplaced copy
//! has every footprint parked to the right of the outline with no courtyard collision,
//! which is exactly what `kicad_placement_state` and `kicad_placement_validate` report.

use std::path::{Path, PathBuf};

use kicad_core::pcb;
use kicad_core::placement;

fn repo_file(rel: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").join(rel)
}

fn load(name: &str) -> pcb::Board {
    let p = repo_file(&format!("demo/placement/{name}"));
    let text = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()));
    pcb::parse_pcb_text(&text, &p.to_string_lossy()).unwrap()
}

#[test]
fn unplaced_copy_is_parked_unrouted_and_collision_free() {
    let b = load("esc-g431-unplaced.kicad_pcb");
    assert_eq!(b.footprints.len(), 149);
    assert_eq!(b.segments.len(), 0);
    assert_eq!(b.vias.len(), 0);
    assert_eq!(b.net_format, pcb::NetFormat::Name);
    assert_eq!(pcb::copper_layers(&b), vec!["F.Cu", "In1.Cu", "In2.Cu", "B.Cu"]);
    // Teardrop zones are gone with the copper; the user's pours stay.
    assert_eq!(b.zones.len(), 49);
    let outline = placement::outline(&b).expect("Edge.Cuts outline");
    assert_eq!((outline.min_x, outline.min_y, outline.max_x, outline.max_y), (104.0, 58.0, 168.0, 132.0));
    let places = placement::placements(&b);
    assert!(places.iter().all(|p| p.rotation == 0.0), "parked footprints are at rotation 0");
    assert!(places.iter().all(|p| p.inside_outline == Some(false)));
    let parked = placement::parked(&places, Some(&outline));
    assert_eq!(parked.len(), 149, "every footprint sits to the right of the outline");
    assert!(places.iter().all(|p| p.courtyard.min_x >= 173.0 - 1e-6), "5 mm margin from the outline");
    assert_eq!(placement::overlaps(&places), Vec::<placement::Overlap>::new(), "the parking grid has no courtyard collisions");
    assert!(places.iter().all(|p| p.side == "F.Cu"), "the ESC is assembled on one side and the tool keeps every side as it was");
    let with_courtyard = places.iter().filter(|p| p.courtyard_source == "courtyard").count();
    assert!(with_courtyard > 140, "{with_courtyard} of 149 have a courtyard shape");
    let s = placement::state(&b, false);
    assert_eq!(s["footprintCount"], 149);
    assert_eq!(s["padNetsOmitted"], false);
    assert!(s["ratsnest"]["totalMm"].as_f64().unwrap() > 1000.0, "parked parts have a long ratsnest");
    let v = placement::validate(&b);
    assert_eq!(v["placed"], false);
    assert_eq!(v["courtyardOverlapCount"], 0);
    assert_eq!(v["parked"].as_array().unwrap().len(), 149);
}

#[test]
fn unrouted_copy_keeps_the_original_placement() {
    let b = load("esc-g431-unrouted.kicad_pcb");
    assert_eq!(b.footprints.len(), 149);
    assert_eq!(b.segments.len(), 0);
    assert_eq!(b.vias.len(), 0);
    assert_eq!(b.zones.len(), 49);
    let u2 = b.footprints.iter().find(|f| f.reference == "U2").unwrap();
    let at = u2.position.as_ref().unwrap();
    assert_eq!((at.x, at.y, at.rotation), (130.895002, 87.0, Some(-90.0)));
    let places = placement::placements(&b);
    let inside = places.iter().filter(|p| p.inside_outline == Some(true)).count();
    assert!(inside >= 140, "{inside} of 149 inside the outline (the board's own placement)");
    let unplaced = load("esc-g431-unplaced.kicad_pcb");
    let (_, routed_total) = placement::nets(&b);
    let (nets, parked_total) = placement::nets(&unplaced);
    assert!(parked_total > routed_total * 2.0, "the ratsnest estimate falls when parts are placed: {parked_total} parked vs {routed_total} placed");
    let gnd = nets.iter().find(|n| n.name == "GND").expect("GND net");
    assert!(gnd.refs.len() > 20);
}