//! Thin Extrude (#721): walls along sketch curves, their volumes worked out by hand (a wall's //! area times its height). 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}"), } } fn volume(s: &mut Session) -> f64 { run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN) } fn body_count(s: &mut Session) -> u64 { run(s, "inspect.measure", json!({}))["body_count"].as_u64().unwrap_or(0) } fn close(a: f64, b: f64) { assert!((a - b).abs() <= 1e-6 * b.abs().max(1.0), "{a} != {b}"); } /// A sketch on XY holding a 40 × 20 rectangle from (5, 5); returns its side ids. fn rectangle(s: &mut Session) -> Vec { run(s, "sketch.create", json!({"plane": "XY", "name": "Outline"})); let r = run(s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [45, 25]})); run(s, "sketch.finish", json!({})); ["bottom", "right", "top", "left"].iter().map(|k| r["sides"][k].as_str().unwrap_or_default().to_string()).collect() } #[test] fn a_rectangle_makes_a_ring_wall_on_either_side() { for (side, extra, area) in [ ("center", json!({}), 42.0 * 22.0 - 38.0 * 18.0), ("inside", json!({}), 40.0 * 20.0 - 36.0 * 16.0), ("outside", json!({}), 44.0 * 24.0 - 40.0 * 20.0), ("two_sides", json!({"thickness2": 1.5}), 43.0 * 23.0 - 39.0 * 19.0), ] { let mut s = Session::default(); rectangle(&mut s); let mut p = json!({"sketch": "Outline", "thin": true, "thickness": 2, "side": side, "distance": 10}); if let (Some(p), Some(extra)) = (p.as_object_mut(), extra.as_object()) { p.extend(extra.clone()); } if side == "two_sides" { p["thickness"] = json!(0.5); } run(&mut s, "solid.extrude", p); close(volume(&mut s), area * 10.0); assert_eq!(body_count(&mut s), 1, "{side}"); } } #[test] fn the_thickness_is_a_parameter_and_the_wall_survives_reopening() { let mut s = Session::default(); rectangle(&mut s); let r = run(&mut s, "solid.extrude", json!({"sketch": "Outline", "thin": true, "thickness": 2, "distance": 10, "name": "Wall"})); assert!(r["created_bodies"].as_array().is_some_and(|b| b.len() == 1)); close(volume(&mut s), 240.0 * 10.0); let f = s.doc.features.iter().find(|f| f.name == "Wall").cloned().unwrap(); let t = f.param_names.iter().zip(f.kind.inputs()).find(|(_, (label, _, _))| *label == "Thickness").map(|(n, _)| n.clone()).unwrap(); run(&mut s, "parameters.change", json!({"name": t, "expression": "4"})); close(volume(&mut s), (44.0 * 24.0 - 36.0 * 16.0) * 10.0); let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() }; reopened.refresh(); close(volume(&mut reopened), (44.0 * 24.0 - 36.0 * 16.0) * 10.0); // A plain extrude of the same sketch fills the rectangle. let mut s = Session::default(); rectangle(&mut s); run(&mut s, "solid.extrude", json!({"sketch": "Outline", "distance": 10})); close(volume(&mut s), 800.0 * 10.0); } #[test] fn chosen_curves_make_an_open_wall_that_joins_and_cuts() { // Three sides of the rectangle: a U, inside toward the open side's middle. let mut s = Session::default(); let sides = rectangle(&mut s); let u = [&sides[0], &sides[1], &sides[2]]; run(&mut s, "solid.extrude", json!({"sketch": "Outline", "thin": true, "curves": u, "thickness": 2, "side": "inside", "distance": 5})); // 40 × 2 twice and 20 × 2, less the two 2 × 2 corners they share. close(volume(&mut s), (80.0 + 40.0 + 80.0 - 8.0) * 5.0); // Cut a groove 3 deep along the whole outline into a block under it. let mut s = Session::default(); run(&mut s, "solid.box", json!({"length": 50, "width": 30, "height": 10, "corner": [0, 0, -10], "body_name": "Block"})); rectangle(&mut s); run( &mut s, "solid.extrude", json!({"sketch": "Outline", "thin": true, "thickness": 2, "distance": 3, "direction": "negative", "operation": "cut"}), ); close(volume(&mut s), 15000.0 - 240.0 * 3.0); assert_eq!(body_count(&mut s), 1); // And join a rim on top of it. let mut s = Session::default(); run(&mut s, "solid.box", json!({"length": 50, "width": 30, "height": 10, "corner": [0, 0, -10], "body_name": "Block"})); rectangle(&mut s); run(&mut s, "solid.extrude", json!({"sketch": "Outline", "thin": true, "thickness": 2, "distance": 4, "operation": "auto"})); close(volume(&mut s), 15000.0 + 240.0 * 4.0); assert_eq!(body_count(&mut s), 1); } #[test] fn a_ring_and_a_cross_of_walls_are_one_body() { let mut s = Session::default(); run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Target"})); run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 20})); run(&mut s, "sketch.line", json!({"points": [[-20, 0], [20, 0]]})); run(&mut s, "sketch.line", json!({"points": [[0, -20], [0, 20]]})); run(&mut s, "sketch.finish", json!({})); run(&mut s, "solid.extrude", json!({"sketch": "Target", "thin": true, "thickness": 1, "distance": 5})); assert_eq!(body_count(&mut s), 1); // The ring, plus the bars inside it less where they cross and where they run into it. let ring = std::f64::consts::PI * (20.5f64.powi(2) - 19.5f64.powi(2)); let v = volume(&mut s); assert!(v > (ring + 2.0 * 38.0 - 1.0) * 5.0 && v < (ring + 2.0 * 40.0) * 5.0, "{v}"); } #[test] fn bad_thin_walls_are_refused_and_change_nothing() { let mut s = Session::default(); rectangle(&mut s); let before = s.doc.to_json(); for (p, says) in [ (json!({"thin": true, "thickness": 2, "distance": 5, "curves": ["nope"]}), "no curve"), (json!({"thin": true, "distance": 5}), "thickness"), (json!({"thin": true, "thickness": 2, "distance": 5, "side": "two_sides"}), "thickness2"), (json!({"thin": true, "thickness": 2, "thickness2": 1, "distance": 5}), "two_sides"), (json!({"thin": true, "thickness": 2, "distance": 5, "side": "sideways"}), "unknown side"), (json!({"thin": true, "thickness": 2, "distance": 5, "profiles": [0]}), "profiles"), (json!({"thin": true, "thickness": 0, "distance": 5}), "thickness"), (json!({"thin": true, "thickness": -1, "distance": 5}), "thickness"), (json!({"thin": "yes", "thickness": 1, "distance": 5}), "`thin`"), ] { let mut p = p; p["sketch"] = json!("Outline"); let e = s.execute("solid.extrude", &p).err().map(|e| e.to_string()).unwrap_or_default(); assert!(e.contains(says), "{p}: {e}"); assert_eq!(s.doc.to_json(), before); } // A wall thicker than an arc it follows bends. let mut s = Session::default(); run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Small"})); run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 1})); run(&mut s, "sketch.finish", json!({})); let e = s.execute("solid.extrude", &json!({"sketch": "Small", "thin": true, "thickness": 3, "side": "inside", "distance": 5})); assert!(e.is_err_and(|e| e.to_string().contains("thicker"))); run(&mut s, "solid.extrude", json!({"sketch": "Small", "thin": true, "thickness": 3, "side": "outside", "distance": 5})); // Measured from the tessellation, within its 1e-4. let (v, exact) = (volume(&mut s), std::f64::consts::PI * (16.0 - 1.0) * 5.0); assert!((v - exact).abs() < 1e-4 * exact, "{v} vs {exact}"); } #[test] fn hostile_thin_params_never_panic() { let keys = ["curves", "thickness", "thickness2", "side", "distance", "taper", "direction", "to", "operation"]; for v in crate::tests::hostile_values().into_iter().chain([json!(1e9), json!(1e-9), json!("1e6 m"), json!(["l1", "l1", "l1"])]) { for k in keys { let mut s = Session::default(); run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10})); rectangle(&mut s); let mut p = json!({"sketch": "Outline", "thin": true, "thickness": 2, "side": "two_sides", "thickness2": 1, "distance": 5}); p[k] = v.clone(); if let Err(crate::EngineError::Internal(id, msg)) = s.execute("solid.extrude", &p) { panic!("{id} {p}: {msg}"); } } } }