Tapped holes: an Rc or NPT hole takes the stocked drill under its standard minor

An Rc or NPT hole was drilled to the exact minor diameter where the thread ends, a size no shop
stocks. It now takes the largest stocked drill not larger than that: metric drills of the DIN 345
series for Rc (to 100 mm), letter and fractional inch drills by 64ths (to 1 1/2 in, ASME B94.11M)
for NPT; past the series it is bored to the exact value. The Hole dialog's drill shows the inch
size for NPT, and its tooltip the exact standard value. Rc 1/2: 17.807 mm, drilled 17.75 mm;
1/2 NPT: 17.473 mm, drilled 11/16 in (17.4625 mm).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-27 10:56:34 -04:00
committed by Irakli Kourtis
parent 376a91ad75
commit b6c3b32a98
5 changed files with 200 additions and 35 deletions
+14 -7
View File
@@ -3,42 +3,49 @@
"thread.form_bspp": "Pipe G (ISO 228-1)",
"thread.form_bspt": "Pipe taper R, Rc (ISO 7-1)",
"thread.form_npt": "Pipe taper NPT (ASME B1.20.1)",
"thread.form_rp": "Pipe Rp (ISO 7-1)"
"thread.form_rp": "Pipe Rp (ISO 7-1)",
"thread.taper_drill_exact": "The standard's minor diameter where the thread ends is {exact} mm; this is the stocked drill under it"
},
"ar": {
"thread.form_bspp": "سن أنابيب G (ISO 228-1)",
"thread.form_bspt": "سن أنابيب مخروطي R، Rc (ISO 7-1)",
"thread.form_npt": "سن أنابيب مخروطي NPT (ASME B1.20.1)",
"thread.form_rp": "سن أنابيب داخلي متوازٍ Rp (ISO 7-1)"
"thread.form_rp": "سن أنابيب داخلي متوازٍ Rp (ISO 7-1)",
"thread.taper_drill_exact": "القطر الأصغر القياسي عند نهاية السن {exact} مم؛ هذا هو المثقاب المتوفر الأصغر منه مباشرة"
},
"he": {
"thread.form_bspp": "תבריג צנרת G (ISO 228-1)",
"thread.form_bspt": "תבריג צנרת קוני R, Rc (ISO 7-1)",
"thread.form_npt": "תבריג צנרת קוני NPT (ASME B1.20.1)",
"thread.form_rp": "תבריג צנרת פנימי מקביל Rp (ISO 7-1)"
"thread.form_rp": "תבריג צנרת פנימי מקביל Rp (ISO 7-1)",
"thread.taper_drill_exact": "הקוטר הקטן התקני בסוף התבריג הוא {exact} מ\"מ; זהו המקדח המלאי שמתחתיו"
},
"ja": {
"thread.form_bspp": "管用平行ねじ G(ISO 228-1)",
"thread.form_bspt": "管用テーパねじ R、Rc(ISO 7-1)",
"thread.form_npt": "管用テーパねじ NPT(ASME B1.20.1)",
"thread.form_rp": "管用平行めねじ Rp(ISO 7-1)"
"thread.form_rp": "管用平行めねじ Rp(ISO 7-1)",
"thread.taper_drill_exact": "ねじ終端での規格の谷径は {exact} mm です。これはそれ以下で最も近い市販ドリルです"
},
"zh": {
"thread.form_bspp": "管螺纹 G(ISO 228-1)",
"thread.form_bspt": "圆锥管螺纹 R、Rc(ISO 7-1)",
"thread.form_npt": "圆锥管螺纹 NPT(ASME B1.20.1)",
"thread.form_rp": "圆柱内管螺纹 Rp(ISO 7-1)"
"thread.form_rp": "圆柱内管螺纹 Rp(ISO 7-1)",
"thread.taper_drill_exact": "螺纹终端处的标准小径为 {exact} mm;这是不大于它的常备钻头"
},
"de": {
"thread.form_bspp": "Rohrgewinde G (ISO 228-1)",
"thread.form_bspt": "Kegeliges Rohrgewinde R, Rc (ISO 7-1)",
"thread.form_npt": "Kegeliges Rohrgewinde NPT (ASME B1.20.1)",
"thread.form_rp": "Zylindrisches Innenrohrgewinde Rp (ISO 7-1)"
"thread.form_rp": "Zylindrisches Innenrohrgewinde Rp (ISO 7-1)",
"thread.taper_drill_exact": "Der genormte Kerndurchmesser am Gewindeende ist {exact} mm; dies ist der nächstkleinere Lagerbohrer"
},
"ko": {
"thread.form_bspp": "관용 평행나사 G (ISO 228-1)",
"thread.form_bspt": "관용 테이퍼나사 R, Rc (ISO 7-1)",
"thread.form_npt": "관용 테이퍼나사 NPT (ASME B1.20.1)",
"thread.form_rp": "관용 평행 암나사 Rp (ISO 7-1)"
"thread.form_rp": "관용 평행 암나사 Rp (ISO 7-1)",
"thread.taper_drill_exact": "나사 끝에서의 규격 골지름은 {exact} mm이며, 이는 그 이하의 가장 가까운 상용 드릴입니다"
}
}
+147 -15
View File
@@ -46,22 +46,138 @@ fn pipe_size(size: crate::pipe_thread_catalog::PipeThread) -> TappedThread {
}
}
/// The drill of an internal taper thread (Rc, NPT): `exact` is the standard's internal minor
/// diameter where the thread ends (mm), the reference plane's minor at the face (d - 2h for ISO
/// 7-1, E1 - h for NPT) less the 1 in 16 taper over the thread's length (ISO 7-1's useful thread,
/// NPT's effective thread L2), so a tap cuts full form over the whole length. The hole is
/// drilled with the largest stocked drill not larger than that (`name`, `diameter`): metric for
/// Rc, inch for NPT. Above the drill series it is bored to `exact` (no `name`).
#[derive(Clone, Debug, PartialEq)]
pub struct TaperDrill {
pub exact: f64,
pub diameter: f64,
pub name: Option<String>,
}
/// Metric twist drill diameters (mm), DIN 345:2006 (taper shank twist drills, the series up to
/// 100 mm), as reproduced at <https://www.fasten.it/en/Norms/Norm/DIN%20345>; three digits that
/// reproduction drops are restored from their neighbours (15.25, 34.5, 54).
const DIN_345: &[f64] = &[
3.0, 3.2, 3.5, 3.8, 4.0, 4.2, 4.5, 4.8, 5.0, 5.2, 5.5, 5.8, 6.0, 6.2, 6.5, 6.8, 7.0, 7.2, 7.5, 7.8, 8.0, 8.2, 8.5, 8.8, 9.0, 9.2, 9.5, 9.8, 10.0,
10.2, 10.5, 10.8, 11.0, 11.2, 11.5, 11.8, 12.0, 12.2, 12.5, 12.8, 13.0, 13.2, 13.5, 13.8, 14.0, 14.25, 14.5, 14.75, 15.0, 15.25, 15.5, 15.75,
16.0, 16.25, 16.5, 16.75, 17.0, 17.25, 17.5, 17.75, 18.0, 18.25, 18.5, 18.75, 19.0, 19.25, 19.5, 19.75, 20.0, 20.25, 20.5, 20.75, 21.0, 21.25,
21.5, 21.75, 22.0, 22.25, 22.5, 22.75, 23.0, 23.25, 23.5, 23.75, 24.0, 24.25, 24.5, 24.75, 25.0, 25.25, 25.5, 25.75, 26.0, 26.25, 26.5, 26.75,
27.25, 27.5, 27.75, 28.0, 28.25, 28.5, 28.75, 29.0, 29.25, 29.5, 29.75, 30.0, 30.25, 30.5, 30.75, 31.0, 31.25, 31.5, 31.75, 32.0, 32.5, 33.0,
33.5, 34.5, 35.0, 35.5, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0, 41.5, 42.0, 42.5, 43.0, 43.5, 44.0, 44.5, 45.0, 45.5,
46.0, 46.5, 47.0, 47.5, 48.0, 48.5, 49.0, 49.5, 50.0, 50.5, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0, 64.0,
65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 72.0, 73.0, 74.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0,
90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0,
];
/// Letter drill sizes (in), ASME B94.11M (twist drills).
const LETTER_DRILLS: &[(char, f64)] = &[
('A', 0.234),
('B', 0.238),
('C', 0.242),
('D', 0.246),
('E', 0.250),
('F', 0.257),
('G', 0.261),
('H', 0.266),
('I', 0.272),
('J', 0.277),
('K', 0.281),
('L', 0.290),
('M', 0.295),
('N', 0.302),
('O', 0.316),
('P', 0.323),
('Q', 0.332),
('R', 0.339),
('S', 0.348),
('T', 0.358),
('U', 0.368),
('V', 0.377),
('W', 0.386),
('X', 0.397),
('Y', 0.404),
('Z', 0.413),
];
/// Fractional drill sizes by 64ths of an inch up to this many 64ths (1 1/2 in), ASME B94.11M.
const FRACTIONAL_64THS: u32 = 96;
/// A fraction of an inch in lowest terms: `11/16 in`, `1 1/4 in`.
fn inch_fraction(n64: u32) -> String {
let (whole, mut num, mut den) = (n64 / 64, n64 % 64, 64);
while num > 0 && num % 2 == 0 {
num /= 2;
den /= 2;
}
match (whole, num) {
(0, _) => format!("{num}/{den} in"),
(w, 0) => format!("{w} in"),
(w, _) => format!("{w} {num}/{den} in"),
}
}
/// The largest stocked drill not larger than `exact` mm: metric (DIN 345) or inch (letter and
/// fractional drills, ASME B94.11M). None past the series.
fn stocked_drill(exact: f64, inch: bool) -> Option<(f64, String)> {
let fits = |d: f64| d <= exact + 1e-9;
if !inch {
let max = DIN_345.iter().copied().fold(0.0, f64::max);
if exact > max + 1.0 {
return None;
}
return DIN_345
.iter()
.copied()
.filter(|d| fits(*d))
.fold(None, |a: Option<f64>, d| Some(a.map_or(d, |a| a.max(d))))
.map(|d| (d, format!("{d} mm")));
}
if exact > f64::from(FRACTIONAL_64THS) / 64.0 * 25.4 + 25.4 / 64.0 {
return None;
}
let letters = LETTER_DRILLS.iter().map(|(c, d)| (d * 25.4, format!("{c} ({d:.3} in)")));
let fractions = (1..=FRACTIONAL_64THS).map(|n| (f64::from(n) / 64.0 * 25.4, inch_fraction(n)));
letters.chain(fractions).filter(|(d, _)| fits(*d)).max_by(|a, b| a.0.total_cmp(&b.0))
}
/// The drill of an Rc or NPT size ([`TaperDrill`]).
fn taper_drill(family: TappedThreadFamily, size: &crate::pipe_thread_sizes::PipeSize) -> TaperDrill {
let p = size.pitch();
let h = if family == TappedThreadFamily::Npt { crate::pipe_thread_sizes::NPT_HEIGHT * p } else { 0.640327 * p };
let exact = size.major - 2.0 * h - size.length / 16.0;
match stocked_drill(exact, family == TappedThreadFamily::Npt) {
Some((diameter, name)) => TaperDrill { exact, diameter, name: Some(name) },
None => TaperDrill { exact, diameter: (exact * 1000.0).floor() / 1000.0, name: None },
}
}
impl TappedThread {
/// An Rc or NPT size's drill: the standard's exact value and the stocked drill it is.
pub fn taper_drill(&self) -> Option<TaperDrill> {
let sizes = match self.family {
TappedThreadFamily::Rc => crate::pipe_thread_sizes::iso_7_1().collect::<Vec<_>>(),
TappedThreadFamily::Npt => crate::pipe_thread_sizes::npt().collect(),
_ => return None,
};
sizes.iter().find(|z| (z.major - self.major_diameter).abs() < 1e-9).map(|z| taper_drill(self.family, z))
}
}
/// An internal taper thread (Rc, NPT) of the size tables a modelled thread cuts
/// ([`crate::pipe_thread_sizes`]). Its drill is the internal minor diameter where the thread
/// ends, the reference plane's d - 2h at the face less the taper over the thread's length (ISO
/// 7-1's useful thread, NPT's effective thread L2): derived from the standards' dimensions, so
/// the tap cuts full form over the whole length. It is not a tap maker's recommendation.
/// ([`crate::pipe_thread_sizes`]), drilled with [`TaperDrill`]'s stocked drill.
fn taper_size(family: TappedThreadFamily, size: crate::pipe_thread_sizes::PipeSize) -> TappedThread {
use crate::thread_forms::ThreadForm;
let p = size.pitch();
let (form, h) = match family {
TappedThreadFamily::Npt => (ThreadForm::Npt, crate::pipe_thread_sizes::NPT_HEIGHT * p),
_ => (ThreadForm::Bspt, 0.640327 * p),
};
let form = if family == TappedThreadFamily::Npt { ThreadForm::Npt } else { ThreadForm::Bspt };
let designation = form.format(size.major, p, p);
let designation = if form == ThreadForm::Bspt { designation.replacen("R ", "Rc ", 1) } else { designation };
let drill = size.major - 2.0 * h - size.length / 16.0;
TappedThread { family, designation, major_diameter: size.major, pitch: p, tap_drill: (drill * 1000.0).floor() / 1000.0 }
let tap_drill = taper_drill(family, &size).diameter;
TappedThread { family, designation, major_diameter: size.major, pitch: p, tap_drill }
}
// Diameter/pitch pairs verified against Guhring, plus Yamawa for M12x1.25.
@@ -203,17 +319,33 @@ mod tests {
}
}
/// Rc and NPT holes: the size tables' diameters, the internal minor where the thread ends for
/// the drill (Rc 1/2: d1 18.631 at the face, 13.2 mm of useful thread at 1 in 16; 1/2 NPT:
/// E1 - h = 0.77843 - 0.05714 in at the face, L2 = 0.5337 in), and no external R.
/// Rc and NPT holes: the size tables' diameters, and the stocked drill just under the internal
/// minor where the thread ends (Rc 1/2: d1 18.631 at the face less 13.2 mm of useful thread at
/// 1 in 16, 17.807, drilled 17.75 mm; 1/2 NPT: E1 - h = 0.77843 - 0.05714 in at the face less
/// L2 = 0.5337 in at 1 in 16, 0.6879 in, drilled 11/16 in), and no external R.
#[test]
fn internal_taper_threads_drill_to_their_minor_where_the_thread_ends() {
let rc = parse_tapped_thread("Rc1/2").unwrap();
assert_eq!((rc.family, rc.designation.as_str(), rc.major_diameter), (TappedThreadFamily::Rc, "Rc 1/2", 20.955));
assert!((rc.tap_drill - (18.631 - 13.2 / 16.0)).abs() < 2e-3, "{}", rc.tap_drill);
let d = rc.taper_drill().unwrap();
assert!((d.exact - (18.631 - 13.2 / 16.0)).abs() < 2e-3, "{d:?}");
assert_eq!((rc.tap_drill, d.name.as_deref()), (17.75, Some("17.75 mm")));
let npt = parse_tapped_thread("1/2 NPT").unwrap();
assert_eq!((npt.family, npt.designation.as_str()), (TappedThreadFamily::Npt, "1/2-14 NPT"));
assert!((npt.tap_drill - (0.77843 - 0.8 / 14.0 - 0.5337 / 16.0) * 25.4).abs() < 2e-3, "{}", npt.tap_drill);
let d = npt.taper_drill().unwrap();
assert!((d.exact - (0.77843 - 0.8 / 14.0 - 0.5337 / 16.0) * 25.4).abs() < 2e-3, "{d:?}");
assert_eq!((npt.tap_drill, d.name.as_deref()), (11.0 / 16.0 * 25.4, Some("11/16 in")));
// A letter drill where it is the closer stocked size (1/8 NPT), and a bore past the series.
let small = parse_tapped_thread("1/8 NPT").unwrap().taper_drill().unwrap();
assert!(small.diameter <= small.exact && small.name.is_some(), "{small:?}");
let large = parse_tapped_thread("Rc 6").unwrap().taper_drill().unwrap();
assert!(large.name.is_none() && (large.diameter - large.exact).abs() < 1e-3, "{large:?}");
for family in [TappedThreadFamily::Rc, TappedThreadFamily::Npt] {
for s in tapped_thread_sizes(family) {
let d = s.taper_drill().unwrap();
assert!(d.diameter <= d.exact + 1e-9 && d.exact - d.diameter < 0.02 * d.exact + 0.4, "{}: {d:?}", s.designation);
}
}
assert_eq!(rc.profile_degrees(), 55.0);
assert_eq!(npt.profile_degrees(), 60.0);
assert!(parse_tapped_thread("R 1/2").is_none(), "an external taper thread is no hole's");
+5 -4
View File
@@ -1216,8 +1216,8 @@ fn a_taper_pipe_thread_starts_at_a_free_end_and_is_named_for_its_side() {
/// A tapped hole's thread cut into its wall (`modeled`), as the thread feature cuts one: an M8
/// blind hole whose groove stops clear of the drill point's bottom, or fades out before it
/// (`ease_out`, ISO 3508), which leaves more of the wall than the full form there; an Rc 1/2
/// through a plate, drilled to its minor diameter where the thread ends and cut to its taper;
/// each a valid body that reopens the same.
/// through a plate, drilled with the stocked drill under its minor where the thread ends and cut
/// to its taper; each a valid body that reopens the same.
#[test]
fn a_modelled_tapped_hole_stops_clear_of_its_bottom_or_fades_out_before_it() {
let blind = |extra: Value| {
@@ -1243,8 +1243,9 @@ fn a_modelled_tapped_hole_stops_clear_of_its_bottom_or_fades_out_before_it() {
run(&mut s, "solid.box", json!({"length":40,"width":40,"height":12,"body_name":"Plate"}));
let out = run(&mut s, "solid.hole", json!({"position":[20,20,12],"direction":[0,0,-1],"thread":"Rc 1/2"}));
let drilled = exact_volume(&s, "Plate");
// d1 at the face less 13.2 mm of useful thread at 1 in 16, to the micrometre below.
let drill = ((20.955_f64 - 2.0 * 0.640327 * 25.4 / 14.0 - 13.2 / 16.0) * 1000.0).floor() / 1000.0;
// d1 at the face less 13.2 mm of useful thread at 1 in 16 is 17.807 mm: the stocked drill
// under it is 17.75 mm (DIN 345).
let drill: f64 = 17.75;
assert!((drilled - (40.0 * 40.0 * 12.0 - PI * (drill / 2.0).powi(2) * 12.0)).abs() < 1e-3, "{drilled}");
let id = out["feature"].as_u64().unwrap();
let redefine = json!({"position":[20,20,12],"direction":[0,0,-1],"thread":"Rc 1/2","modeled":true});
+15 -3
View File
@@ -155,9 +155,21 @@ pub(crate) fn hole_rows(ui: &mut egui::Ui, doc: &Document, designation: &mut Str
}
form_row(ui, "Tap drill diameter", |ui| {
match parse_tapped_thread(designation).filter(|_| custom.as_ref().is_none_or(|draft| draft.error.is_none())) {
Some(size) => {
ui.label(format!("{} mm", size.tap_drill));
}
// An Rc or NPT hole is drilled with the stocked drill under the standard's minor
// diameter where the thread ends; that exact value is the tooltip.
Some(size) => match size.taper_drill() {
Some(d) => {
let shown = d
.name
.filter(|n| !n.ends_with(" mm"))
.map_or_else(|| format!("{} mm", size.tap_drill), |n| format!("{} mm ({n})", size.tap_drill));
let exact = format!("{:.3}", d.exact);
ui.label(shown).on_hover_text(crate::i18n::format("thread.taper_drill_exact", &[("exact", &exact)]));
}
None => {
ui.label(format!("{} mm", size.tap_drill));
}
},
None => {
ui.label(tr("thread.select_size"));
}
+19 -6
View File
@@ -36,12 +36,18 @@ thread classes and sealing behaviour are outside this catalogue.
## Taper Rc and NPT threads
An Rc or NPT hole is drilled to the internal thread's minor diameter where the
thread ends: the reference plane's minor (d - 2h for ISO 7-1, E1 - h for NPT) at
the face, less the 1 in 16 taper over the thread's length (ISO 7-1's useful
thread, NPT's effective thread L2). The drill is derived from the standards'
dimensions so that the tap cuts full form over the whole length; it is not a tap
maker's recommendation, and an explicit `diameter` overrides it. Listed, the hole
An Rc or NPT hole is drilled with the largest stocked drill not larger than the
internal thread's minor diameter where the thread ends: the reference plane's
minor (d - 2h for ISO 7-1, E1 - h for NPT) at the face, less the 1 in 16 taper
over the thread's length (ISO 7-1's useful thread, NPT's effective thread L2), so
that the tap cuts full form over the whole length. Rc takes a metric drill of the
DIN 345 series (to 100 mm), NPT a letter or fractional inch drill (by 64ths to
1 1/2 in, ASME B94.11M); past the series the hole is bored to the exact value. The
Hole dialog shows the drill and, as its tooltip, the exact value: Rc 1/2 is
17.807 mm, drilled 17.75 mm; 1/2 NPT is 17.473 mm (0.6879 in), drilled 11/16 in
(17.4625 mm). This is derived from the standards, not a tap maker's
recommendation (which often allows for reaming); an explicit `diameter`
overrides it. Listed, the hole
is that cylindrical drill; modelled, the thread's taper is cut into it (the tap's
minor cone and the helical groove).
@@ -72,6 +78,13 @@ cylindrical pilot plus the designation; modelled, the helical groove is cut too.
## Primary sources
- DIN 345:2006 (taper shank twist drills), the metric drill diameters to 100 mm as
reproduced at <https://www.fasten.it/en/Norms/Norm/DIN%20345>: the stocked drills
of Rc holes. Three digits that reproduction drops (15.25, 34.5, 54) are restored
from their neighbours.
- ASME B94.11M (twist drills): the letter drill sizes A to Z and fractional sizes
by 64ths of an inch, the stocked drills of NPT holes.
- [Yamawa, YEU23 European catalogue](https://www.yamawa.eu/files/Yamawa_European_Catalogue_YEU23.pdf),
printed page 668: basic G/Rp dimensions; page 707: separate G and Rp drill and
recommended bore columns. These supply the finite parallel-pipe entries above.