Skip to main content

geop_ops_hole/
iso.rs

1//! ISO metric screw threads and the holes made for them, in millimetres:
2//! the coarse series of ISO 261 from M1.6 to M24, with
3//!
4//! - the pitch (ISO 261) and the basic minor diameter `D1 = D - 1.0825 P`
5//!   (ISO 724);
6//! - the tap drill for a 6H internal thread (ISO 2306);
7//! - clearance holes, close (fine), normal (medium) and loose (coarse) fit
8//!   (ISO 273);
9//! - the counterbore for a socket head cap screw (ISO 4762): its diameter as
10//!   DIN 974-1 gives it, its depth the screw's head height `k = D`, so the
11//!   head sits flush;
12//! - the 90° countersink for a countersunk socket screw (ISO 10642): its
13//!   diameter as ISO 15065 gives it, which stops at M20.
14
15use geop_core_math::geop_error::{GeopError, GeopResult};
16use serde::{Deserialize, Serialize};
17
18/// One size of the coarse series.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct MetricSize {
21    /// `M6`.
22    pub name: &'static str,
23    /// The nominal (major) diameter.
24    pub diameter: f64,
25    pub pitch: f64,
26    pub tap_drill: f64,
27    /// Close, normal and loose clearance holes.
28    pub clearance: [f64; 3],
29    /// The counterbore's diameter; its depth is the head height, `diameter`.
30    pub counterbore: f64,
31    /// The countersink's diameter, if ISO 15065 gives one.
32    pub countersink: Option<f64>,
33}
34
35/// `H`, the height of the fundamental triangle of the ISO metric profile,
36/// per unit of pitch: `sqrt(3) / 2`.
37pub const TRIANGLE_HEIGHT: f64 = 0.866_025_403_784_438_6;
38
39impl MetricSize {
40    /// The basic minor diameter, ISO 724: `D - 5/4 H`, twice over.
41    pub fn minor_diameter(&self) -> f64 {
42        self.diameter - 1.25 * TRIANGLE_HEIGHT * self.pitch
43    }
44
45    /// How the thread is called: `M6x1`.
46    pub fn designation(&self) -> String {
47        format!("{}x{}", self.name, self.pitch)
48    }
49
50    /// The clearance hole for `fit`.
51    pub fn clearance(&self, fit: Fit) -> f64 {
52        self.clearance[fit as usize]
53    }
54}
55
56/// How much play a clearance hole leaves around its screw (ISO 273).
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum Fit {
60    /// Fine.
61    Close = 0,
62    /// Medium.
63    #[default]
64    Normal = 1,
65    /// Coarse.
66    Loose = 2,
67}
68
69impl Fit {
70    pub const ALL: [Fit; 3] = [Fit::Close, Fit::Normal, Fit::Loose];
71
72    /// As it serializes: `close`.
73    pub fn value(self) -> &'static str {
74        match self {
75            Fit::Close => "close",
76            Fit::Normal => "normal",
77            Fit::Loose => "loose",
78        }
79    }
80
81    pub fn label(self) -> &'static str {
82        match self {
83            Fit::Close => "Close",
84            Fit::Normal => "Normal",
85            Fit::Loose => "Loose",
86        }
87    }
88
89    pub fn from_value(value: &str) -> Option<Fit> {
90        Fit::ALL.into_iter().find(|f| f.value() == value)
91    }
92}
93
94const fn size(
95    name: &'static str,
96    diameter: f64,
97    pitch: f64,
98    tap_drill: f64,
99    clearance: [f64; 3],
100    counterbore: f64,
101    countersink: Option<f64>,
102) -> MetricSize {
103    MetricSize {
104        name,
105        diameter,
106        pitch,
107        tap_drill,
108        clearance,
109        counterbore,
110        countersink,
111    }
112}
113
114/// The coarse series, smallest first.
115pub const SIZES: [MetricSize; 16] = [
116    size("M1.6", 1.6, 0.35, 1.25, [1.7, 1.8, 2.0], 3.5, Some(3.6)),
117    size("M2", 2.0, 0.4, 1.6, [2.2, 2.4, 2.6], 4.4, Some(4.4)),
118    size("M2.5", 2.5, 0.45, 2.05, [2.7, 2.9, 3.1], 5.5, Some(5.5)),
119    size("M3", 3.0, 0.5, 2.5, [3.2, 3.4, 3.6], 6.5, Some(6.3)),
120    size("M4", 4.0, 0.7, 3.3, [4.3, 4.5, 4.8], 8.0, Some(9.4)),
121    size("M5", 5.0, 0.8, 4.2, [5.3, 5.5, 5.8], 10.0, Some(10.4)),
122    size("M6", 6.0, 1.0, 5.0, [6.4, 6.6, 7.0], 11.0, Some(12.6)),
123    size("M8", 8.0, 1.25, 6.8, [8.4, 9.0, 10.0], 15.0, Some(17.3)),
124    size("M10", 10.0, 1.5, 8.5, [10.5, 11.0, 12.0], 18.0, Some(20.0)),
125    size(
126        "M12",
127        12.0,
128        1.75,
129        10.2,
130        [13.0, 13.5, 14.5],
131        20.0,
132        Some(24.0),
133    ),
134    size("M14", 14.0, 2.0, 12.0, [15.0, 15.5, 16.5], 24.0, Some(28.0)),
135    size("M16", 16.0, 2.0, 14.0, [17.0, 17.5, 18.5], 26.0, Some(32.0)),
136    size("M18", 18.0, 2.5, 15.5, [19.0, 20.0, 21.0], 30.0, Some(36.0)),
137    size("M20", 20.0, 2.5, 17.5, [21.0, 22.0, 24.0], 33.0, Some(40.0)),
138    size("M22", 22.0, 2.5, 19.5, [23.0, 24.0, 26.0], 36.0, None),
139    size("M24", 24.0, 3.0, 21.0, [25.0, 26.0, 28.0], 40.0, None),
140];
141
142/// The size called `name`: `M6`.
143pub fn metric(name: &str) -> GeopResult<&'static MetricSize> {
144    SIZES.iter().find(|s| s.name == name).ok_or_else(|| {
145        let known: Vec<&str> = SIZES.iter().map(|s| s.name).collect();
146        GeopError::new(format!(
147            "{name:?} is no ISO metric coarse thread size; there are {}",
148            known.join(", ")
149        ))
150    })
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    /// The tables hold together: each hole is larger than the one it
158    /// clears — tap drill below the nominal diameter, between minor and
159    /// nominal; clearance holes above it, close to loose; heads wider still.
160    #[test]
161    fn the_tables_are_consistent() {
162        for s in &SIZES {
163            assert!(
164                s.minor_diameter() < s.tap_drill && s.tap_drill < s.diameter,
165                "{s:?}"
166            );
167            assert!(s.diameter < s.clearance[0], "{s:?}");
168            assert!(
169                s.clearance[0] < s.clearance[1] && s.clearance[1] < s.clearance[2],
170                "{s:?}"
171            );
172            assert!(s.clearance[2] < s.counterbore, "{s:?}");
173            if let Some(cs) = s.countersink {
174                assert!(s.clearance[2] < cs, "{s:?}");
175            }
176        }
177        assert_eq!(metric("M6").unwrap().designation(), "M6x1");
178        assert_eq!(metric("M8").unwrap().designation(), "M8x1.25");
179        assert!(metric("M7").is_err());
180    }
181}