use crate::linter::{Diagnostic, LintContext, Rule};
use crate::parser::Pad;
const MIN_CLEARANCE_MM: f64 = 0.15;
pub struct TraceClearanceRule;
#[derive(Debug)]
struct CopperFeature {
pub net_id: String,
pub net_name: String,
pub layer: String,
pub x: f64,
pub y: f64,
pub half_w: f64,
pub half_h: f64,
}
impl Rule for TraceClearanceRule {
fn name(&self) -> &'static str {
"trace_clearance"
}
fn check(&self, ctx: &LintContext<'_>) -> Vec<Diagnostic> {
let features = extract_copper_features(ctx);
let mut diagnostics = Vec::new();
for i in 0..features.len() {
for j in (i + 1)..features.len() {
let f1 = &features[i];
let f2 = &features[j];
if f1.net_id == f2.net_id || !f1.layer.eq_ignore_ascii_case(&f2.layer) {
continue;
}
let dx = (f1.x - f2.x).abs();
let dy = (f1.y - f2.y).abs();
let clearance_gap = ((dx - (f1.half_w + f2.half_w)).max(0.0))
.hypot((dy - (f1.half_h + f2.half_h)).max(0.0));
if clearance_gap < MIN_CLEARANCE_MM {
let is_direct_short =
dx < (f1.half_w + f2.half_w) && dy < (f1.half_h + f2.half_h);
let msg = if is_direct_short {
format!(
"direct copper short-circuit on {}: net \"{}\" and net \"{}\" overlap at ({:.2}, {:.2})",
f1.layer, f1.net_name, f2.net_name, f1.x, f1.y
)
} else {
format!(
"copper clearance violation on {}: net \"{}\" and net \"{}\" have only {:.3}mm spacing (minimum {MIN_CLEARANCE_MM}mm)",
f1.layer, f1.net_name, f2.net_name, clearance_gap
)
};
diagnostics.push(Diagnostic {
rule: self.name().to_string(),
message: msg,
help: Some(
"reroute copper or relocate pads to maintain at least 0.15mm clearance to prevent manufacturing defects"
.to_string(),
),
});
}
}
}
diagnostics
}
}
fn half_extents(pad: &Pad) -> (f64, f64) {
match (pad.width, pad.height, pad.radius) {
(None, None, Some(r)) => (r, r),
(w, h, _) => (w.unwrap_or(1.0) / 2.0, h.unwrap_or(1.0) / 2.0),
}
}
fn extract_copper_features(ctx: &LintContext<'_>) -> Vec<CopperFeature> {
ctx.circuit
.pads()
.enumerate()
.filter_map(|(index, pad)| {
let (x, y) = (pad.x?, pad.y?);
let (half_w, half_h) = half_extents(pad);
let label = pad.pad_id.clone().unwrap_or_else(|| format!("#{index}"));
let (net_id, net_name) = match pad
.pad_id
.as_deref()
.and_then(|id| ctx.netlist.pad_to_net.get(id))
.and_then(|&idx| ctx.netlist.nets.get(idx))
{
Some(node) => (node.id.clone(), node.display_name().to_string()),
None => (
format!("unresolved pad {label}"),
format!("<unresolved pad {label}>"),
),
};
Some(CopperFeature {
net_id,
net_name,
layer: pad.layer.clone().unwrap_or_else(|| "top".to_string()),
x,
y,
half_w,
half_h,
})
})
.collect()
}