Skip to main content

pcb_toolkit/
copper.rs

1//! Copper weight to thickness conversion tables.
2//!
3//! Data extracted from Saturn PCB Toolkit v8.44 binary (see NOTES.md).
4
5use serde::{Deserialize, Serialize};
6
7use crate::CalcError;
8
9/// Standard copper weight options (oz/ft²).
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub enum CopperWeight {
12    /// 0.25 oz/ft²
13    Oz025,
14    /// 0.5 oz/ft²
15    Oz05,
16    /// 1 oz/ft²
17    Oz1,
18    /// 1.5 oz/ft²
19    Oz15,
20    /// 2 oz/ft²
21    Oz2,
22    /// 2.5 oz/ft²
23    Oz25,
24    /// 3 oz/ft²
25    Oz3,
26    /// 4 oz/ft²
27    Oz4,
28    /// 5 oz/ft²
29    Oz5,
30}
31
32impl CopperWeight {
33    /// Copper thickness in mils.
34    pub fn thickness_mils(self) -> f64 {
35        match self {
36            Self::Oz025 => 0.35,
37            Self::Oz05 => 0.70,
38            Self::Oz1 => 1.40,
39            Self::Oz15 => 2.10,
40            Self::Oz2 => 2.80,
41            Self::Oz25 => 3.50,
42            Self::Oz3 => 4.20,
43            Self::Oz4 => 5.60,
44            Self::Oz5 => 7.00,
45        }
46    }
47
48    /// Copper thickness in mm.
49    pub fn thickness_mm(self) -> f64 {
50        match self {
51            Self::Oz025 => 0.009,
52            Self::Oz05 => 0.018,
53            Self::Oz1 => 0.035,
54            Self::Oz15 => 0.053,
55            Self::Oz2 => 0.070,
56            Self::Oz25 => 0.088,
57            Self::Oz3 => 0.106,
58            Self::Oz4 => 0.142,
59            Self::Oz5 => 0.178,
60        }
61    }
62
63    /// Parse from a string like "1oz", "0.5oz", "2.5oz".
64    pub fn from_str_oz(s: &str) -> Result<Self, CalcError> {
65        match s.trim().to_lowercase().trim_end_matches("oz").trim() {
66            "0.25" => Ok(Self::Oz025),
67            "0.5" => Ok(Self::Oz05),
68            "1" => Ok(Self::Oz1),
69            "1.5" => Ok(Self::Oz15),
70            "2" => Ok(Self::Oz2),
71            "2.5" => Ok(Self::Oz25),
72            "3" => Ok(Self::Oz3),
73            "4" => Ok(Self::Oz4),
74            "5" => Ok(Self::Oz5),
75            _ => Err(CalcError::UnknownCopperWeight(s.to_string())),
76        }
77    }
78}
79
80/// Plating thickness options.
81#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
82pub enum PlatingThickness {
83    Bare,
84    Oz05,
85    Oz1,
86    Oz15,
87    Oz2,
88    Oz25,
89    Oz3,
90}
91
92impl PlatingThickness {
93    /// Plating thickness in mils.
94    pub fn thickness_mils(self) -> f64 {
95        match self {
96            Self::Bare => 0.0,
97            Self::Oz05 => 0.70,
98            Self::Oz1 => 1.40,
99            Self::Oz15 => 2.10,
100            Self::Oz2 => 2.80,
101            Self::Oz25 => 3.50,
102            Self::Oz3 => 4.20,
103        }
104    }
105}
106
107/// Etch factor affecting conductor cross-section geometry.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109pub enum EtchFactor {
110    /// Rectangular cross-section (no etch compensation).
111    None,
112    /// 1:1 etch — trapezoid with top = W - 2T.
113    OneToOne,
114    /// 2:1 etch — trapezoid with top = W - T.
115    TwoToOne,
116}
117
118impl EtchFactor {
119    /// Cross-sectional area in square mils given width W and thickness T (both in mils).
120    pub fn cross_section_sq_mils(self, width: f64, thickness: f64) -> f64 {
121        match self {
122            Self::None => width * thickness,
123            Self::OneToOne => (width + (width - 2.0 * thickness)) * thickness / 2.0,
124            Self::TwoToOne => (width + (width - thickness)) * thickness / 2.0,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn copper_weight_values() {
135        assert!((CopperWeight::Oz1.thickness_mils() - 1.40).abs() < 1e-10);
136        assert!((CopperWeight::Oz1.thickness_mm() - 0.035).abs() < 1e-10);
137    }
138
139    #[test]
140    fn etch_factor_rectangular() {
141        // 10 mil wide, 1.4 mil thick, no etch → 14 sq mils
142        let area = EtchFactor::None.cross_section_sq_mils(10.0, 1.4);
143        assert!((area - 14.0).abs() < 1e-10);
144    }
145
146    #[test]
147    fn parse_copper_weight() {
148        assert_eq!(CopperWeight::from_str_oz("1oz").unwrap(), CopperWeight::Oz1);
149        assert_eq!(CopperWeight::from_str_oz("0.5oz").unwrap(), CopperWeight::Oz05);
150        assert_eq!(CopperWeight::from_str_oz("2.5").unwrap(), CopperWeight::Oz25);
151    }
152}