Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
70 lines
3.2 KiB
Rust
70 lines
3.2 KiB
Rust
//! Modelled threads: the ISO metric profile cut along a helix, M3 to M20, outside and inside.
|
|
|
|
use std::f64::consts::PI;
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
match s.execute(id, &p) {
|
|
Ok(v) => v,
|
|
Err(e) => panic!("{id} {p}: {e}"),
|
|
}
|
|
}
|
|
|
|
/// Volume from the body's mesh: exact for the faceted threaded body, and quicker than a full
|
|
/// measurement of its thousands of faces.
|
|
fn volume(s: &mut Session, b: &str) -> f64 {
|
|
let st = s.model.state();
|
|
st.body(b).map(|mb| mb.body.tessellate(1e-3).unwrap().measure().volume).unwrap_or(0.0)
|
|
}
|
|
|
|
const SIZES: [(f64, f64); 9] = [(3.0, 0.5), (4.0, 0.7), (5.0, 0.8), (6.0, 1.0), (8.0, 1.25), (10.0, 1.5), (12.0, 1.75), (16.0, 2.0), (20.0, 2.5)];
|
|
|
|
/// ∫ 2π r · width(r) dr over [a, b]: the volume one turn of the groove takes out (a screw
|
|
/// motion of a section in an axial plane sweeps 2π∫r dA per turn).
|
|
fn per_turn(a: f64, b: f64, width: impl Fn(f64) -> f64) -> f64 {
|
|
let n = 400;
|
|
(0..n).map(|i| a + (b - a) * (i as f64 + 0.5) / n as f64).map(|r| 2.0 * PI * r * width(r) * (b - a) / n as f64).sum()
|
|
}
|
|
|
|
#[test]
|
|
fn modelled_external_threads_m3_to_m20() {
|
|
let h = |p: f64| p * 3f64.sqrt() / 2.0;
|
|
let slope = (PI / 6.0).tan();
|
|
for (d, p) in SIZES {
|
|
let mut s = Session::default();
|
|
let len = 4.0 * p;
|
|
run(&mut s, "solid.cylinder", json!({"radius": d / 2.0, "height": len + 2.0, "body_name": "Shaft"}));
|
|
let before = volume(&mut s, "Shaft");
|
|
let th = run(&mut s, "solid.thread", json!({"face": [d / 2.0, 0, 1], "length": len, "modeled": true}));
|
|
assert!(th["thread"].is_null(), "a modelled thread is real geometry, not an annotation: {th}");
|
|
let r_root = d / 2.0 - 5.0 * h(p) / 8.0;
|
|
let want = per_turn(r_root, d / 2.0, |r| p / 4.0 + 2.0 * (r - r_root) * slope) * len / p;
|
|
let got = before - volume(&mut s, "Shaft");
|
|
assert!((got - want).abs() < want * 0.03, "M{d}: removed {got}, ISO profile {want}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn modelled_internal_threads_m3_to_m20() {
|
|
let h = |p: f64| p * 3f64.sqrt() / 2.0;
|
|
let slope = (PI / 6.0).tan();
|
|
for (d, p) in SIZES {
|
|
let mut s = Session::default();
|
|
let len = 4.0 * p;
|
|
let d1 = d - 2.0 * 5.0 * h(p) / 8.0;
|
|
run(&mut s, "solid.box", json!({"corner": [-d, -d, 0], "length": 2.0 * d, "width": 2.0 * d, "height": len, "body_name": "Nut"}));
|
|
run(&mut s, "solid.cylinder", json!({"base": [0, 0, -1], "radius": d1 / 2.0, "height": len + 2.0, "body_name": "Bore"}));
|
|
run(&mut s, "solid.combine", json!({"target": "Nut", "tools": ["Bore"], "operation": "cut"}));
|
|
let before = volume(&mut s, "Nut");
|
|
let th = run(&mut s, "solid.thread", json!({"face": [d1 / 2.0, 0, len / 2.0], "modeled": true}));
|
|
assert!(th["thread"].is_null(), "{th}");
|
|
// Bolt-shaped space: P/8 flat at the major diameter, widening towards the bore.
|
|
let want = per_turn(d1 / 2.0, d / 2.0, |r| p / 8.0 + 2.0 * (d / 2.0 - r) * slope) * len / p;
|
|
let got = before - volume(&mut s, "Nut");
|
|
assert!((got - want).abs() < want * 0.03, "M{d}: removed {got}, ISO profile {want}");
|
|
}
|
|
}
|