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"}));