use geo::{Coord as GeoCoord, EuclideanDistance, Line, Point, Polygon, Rect, point};
use crate::records::pcb::{PcbComponent, PcbPad, PcbPadShape, PcbRecord};
use crate::types::{Coord, CoordPoint};
#[derive(Debug, Clone)]
pub struct Measurement {
pub mm: f64,
pub mils: f64,
}
impl Measurement {
pub fn from_coord(coord: Coord) -> Self {
Self {
mm: coord.to_mms(),
mils: coord.to_mils(),
}
}
pub fn from_mm(mm: f64) -> Self {
Self {
mm,
mils: mm / 0.0254,
}
}
pub fn display(&self) -> String {
format!("{:.3}mm ({:.1}mil)", self.mm, self.mils)
}
}
#[derive(Debug, Clone)]
pub struct PadDistance {
pub pad1: String,
pub pad2: String,
pub center_to_center: Measurement,
pub edge_to_edge: Measurement,
}
#[derive(Debug, Clone)]
pub struct PitchAnalysis {
pub pitch: Measurement,
pub count: usize,
pub direction: String,
pub pad_pairs: Vec<(String, String, Measurement)>,
}
#[derive(Debug, Clone)]
pub struct FootprintDimensions {
pub width: Measurement,
pub height: Measurement,
pub min_x: Measurement,
pub max_x: Measurement,
pub min_y: Measurement,
pub max_y: Measurement,
}
#[derive(Debug, Clone)]
pub struct PadInfo {
pub designator: String,
pub x: Measurement,
pub y: Measurement,
pub width: Measurement,
pub height: Measurement,
pub hole: Option<Measurement>,
pub shape: String,
}
#[derive(Debug, Clone)]
pub struct ClearanceResult {
pub feature1: String,
pub feature2: String,
pub clearance: Measurement,
pub location: String,
}
fn to_geo_point(p: CoordPoint) -> Point<f64> {
point!(x: p.x.to_mms(), y: p.y.to_mms())
}
fn pad_center(pad: &PcbPad) -> Point<f64> {
to_geo_point(pad.location)
}
#[allow(dead_code)] fn pad_rect(pad: &PcbPad) -> Rect<f64> {
let size = pad.size_top();
let half_w = size.x.to_mms() / 2.0;
let half_h = size.y.to_mms() / 2.0;
let cx = pad.location.x.to_mms();
let cy = pad.location.y.to_mms();
Rect::new(
GeoCoord {
x: cx - half_w,
y: cy - half_h,
},
GeoCoord {
x: cx + half_w,
y: cy + half_h,
},
)
}
#[allow(dead_code)] fn pad_polygon(pad: &PcbPad) -> Polygon<f64> {
let size = pad.size_top();
let half_w = size.x.to_mms() / 2.0;
let half_h = size.y.to_mms() / 2.0;
let cx = pad.location.x.to_mms();
let cy = pad.location.y.to_mms();
let corners = vec![
(cx - half_w, cy - half_h),
(cx + half_w, cy - half_h),
(cx + half_w, cy + half_h),
(cx - half_w, cy + half_h),
(cx - half_w, cy - half_h), ];
Polygon::new(geo::LineString::from(corners), vec![])
}
fn pad_edge_distance(pad1: &PcbPad, pad2: &PcbPad) -> f64 {
let center_dist = pad_center(pad1).euclidean_distance(&pad_center(pad2));
let dx = pad2.location.x.to_mms() - pad1.location.x.to_mms();
let dy = pad2.location.y.to_mms() - pad1.location.y.to_mms();
if center_dist < 1e-9 {
return 0.0; }
let nx = dx / center_dist;
let ny = dy / center_dist;
let size1 = pad1.size_top();
let size2 = pad2.size_top();
let extent1 = effective_extent(size1.x.to_mms(), size1.y.to_mms(), nx, ny, pad1.shape_top());
let extent2 = effective_extent(size2.x.to_mms(), size2.y.to_mms(), nx, ny, pad2.shape_top());
(center_dist - extent1 - extent2).max(0.0)
}
fn effective_extent(width: f64, height: f64, nx: f64, ny: f64, shape: PcbPadShape) -> f64 {
match shape {
PcbPadShape::Round | PcbPadShape::Circle | PcbPadShape::NoShape => {
width.min(height) / 2.0
}
PcbPadShape::Rectangular
| PcbPadShape::RoundedRectangle
| PcbPadShape::Octagonal
| PcbPadShape::RoundRect
| PcbPadShape::RotatedRect
| PcbPadShape::Arc
| PcbPadShape::Terminator => {
let half_w = width / 2.0;
let half_h = height / 2.0;
let abs_nx = nx.abs();
let abs_ny = ny.abs();
if abs_nx < 1e-9 {
half_h
} else if abs_ny < 1e-9 {
half_w
} else {
let t_x = half_w / abs_nx;
let t_y = half_h / abs_ny;
t_x.min(t_y) * (abs_nx * abs_nx + abs_ny * abs_ny).sqrt()
}
}
}
}
pub fn measure_pad_distance(
component: &PcbComponent,
pad1_des: &str,
pad2_des: &str,
) -> Option<PadDistance> {
let pad1 = component.pads().find(|p| p.designator == pad1_des)?;
let pad2 = component.pads().find(|p| p.designator == pad2_des)?;
let center_dist = pad_center(pad1).euclidean_distance(&pad_center(pad2));
let edge_dist = pad_edge_distance(pad1, pad2);
Some(PadDistance {
pad1: pad1_des.to_string(),
pad2: pad2_des.to_string(),
center_to_center: Measurement::from_mm(center_dist),
edge_to_edge: Measurement::from_mm(edge_dist),
})
}
pub fn analyze_pitch(component: &PcbComponent) -> Vec<PitchAnalysis> {
let pads: Vec<&PcbPad> = component.pads().collect();
if pads.len() < 2 {
return vec![];
}
let mut horizontal_pitches: Vec<(String, String, f64)> = vec![];
let mut vertical_pitches: Vec<(String, String, f64)> = vec![];
let tolerance = 0.1;
for i in 0..pads.len() {
for j in (i + 1)..pads.len() {
let p1 = pads[i];
let p2 = pads[j];
let dx = (p2.location.x.to_mms() - p1.location.x.to_mms()).abs();
let dy = (p2.location.y.to_mms() - p1.location.y.to_mms()).abs();
if dy < tolerance && dx > tolerance {
horizontal_pitches.push((p1.designator.clone(), p2.designator.clone(), dx));
}
if dx < tolerance && dy > tolerance {
vertical_pitches.push((p1.designator.clone(), p2.designator.clone(), dy));
}
}
}
let mut results = vec![];
if !horizontal_pitches.is_empty() {
if let Some(analysis) = analyze_pitch_group(horizontal_pitches, "horizontal") {
results.push(analysis);
}
}
if !vertical_pitches.is_empty() {
if let Some(analysis) = analyze_pitch_group(vertical_pitches, "vertical") {
results.push(analysis);
}
}
results
}
fn analyze_pitch_group(
mut pitches: Vec<(String, String, f64)>,
direction: &str,
) -> Option<PitchAnalysis> {
if pitches.is_empty() {
return None;
}
pitches.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
let min_pitch = pitches[0].2;
let tolerance = min_pitch * 0.1; let adjacent: Vec<_> = pitches
.into_iter()
.filter(|(_, _, d)| (*d - min_pitch).abs() < tolerance)
.collect();
if adjacent.is_empty() {
return None;
}
let avg_pitch = adjacent.iter().map(|(_, _, d)| d).sum::<f64>() / adjacent.len() as f64;
Some(PitchAnalysis {
pitch: Measurement::from_mm(avg_pitch),
count: adjacent.len(),
direction: direction.to_string(),
pad_pairs: adjacent
.into_iter()
.map(|(p1, p2, d)| (p1, p2, Measurement::from_mm(d)))
.collect(),
})
}
pub fn measure_dimensions(component: &PcbComponent) -> FootprintDimensions {
let bounds = component.calculate_bounds();
FootprintDimensions {
width: Measurement::from_coord(bounds.width()),
height: Measurement::from_coord(bounds.height()),
min_x: Measurement::from_coord(bounds.location1.x),
max_x: Measurement::from_coord(bounds.location2.x),
min_y: Measurement::from_coord(bounds.location1.y),
max_y: Measurement::from_coord(bounds.location2.y),
}
}
pub fn measure_pad(component: &PcbComponent, designator: &str) -> Option<PadInfo> {
let pad = component.pads().find(|p| p.designator == designator)?;
let size = pad.size_top();
let shape_name = match pad.shape_top() {
PcbPadShape::NoShape => "None",
PcbPadShape::Round => "Round",
PcbPadShape::Rectangular => "Rectangular",
PcbPadShape::Octagonal => "Octagonal",
PcbPadShape::Circle => "Circle",
PcbPadShape::Arc => "Arc",
PcbPadShape::Terminator => "Terminator",
PcbPadShape::RoundRect => "Round Rect",
PcbPadShape::RotatedRect => "Rotated Rect",
PcbPadShape::RoundedRectangle => "Rounded Rectangle",
};
Some(PadInfo {
designator: designator.to_string(),
x: Measurement::from_coord(pad.location.x),
y: Measurement::from_coord(pad.location.y),
width: Measurement::from_coord(size.x),
height: Measurement::from_coord(size.y),
hole: if pad.has_hole() {
Some(Measurement::from_coord(pad.hole_size))
} else {
None
},
shape: shape_name.to_string(),
})
}
pub fn measure_all_pads(component: &PcbComponent) -> Vec<PadInfo> {
component
.pads()
.map(|pad| {
let size = pad.size_top();
let shape_name = match pad.shape_top() {
PcbPadShape::NoShape => "None",
PcbPadShape::Round => "Round",
PcbPadShape::Rectangular => "Rectangular",
PcbPadShape::Octagonal => "Octagonal",
PcbPadShape::Circle => "Circle",
PcbPadShape::Arc => "Arc",
PcbPadShape::Terminator => "Terminator",
PcbPadShape::RoundRect => "Round Rect",
PcbPadShape::RotatedRect => "Rotated Rect",
PcbPadShape::RoundedRectangle => "Rounded Rectangle",
};
PadInfo {
designator: pad.designator.clone(),
x: Measurement::from_coord(pad.location.x),
y: Measurement::from_coord(pad.location.y),
width: Measurement::from_coord(size.x),
height: Measurement::from_coord(size.y),
hole: if pad.has_hole() {
Some(Measurement::from_coord(pad.hole_size))
} else {
None
},
shape: shape_name.to_string(),
}
})
.collect()
}
pub fn minimum_pad_clearance(component: &PcbComponent) -> Option<ClearanceResult> {
let pads: Vec<&PcbPad> = component.pads().collect();
if pads.len() < 2 {
return None;
}
let mut min_clearance = f64::MAX;
let mut min_pair = ("", "");
for i in 0..pads.len() {
for j in (i + 1)..pads.len() {
let dist = pad_edge_distance(pads[i], pads[j]);
if dist < min_clearance {
min_clearance = dist;
min_pair = (&pads[i].designator, &pads[j].designator);
}
}
}
if min_clearance == f64::MAX {
return None;
}
Some(ClearanceResult {
feature1: format!("Pad {}", min_pair.0),
feature2: format!("Pad {}", min_pair.1),
clearance: Measurement::from_mm(min_clearance),
location: format!("Between pads {} and {}", min_pair.0, min_pair.1),
})
}
pub fn pad_to_silkscreen_clearance(component: &PcbComponent) -> Option<ClearanceResult> {
let pads: Vec<&PcbPad> = component.pads().collect();
let tracks: Vec<_> = component
.primitives
.iter()
.filter_map(|p| {
if let PcbRecord::Track(t) = p {
if t.common.layer.is_overlay() {
return Some(t);
}
}
None
})
.collect();
let arcs: Vec<_> = component
.primitives
.iter()
.filter_map(|p| {
if let PcbRecord::Arc(a) = p {
if a.common.layer.is_overlay() {
return Some(a);
}
}
None
})
.collect();
if pads.is_empty() || (tracks.is_empty() && arcs.is_empty()) {
return None;
}
let mut min_clearance = f64::MAX;
let mut min_pad = "";
let mut min_type = "";
for pad in &pads {
let pad_center = pad_center(pad);
let size = pad.size_top();
let pad_radius = (size.x.to_mms().max(size.y.to_mms())) / 2.0;
for track in &tracks {
let start = to_geo_point(track.start);
let end = to_geo_point(track.end);
let line = Line::new(start.0, end.0);
let line_width = track.width.to_mms() / 2.0;
let dist = pad_center.euclidean_distance(&line);
let clearance = dist - pad_radius - line_width;
if clearance < min_clearance {
min_clearance = clearance;
min_pad = &pad.designator;
min_type = "track";
}
}
for arc in &arcs {
let arc_center = to_geo_point(arc.location);
let dist_to_center = pad_center.euclidean_distance(&arc_center);
let arc_radius = arc.radius.to_mms();
let arc_width = arc.width.to_mms() / 2.0;
let dist_to_arc = (dist_to_center - arc_radius).abs();
let clearance = dist_to_arc - pad_radius - arc_width;
if clearance < min_clearance {
min_clearance = clearance;
min_pad = &pad.designator;
min_type = "arc";
}
}
}
if min_clearance == f64::MAX {
return None;
}
Some(ClearanceResult {
feature1: format!("Pad {}", min_pad),
feature2: format!("Silkscreen {}", min_type),
clearance: Measurement::from_mm(min_clearance.max(0.0)),
location: format!("Pad {} to silkscreen", min_pad),
})
}
pub fn measure_row_span(component: &PcbComponent) -> Option<Measurement> {
let pads: Vec<&PcbPad> = component.pads().collect();
if pads.len() < 2 {
return None;
}
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for pad in &pads {
let y = pad.location.y.to_mms();
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
let y_span = max_y - min_y;
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
for pad in &pads {
let x = pad.location.x.to_mms();
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
}
let x_span = max_x - min_x;
if y_span < x_span && y_span > 0.1 {
Some(Measurement::from_mm(y_span))
} else if x_span > 0.1 {
Some(Measurement::from_mm(x_span))
} else {
None
}
}
#[derive(Debug)]
pub struct MeasurementReport {
pub name: String,
pub dimensions: FootprintDimensions,
pub pads: Vec<PadInfo>,
pub pitch: Vec<PitchAnalysis>,
pub min_pad_clearance: Option<ClearanceResult>,
pub silkscreen_clearance: Option<ClearanceResult>,
pub row_span: Option<Measurement>,
}
pub fn generate_report(component: &PcbComponent) -> MeasurementReport {
MeasurementReport {
name: component.pattern.clone(),
dimensions: measure_dimensions(component),
pads: measure_all_pads(component),
pitch: analyze_pitch(component),
min_pad_clearance: minimum_pad_clearance(component),
silkscreen_clearance: pad_to_silkscreen_clearance(component),
row_span: measure_row_span(component),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_measurement_display() {
let m = Measurement::from_mm(1.27);
assert!(m.display().contains("1.27"));
assert!(m.display().contains("mil"));
}
}