Skip to main content

pcb_toolkit/
padstack.rs

1//! Padstack calculator — pad sizing for TH, BGA, and routing.
2//!
3//! 7 sub-calculators:
4//! 1. Thru-Hole Pad
5//! 2. BGA Land Size (IPC-7351A)
6//! 3. Conductor/Pad TH
7//! 4. Conductor/Pad BGA
8//! 5. 2 Conductors/Pad TH
9//! 6. 2 Conductors/Pad BGA
10//! 7. Corner to Corner
11
12use serde::{Deserialize, Serialize};
13
14use crate::CalcError;
15
16/// Input parameters for the thru-hole pad calculator.
17///
18/// All dimensions in mils.
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct ThruHoleInput {
21    /// Drilled hole diameter in mils.
22    pub hole_diameter_mils: f64,
23    /// Annular ring width (copper from hole edge to pad edge) in mils.
24    pub annular_ring_mils: f64,
25    /// Isolation width (clearance from pad edge to plane copper) in mils.
26    pub isolation_width_mils: f64,
27}
28
29/// Computed pad sizes for a plated thru-hole.
30///
31/// All dimensions in mils.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct ThruHoleResult {
34    /// Pad diameter on external (signal) layers in mils.
35    ///
36    /// `hole_diameter + 2 × annular_ring`
37    pub pad_external_mils: f64,
38    /// Pad diameter on internal signal layers in mils.
39    ///
40    /// Same as external for plated thru-hole.
41    pub pad_internal_signal_mils: f64,
42    /// Anti-pad (clearance opening) diameter on internal plane layers in mils.
43    ///
44    /// `pad_external + 2 × isolation_width`
45    pub pad_internal_plane_mils: f64,
46}
47
48/// Calculate pad sizes for a plated thru-hole component pad.
49///
50/// # Arguments
51/// - `input` — hole geometry and design rule parameters
52///
53/// # Errors
54/// Returns [`CalcError::NegativeDimension`] for non-positive dimensions.
55pub fn thru_hole(input: &ThruHoleInput) -> Result<ThruHoleResult, CalcError> {
56    if input.hole_diameter_mils <= 0.0 {
57        return Err(CalcError::NegativeDimension {
58            name: "hole_diameter_mils",
59            value: input.hole_diameter_mils,
60        });
61    }
62    if input.annular_ring_mils < 0.0 {
63        return Err(CalcError::NegativeDimension {
64            name: "annular_ring_mils",
65            value: input.annular_ring_mils,
66        });
67    }
68    if input.isolation_width_mils < 0.0 {
69        return Err(CalcError::NegativeDimension {
70            name: "isolation_width_mils",
71            value: input.isolation_width_mils,
72        });
73    }
74
75    let pad_external_mils =
76        input.hole_diameter_mils + 2.0 * input.annular_ring_mils;
77    let pad_internal_signal_mils = pad_external_mils;
78    let pad_internal_plane_mils =
79        pad_external_mils + 2.0 * input.isolation_width_mils;
80
81    Ok(ThruHoleResult {
82        pad_external_mils,
83        pad_internal_signal_mils,
84        pad_internal_plane_mils,
85    })
86}
87
88/// Calculate the corner-to-corner (diagonal) distance between two points.
89///
90/// # Arguments
91/// - `a_mils` — horizontal span in mils (must be ≥ 0)
92/// - `b_mils` — vertical span in mils (must be ≥ 0)
93///
94/// Returns the Euclidean distance `√(a² + b²)` in mils.
95///
96/// # Errors
97/// Returns [`CalcError::NegativeDimension`] if either dimension is negative.
98pub fn corner_to_corner(a_mils: f64, b_mils: f64) -> Result<f64, CalcError> {
99    if a_mils < 0.0 {
100        return Err(CalcError::NegativeDimension {
101            name: "a_mils",
102            value: a_mils,
103        });
104    }
105    if b_mils < 0.0 {
106        return Err(CalcError::NegativeDimension {
107            name: "b_mils",
108            value: b_mils,
109        });
110    }
111
112    Ok((a_mils * a_mils + b_mils * b_mils).sqrt())
113}
114
115#[cfg(test)]
116mod tests {
117    use approx::assert_relative_eq;
118
119    use super::*;
120
121    // Saturn PCB Toolkit help PDF page 23:
122    //   Thru-Hole Pad, hole=32 mils, annular ring=12 mils, isolation=12 mils
123    //
124    //   External layers        = 56.00 mils
125    //   Internal signal layers = 56.00 mils
126    //   Internal plane layers  = 80.00 mils
127    #[test]
128    fn saturn_page23_thru_hole_vector() {
129        let input = ThruHoleInput {
130            hole_diameter_mils: 32.0,
131            annular_ring_mils: 12.0,
132            isolation_width_mils: 12.0,
133        };
134        let result = thru_hole(&input).unwrap();
135        assert_relative_eq!(result.pad_external_mils, 56.0, epsilon = 1e-10);
136        assert_relative_eq!(result.pad_internal_signal_mils, 56.0, epsilon = 1e-10);
137        assert_relative_eq!(result.pad_internal_plane_mils, 80.0, epsilon = 1e-10);
138    }
139
140    #[test]
141    fn corner_to_corner_3_4_5() {
142        let d = corner_to_corner(3.0, 4.0).unwrap();
143        assert_relative_eq!(d, 5.0, epsilon = 1e-10);
144    }
145
146    #[test]
147    fn corner_to_corner_zero() {
148        let d = corner_to_corner(0.0, 0.0).unwrap();
149        assert_relative_eq!(d, 0.0, epsilon = 1e-10);
150    }
151
152    #[test]
153    fn error_on_zero_hole() {
154        let input = ThruHoleInput {
155            hole_diameter_mils: 0.0,
156            annular_ring_mils: 12.0,
157            isolation_width_mils: 12.0,
158        };
159        assert!(thru_hole(&input).is_err());
160    }
161
162    #[test]
163    fn error_on_negative_annular_ring() {
164        let input = ThruHoleInput {
165            hole_diameter_mils: 32.0,
166            annular_ring_mils: -1.0,
167            isolation_width_mils: 12.0,
168        };
169        assert!(thru_hole(&input).is_err());
170    }
171
172    #[test]
173    fn error_on_negative_corner_dimension() {
174        assert!(corner_to_corner(-1.0, 4.0).is_err());
175        assert!(corner_to_corner(3.0, -1.0).is_err());
176    }
177}