main
Caleb Publish 0.5.0 1984b40
//! Design rules, set once at `init` and used everywhere after: the KiCad project's net class and board
//! minimums, every zone's clearance, the router, and the DRC gate. The default
//! profile is KiCad's own default rules, because the shared KiCad service checks a lone board file
//! against exactly those: a router working to tighter rules than the gate checks produced hundreds of
//! false clearance errors on the Driver run (2026-11-03).
use serde_json::{json, Value};

/// Named profiles. Sizes in mm; vias as [diameter, drill].
pub fn profile(name: &str) -> Option<Value> {
    match name {
        "kicad-default" => Some(json!({"name": "kicad-default", "clearance": 0.2, "track": 0.2, "powerTrack": 0.5, "viaSignal": [0.6, 0.3], "viaPower": [0.8, 0.4],
                                         "edgeClearance": 0.5, "holeClearance": 0.25, "holeToHole": 0.25, "minDrill": 0.3,
                                         "why": "KiCad's default rules: what the shared KiCad service checks a lone board against"})),
        "jlc-4l" => Some(json!({"name": "jlc-4l", "clearance": 0.127, "track": 0.127, "powerTrack": 0.5, "viaSignal": [0.45, 0.2], "viaPower": [0.6, 0.3],
                                  "edgeClearance": 0.3, "holeClearance": 0.2, "holeToHole": 0.254, "minDrill": 0.2,
                                  "why": "JLCPCB 4-layer standard capabilities; needs a native kicad-cli for the gate to see them"})),
        _ => None,
    }
}

/// A profile name, or a JSON file with the same keys.
pub fn load(arg: &str) -> Result<Value, String> {
    if let Some(p) = profile(arg) { return Ok(p); }
    let v: Value = serde_json::from_str(&std::fs::read_to_string(arg).map_err(|e| format!("rules {arg}: not a profile (kicad-default, jlc-4l) and not a file: {e}"))?).map_err(|e| format!("{arg}: {e}"))?;
    for k in ["clearance", "track", "viaSignal", "edgeClearance"] { if v.get(k).is_none() { return Err(format!("{arg}: `{k}` missing")); } }
    Ok(v)
}

pub fn num(r: &Value, k: &str, d: f64) -> f64 { r[k].as_f64().unwrap_or(d) }
pub fn via(r: &Value, k: &str) -> (f64, f64) { (r[k][0].as_f64().unwrap_or(0.6), r[k][1].as_f64().unwrap_or(0.3)) }

/// The KiCad project file carrying the rules (net class and board minimums).
pub fn kicad_pro(r: &Value, name: &str) -> String {
    let (vd, vh) = via(r, "viaSignal");
    serde_json::to_string_pretty(&json!({
        "board": {"design_settings": {"defaults": {}, "rules": {
            "min_clearance": num(r, "clearance", 0.2), "min_track_width": num(r, "track", 0.2), "min_via_diameter": vd, "min_via_annular_width": ((vd - vh) / 2.0 * 1000.0).round() / 1000.0,
            "min_through_hole_diameter": num(r, "minDrill", 0.3), "min_copper_edge_clearance": num(r, "edgeClearance", 0.5), "min_hole_clearance": num(r, "holeClearance", 0.25), "min_hole_to_hole": num(r, "holeToHole", 0.25)}}},
        "meta": {"filename": format!("{name}.kicad_pro"), "version": 1},
        "net_settings": {"classes": [{"name": "Default", "clearance": num(r, "clearance", 0.2), "track_width": num(r, "track", 0.2), "via_diameter": vd, "via_drill": vh}]},
        // without these KiCad draws every schematic wire and pin as a hairline once a project file exists
        "schematic": {"drawing": {"default_line_thickness": 6.0, "default_text_size": 50.0, "pin_symbol_size": 25.0, "text_offset_ratio": 0.15, "label_size_ratio": 0.375, "junction_size_choice": 3, "dashed_lines_dash_length_ratio": 12.0, "dashed_lines_gap_length_ratio": 3.0, "overbar_offset_ratio": 1.23}},
        "adom_reverse_rules": r,
    })).unwrap() + "\n"
}


#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn profiles_carry_everything() {
        let r = profile("kicad-default").unwrap();
        assert!(kicad_pro(&r, "x").contains("\"min_clearance\": 0.2"));
        assert!(load("nope").is_err());
    }
}