main
Caleb Publish 0.2.0 cbe04a0 11h ago
//! A plain SVG of the copper, to look at a result. Top layer red, bottom blue, vias white.

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

/// How to draw. `fills`: the zones' filled copper as saved, not their outlines, so a board KiCad has
/// refilled shows what the fab gets. `highlight`: only these nets at full strength. `labels`: name pads.
#[derive(Default)]
pub struct View {
    pub fills: bool,
    pub highlight: Vec<String>,
    pub labels: bool,
}

pub fn svg(b: &Board, window: Option<(f64, f64, f64, f64)>) -> String {
    svg_view(b, window, &View { labels: window.is_some(), ..Default::default() })
}

pub fn svg_view(b: &Board, window: Option<(f64, f64, f64, f64)>, view: &View) -> String {
    let bb = window.map(|w| (w.0 * NM, w.1 * NM, w.2 * NM, w.3 * NM)).unwrap_or(b.bbox);
    let (w, h) = ((bb.2 - bb.0) / NM, (bb.3 - bb.1) / NM);
    let last = b.layers.len() - 1;
    let mut s = String::new();
    let hit = |b: &Board, n: NetId| view.highlight.is_empty() || view.highlight.iter().any(|h| h == b.net_name(n));
    let op = |b: &Board, n: NetId, full: f64| if hit(b, n) { full } else { 0.18 };
    let _ = write!(s, r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}" height="{}"><rect x="{}" y="{}" width="{}" height="{}" fill="#101418"/>"##, bb.0 / NM, bb.1 / NM, w, h, 1500.0, 1500.0 * h / w, bb.0 / NM, bb.1 / NM, w, h);
    for z in &b.zones {
        if z.keepout_tracks || z.outline.len() < 3 || !(z.layers.contains(&0) || z.layers.contains(&last)) {
            continue;
        }
        let col = if z.layers.contains(&0) { "#a03030" } else { "#3050a0" };
        let lit = hit(b, z.net);
        if view.fills && !z.fills.is_empty() {
            for (l, f) in &z.fills {
                if *l != 0 && *l != last {
                    continue;
                }
                let col = if *l == 0 { "#a03030" } else { "#3050a0" };
                let pts: String = f.iter().map(|p| format!("{:.4},{:.4} ", p.0 / NM, p.1 / NM)).collect();
                let _ = write!(s, r#"<polygon points="{pts}" fill="{col}" fill-opacity="{}"/>"#, if lit { 0.55 } else { 0.22 });
            }
            continue;
        }
        let pts: String = z.outline.iter().map(|p| format!("{:.4},{:.4} ", p.0 / NM, p.1 / NM)).collect();
        let _ = write!(s, r#"<polygon points="{pts}" fill="{col}" fill-opacity="{}"/>"#, if lit { 0.4 } else { 0.25 });
    }
    fn draw(s: &mut String, shape: &Shape, col: &str) {
        match shape {
            Shape::Circle { c, r } => {
                let _ = write!(s, r#"<circle cx="{:.4}" cy="{:.4}" r="{:.4}" fill="{col}"/>"#, c.0 / NM, c.1 / NM, r / NM);
            }
            Shape::Capsule { a, b, r } => {
                let _ = write!(s, r#"<line x1="{:.4}" y1="{:.4}" x2="{:.4}" y2="{:.4}" stroke="{col}" stroke-width="{:.4}" stroke-linecap="round"/>"#, a.0 / NM, a.1 / NM, b.0 / NM, b.1 / NM, r * 2.0 / NM);
            }
            Shape::Poly { pts, .. } => {
                let t: String = pts.iter().map(|p| format!("{:.4},{:.4} ", p.0 / NM, p.1 / NM)).collect();
                let _ = write!(s, r#"<polygon points="{t}" fill="{col}"/>"#);
            }
            Shape::Multi(v) => v.iter().for_each(|x| draw(s, x, col)),
        }
    }
    let pad = |s: &mut String, p: &Pad, col: &str| draw(s, &p.shape, col);
    for p in b.pads.iter().filter(|p| p.layers.contains(&last) && !p.layers.contains(&0)) {
        pad(&mut s, p, "#5070c0");
    }
    for t in b.tracks.iter().filter(|t| t.layer != 0) {
        let _ = write!(s, r##"<line x1="{:.4}" y1="{:.4}" x2="{:.4}" y2="{:.4}" stroke="#4d8dff" stroke-opacity="{:.2}" stroke-width="{:.4}" stroke-linecap="round"/>"##, to_mm(t.a.0), to_mm(t.a.1), to_mm(t.b.0), to_mm(t.b.1), op(b, t.net, 0.85), to_mm(t.width));
    }
    for p in b.pads.iter().filter(|p| p.layers.contains(&0)) {
        pad(&mut s, p, "#c8a040");
    }
    for t in b.tracks.iter().filter(|t| t.layer == 0) {
        let _ = write!(s, r##"<line x1="{:.4}" y1="{:.4}" x2="{:.4}" y2="{:.4}" stroke="#ff5050" stroke-opacity="{:.2}" stroke-width="{:.4}" stroke-linecap="round"/>"##, to_mm(t.a.0), to_mm(t.a.1), to_mm(t.b.0), to_mm(t.b.1), op(b, t.net, 0.9), to_mm(t.width));
    }
    for v in b.vias.iter().filter(|v| hit(b, v.net)) {
        let _ = write!(s, r##"<circle cx="{:.4}" cy="{:.4}" r="{:.4}" fill="#e8e8e8"/><circle cx="{:.4}" cy="{:.4}" r="{:.4}" fill="#101418"/>"##, to_mm(v.at.0), to_mm(v.at.1), to_mm(v.size) / 2.0, to_mm(v.at.0), to_mm(v.at.1), to_mm(v.drill) / 2.0);
    }
    // Zoomed in, name the pads: ref.pin and net, small enough to sit inside the view.
    if view.labels {
        let size = w.min(h) / 42.0;
        for p in &b.pads {
            let c: V = p.center.into();
            if c.0 < bb.0 || c.0 > bb.2 || c.1 < bb.1 || c.1 > bb.3 || p.layers.is_empty() || !hit(b, p.net) {
                continue;
            }
            let net = b.net_name(p.net).rsplit('/').next().unwrap_or("");
            let net = if net.starts_with("unconnected-") { "n/c" } else { net };
            let label = format!("{} {}", b.pad_name(b.pads.iter().position(|q| std::ptr::eq(q, p)).unwrap()), net.replace('&', "and").replace('<', "").replace('>', ""));
            let tw = label.len() as f64 * size * 0.56;
            let _ = write!(s, r##"<rect x="{:.4}" y="{:.4}" width="{tw:.4}" height="{:.4}" fill="#000000" fill-opacity="0.7"/><text x="{:.4}" y="{:.4}" font-family="Arial" font-size="{size:.4}" fill="#ffffff" text-anchor="middle">{label}</text>"##, c.0 / NM - tw / 2.0, c.1 / NM - size * 0.75, size * 1.15, c.0 / NM, c.1 / NM + size * 0.15);
        }
    }
    for (a, c) in &b.edges {
        let _ = write!(s, r##"<line x1="{:.4}" y1="{:.4}" x2="{:.4}" y2="{:.4}" stroke="#d0d040" stroke-width="0.08"/>"##, a.0 / NM, a.1 / NM, c.0 / NM, c.1 / NM);
    }
    s.push_str("</svg>");
    s
}