Thin Extrude builds walls of a set thickness along sketch curves, open chains and closed loops, with the wall centred, inside, outside or on both sides at two thicknesses. Saved under format 20 (thin on an Extrude); an extrude without thin saves as before. Closes #721.
175 lines
8.3 KiB
Rust
175 lines
8.3 KiB
Rust
//! 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<String> {
|
||
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}");
|
||
}
|
||
}
|
||
}
|
||
}
|