//! 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}"); } } /// Issue 602: rounding the edges of a hex boss on a threaded shaft froze the app. The thread's /// thousands of facets were healed pair by pair, once per feature after it, and the fillet's /// preview then compared each of them with the whole old body. #[test] fn fillet_beside_a_modelled_thread_finishes() { let mut s = Session::default(); for (id, p) in [ ("sketch.create", json!({"plane": "XY"})), ("sketch.circle.center", json!({"center": [0, 0], "diameter": 5})), ("sketch.finish", json!({})), ("solid.extrude", json!({"distance": 10, "body_name": "Screw"})), ("sketch.create", json!({"plane": {"face": [0, 0, 10]}})), ("sketch.polygon.circumscribed", json!({"center": [0, 0], "radius": 2, "sides": 6})), ("sketch.finish", json!({})), ("solid.extrude", json!({"distance": 3, "profiles": [{"point": [0, 0]}], "operation": "join", "targets": ["Screw"]})), ("solid.thread", json!({"face": [2.5, 0, 5], "designation": "M5", "length": 4, "modeled": true})), ] { run(&mut s, id, p); } let faces_before = s.model.state().body("Screw").map(|b| b.body.face_count()).unwrap_or(0); assert!(faces_before > 1000, "the thread is faceted: {faces_before} faces"); let volume_before = volume(&mut s, "Screw"); let top: Vec = (0..6) .map(|i| { let a = (30.0 + 60.0 * i as f64).to_radians(); json!([2.0 * a.cos(), 2.0 * a.sin(), 13]) }) .collect(); let started = std::time::Instant::now(); run(&mut s, "solid.fillet", json!({"edges": top, "radius": 0.3})); assert!(started.elapsed().as_secs() < 30, "the fillet took {:?}", started.elapsed()); let volume_after = volume(&mut s, "Screw"); assert!(volume_after < volume_before && volume_after > volume_before * 0.99, "{volume_before} -> {volume_after}"); }