main
Caleb Publish 0.2.0 cbe04a0 11h ago
//! Writes copper into a copy of the board. Nothing else in the file changes. The input file is
//! never overwritten (4.6: never overwrite a board someone has reviewed).

use crate::board::*;
use crate::geom::*;
use crate::router::*;
use std::fmt::Write;

fn uuid(seed: u64) -> String {
    // A fixed mix: stable across runs for the same order, unique within a file.
    let mut x = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0xA5A5_5A5A_DEAD_BEEF;
    let mut next = || {
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        x
    };
    let (a, b) = (next(), next());
    format!("{:08x}-{:04x}-4{:03x}-8{:03x}-{:012x}", (a >> 32) as u32, (a >> 16) as u16, a & 0xfff, (b >> 48) & 0xfff, b & 0xffff_ffff_ffff)
}

/// The board file's own number for each net name, from its `(net N "name")` table. A file that
/// numbers its nets (KiCad 9 and earlier) has to get those numbers back on new copper: the parser
/// numbers nets in the order it meets them, which is not the file's order. Written with the
/// parser's number, every track on a small sensor board carried another net's number
/// (2026-11-10); KiCad re-derives a track's net from the pads it touches when it loads a board, so
/// its DRC said nothing, and only this program's own check of the written file showed it.
fn file_numbers(board: &Board) -> std::collections::HashMap<String, String> {
    let mut out = std::collections::HashMap::new();
    for line in board.text.lines() {
        let l = line.trim();
        let Some(rest) = l.strip_prefix("(net ") else { continue };
        let Some((num, name)) = rest.split_once(' ') else { continue };
        if !num.chars().all(|c| c.is_ascii_digit()) || !name.starts_with('"') || !name.ends_with("\")") {
            continue;
        }
        let name = &name[1..name.len() - 2];
        out.entry(name.replace("\\\"", "\"").replace("\\\\", "\\")).or_insert_with(|| num.to_string());
    }
    out
}

fn net_field(board: &Board, net: NetId, numbers: Option<&std::collections::HashMap<String, String>>) -> String {
    match numbers {
        Some(table) => table.get(board.net_name(net)).cloned().unwrap_or_else(|| "0".to_string()),
        None => format!("\"{}\"", board.net_name(net).replace('\\', "\\\\").replace('"', "\\\"")),
    }
}

fn n(v: i64) -> String {
    let s = format!("{:.6}", v as f64 / NM);
    let t = s.trim_end_matches('0').trim_end_matches('.');
    t.to_string()
}

pub fn render(board: &Board, routes: &[&Route], rules: &crate::rules::Rules) -> String {
    let numeric = board.text.contains("\n\t(net 0 ") || board.text.contains("\n  (net 0 ");
    let numbers = numeric.then(|| file_numbers(board));
    let mut out = String::new();
    let mut seed = 1u64;
    for r in routes {
        let net = net_field(board, r.task.net, numbers.as_ref());
        for s in &r.segs {
            seed += 1;
            let _ = write!(out, "\t(segment\n\t\t(start {} {})\n\t\t(end {} {})\n\t\t(width {})\n\t\t(layer \"{}\")\n\t\t(net {})\n\t\t(uuid \"{}\")\n\t)\n", n(s.a.0), n(s.a.1), n(s.b.0), n(s.b.1), n(s.width), board.layers[s.layer].name, net, uuid(seed));
        }
        for v in &r.vias {
            seed += 1;
            // A via with no track of its own (a stitching via, a return via, a via in a pad) is marked
            // free. KiCad gives any other via the net of whatever copper it lands in when the board
            // loads, and the file still holds the OLD plane fills: on a large Ethernet switch board a
            // GND via through the +3.3V plane came back as a +3.3V via, 0.11 mm from a GND pad.
            // Checked in KiCad 10: with the flag the via keeps its net and the finding is gone.
            let free = if r.segs.is_empty() { "\n\t\t(free yes)" } else { "" };
            let _ = write!(out, "\t(via\n\t\t(at {} {})\n\t\t(size {})\n\t\t(drill {})\n\t\t(layers \"F.Cu\" \"B.Cu\"){free}\n\t\t(net {})\n\t\t(uuid \"{}\")\n\t)\n", n(v.0), n(v.1), n(rules.via_for(r.task.net).0), n(rules.via_for(r.task.net).1), net, uuid(seed));
        }
    }
    let end = board.text.rfind(')').unwrap_or(board.text.len());
    let mut text = String::with_capacity(board.text.len() + out.len());
    text.push_str(&board.text[..end]);
    text.push_str(&out);
    text.push_str(&board.text[end..]);
    text
}

/// A copy of the file with every track and via removed, for measuring against a reference.
pub fn strip(board: &Board) -> String {
    let mut spans: Vec<(usize, usize)> = board.tracks.iter().map(|t| t.span).chain(board.vias.iter().map(|v| v.span)).chain(board.teardrop_spans.iter().copied()).collect();
    spans.sort();
    spans.dedup();
    let mut text = String::with_capacity(board.text.len());
    let mut at = 0;
    for (a, b) in spans {
        if a < at {
            continue;
        }
        text.push_str(&board.text[at..a]);
        at = b;
    }
    text.push_str(&board.text[at..]);
    text
}