From 5cc0d5e9c499b505bdca8a6701b8b858df28d4ef Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:10:51 -0400 Subject: [PATCH] Threads: a metric size without a coarse pitch suggests its fine pitches again The thread forms (b4549d270) parse designations in thread_forms, whose refusal for a plain M28 said "give it: M28x1" and, for M0 or M1e9, still offered a pitch. The hint the hole command gave (the two coarsest ISO fine pitches that suit the diameter: M28x2 or M28x1.5) is now thread_forms::no_coarse_pitch, used by both; a diameter no metric thread has is an unknown metric thread. The test's "unknown thread" follows the forms' wording ("unknown metric thread"). Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/thread_forms.rs | 14 +++++++++++++- crates/engine/src/cmd/features.rs | 10 +--------- crates/engine/src/thread_tests.rs | 3 ++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/doc/src/thread_forms.rs b/crates/doc/src/thread_forms.rs index cc508afe66..b4f4302429 100644 --- a/crates/doc/src/thread_forms.rs +++ b/crates/doc/src/thread_forms.rs @@ -400,7 +400,7 @@ impl ThreadForm { } None => { let d = num(body).ok_or_else(bad)?; - let p = self.coarse(d).ok_or_else(|| format!("no coarse pitch for M{} (give it: M{}x1)", show(d), show(d)))?; + let p = self.coarse(d).ok_or_else(|| no_coarse_pitch(d).unwrap_or_else(bad))?; (d, p, p * n) } } @@ -663,6 +663,18 @@ fn inches(s: &str) -> Option { num(s) } +/// The refusal for a plain `M` with no ISO coarse pitch: the two coarsest fine pitches to +/// give that suit the diameter. `None` for a diameter no metric thread has. +pub fn no_coarse_pitch(d: f64) -> Option { + const PITCHES: [f64; 9] = [2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.35, 0.25, 0.2]; + if !(d.is_finite() && d > 0.2 && d <= 1e4) { + return None; + } + let first = PITCHES.iter().position(|p| *p <= 0.12 * d).unwrap_or(PITCHES.len() - 1); + let pitches: Vec = PITCHES.iter().skip(first).take(2).filter(|p| **p < d).map(|p| format!("M{}x{}", show(d), show(*p))).collect(); + (!pitches.is_empty()).then(|| format!("M{} has no coarse ISO pitch: give one, e.g. {}", show(d), pitches.join(" or "))) +} + fn show(v: f64) -> String { let s = format!("{v:.6}"); s.trim_end_matches('0').trim_end_matches('.').to_string() diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 506101442b..816143638a 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -2343,16 +2343,8 @@ fn clearance_diameter(size: &str, fit: &str) -> Option { /// The pitches to try for a plain `M` that has no ISO coarse pitch, `None` for anything else. fn fine_only_hint(t: &str) -> Option { - const PITCHES: [f64; 9] = [2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.35, 0.25, 0.2]; let diameter = t.trim().strip_prefix(['M', 'm']).and_then(|d| d.trim().parse::().ok()); - if let Some(d) = diameter.filter(|d| d.is_finite() && *d > 0.2 && *d <= 1e4) { - let first = PITCHES.iter().position(|p| *p <= 0.12 * d).unwrap_or(PITCHES.len() - 1); - let pitches: Vec = PITCHES.iter().skip(first).take(2).filter(|p| **p < d).map(|p| format!("M{d}x{p}")).collect(); - if !pitches.is_empty() { - return Some(format!("M{d} has no coarse ISO pitch: give one, e.g. {}", pitches.join(" or "))); - } - } - None + diameter.and_then(solidcraft_doc::thread_forms::no_coarse_pitch) } /// Largest nominal diameter automatic thread sizing knows (the last ISO coarse size). diff --git a/crates/engine/src/thread_tests.rs b/crates/engine/src/thread_tests.rs index b9c2a6b4bf..7a9fea1265 100644 --- a/crates/engine/src/thread_tests.rs +++ b/crates/engine/src/thread_tests.rs @@ -270,7 +270,8 @@ fn a_thread_without_a_coarse_pitch_suggests_explicit_pitches() { } for designation in ["MNaN", "Minf", "M-28", "M0", "M1e9", "M1e999", "M28x", "M28xNaN"] { let e = s.execute("solid.thread", &json!({"face": [14, 0, 5], "designation": designation})).unwrap_err().to_string(); - assert!(e.contains("unknown thread"), "{designation}: {e}"); + // Each thread form names itself in its refusal: `unknown metric thread`. + assert!(e.contains("unknown metric thread"), "{designation}: {e}"); assert_eq!(s.doc.to_json(), saved); } run(&mut s, "solid.thread", json!({"face": [14, 0, 5], "designation": "M28x2"}));