use crate::linter::{Diagnostic, LintContext, Rule};
use crate::parser::{Pad, PadPoint};
const MIN_CLEARANCE_MM: f64 = 0.15;
const CLEARANCE_HELP: &str = "reroute copper or relocate pads to maintain at least 0.15mm \
clearance to prevent manufacturing defects";
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, f1) in features.iter().enumerate() {
for f2 in &features[i + 1..] {
diagnostics.extend(clearance_violation(f1, f2).map(|message| Diagnostic {
rule: self.name().to_string(),
message,
help: Some(CLEARANCE_HELP.to_string()),
..Default::default()
}));
}
}
diagnostics
}
}
fn clearance_violation(f1: &CopperFeature, f2: &CopperFeature) -> Option<String> {
if f1.net_id == f2.net_id || !f1.layer.eq_ignore_ascii_case(&f2.layer) {
return None;
}
let dx = (f1.x - f2.x).abs();
let dy = (f1.y - f2.y).abs();
let reach_x = f1.half_w + f2.half_w;
let reach_y = f1.half_h + f2.half_h;
let clearance_gap = ((dx - reach_x).max(0.0)).hypot((dy - reach_y).max(0.0));
(clearance_gap < MIN_CLEARANCE_MM).then(|| {
if dx < reach_x && dy < reach_y {
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
)
}
})
}
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 pad_box(pad: &Pad) -> Option<(f64, f64, f64, f64)> {
if let (Some(x), Some(y)) = (pad.x, pad.y) {
let (half_w, half_h) = half_extents(pad);
return Some((x, y, half_w, half_h));
}
let points = pad.points.as_deref().filter(|p| !p.is_empty())?;
let fold = |f: fn(f64, f64) -> f64, init: f64, axis: fn(&PadPoint) -> f64| {
points.iter().map(axis).fold(init, f)
};
let (min_x, max_x) = (
fold(f64::min, f64::MAX, |p| p.x),
fold(f64::max, f64::MIN, |p| p.x),
);
let (min_y, max_y) = (
fold(f64::min, f64::MAX, |p| p.y),
fold(f64::max, f64::MIN, |p| p.y),
);
Some((
(min_x + max_x) / 2.0,
(min_y + max_y) / 2.0,
(max_x - min_x) / 2.0,
(max_y - min_y) / 2.0,
))
}
fn extract_copper_features(ctx: &LintContext<'_>) -> Vec<CopperFeature> {
ctx.circuit
.pads()
.enumerate()
.filter_map(|(index, pad)| {
let (x, y, half_w, half_h) = pad_box(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()
}