//! A flange joined flush with the end of a bent, tapered lofted half tube (#1391). //! //! The tube is a half annulus with a 6 mm wall, lofted through three sections along a bend of radius 300 mm and 20 degrees, its radius //! growing from 60 to 75 mm. The flange is a thicker half annulus (3 mm into the wall, 25 mm beyond it), 8 mm thick, extruded back from //! the tube's end plane and joined. Its front face lies in that plane, coplanar with the tube's end face, and the neighbours of that //! face are faces of the loft, which curve and lean: the faces cannot be pushed apart, and the join failed with "Union: no result". //! (With a 40 degree bend the retry with the tool displaced joins it; with this bend that retry fails too.) use std::f64::consts::PI; use super::*; /// Radius of the bend of the tube's axis, and the turn of it (radians). const BEND: f64 = 300.0; const TURN: f64 = 20.0 * PI / 180.0; /// Inner radius of the tube at its start and at its end, and the wall. const R0: f64 = 60.0; const R1: f64 = 75.0; const WALL: f64 = 6.0; /// How far the flange reaches into the tube's wall, how far beyond its outer face, and how thick it is. const LAND_IN: f64 = 3.0; const LAND_OUT: f64 = 25.0; const THICK: f64 = 8.0; /// Sketch coordinates are given to nine decimals, as a design's own would be. fn r9(x: f64) -> f64 { (x * 1e9).round() / 1e9 } /// The plane of the section at the bend angle `a`, moved `over` along the axis (the sketch's x is up, its y across the tube). fn plane(a: f64, over: f64) -> Value { let (tx, ty) = (a.cos(), a.sin()); json!({ "origin": [r9(BEND * a.sin() + over * tx), r9(BEND * (1.0 - a.cos()) + over * ty), 0], "x_dir": [0, 0, 1], "y_dir": [r9(ty), r9(-tx), 0], }) } /// A half annulus (the half above z = 0) on `plane`, between the radii `r_in` and `r_out`. fn half_annulus(s: &mut Session, name: &str, plane: Value, r_in: f64, r_out: f64) { let (r_in, r_out) = (r9(r_in), r9(r_out)); run(s, "sketch.create", json!({"plane": plane, "name": name})); run(s, "sketch.arc.three_point", json!({"start": [0, -r_out], "end": [0, r_out], "through": [r_out, 0]})); run(s, "sketch.line", json!({"points": [[0, r_out], [0, r_in]]})); run(s, "sketch.arc.three_point", json!({"start": [0, r_in], "end": [0, -r_in], "through": [r_in, 0]})); run(s, "sketch.line", json!({"points": [[0, -r_in], [0, -r_out]]})); run(s, "sketch.finish", json!({})); } /// The tube: a loft through three sections. fn tube(s: &mut Session) { for i in 0..3 { let t = i as f64 / 2.0; let r_in = R0 + (R1 - R0) * t; half_annulus(s, &format!("S{i}"), plane(TURN * t, 0.0), r_in, r_in + WALL); } let sections: Vec = (0..3).map(|i| json!({"sketch": format!("S{i}")})).collect(); run(s, "solid.loft", json!({"sections": sections, "operation": "new", "name": "TubeLoft", "body_name": "Tube"})); } /// The flange, joined to the tube. Its front face is `over` mm past the tube's end plane (0: in it). fn flange(s: &mut Session, over: f64) { half_annulus(s, "Flange", plane(TURN, over), R1 + LAND_IN, R1 + WALL + LAND_OUT); let join = json!({"sketch": "Flange", "distance": THICK + over, "direction": "negative", "operation": "join", "targets": ["Tube"], "name": "JoinFlange"}); run(s, "solid.extrude", join); } #[test] fn a_flange_flush_with_the_end_of_a_bent_tapered_tube_joins() { // The flange's face, the half annulus, has this area. let (r_in, r_out) = (R1 + LAND_IN, R1 + WALL + LAND_OUT); let footprint = PI / 2.0 * (r_out * r_out - r_in * r_in); // The reference: with the flange 2 mm past the end plane nothing is coplanar with the tube's end face (that join is made by another // route), and what it adds beyond that plane is the footprint, 2 mm deep. let mut s = Session::default(); tube(&mut s); flange(&mut s, 2.0); let reference = volume(&mut s) - 2.0 * footprint; let mut s = Session::default(); tube(&mut s); flange(&mut s, 0.0); let v = volume(&mut s); assert!(rel(v, reference) < 3e-4, "{v} against {reference}"); // One valid body in one piece, and nothing of it beyond the end plane (to a micrometre: the curved faces end on it within the // tolerance of their edges): the face the flange and the tube share is one flat face. let state = s.model.state(); assert_eq!(state.bodies.len(), 1); let body = &state.bodies[0].body; assert!(body.validity().is_empty(), "{:?}", body.validity()); assert_eq!(body.lumps().unwrap().len(), 1); let (origin, normal) = (Vec3::new(BEND * TURN.sin(), BEND * (1.0 - TURN.cos()), 0.0), Vec3::new(TURN.cos(), TURN.sin(), 0.0)); let mesh = body.tessellate(0.05).unwrap(); let beyond = mesh.positions.iter().map(|p| (*p - origin).dot(normal)).fold(f64::MIN, f64::max); assert!(beyond < 1e-3, "the join reaches {beyond} mm past the end plane"); }