main
Colby Knox adom-fields 0.10.7: the board's ground (issue 25); coincident lands as one terminal (issue 26); verify keeps via copper… 554eeec
//! Explicit signed pin currents. Positive enters copper; negative leaves it.
use crate::board::{Board, Pad};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Terminal {
    pub reference: String,
    pub pad: String,
    pub amps: f64,
    /// Optional board-space pad center in mm, for repeated logical pin numbers.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub at: Option<[f64; 2]>,
}

pub fn resolve<'a>(board: &'a Board, net: &str, amps: f64, rows: &[Terminal])
    -> Result<Vec<(&'a Pad, f64)>, String>
{
    if rows.is_empty() { return Err(format!("{net}: explicit terminals must not be empty")); }
    let mut seen = BTreeSet::new();
    let mut result = Vec::new();
    let (mut positive, mut negative) = (0.0, 0.0);
    for t in rows {
        if !t.amps.is_finite() || t.at.is_some_and(|xy| xy.iter().any(|v| !v.is_finite())) {
            return Err(format!("{net}: terminal currents must be finite and pin assignments unique"));
        }
        let mut pads: Vec<_> = board.pads.iter().enumerate().filter(|(_, p)| {
            p.reference == t.reference && p.pad == t.pad
                && t.at.is_none_or(|[x, y]| (p.x - x).abs() <= 1e-6 && (p.y - y).abs() <= 1e-6)
        }).collect();
        // lands of one pad number at one centre on one net are one piece of copper, and KiCad unions them (issue 26):
        // `at` names the piece, and the largest land stands for it
        if t.at.is_some() && pads.len() > 1 && pads.iter().all(|(_, p)| p.net == net) {
            let largest = pads.iter().copied().max_by(|a, b| a.1.area.total_cmp(&b.1.area)).unwrap();
            pads = vec![largest];
        }
        if pads.len() != 1 || pads[0].1.net != net {
            let hint = if t.at.is_none() && pads.len() > 1 { format!("; pad {} repeats {} times, so give `at` with the centre of one land", t.pad, pads.len()) } else { String::new() };
            return Err(format!("{net}: {}.{} must identify exactly one pad on this net{hint}", t.reference, t.pad));
        }
        if !seen.insert(pads[0].0) {
            return Err(format!("{net}: physical pad assigned more than once"));
        }
        positive += t.amps.max(0.0);
        negative += (-t.amps).max(0.0);
        result.push((pads[0].1, t.amps));
    }
    let tolerance = 1e-9_f64.max(amps.abs() * 1e-8);
    if !amps.is_finite() || amps < 0.0 || (positive - amps).abs() > tolerance || (negative - amps).abs() > tolerance {
        return Err(format!("{net}: sources ({positive} A) and sinks ({negative} A) must each equal load ({amps} A)"));
    }
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    fn board() -> Board {
        Board { pads: [("J1", "1", "V"), ("F1", "1", "V"), ("R1", "1", "V"), ("J2", "1", "GND")].into_iter().map(|(r,p,n)| Pad { reference:r.into(), pad:p.into(), net:n.into(), x:0., y:0., layers:vec!["F.Cu".into()], area:1., func:String::new() }).collect(), ..Board::default() }
    }
    fn row(reference: &str, amps: f64) -> Terminal { Terminal { reference:reference.into(), pad:"1".into(), amps, at: None } }
    #[test] fn main_and_sense_branches_preserve_assigned_currents() {
        let b=board();
        let r=resolve(&b,"V",3.,&[row("J1",3.),row("F1",-2.9999),row("R1",-0.0001)]).unwrap();
        assert_eq!(r[1].0.reference,"F1"); assert_eq!(r[2].1,-0.0001);
    }
    #[test] fn reject_wrong_net_missing_duplicate_and_unbalanced_terminals() {
        let b=board();
        for rows in [vec![],vec![row("J1",3.),row("J2",-3.)],vec![row("J1",3.),row("MISSING",-3.)],vec![row("J1",3.),row("F1",-2.)],vec![row("J1",3.),row("F1",-1.5),row("F1",-1.5)],vec![row("J1",f64::NAN),row("F1",-3.)]] {
            assert!(resolve(&b,"V",3.,&rows).is_err());
        }
    }
    #[test] fn reject_ambiguous_repeated_pad_number() {
        let mut b=board(); b.pads.push(b.pads[0].clone());
        assert!(resolve(&b,"V",3.,&[row("J1",3.),row("F1",-3.)]).is_err());
    }
    #[test] fn repeated_pin_physical_pads_require_explicit_centers() {
        let mut b=board();
        let mut second=b.pads[1].clone(); second.x=2.; b.pads.push(second);
        let mut first=row("F1",-1.5); first.at=Some([0.,0.]);
        let mut second=row("F1",-1.5); second.at=Some([2.,0.]);
        let resolved=resolve(&b,"V",3.,&[row("J1",3.),first.clone(),second.clone()]).unwrap();
        assert_eq!(resolved[1].0.x,0.); assert_eq!(resolved[2].0.x,2.);
        assert!(resolve(&b,"V",3.,&[row("J1",3.),first.clone(),first.clone()]).is_err());
        for at in [Some([1.,0.]),Some([f64::NAN,0.]),None] {
            second.at=at;
            assert!(resolve(&b,"V",3.,&[row("J1",3.),first.clone(),second.clone()]).is_err());
        }
    }
    // issue 26: Texas DGN0008B has pad 9 eight times, two of them coincident at the centre with different sizes
    #[test] fn coincident_lands_are_one_land_named_by_at() {
        let mut b = board();
        let land = |x: f64, y: f64, area: f64, net: &str| Pad { reference: "U1".into(), pad: "9".into(), net: net.into(), x, y, layers: vec!["F.Cu".into()], area, func: String::new() };
        for p in [land(5., 5., 6.0, "V"), land(5., 5., 4.7, "V"), land(4.5, 4., 0.2, "V"), land(5.5, 4., 0.2, "V")] { b.pads.push(p); }
        let rows = |r: Vec<Terminal>| { let mut v = vec![row("J1", 3.)]; v.extend(r); v };
        let nine = |amps: f64, at: Option<[f64; 2]>| Terminal { reference: "U1".into(), pad: "9".into(), amps, at };
        let r = resolve(&b, "V", 3., &rows(vec![nine(-3., Some([5., 5.]))])).unwrap();
        assert_eq!(r[1].0.area, 6.0, "the largest land stands for the piece");
        // the order on the board doesn't matter
        let mut swapped = b.clone(); let n = swapped.pads.len(); swapped.pads.swap(n - 4, n - 3);
        assert_eq!(resolve(&swapped, "V", 3., &rows(vec![nine(-3., Some([5., 5.]))])).unwrap()[1].0.area, 6.0);
        // a via pad beside it is still its own land, and the piece can't be assigned twice
        let split = resolve(&b, "V", 3., &rows(vec![nine(-2., Some([5., 5.])), nine(-1., Some([4.5, 4.]))])).unwrap();
        assert_eq!(split[2].0.x, 4.5);
        let twice = resolve(&b, "V", 3., &rows(vec![nine(-1.5, Some([5., 5.])), nine(-1.5, Some([5., 5.]))])).unwrap_err();
        assert!(twice.contains("more than once"), "{twice}");
        // no centre given stays ambiguous, and says what to do
        let none = resolve(&b, "V", 3., &rows(vec![nine(-3., None)])).unwrap_err();
        assert!(none.contains("give `at`"), "{none}");
        // coincident lands on two nets are not one piece
        b.pads.push(land(8., 8., 1.0, "V")); b.pads.push(land(8., 8., 2.0, "GND"));
        assert!(resolve(&b, "V", 3., &rows(vec![nine(-3., Some([8., 8.]))])).is_err());
    }
    #[test] fn selector_cannot_bypass_wrong_net_or_duplicate_assignment() {
        let b=board(); let mut wrong=row("J2",-3.);wrong.at=Some([0.,0.]);
        assert!(resolve(&b,"V",3.,&[row("J1",3.),wrong]).is_err());
        let mut selected=row("F1",-1.5);selected.at=Some([0.,0.]);
        assert!(resolve(&b,"V",3.,&[row("J1",3.),row("F1",-1.5),selected]).is_err());
    }
    #[test] fn old_json_remains_valid_and_new_selector_roundtrips() {
        let old:Terminal=serde_json::from_str(r#"{"reference":"F1","pad":"1","amps":-3}"#).unwrap();
        assert!(old.at.is_none());
        let mut new=old;new.at=Some([111.138,90.08]);
        let restored:Terminal=serde_json::from_str(&serde_json::to_string(&new).unwrap()).unwrap();
        assert_eq!(restored.at,new.at);
    }

}