Tapped holes: the tap maker's drill for every form, and a drill of the user's within 5%

Every tapped hole now drills the tap maker's tabulated drill. ISO metric takes Yamawa YEU23's
drill size column for cutting taps (printed p.704, M coarse; pp.704 to 706, MF fine): M8 drills
6.8 mm and M12 10.3 mm, where the major less the pitch gave 6.75 and 10.25. A custom metric size
the table lacks keeps the major less the pitch. UNC/UNF (Guhring), G, Rp, Rc and NPT (Yamawa)
were tabulated already. The Thread command's suggested hole for a metric size follows.

The Hole dialog's Tap drill diameter is now a field, the tap maker's drill by default, and the
command's `diameter` with a thread is checked the same way: a drill is accepted from the
internal thread's standard minor diameter (or the tap maker's drill where that lies below it:
Rp, Rc, NPT) to 5% over the tap maker's drill. Outside that the command names the range and
why. The field's tooltip gives the drill, the minor diameter and the range. Presets keep a
chosen drill.

A saved design keeps the diameter it stored, so it builds as before, and editing such a hole
keeps its drill: no preference and no format change. Scripts that give a thread without a
diameter drill the new default; ladder 05/007, 11/003 and rig case threads/2115 follow
analytically, and their golden entries are re-measured from runs (tier 05 re-sorted by time).

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 11:53:22 -04:00
committed by Irakli Kourtis
parent e7994d48cc
commit 05f1c6b9ac
21 changed files with 585 additions and 70 deletions
+7 -14
View File
@@ -3,49 +3,42 @@
"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.taper_drill_exact": "The standard's minor diameter where the thread ends is {exact} mm; the drill is the tap maker's recommendation"
"thread.form_rp": "Pipe Rp (ISO 7-1)"
},
"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.taper_drill_exact": "القطر الأصغر القياسي عند نهاية السن {exact} مم؛ المثقاب هو ما يوصي به صانع اللقمة"
"thread.form_rp": "سن أنابيب داخلي متوازٍ Rp (ISO 7-1)"
},
"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.taper_drill_exact": "הקוטר הקטן התקני בסוף התבריג הוא {exact} מ\"מ; המקדח הוא המלצת יצרן המברז"
"thread.form_rp": "תבריג צנרת פנימי מקביל Rp (ISO 7-1)"
},
"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.taper_drill_exact": "ねじ終端での規格の谷径は {exact} mm です。下穴ドリルはタップメーカーの推奨値です"
"thread.form_rp": "管用平行めねじ Rp(ISO 7-1)"
},
"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.taper_drill_exact": "螺纹终端处的标准小径为 {exact} mm;钻头为丝锥制造商的推荐值"
"thread.form_rp": "圆柱内管螺纹 Rp(ISO 7-1)"
},
"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.taper_drill_exact": "Der genormte Kerndurchmesser am Gewindeende ist {exact} mm; der Bohrer ist die Empfehlung des Gewindebohrerherstellers"
"thread.form_rp": "Zylindrisches Innenrohrgewinde Rp (ISO 7-1)"
},
"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.taper_drill_exact": "나사 끝에서의 규격 골지름은 {exact} mm이며, 드릴은 탭 제조사의 권장값입니다"
"thread.form_rp": "관용 평행 암나사 Rp (ISO 7-1)"
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"en": {
"thread.drill_range": "The tap maker's drill is {drill} mm. The internal thread's standard minor diameter is {minor} mm. A drill from {low} to {high} mm is accepted.",
"thread.drill_range_custom": "The drill is the major diameter less the pitch, {drill} mm. The internal thread's standard minor diameter is {minor} mm. A drill from {low} to {high} mm is accepted.",
"thread.drill_range_taper": "The tap maker's drill is {drill} mm. The standard's minor diameter where the thread ends is {minor} mm. A drill from {low} to {high} mm is accepted."
},
"ar": {
"thread.drill_range": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_custom": "المثقاب هو القطر الأكبر ناقص الخطوة، {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_taper": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم."
},
"he": {
"thread.drill_range": "מקדח יצרן המברז הוא {drill} מ\"מ. הקוטר הקטן התקני של התבריג הפנימי הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ.",
"thread.drill_range_custom": "המקדח הוא הקוטר הגדול פחות הפסיעה, {drill} מ\"מ. הקוטר הקטן התקני של התבריג הפנימי הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ.",
"thread.drill_range_taper": "מקדח יצרן המברז הוא {drill} מ\"מ. הקוטר הקטן התקני בסוף התבריג הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ."
},
"ja": {
"thread.drill_range": "タップメーカーの下穴ドリルは {drill} mm です。めねじの規格の内径は {minor} mm です。{low}~{high} mm のドリルを使用できます。",
"thread.drill_range_custom": "下穴ドリルは外径からピッチを引いた {drill} mm です。めねじの規格の内径は {minor} mm です。{low}~{high} mm のドリルを使用できます。",
"thread.drill_range_taper": "タップメーカーの下穴ドリルは {drill} mm です。ねじ終端での規格の内径は {minor} mm です。{low}~{high} mm のドリルを使用できます。"
},
"zh": {
"thread.drill_range": "丝锥制造商的钻头为 {drill} mm。内螺纹的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。",
"thread.drill_range_custom": "钻头为大径减螺距,{drill} mm。内螺纹的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。",
"thread.drill_range_taper": "丝锥制造商的钻头为 {drill} mm。螺纹终端处的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。"
},
"de": {
"thread.drill_range": "Der Bohrer des Gewindebohrerherstellers ist {drill} mm. Der genormte Kerndurchmesser des Innengewindes ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen.",
"thread.drill_range_custom": "Der Bohrer ist der Nenndurchmesser minus Steigung, {drill} mm. Der genormte Kerndurchmesser des Innengewindes ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen.",
"thread.drill_range_taper": "Der Bohrer des Gewindebohrerherstellers ist {drill} mm. Der genormte Kerndurchmesser am Gewindeende ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen."
},
"ko": {
"thread.drill_range": "탭 제조사의 드릴은 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
"thread.drill_range_custom": "드릴은 바깥지름에서 피치를 뺀 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
"thread.drill_range_taper": "탭 제조사의 드릴은 {drill} mm입니다. 나사 끝에서의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다."
}
}
+3 -1
View File
@@ -53,7 +53,9 @@ mod points;
mod profile_refs;
pub mod progress;
mod thread_catalog;
pub use thread_catalog::{TaperDrill, TappedThread, TappedThreadFamily, parse_tapped_thread, tapped_thread_sizes, untabulated_taper_tap};
pub use thread_catalog::{
DRILL_ALLOWANCE, TaperDrill, TappedThread, TappedThreadFamily, metric_tap_drill, parse_tapped_thread, tapped_thread_sizes, untabulated_taper_tap,
};
mod walk;
pub use profile_refs::{capture_profile_selection, profile_fallback_notes, profile_selection_indices, profile_selection_indices_lenient};
pub mod plastic;
+345 -3
View File
@@ -202,14 +202,308 @@ const UNIFIED: &[(&str, TappedThreadFamily, f64, u32, f64)] = &[
("1-12 UNF", TappedThreadFamily::Unf, 25.4, 12, 23.3),
];
/// ISO metric cutting-tap drills (nominal diameter, pitch, drill; mm): Yamawa YEU23, "Drill size
/// and recommended hole size for cutting taps", printed p.704 (M, coarse) and pp.704 to 706 (MF,
/// fine), the drill size column. MF 70 x 1.5 is left out: its printed drill (68.00) lies below
/// the printed minimum minor diameter (68.376), a misprint.
/// <https://www.yamawa.eu/files/Yamawa_European_Catalogue_YEU23.pdf>
const METRIC_DRILLS: &[(f64, f64, f64)] = &[
(1.0, 0.2, 0.8),
(1.0, 0.25, 0.75),
(1.1, 0.2, 0.9),
(1.1, 0.25, 0.85),
(1.2, 0.2, 1.0),
(1.2, 0.25, 0.95),
(1.4, 0.2, 1.2),
(1.4, 0.3, 1.1),
(1.6, 0.2, 1.4),
(1.6, 0.35, 1.25),
(1.7, 0.2, 1.5),
(1.7, 0.35, 1.35),
(1.8, 0.2, 1.6),
(1.8, 0.35, 1.45),
(2.0, 0.25, 1.75),
(2.0, 0.4, 1.6),
(2.2, 0.25, 1.95),
(2.2, 0.45, 1.75),
(2.3, 0.25, 2.05),
(2.3, 0.4, 1.9),
(2.5, 0.35, 2.2),
(2.5, 0.45, 2.1),
(2.6, 0.35, 2.3),
(2.6, 0.45, 2.2),
(3.0, 0.35, 2.7),
(3.0, 0.5, 2.5),
(3.5, 0.35, 3.2),
(3.5, 0.6, 2.9),
(4.0, 0.5, 3.5),
(4.0, 0.7, 3.3),
(4.5, 0.5, 4.0),
(4.5, 0.75, 3.8),
(5.0, 0.5, 4.5),
(5.0, 0.8, 4.2),
(5.5, 0.5, 5.0),
(6.0, 0.5, 5.5),
(6.0, 0.75, 5.3),
(6.0, 1.0, 5.0),
(7.0, 0.5, 6.5),
(7.0, 0.75, 6.3),
(7.0, 1.0, 6.0),
(8.0, 0.5, 7.5),
(8.0, 0.75, 7.3),
(8.0, 1.0, 7.0),
(8.0, 1.25, 6.8),
(9.0, 0.75, 8.3),
(9.0, 1.0, 8.0),
(9.0, 1.25, 7.8),
(10.0, 0.5, 9.5),
(10.0, 0.75, 9.3),
(10.0, 1.0, 9.0),
(10.0, 1.25, 8.8),
(10.0, 1.5, 8.5),
(11.0, 0.5, 10.5),
(11.0, 0.75, 10.3),
(11.0, 1.0, 10.0),
(11.0, 1.5, 9.5),
(12.0, 0.5, 11.5),
(12.0, 1.0, 11.0),
(12.0, 1.25, 10.8),
(12.0, 1.5, 10.5),
(12.0, 1.75, 10.3),
(14.0, 1.0, 13.0),
(14.0, 1.5, 12.5),
(14.0, 2.0, 12.0),
(15.0, 1.0, 14.0),
(15.0, 1.5, 13.5),
(16.0, 1.0, 15.0),
(16.0, 1.5, 14.5),
(16.0, 2.0, 14.0),
(17.0, 1.0, 16.0),
(17.0, 1.5, 15.5),
(18.0, 1.0, 17.0),
(18.0, 1.5, 16.5),
(18.0, 2.0, 16.0),
(18.0, 2.5, 15.5),
(20.0, 1.0, 19.0),
(20.0, 1.5, 18.5),
(20.0, 2.0, 18.0),
(20.0, 2.5, 17.5),
(22.0, 1.0, 21.0),
(22.0, 1.5, 20.5),
(22.0, 2.0, 20.0),
(22.0, 2.5, 19.5),
(24.0, 1.0, 23.0),
(24.0, 1.5, 22.5),
(24.0, 2.0, 22.0),
(24.0, 3.0, 21.0),
(25.0, 1.0, 24.0),
(25.0, 1.5, 23.5),
(25.0, 2.0, 23.0),
(26.0, 1.5, 24.5),
(27.0, 1.0, 26.0),
(27.0, 1.5, 25.5),
(27.0, 2.0, 25.0),
(27.0, 3.0, 24.0),
(28.0, 1.0, 27.0),
(28.0, 1.5, 26.5),
(28.0, 2.0, 26.0),
(30.0, 1.0, 29.0),
(30.0, 1.5, 28.5),
(30.0, 2.0, 28.0),
(30.0, 3.0, 27.0),
(30.0, 3.5, 26.5),
(32.0, 1.5, 30.5),
(32.0, 2.0, 30.0),
(33.0, 1.5, 31.5),
(33.0, 2.0, 31.0),
(33.0, 3.0, 30.0),
(33.0, 3.5, 29.5),
(35.0, 1.5, 33.5),
(36.0, 1.5, 34.5),
(36.0, 2.0, 34.0),
(36.0, 3.0, 33.0),
(36.0, 4.0, 32.0),
(38.0, 1.5, 36.5),
(39.0, 1.5, 37.5),
(39.0, 2.0, 37.0),
(39.0, 3.0, 36.0),
(39.0, 4.0, 35.0),
(40.0, 1.5, 38.5),
(40.0, 2.0, 38.0),
(40.0, 3.0, 37.0),
(42.0, 1.5, 40.5),
(42.0, 2.0, 40.0),
(42.0, 3.0, 39.0),
(42.0, 4.0, 38.0),
(42.0, 4.5, 37.5),
(45.0, 1.5, 43.5),
(45.0, 2.0, 43.0),
(45.0, 3.0, 42.0),
(45.0, 4.0, 41.0),
(45.0, 4.5, 40.5),
(48.0, 1.5, 46.5),
(48.0, 2.0, 46.0),
(48.0, 3.0, 45.0),
(48.0, 4.0, 44.0),
(48.0, 5.0, 43.0),
(50.0, 1.5, 48.5),
(50.0, 2.0, 48.0),
(50.0, 3.0, 47.0),
(52.0, 1.5, 50.5),
(52.0, 2.0, 50.0),
(52.0, 3.0, 49.0),
(52.0, 4.0, 48.0),
(52.0, 5.0, 47.0),
(55.0, 1.5, 53.5),
(55.0, 2.0, 53.0),
(55.0, 3.0, 52.0),
(55.0, 4.0, 51.0),
(56.0, 1.5, 54.5),
(56.0, 2.0, 54.0),
(56.0, 3.0, 53.0),
(56.0, 4.0, 52.0),
(56.0, 5.5, 50.5),
(58.0, 1.5, 56.5),
(58.0, 2.0, 56.0),
(58.0, 3.0, 55.0),
(58.0, 4.0, 54.0),
(60.0, 1.5, 58.5),
(60.0, 2.0, 58.0),
(60.0, 3.0, 57.0),
(60.0, 4.0, 56.0),
(60.0, 5.5, 54.5),
(62.0, 1.5, 60.5),
(62.0, 2.0, 60.0),
(62.0, 3.0, 59.0),
(62.0, 4.0, 58.0),
(64.0, 1.5, 62.5),
(64.0, 2.0, 62.0),
(64.0, 3.0, 61.0),
(64.0, 4.0, 60.0),
(64.0, 6.0, 58.0),
(65.0, 1.5, 63.5),
(65.0, 2.0, 63.0),
(65.0, 3.0, 62.0),
(65.0, 4.0, 61.0),
(68.0, 1.5, 66.5),
(68.0, 2.0, 66.0),
(68.0, 3.0, 65.0),
(68.0, 4.0, 64.0),
(68.0, 6.0, 62.0),
(70.0, 2.0, 68.0),
(70.0, 3.0, 67.0),
(70.0, 4.0, 66.0),
(70.0, 6.0, 64.0),
(72.0, 1.5, 70.5),
(72.0, 2.0, 70.0),
(72.0, 3.0, 69.0),
(72.0, 4.0, 68.0),
(72.0, 6.0, 66.0),
(75.0, 1.5, 73.5),
(75.0, 2.0, 73.0),
(75.0, 3.0, 72.0),
(75.0, 4.0, 71.0),
(76.0, 1.5, 74.5),
(76.0, 2.0, 74.0),
(76.0, 3.0, 73.0),
(76.0, 4.0, 72.0),
(76.0, 6.0, 70.0),
(78.0, 2.0, 76.0),
(80.0, 1.5, 78.5),
(80.0, 2.0, 78.0),
(80.0, 3.0, 77.0),
(80.0, 4.0, 76.0),
(80.0, 6.0, 74.0),
(82.0, 2.0, 80.0),
(85.0, 2.0, 83.0),
(85.0, 3.0, 82.0),
(85.0, 4.0, 81.0),
(85.0, 6.0, 79.0),
(90.0, 2.0, 88.0),
(90.0, 3.0, 87.0),
(90.0, 4.0, 86.0),
(90.0, 6.0, 84.0),
(95.0, 2.0, 93.0),
(95.0, 3.0, 92.0),
(95.0, 4.0, 91.0),
(95.0, 6.0, 89.0),
(100.0, 2.0, 98.0),
(100.0, 3.0, 97.0),
(100.0, 4.0, 96.0),
(100.0, 6.0, 94.0),
];
/// The drill of an ISO metric tapped hole: the tap maker's for a tabulated size, else (a custom
/// size) the major diameter less the pitch, to the micrometre.
pub fn metric_tap_drill(major_diameter: f64, pitch: f64) -> f64 {
let same = |a: f64, b: f64| (a - b).abs() < 1e-9;
METRIC_DRILLS
.iter()
.find(|(d, p, _)| same(*d, major_diameter) && same(*p, pitch))
.map_or_else(|| ((major_diameter - pitch) * 1000.0).round() / 1000.0, |r| r.2)
}
fn metric_size(major_diameter: f64, pitch: f64) -> TappedThread {
TappedThread {
family: TappedThreadFamily::Metric,
designation: crate::thread_forms::ThreadForm::Metric.format(major_diameter, pitch, pitch),
major_diameter,
pitch,
// Preserve the existing metric estimate and rounding used by solid.hole.
tap_drill: ((major_diameter - pitch) * 1000.0).round() / 1000.0,
tap_drill: metric_tap_drill(major_diameter, pitch),
}
}
/// How much larger than the tap maker's drill a tapped hole may be drilled: 5%.
pub const DRILL_ALLOWANCE: f64 = 0.05;
impl TappedThread {
/// Whether the drill is a tap maker's tabulated one (every catalogued size; not a custom
/// metric size, which takes the major less the pitch).
pub fn drill_is_tabulated(&self) -> bool {
self.family != TappedThreadFamily::Metric
|| METRIC_DRILLS.iter().any(|(d, p, _)| (d - self.major_diameter).abs() < 1e-9 && (p - self.pitch).abs() < 1e-9)
}
/// The internal thread's standard minor diameter: ISO metric and Unified D1 = D - 1.082532 P
/// (the basic profile, ISO 68-1 / ASME B1.1), G and Rp the basic minor of their table, Rc and
/// NPT the minor where the thread ends ([`TaperDrill`]).
pub fn minor_diameter(&self) -> f64 {
match self.family {
TappedThreadFamily::Metric | TappedThreadFamily::Unc | TappedThreadFamily::Unf => {
self.major_diameter - 1.25 * 3f64.sqrt() / 2.0 * self.pitch
}
TappedThreadFamily::G | TappedThreadFamily::Rp => crate::pipe_thread_catalog::parse_pipe_thread(&self.designation)
.map_or(self.major_diameter - 1.280654 * self.pitch, |t| t.size().minor),
TappedThreadFamily::Rc | TappedThreadFamily::Npt => self.taper_drill().map_or(self.tap_drill, |d| d.exact),
}
}
/// The drills a tapped hole of this size accepts (mm): from the standard minor diameter (or
/// the tap maker's drill, where that lies below it, as a taper or Rp drill does) up to
/// [`DRILL_ALLOWANCE`] over the tap maker's drill.
pub fn drill_range(&self) -> (f64, f64) {
(self.minor_diameter().min(self.tap_drill), self.tap_drill * (1.0 + DRILL_ALLOWANCE))
}
/// Whether a hole of `diameter` mm can be tapped to this size; the error names the range.
pub fn check_drill(&self, diameter: f64) -> Result<(), String> {
let (lo, hi) = self.drill_range();
let (show, d) =
(|v: f64| format!("{:.3}", (v * 1000.0).round() / 1000.0).trim_end_matches('0').trim_end_matches('.').to_string(), &self.designation);
let why = if !(diameter.is_finite() && diameter >= lo - 1e-9) {
"smaller than the thread's minor diameter: the tap cannot cut it"
} else if diameter > hi + 1e-9 {
"more than 5% over the tap maker's drill: too little of the thread would engage"
} else {
return Ok(());
};
Err(format!(
"a {d} tapped hole takes a drill from {} to {} mm (the tap maker's drill {} mm): {} mm is {why}",
show(lo),
show(hi),
show(self.tap_drill),
show(diameter)
))
}
}
@@ -319,6 +613,52 @@ mod tests {
}
}
/// A tapped hole may be drilled from the standard minor diameter (or the tap maker's drill
/// where it lies below, Rc and Rp) up to 5% over the tap maker's drill.
#[test]
fn a_tapped_hole_accepts_drills_from_its_minor_to_five_percent_over() {
let m8 = parse_tapped_thread("M8").unwrap();
let (lo, hi) = m8.drill_range();
assert!((lo - (8.0 - 1.082532 * 1.25)).abs() < 1e-5 && (hi - 6.8 * 1.05).abs() < 1e-9, "{lo} {hi}");
for ok in [6.8, 6.75, 6.648, 7.14] {
assert!(m8.check_drill(ok).is_ok(), "{ok}");
}
assert!(m8.check_drill(6.5).unwrap_err().contains("the tap cannot cut"));
let e = m8.check_drill(7.2).unwrap_err();
assert!(e.contains("from 6.647 to 7.14 mm") && e.contains("too little of the thread"), "{e}");
let rc = parse_tapped_thread("Rc 1/2").unwrap();
assert_eq!(rc.drill_range().0, 17.6, "a taper's drill lies under its minor at the end");
assert!(rc.check_drill(17.6).is_ok() && rc.check_drill(17.5).is_err());
let rp = parse_tapped_thread("Rp 1/2").unwrap();
assert_eq!(rp.drill_range().0, 18.5);
for family in [
TappedThreadFamily::Metric,
TappedThreadFamily::Unc,
TappedThreadFamily::Unf,
TappedThreadFamily::G,
TappedThreadFamily::Rp,
TappedThreadFamily::Rc,
TappedThreadFamily::Npt,
] {
for s in tapped_thread_sizes(family) {
assert!(s.check_drill(s.tap_drill).is_ok(), "{}", s.designation);
assert!(s.minor_diameter() < s.major_diameter && s.minor_diameter() > 0.5 * s.major_diameter, "{}", s.designation);
}
}
}
/// The metric drills as Yamawa prints them: each between its thread's minimum (basic) minor
/// diameter and the major.
#[test]
fn metric_drills_lie_between_the_minor_and_the_major() {
for &(d, p, drill) in METRIC_DRILLS {
let minor = d - 1.25 * 3f64.sqrt() / 2.0 * p;
assert!(drill >= minor - 1.5e-3 && drill < d, "M{d}x{p}: {drill}");
}
assert_eq!(metric_tap_drill(8.0, 1.25), 6.8);
assert_eq!(metric_tap_drill(8.5, 1.0), 7.5, "a custom size: the major less the pitch");
}
#[test]
fn tapped_catalogue_has_unique_valid_sizes_and_round_trips() {
let mut names = std::collections::HashSet::new();
@@ -365,7 +705,9 @@ mod tests {
#[test]
fn tapped_catalogue_preserves_metric_custom_and_rejects_unverified_threads() {
for (name, d, p, drill) in [("M8", 8.0, 1.25, 6.75), ("M8x1", 8.0, 1.0, 7.0), ("M8.5x1", 8.5, 1.0, 7.5)] {
// The tap maker's drill where it tabulates the size (Yamawa p.704: M8 6.8, MF 8 x 1 7.0),
// the major less the pitch for a custom one.
for (name, d, p, drill) in [("M8", 8.0, 1.25, 6.8), ("M8x1", 8.0, 1.0, 7.0), ("M8.5x1", 8.5, 1.0, 7.5), ("M12", 12.0, 1.75, 10.3)] {
let size = parse_tapped_thread(name).unwrap();
assert_eq!((size.major_diameter, size.pitch, size.tap_drill), (d, p, drill));
}
+3 -3
View File
@@ -544,11 +544,11 @@ impl ThreadForm {
}
}
/// The minor diameter a hole for this size is drilled to: the tap drill (major − pitch) for
/// metric, the internal thread's minor diameter for the others.
/// The minor diameter a hole for this size is drilled to: the tap maker's drill for metric
/// ([`crate::metric_tap_drill`]), the internal thread's minor diameter for the others.
pub fn drill(self, d: f64, p: f64) -> f64 {
match self {
ThreadForm::Metric => d - p,
ThreadForm::Metric => crate::metric_tap_drill(d, p),
_ => 2.0 * self.groove(d, p, true).crest_r,
}
}
+9 -1
View File
@@ -69,7 +69,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SOLID", "CREATE")
.icon("hole")
.key("H")
.params("position: [x,y,z] on a face (numbers, or length expressions the hole follows) | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); flip?: bool (sketch points: drill to the other side); single?: bool (with one sketch point: a Single hole that follows the point, drilled into the face there); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points unless single); diameter | thread (a tapped hole: metric major minus pitch or tabulated UNC/UNF, G or Rp cut-tap drill, or the Rc/NPT tap maker's tabulated drill, Rc 1/16 to 4 and NPT 1/16 to 3) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); references?: [{edge: [x,y,z] on a straight edge of the face, distance: expr}] (at most two; the centre goes that far from each edge's line, in the face's plane, and follows the edges; with a numeric position only); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\", \"1/4-20 UNC\", \"G 1/4\", \"Rp 1/4\", \"Rc 1/4\" or \"1/4-18 NPT\" (cosmetic unless modeled); hand?: right|left (the tapped thread's hand; default right; needs thread); thread_depth?: positive length independent of drill depth (default: full useful cylindrical wall; must fit that wall); modeled?: bool (needs thread: the tapped thread cut into the wall, the groove between its teeth, as solid.thread's modelled one; default false: listed only); ease_out?: expr|standard|short (modelled tapped threads: the inner end, towards the bottom, fades out over this length, ISO 3508's runout, standard 2.5 P, short 1.25 P; without it the groove stops square at thread_depth, or clear of a blind hole's bottom); targets?: [body] (the bodies to drill; holes at the points of a sketch on a body's face default to that body, found along the holes' direction on it alone; explicit targets or participants override, [\"*\"] every body)"),
.params("position: [x,y,z] on a face (numbers, or length expressions the hole follows) | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); flip?: bool (sketch points: drill to the other side); single?: bool (with one sketch point: a Single hole that follows the point, drilled into the face there); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points unless single); diameter | thread (a tapped hole, drilled with the tap maker's tabulated drill: ISO metric (a custom metric size: major minus pitch), UNC/UNF, G, Rp, Rc 1/16 to 4, NPT 1/16 to 3; a `diameter` given with it chooses another drill, from the thread's minor diameter to 5% over the tap maker's) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); references?: [{edge: [x,y,z] on a straight edge of the face, distance: expr}] (at most two; the centre goes that far from each edge's line, in the face's plane, and follows the edges; with a numeric position only); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\", \"1/4-20 UNC\", \"G 1/4\", \"Rp 1/4\", \"Rc 1/4\" or \"1/4-18 NPT\" (cosmetic unless modeled); hand?: right|left (the tapped thread's hand; default right; needs thread); thread_depth?: positive length independent of drill depth (default: full useful cylindrical wall; must fit that wall); modeled?: bool (needs thread: the tapped thread cut into the wall, the groove between its teeth, as solid.thread's modelled one; default false: listed only); ease_out?: expr|standard|short (modelled tapped threads: the inner end, towards the bottom, fades out over this length, ISO 3508's runout, standard 2.5 P, short 1.25 P; without it the groove stops square at thread_depth, or clear of a blind hole's bottom); targets?: [body] (the bodies to drill; holes at the points of a sketch on a body's face default to that body, found along the holes' direction on it alone; explicit targets or participants override, [\"*\"] every body)"),
CommandSpec::new("solid.pipe", "Pipe", pipe)
.at("SOLID", "CREATE")
.icon("pipe")
@@ -2550,6 +2550,14 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
(None, None, None) => return Err(bad(cmd, "needs `diameter`, `thread` (a tapped hole) or `clearance` (a bolt size)")),
};
check_expr(s, &diameter, Kind::Length, cmd, "diameter")?;
// A tapped hole drilled to another size than the tap maker's: from the thread's minor
// diameter to 5% over that drill.
if let (Some(t), Some(_)) = (&thread, expr(p, "diameter"))
&& let Some(size) = solidcraft_doc::parse_tapped_thread(t)
{
let given = s.doc.eval(&diameter, Kind::Length).map_err(|e| bad(cmd, format!("`diameter`: {e}")))?;
size.check_drill(given).map_err(|e| bad(cmd, e))?;
}
let depth = expr(p, "depth");
if let Some(d) = &depth {
check_expr(s, d, Kind::Length, cmd, "depth")?;
+11 -1
View File
@@ -386,7 +386,7 @@ fn a_hole_too_narrow_or_a_shaft_too_thick_says_which_and_what_fits() {
let before = volume(&mut s, "Nut");
let e = s.execute("solid.thread", &json!({"face": [3.0, 0, 2], "designation": "M8", "modeled": true})).err().map(|e| e.to_string());
let e = e.unwrap_or_default();
assert!(e.contains("this 6 mm hole is too narrow for a modelled M8 thread") && e.contains("drill 6.75 mm for M8"), "{e}");
assert!(e.contains("this 6 mm hole is too narrow for a modelled M8 thread") && e.contains("drill 6.8 mm for M8"), "{e}");
assert!(!e.contains("too far"), "{e}");
assert!((volume(&mut s, "Nut") - before).abs() < 1e-9);
@@ -1256,4 +1256,14 @@ fn a_modelled_tapped_hole_stops_clear_of_its_bottom_or_fades_out_before_it() {
assert!(err(json!({"position":[0,0,0],"diameter":5,"modeled":true})).contains("`modeled` needs `thread`"));
assert!(err(json!({"position":[0,0,0],"thread":"M8","ease_out":"standard"})).contains("modelled thread"));
assert!(err(json!({"position":[0,0,0],"thread":"Rc 5"})).contains("no tap drill is tabulated"));
// Another drill than the tap maker's (M8: 6.8 mm): from the minor diameter to 5% over it.
let drill = |d: f64| {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length":30,"width":30,"height":20,"body_name":"Plate"}));
s.execute("solid.hole", &json!({"position":[15,15,20],"thread":"M8","diameter":d})).err().map(|e| e.to_string()).unwrap_or_default()
};
let e = drill(7.5);
assert!(e.contains("from 6.647 to 7.14 mm") && e.contains("too little of the thread"), "{e}");
assert!(drill(6.5).contains("the tap cannot cut"));
assert!(drill(7.1).is_empty() && drill(6.7).is_empty(), "inside the range");
}
+13
View File
@@ -424,6 +424,9 @@ pub struct HoleOpts {
pub modeled: bool,
/// A modelled tapped thread's inner end, towards the bottom: square or a fade.
pub ease_out: crate::dialogs_thread::Ease,
/// A tapped hole's drill when it is not the tap maker's for the size (the command's
/// `diameter`); none: the tap maker's.
pub drill: Option<String>,
}
impl Default for HoleOpts {
@@ -442,6 +445,7 @@ impl Default for HoleOpts {
hand: solidcraft_engine::doc::thread_forms::ThreadHand::Right,
modeled: false,
ease_out: crate::dialogs_thread::Ease::default(),
drill: None,
}
}
}
@@ -4478,6 +4482,8 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
let mut params = place;
if !opts.tapped {
params["diameter"] = json!(diameter);
} else if let Some(drill) = opts.drill.as_deref().map(str::trim).filter(|d| !d.is_empty()) {
params["diameter"] = json!(drill);
}
params["type"] = json!(ty);
if to_object_on(&d.inputs) {
@@ -5457,6 +5463,13 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
opts.hand = *hand;
opts.modeled = *modeled;
opts.ease_out = crate::dialogs_thread::Ease::of(ease_out);
// A saved drill other than the tap maker's for the size stays the hole's
// (an older design's d - P metric drill among them).
let saved = s.doc.eval(diameter, ValueKind::Length).ok();
let tabulated = solidcraft_engine::doc::parse_tapped_thread(th).map(|t| t.tap_drill);
if saved.zip(tabulated).is_none_or(|(a, b)| (a - b).abs() > 1e-9) {
opts.drill = Some(diameter.to_string());
}
}
opts.thread_full_depth = thread_depth.is_none();
if let Some(depth) = thread_depth {
+1 -1
View File
@@ -566,7 +566,7 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
opts.tapped = tap == 1;
});
if opts.tapped {
enter |= crate::dialogs_thread::hole_rows(ui, &app.session.doc, &mut opts.thread, &mut opts.custom_thread);
enter |= crate::dialogs_thread::hole_rows(ui, &app.session.doc, &mut opts.thread, &mut opts.custom_thread, &mut opts.drill);
crate::dialogs_thread::hole_hand_row(ui, &mut opts.hand);
ui_kit::checkbox_grid(ui, &mut [("Full thread depth", &mut opts.thread_full_depth), ("Modeled", &mut opts.modeled)], 1);
// A modelled thread's inner end, towards the bottom, as the thread dialog's.
+5 -2
View File
@@ -71,7 +71,10 @@ fn bore_diameter(diameter: &str, opts: &HoleOpts, eval: &dyn Fn(&str, ValueKind)
if opts.custom_thread.as_ref().is_some_and(|draft| draft.error.is_some()) {
return None;
}
solidcraft_engine::doc::parse_tapped_thread(&opts.thread).map(|size| size.tap_drill)
match opts.drill.as_deref().map(str::trim).filter(|d| !d.is_empty()) {
Some(drill) => eval(drill, ValueKind::Length).filter(|v| v.is_finite() && *v > 0.0),
None => solidcraft_engine::doc::parse_tapped_thread(&opts.thread).map(|size| size.tap_drill),
}
} else {
eval(diameter, ValueKind::Length).filter(|v| v.is_finite() && *v > 0.0)
}
@@ -449,7 +452,7 @@ mod tests {
fn tapped_section_uses_the_current_nominal_size_and_pitch() {
let mut opts = HoleOpts { tapped: true, thread: "M8".into(), ..HoleOpts::default() };
let stale_diameter = |_: &str, _: ValueKind| Some(4.0);
assert_eq!(bore_diameter("4 mm", &opts, &stale_diameter), Some(6.75));
assert_eq!(bore_diameter("4 mm", &opts, &stale_diameter), Some(6.8));
opts.thread = "M8x1.2".into();
assert_eq!(bore_diameter("4 mm", &opts, &stale_diameter), Some(6.8));
for (designation, drill) in [("G 1/4", 11.75), ("Rp 1/4", 11.4), ("1/4-20 UNC", 5.1054)] {
+86 -18
View File
@@ -68,7 +68,14 @@ impl HoleThreadDraft {
/// Tapped holes use a shared, finite catalogue; the engine derives the physical drill.
/// Custom remains a dialog mode even when its current values match a catalogue size.
pub(crate) fn hole_rows(ui: &mut egui::Ui, doc: &Document, designation: &mut String, custom: &mut Option<HoleThreadDraft>) -> bool {
pub(crate) fn hole_rows(
ui: &mut egui::Ui,
doc: &Document,
designation: &mut String,
custom: &mut Option<HoleThreadDraft>,
drill: &mut Option<String>,
) -> bool {
let before = designation.clone();
let designation_id = ui.make_persistent_id("hole_thread_designation");
let families = [
TappedThreadFamily::Metric,
@@ -153,25 +160,39 @@ pub(crate) fn hole_rows(ui: &mut egui::Ui, doc: &Document, designation: &mut Str
if response.changed() && custom.is_some() {
*custom = Some(HoleThreadDraft::new(designation));
}
// The drill: the tap maker's for the size, or one the user types, from the thread's minor
// diameter to 5% over the tap maker's. A new size goes back to its own drill.
if *designation != before {
*drill = None;
}
let mut drill_enter = false;
form_row(ui, "Tap drill diameter", |ui| {
match parse_tapped_thread(designation).filter(|_| custom.as_ref().is_none_or(|draft| draft.error.is_none())) {
// An Rc or NPT hole takes the tap maker's drill; the standard's minor diameter where
// the thread ends is the tooltip.
Some(size) => match size.taper_drill() {
Some(d) => {
let exact = format!("{:.3}", d.exact);
ui.label(format!("{} mm", size.tap_drill)).on_hover_text(crate::i18n::format("thread.taper_drill_exact", &[("exact", &exact)]));
Some(size) => {
let default = format!("{} mm", size.tap_drill);
let (lo, hi) = size.drill_range();
let mm = |v: f64| format!("{:.3}", v);
let args = [("drill", mm(size.tap_drill)), ("minor", mm(size.minor_diameter())), ("low", mm(lo)), ("high", mm(hi))];
let args: Vec<(&str, &str)> = args.iter().map(|(k, v)| (*k, v.as_str())).collect();
let id = match (size.taper_drill().is_some(), size.drill_is_tabulated()) {
(true, _) => "thread.drill_range_taper",
(false, true) => "thread.drill_range",
(false, false) => "thread.drill_range_custom",
};
let tip = crate::i18n::format(id, &args);
let mut text = drill.clone().unwrap_or_else(|| default.clone());
let (submitted, changed) = crate::ui_kit::text_field_tip(ui, &mut text, &default, &tip);
if changed {
*drill = (text.trim() != default && !text.trim().is_empty()).then_some(text);
}
None => {
ui.label(format!("{} mm", size.tap_drill));
}
},
drill_enter = submitted;
}
None => {
ui.label(tr("thread.select_size"));
}
}
});
enter || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
enter || drill_enter || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
}
/// The forms offered. The square thread is left out: no standard defines it, and the command
@@ -821,6 +842,53 @@ mod contribution_tests {
assert_eq!((&again["modeled"], &again["ease_out"]), (&json!(true), &json!("standard")), "{again}");
}
/// A tapped hole's drill: the tap maker's for the size (M8: 6.8 mm), or a typed one from the
/// thread's minor diameter to 5% over it; outside that the command names the range. A new
/// size takes its own drill again, and a saved hole drilled otherwise (an older design's
/// d - P) reopens with its drill.
#[test]
fn a_tapped_holes_drill_is_the_tap_makers_or_one_within_its_range() {
let (app, dialog) = hole();
let mut h = crate::scenario::Harness::new();
h.app = app;
h.app.dialog = Some(dialog);
h.frames(3);
for handle in ["dialog:Hole Tap Type", "dialog:Hole Tap Type:Tapped"] {
h.step(&json!({"click":{"handle":handle}})).unwrap();
}
let set = |h: &mut crate::scenario::Harness, field: &str, text: &str| {
h.step(&json!({"click":{"handle":format!("dialog:{field}")}})).unwrap();
h.step(&json!({"key":"A","cmd":true})).unwrap();
h.step(&json!({"text":text})).unwrap();
h.frames(2);
};
set(&mut h, "Designation", "M8");
let params = |h: &crate::scenario::Harness| apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0).1;
assert!(params(&h).get("diameter").is_none(), "the tap maker's drill is the command's own");
set(&mut h, "Tap drill diameter", "7 mm");
assert_eq!(params(&h)["diameter"], json!("7 mm"));
let (cmd, p) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
let id = h.app.run(&cmd, p.clone()).unwrap()["feature"].as_u64().unwrap();
set(&mut h, "Tap drill diameter", "7.5 mm");
let (cmd, p) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
let e = h.app.session.execute(&cmd, &p).unwrap_err().to_string();
assert!(e.contains("from 6.647 to 7.14 mm") && e.contains("too little of the thread"), "{e}");
set(&mut h, "Designation", "M10");
assert!(params(&h).get("diameter").is_none(), "a new size takes its own drill");
// An older design's M8 hole, drilled 6.75 mm (d - P), keeps that drill when edited.
let solidcraft_engine::doc::FeatureKind::Hole { diameter, .. } = &mut h.app.session.doc_mut().feature_mut(id).unwrap().kind else {
panic!("not Hole")
};
*diameter = "6.75 mm".into();
h.app.dialog = None;
h.app.edit_feature(id);
h.frames(2);
let (_, again) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
let again = if again.get("params").is_some() { again["params"].clone() } else { again };
assert_eq!(again["diameter"], json!("6.75 mm"), "{again}");
}
#[test]
fn manual_designation_keeps_keyboard_focus_when_custom_fields_appear() {
let (app, dialog) = hole();
@@ -914,7 +982,7 @@ mod contribution_tests {
let doc = Document::default();
let _ = ctx.run_ui(Default::default(), |ui| {
egui::Grid::new("hole_catalogue").show(ui, |ui| {
hole_rows(ui, &doc, &mut designation, &mut custom);
hole_rows(ui, &doc, &mut designation, &mut custom, &mut None);
});
});
assert_eq!(designation, original);
@@ -960,7 +1028,7 @@ mod contribution_tests {
for (name, major, pitch, drill) in [
("1/4-20 UNC", 6.35, 1.27, 5.1054),
("#10-32 UNF", 4.826, 25.4 / 32.0, 4.0386),
("M8", 8.0, 1.25, 6.75),
("M8", 8.0, 1.25, 6.8),
("M8x1", 8.0, 1.0, 7.0),
("M8.5x1", 8.5, 1.0, 7.5),
] {
@@ -1003,12 +1071,12 @@ mod contribution_tests {
let before = app.session.doc.to_json();
for designation in ["1/4-20 UNC", "G 1/4", "Rp 1/4"] {
for modeled in [false, true] {
assert!(app.run("solid.thread", json!({"face":[13.375,10,5],"designation":designation,"modeled":modeled})).is_err());
assert!(app.run("solid.thread", json!({"face":[13.4,10,5],"designation":designation,"modeled":modeled})).is_err());
assert_eq!(app.session.doc.to_json(), before);
}
}
assert_eq!(app.session.doc.to_json(), before);
let out = app.run("solid.thread", json!({"face":[13.375,10,5],"designation":"M8"})).unwrap();
let out = app.run("solid.thread", json!({"face":[13.4,10,5],"designation":"M8"})).unwrap();
let solidcraft_engine::doc::FeatureKind::Thread { designation, modeled, .. } =
&mut app.session.doc_mut().feature_mut(out["feature"].as_u64().unwrap()).unwrap().kind
else {
@@ -1063,7 +1131,7 @@ mod contribution_tests {
let doc = Document::default();
let _ = context.run_ui(Default::default(), |ui| {
egui::Grid::new("authored_hole").show(ui, |ui| {
hole_rows(ui, &doc, &mut hole, &mut custom);
hole_rows(ui, &doc, &mut hole, &mut custom, &mut None);
});
});
assert_eq!(hole, original);
@@ -1072,7 +1140,7 @@ mod contribution_tests {
#[test]
fn contribution_metric_designation_keeps_domain_pitch_drill_units_and_reopen() {
for (diameter, pitch, drill) in [(8., 1.25, 6.75), (8., 1., 7.), (5., 0.5, 4.5)] {
for (diameter, pitch, drill) in [(8., 1.25, 6.8), (8., 1., 7.), (5., 0.5, 4.5)] {
let (mut app, mut dialog) = hole();
app.run("document.units", json!({"units":"in"})).unwrap();
if let Kind::Hole { opts, .. } = &mut dialog.kind {
+19 -3
View File
@@ -46,13 +46,16 @@ struct ThreadOptions {
/// runout, any other text a fade length.
#[serde(skip_serializing_if = "Option::is_none")]
ease_out: Option<String>,
/// A drill other than the tap maker's for the size; none: the tap maker's.
#[serde(skip_serializing_if = "Option::is_none")]
drill: Option<String>,
#[serde(flatten)]
unknown: BTreeMap<String, serde_json::Value>,
}
impl Default for ThreadOptions {
fn default() -> Self {
Self { handedness: "right".into(), full_length: true, depth: None, modeled: false, ease_out: None, unknown: BTreeMap::new() }
Self { handedness: "right".into(), full_length: true, depth: None, modeled: false, ease_out: None, drill: None, unknown: BTreeMap::new() }
}
}
@@ -121,6 +124,7 @@ impl Preset {
if required.into_iter().any(|text| text.trim().is_empty())
|| self.thread_options.depth.as_ref().is_some_and(|text| !valid_text(text, MAX_EXPRESSION))
|| self.thread_options.ease_out.as_ref().is_some_and(|text| text.trim().is_empty() || !valid_text(text, MAX_EXPRESSION))
|| self.thread_options.drill.as_ref().is_some_and(|text| text.trim().is_empty() || !valid_text(text, MAX_EXPRESSION))
|| (self.thread_options.ease_out.is_some() && !(self.tapped && self.thread_options.modeled))
|| !["right", "left"].contains(&self.thread_options.handedness.as_str())
|| self.thread_options.unknown.len() > 8
@@ -210,6 +214,7 @@ impl Preset {
Some(_) if opts.modeled => Some(portable_length(&opts.ease_out.length, true)?),
_ => None,
},
drill: opts.drill.as_deref().filter(|d| !d.trim().is_empty()).map(|d| portable_length(d, true)).transpose()?,
unknown: BTreeMap::new(),
}
} else {
@@ -254,6 +259,10 @@ impl Preset {
|| (self.extent != Extent::All && (self.angled_bottom || self.style == Style::Drilled) && !angle(&self.tip_angle))
|| (self.tapped && solidcraft_engine::doc::parse_tapped_thread(&self.thread).is_none())
|| self.thread_options.ease_out.as_deref().is_some_and(|text| !named_runout(text) && !length(text))
|| self.thread_options.drill.as_deref().is_some_and(|text| {
!solidcraft_engine::doc::parse_tapped_thread(&self.thread)
.is_some_and(|t| doc.eval(text, expr::Kind::Length).is_ok_and(|v| t.check_drill(v).is_ok()))
})
|| (self.tapped && !self.thread_options.full_length && self.thread_options.depth.as_deref().is_none_or(|text| !length(text)))
{
return Err("hole_presets.invalid_parameters");
@@ -295,6 +304,7 @@ impl Preset {
};
opts.thread_depth = self.thread_options.depth.clone().unwrap_or_else(|| crate::dialogs::HoleOpts::default().thread_depth);
opts.modeled = self.thread_options.modeled;
opts.drill.clone_from(&self.thread_options.drill);
opts.ease_out = match self.thread_options.ease_out.as_deref() {
None => crate::dialogs_thread::Ease::default(),
Some(text) if named_runout(text) => crate::dialogs_thread::Ease { fade: true, short: text == "short", ..Default::default() },
@@ -604,6 +614,8 @@ mod tests {
opts.thread_depth = "8 mm".into();
opts.modeled = modeled;
opts.ease_out = ease.clone();
// An Rc 1/2 drilled 17.8 mm, inside its 17.6 to 18.48 mm.
opts.drill = (thread == "Rc 1/2").then(|| "17.8 mm".to_string());
let captured = opts.clone();
let preset = Preset::capture(&dialog, "Pipe", &doc).unwrap();
let row = serde_json::to_value(&preset).unwrap();
@@ -616,8 +628,8 @@ mod tests {
read.recall(&mut fresh).unwrap();
let Kind::Hole { opts, .. } = &fresh.kind else { panic!("expected Hole") };
assert_eq!(
(&opts.thread, opts.hand, opts.thread_full_depth, &opts.thread_depth, opts.modeled, &opts.ease_out),
(&captured.thread, captured.hand, false, &captured.thread_depth, modeled, &ease),
(&opts.thread, opts.hand, opts.thread_full_depth, &opts.thread_depth, opts.modeled, &opts.ease_out, &opts.drill),
(&captured.thread, captured.hand, false, &captured.thread_depth, modeled, &ease, &captured.drill),
"{thread}"
);
}
@@ -629,5 +641,9 @@ mod tests {
let mut preset = Preset::capture(&dialog, "Square", &doc).unwrap();
preset.thread_options.ease_out = Some("standard".into());
assert!(preset.validate().is_err());
// A drill outside the size's range is no preset to recall.
preset.thread_options.ease_out = None;
preset.thread_options.drill = Some("7.5 mm".into());
assert!(preset.validate().is_ok() && preset.validate_active(&doc).is_err(), "M8 takes 6.647 to 7.14 mm");
}
}
+7
View File
@@ -115,6 +115,13 @@ pub(crate) fn text_field(ui: &mut Ui, value: &mut String, hint: &str) -> (bool,
(enter, changed)
}
/// [`text_field`] with a tooltip; returns (Enter, changed). Tokens: `same as field`.
pub(crate) fn text_field_tip(ui: &mut Ui, value: &mut String, hint: &str, tip: &str) -> (bool, bool) {
let (enter, changed, response) = editor(ui, value, hint, false);
response.on_hover_text(tip);
(enter, changed)
}
/// Expression field for a single-value popup which keeps ownership of keyboard focus.
/// Tokens: field tokens. Preserves the legacy Edit Dimension focus behavior.
pub(crate) fn focused_field(ui: &mut Ui, value: &mut String) -> bool {
+23 -7
View File
@@ -2,8 +2,8 @@
The Hole dialog offers ISO metric coarse sizes, 15 additional fine diameter/pitch
pairs, a finite set of 14 UNC and 14 UNF sizes, six sizes each for internal
G/BSPP and Rp parallel pipe threads, and the ISO 7-1 Rc and ASME B1.20.1 NPT
internal taper threads (every size of their tables, `docs/threads.md`). This is a verified subset,
G/BSPP and Rp parallel pipe threads, and the ISO 7-1 Rc (1/16 to 4) and ASME
B1.20.1 NPT (1/16 to 3) internal taper threads. This is a verified subset,
not complete coverage of these standards. Unified sizes range from #4 through
#12 and 1/4 through 1 inch. Include the family in a designation, for example
`1/4-20 UNC` or `#10-32 UNF`.
@@ -21,11 +21,27 @@ depth, tolerances, drilling accuracy and the tap manufacturer's process guidance
still determine the appropriate drill; the command's explicit `diameter` can
override the recommendation.
Metric holes retain the existing major-minus-pitch estimate, rounded to 0.001 mm.
This preserves existing command behavior, including M8's 6.75 mm default. It is
not presented as a tabulated 75% cut-tap recommendation. Existing files retain
their saved diameter expressions. Custom metric dimensions remain editable, and
Custom stays selected even when its current dimensions match a catalogue entry.
Metric holes take the tap maker's drill: Yamawa YEU23's drill size column for
cutting taps, printed p.704 (M, coarse) and pp.704 to 706 (MF, fine); M8 drills
6.8 mm, M12 10.3 mm. A custom size the table lacks takes the major diameter less
the pitch, to 0.001 mm (M8.5x1: 7.5 mm). This replaces the major-minus-pitch
estimate every metric size took before (M8 6.75 mm). A saved design keeps the
diameter it stored, so it builds as before; editing such a hole keeps its drill.
Custom metric dimensions remain editable, and Custom stays selected even when its
current dimensions match a catalogue entry.
## Choosing another drill
The Hole dialog's Tap drill diameter is the tap maker's drill for the size and can
be changed; the command's `diameter` does the same. A drill is accepted from the
internal thread's standard minor diameter (ISO metric and Unified D1 = D −
1.082532 P, G and Rp their basic minor, Rc and NPT the minor where the thread
ends) up to 5% over the tap maker's drill. Where the tap maker's drill lies below
that minor (Rp, Rc, NPT), the drill itself is the lower end. A smaller drill would
leave the tap cutting inside the thread's minor diameter; a larger one leaves too
little of the thread engaged. M8 accepts 6.647 to 7.14 mm. The field's tooltip
gives the minor diameter and the range. A new size takes its own tap maker's drill
again.
Tapped holes create the physical pilot bore and the thread, listed (cosmetic) or,
with `modeled`, cut into the wall as the Thread command cuts a modelled thread:
+1 -1
View File
@@ -4,4 +4,4 @@ Generated size choices use the shared metric catalogue. Its coarse pitch is omit
This changes generated UI text, not the resolved numerical pitch or geometry. Existing authored designation text, including explicit legacy coarse pitches, is retained when a dialog is merely displayed. Custom and fine designations remain editable. Catalogue dimensions are millimetres regardless of the design's display units.
For ISO metric tapped holes, the existing tap-drill estimate remains nominal diameter minus pitch: M8 gives 6.75 mm, while M8x1 gives 7 mm. Saving and reopening retains the resolved pitch. See the [tapped-hole catalogue](thread-catalogue.md) for the additional metric fine choices and finite UNC/UNF and parallel G/Rp catalogues with their tabulated pilot recommendations. The separate Thread command remains ISO metric only.
For ISO metric tapped holes, the drill is the tap maker's for the size (M8 gives 6.8 mm, M8x1 7 mm), and nominal diameter minus pitch for a custom size the table lacks. Saving and reopening retains the resolved pitch. See the [tapped-hole catalogue](thread-catalogue.md) for the additional metric fine choices and finite UNC/UNF and parallel G/Rp catalogues with their tabulated pilot recommendations. The Thread command's forms are in [Threads](threads.md).
+2 -2
View File
@@ -59,8 +59,8 @@ Handbook gives only historical proportions (depth and width P/2; the "modified s
trapezoidal and Acme forms replaced it.
**Sizes.** Without a designation the thread takes the standard size that fits the face: a shaft
its major diameter, a hole its drill (metric: the tap drill d − P; the other forms: the internal
minor diameter). Metric uses the ISO 261/262 coarse pitches (M1.6 to M56); trapezoidal and metric
its major diameter, a hole its drill (metric: the tap maker's drill, `docs/thread-catalogue.md`;
the other forms: the internal minor diameter). Metric uses the ISO 261/262 coarse pitches (M1.6 to M56); trapezoidal and metric
buttress the ISO 2902 middle pitch of each diameter (Tr8x1.5 to Tr130x14; buttress from S10x2),
the series DIN 513-2 shares; Acme ASME B1.5's preferred sizes (1/4-16 to 5-2). Buttress inch has
no size table here: its pitch is always given.
@@ -2,8 +2,8 @@
"sources": [],
"what": "an M8 tapped blind hole 12 deep with a 118 deg drill point",
"source": "analytic",
"method": "M8 coarse pitch 1.25: tap drill (major - pitch) 6.75; removed pi r^2 12 to the shoulder + the point cone pi r^2 (r/tan59)/3",
"method": "M8 coarse pitch 1.25: the tap maker's drill 6.8 (Yamawa YEU23 p.704); removed pi r^2 12 to the shoulder + the point cone pi r^2 (r/tan59)/3",
"body_count": 1,
"volume_mm3": 47546.394232462,
"volume_mm3": 47539.467413298,
"faces": 8
}
@@ -2,9 +2,9 @@
"sources": [],
"what": "the flange with a modelled M30 thread on its hub, between the round and the chamfer, and M8 tapped holes in the disk",
"source": "analytic",
"method": "the hub's cylinder (r15) runs from the r3 round at its root (z 13) to the 1.5 chamfer at its top (z 28.5): the M30 groove (P 3.5) stops w = 1.632286 clear of the round, a shoulder (06/007's rule), and runs out through the chamfer, a free end (06/006's): it removes the stock less 2 pi int int r m(r) dr dz over z 13 + w..28.5 at r 15 and the chamfer (r 15 - (z - 28.5)); the bore r8 is inside the root r 13.1. The M8 tap drills (6.75) go through the 10 mm disk at r 28, 90 deg + k 120, clear of the bolt holes (30 deg apart): 3 pi 3.375^2 10. Bbox: the flange's",
"method": "the hub's cylinder (r15) runs from the r3 round at its root (z 13) to the 1.5 chamfer at its top (z 28.5): the M30 groove (P 3.5) stops w = 1.632286 clear of the round, a shoulder (06/007's rule), and runs out through the chamfer, a free end (06/006's): it removes the stock less 2 pi int int r m(r) dr dz over z 13 + w..28.5 at r 15 and the chamfer (r 15 - (z - 28.5)); the bore r8 is inside the root r 13.1. The M8 tap drills (6.8, the tap maker's) go through the 10 mm disk at r 28, 90 deg + k 120, clear of the bolt holes (30 deg apart): 3 pi 3.4^2 10. Bbox: the flange's",
"body_count": 1,
"volume_mm3": 52978.060766654,
"volume_mm3": 52962.097548983,
"bbox": {
"min": [
-40,
+6 -6
View File
@@ -710,14 +710,14 @@
},
"ladder/05-holes-and-taps/007-tapped-m8-blind": {
"measured": {
"area_mm2": 9060.431918590197,
"area_mm2": 9062.405544647674,
"body_count": 1,
"edges": 17,
"faces": 8,
"vertices": 12,
"volume_mm3": 47546.393985981646
"volume_mm3": 47539.467162202374
},
"seconds": 0.044542724,
"seconds": 0.057993293,
"status": "pass"
},
"ladder/05-holes-and-taps/008-tapped-m5-into-curved-face": {
@@ -1638,14 +1638,14 @@
},
"ladder/11-cumulative-parts/003-threaded-flange": {
"measured": {
"area_mm2": 17355.838402078,
"area_mm2": 17357.35813902275,
"body_count": 1,
"edges": 104,
"faces": 38,
"vertices": 69,
"volume_mm3": 52978.053115805815
"volume_mm3": 52962.08974469333
},
"seconds": 1.821271078,
"seconds": 2.199779541,
"status": "pass"
},
"ladder/11-cumulative-parts/004-housing": {
+1 -1
View File
@@ -84,9 +84,9 @@
"ladder/05-holes-and-taps/009-hole-0.5-from-edge",
"ladder/05-holes-and-taps/010-hole-tangent-to-end-face",
"ladder/05-holes-and-taps/003-drilled-blind-118",
"ladder/05-holes-and-taps/007-tapped-m8-blind",
"ladder/05-holes-and-taps/002-simple-blind-flat",
"ladder/05-holes-and-taps/004-counterbore",
"ladder/05-holes-and-taps/007-tapped-m8-blind",
"ladder/05-holes-and-taps/005-countersink",
"ladder/05-holes-and-taps/011-crossing-holes-steinmetz",
"ladder/05-holes-and-taps/012-crossing-tapped-holes-offset",
@@ -8,10 +8,10 @@
],
"what": "tapped holes (Hole with a thread size, no diameter) are drilled at the tap drill, not at the major diameter",
"source": "analytic",
"method": "Tapped hole rule: the drill is the major diameter minus the pitch (M3 0.5 -> 2.5, M5 0.8 -> 4.2, M8 1.25 -> 6.75, M8.5x1 -> 7.5, M12 1.75 -> 10.25). Plate 100x40x12 with five holes in one row, each a cylinder pi r^2 h: M3: drill 2.5 x 12; M5: drill 4.2 x 12; M8: drill 6.75 x 12; M8.5x1: drill 7.5 x 12; M12: drill 10.25 x 8. V = 48000 - sum = 46155.1547 mm3. Faces: the plate's 6, one wall per through hole (four), a wall and a flat floor for the blind M12: 6 + 4 + 2 = 12.",
"method": "Tapped hole rule: the drill is the tap maker's for the size (Yamawa YEU23 p.704: M3 0.5 -> 2.5, M5 0.8 -> 4.2, M8 1.25 -> 6.8, M12 1.75 -> 10.3), the major diameter minus the pitch for a custom size (M8.5x1 -> 7.5). Plate 100x40x12 with five holes in one row, each a cylinder pi r^2 h: M3: drill 2.5 x 12; M5: drill 4.2 x 12; M8: drill 6.8 x 12; M8.5x1: drill 7.5 x 12; M12: drill 10.3 x 8. V = 48000 - sum = 46142.3134 mm3. Faces: the plate's 6, one wall per through hole (four), a wall and a flat floor for the blind M12: 6 + 4 + 2 = 12.",
"outcome": "solid",
"body_count": 1,
"volume_mm3": 46155.1547,
"volume_mm3": 46142.3134,
"faces": 12,
"bbox": {
"min": [