Files
OneCAD/crates/engine/src/cmd/extrude_convention_tests.rs
T
Karl Von RichterandKarl-VonRichter 1ec33fcdc7 Extrude: a symmetric distance is the total thickness (#1260)
A symmetric solid extrude of 10 mm is now 10 mm thick, 5 mm each side of the sketch plane, and switching from One Side to Symmetric keeps the thickness entered. Under format 21 the extent records its convention; designs saved without it keep the per-side reading, so their geometry and parameters don't change. Fixes #1236.

Co-authored-by: Karl-VonRichter <Karl-VonRichter@users.noreply.github.com>
2026-10-15 01:19:55 -04:00

144 lines
6.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use crate::Session;
use serde_json::{Value, json};
use solidcraft_doc::{Document, ExtrudeConvention, FeatureKind};
fn run(s: &mut Session, cmd: &str, params: Value) {
s.execute(cmd, &params).unwrap_or_else(|e| panic!("{cmd}: {e}"));
}
fn fixture(settings: Value) -> Session {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane":"XY","name":"Square"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[10,10]}));
run(&mut s, "sketch.finish", json!({}));
let mut p = json!({"sketch":"Square","distance":"10 mm","direction":"symmetric","operation":"new","name":"Depth","body_name":"Block"});
p.as_object_mut().unwrap().extend(settings.as_object().unwrap().clone());
run(&mut s, "solid.extrude", p);
s
}
fn check(s: &Session, lo: f64, hi: f64) {
assert!(s.model.results.iter().all(|r| r.error.is_none()));
let st = s.model.state();
let body = st.body("Block").unwrap();
let bounds = body.mesh().bounds();
assert!((bounds.min.z - lo).abs() < 1e-7 && (bounds.max.z - hi).abs() < 1e-7, "{bounds:?}");
let m = solidcraft_kernel::measure(&body.body).unwrap();
assert!((m.volume - 100.0 * (hi - lo)).abs() < 1e-7, "{m:?}");
assert!((m.centroid.z - (lo + hi) * 0.5).abs() < 1e-7);
}
#[test]
fn symmetric_extrude_total_distance_is_centered_and_follows_parameters() {
for distance in ["10 mm", "-10 mm"] {
let mut s = fixture(json!({"distance":distance,"from":[0,0,2],"start_offset":"3 mm"}));
check(&s, 0.0, 10.0);
let FeatureKind::Extrude { extent, .. } = &s.doc.find_feature("Depth").unwrap().kind else { panic!("extrude") };
assert_eq!(extent.convention, ExtrudeConvention::TotalDistance);
let name = s.doc.find_feature("Depth").unwrap().param_names[0].clone();
run(&mut s, "parameters.change", json!({"name":name,"expression":"20 mm"}));
check(&s, -5.0, 15.0);
run(&mut s, "edit.undo", json!({}));
check(&s, 0.0, 10.0);
check(&Session::new(Document::from_json(&s.doc.to_json()).unwrap()), 0.0, 10.0);
}
}
#[test]
fn symmetric_extrude_switch_from_one_side_keeps_total_thickness() {
let mut s = fixture(json!({"direction":"positive","convention":"legacy_per_side"}));
check(&s, 0.0, 10.0);
run(&mut s, "timeline.edit", json!({"feature":"Depth","set":{"extent":{"direction":"symmetric"}}}));
check(&s, -5.0, 5.0);
run(&mut s, "edit.undo", json!({}));
check(&s, 0.0, 10.0);
run(
&mut s,
"timeline.redefine",
json!({"feature":"Depth","command":"solid.extrude","params":{"sketch":"Square","distance":"10 mm","direction":"symmetric","body_name":"Block"}}),
);
check(&s, -5.0, 5.0);
}
#[test]
fn symmetric_extrude_legacy_missing_marker_preserves_geometry_and_edits() {
let s = fixture(json!({"convention":"legacy_per_side"}));
let mut saved: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
saved["format"] = json!("solidcraft/21");
for f in saved["features"].as_array_mut().unwrap() {
if f["type"] == "extrude" {
f["extent"].as_object_mut().unwrap().remove("convention");
}
}
let mut s = Session::new(Document::from_json(&saved.to_string()).unwrap());
check(&s, -10.0, 10.0);
run(&mut s, "timeline.edit", json!({"feature":"Depth","set":{"extent":{"distance":"12 mm"}}}));
check(&s, -12.0, 12.0);
run(&mut s, "edit.undo", json!({}));
run(
&mut s,
"timeline.redefine",
json!({"feature":"Depth","command":"solid.extrude","params":{"sketch":"Square","distance":"10 mm","direction":"symmetric","body_name":"Block"}}),
);
check(&s, -10.0, 10.0);
check(&Session::new(Document::from_json(&s.doc.to_json()).unwrap()), -10.0, 10.0);
}
#[test]
fn symmetric_extrude_through_all_keeps_reach_and_two_sides_keep_independent_distances() {
let mut s = fixture(json!({"distance2":"3 mm"}));
check(&s, -3.0, 10.0);
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"corner":[0,0,100],"body_name":"FarAbove"}));
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"corner":[0,0,-110],"body_name":"FarBelow"}));
run(
&mut s,
"solid.extrude",
json!({"sketch":"Square","direction":"symmetric","through_all":true,"operation":"cut","targets":["FarAbove","FarBelow"]}),
);
assert!(s.model.state().body("FarAbove").is_none());
assert!(s.model.state().body("FarBelow").is_none());
}
#[test]
fn symmetric_extrude_invalid_convention_is_atomic() {
let mut s = fixture(json!({}));
let before = s.doc.to_json();
for convention in [json!("unknown"), json!(7), Value::Null] {
assert!(s.execute("solid.extrude", &json!({"sketch":"Square","distance":10,"direction":"symmetric","convention":convention})).is_err());
assert_eq!(s.doc.to_json(), before);
}
}
#[test]
fn symmetric_extrude_thin_wall_uses_total_distance_and_to_object_remains_one_sided() {
let s = fixture(json!({"thin":true,"thickness":"2 mm"}));
let st = s.model.state();
let body = st.body("Block").unwrap();
let b = body.mesh().bounds();
assert!((b.min.z + 5.0).abs() < 1e-7 && (b.max.z - 5.0).abs() < 1e-7);
// A centred 2 mm wall around a 10 mm square: (12² - 8²) × 10.
assert!((solidcraft_kernel::measure(&body.body).unwrap().volume - 800.0).abs() < 1e-7);
check(&fixture(json!({"to":[0,0,10]})), 0.0, 10.0);
}
#[test]
fn surface_extrude_shared_extent_retains_its_existing_per_side_semantics() {
let mut s = fixture(json!({}));
run(&mut s, "surface.extrude", json!({"sketch":"Square","distance":"10 mm","direction":"symmetric","name":"Sheet","body_name":"Sheet"}));
let check_sheet = |s: &Session| {
let st = s.model.state();
let bounds = st.body("Sheet").unwrap().mesh().bounds();
assert!((bounds.min.z + 10.0).abs() < 1e-7 && (bounds.max.z - 10.0).abs() < 1e-7);
};
check_sheet(&s);
run(
&mut s,
"timeline.redefine",
json!({"feature":"Sheet","command":"surface.extrude","params":{"sketch":"Square","distance":"10 mm","direction":"symmetric","body_name":"Sheet"}}),
);
check_sheet(&s);
check_sheet(&Session::new(Document::from_json(&s.doc.to_json()).unwrap()));
}