Files
OneCAD/crates/engine/src/thread_tests.rs
T

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