Unstitch and Reverse Normal (#856)
Unstitch takes a body's faces out as surface bodies; Reverse Normal turns a surface body's faces over. Both are saved in format 18. Closes #766.
This commit is contained in:
1 parent
b702f0efde
commit
753b0a2eab
16 files changed
+322
-19
No files matched your search
@@ -66,14 +66,14 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SURFACE", "MODIFY")
|
||||
.icon("untrim")
|
||||
.params("planned: face: {body, point}; holes?: all|[[x,y,z]] (points on the inner loops to remove); outer?: bool (restore the natural bounds); in_place?: bool (a surface body only)"),
|
||||
planned!("surface.unstitch", "Unstitch", "766")
|
||||
CommandSpec::new("surface.unstitch", "Unstitch", unstitch)
|
||||
.at("SURFACE", "MODIFY")
|
||||
.icon("unstitch")
|
||||
.params("planned: body; faces?: [[x,y,z]] (points on the faces to split off; default: every face its own surface body)"),
|
||||
planned!("surface.reverse_normal", "Reverse Normal", "766")
|
||||
.params("body; faces?: [[x,y,z]] (points on the faces to take out, each its own surface body; the rest stay together under the body's name; default: every face its own surface body). A solid comes apart into surfaces"),
|
||||
CommandSpec::new("surface.reverse_normal", "Reverse Normal", reverse_normal)
|
||||
.at("SURFACE", "MODIFY")
|
||||
.icon("reverse_normal")
|
||||
.params("planned: body (a surface); faces?: [[x,y,z]] (default: all of them)"),
|
||||
.params("body (a surface); faces?: [[x,y,z]] (points on the faces to turn over; default: all of them)"),
|
||||
planned!("surface.fillet", "Surface Fillet", "769")
|
||||
.at("SURFACE", "MODIFY")
|
||||
.icon("surface_fillet")
|
||||
@@ -110,6 +110,29 @@ fn surface_extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent })
|
||||
}
|
||||
|
||||
/// A body by name, and points on some of its faces (none: all of them).
|
||||
fn body_faces(s: &Session, p: &Value, cmd: &str) -> Result<(String, Vec<solidcraft_geom::Vec3>)> {
|
||||
let body = str_(p, "body").ok_or_else(|| bad(cmd, "`body` is required"))?.to_string();
|
||||
if s.model.state().body(&body).is_none() {
|
||||
return Err(bad(cmd, format!("no body `{body}`")));
|
||||
}
|
||||
let faces: Vec<solidcraft_geom::Vec3> = p.get("faces").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
|
||||
if faces.len() > 10_000 {
|
||||
return Err(bad(cmd, "too many faces"));
|
||||
}
|
||||
Ok((body, faces))
|
||||
}
|
||||
|
||||
fn unstitch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (body, faces) = body_faces(s, p, "surface.unstitch")?;
|
||||
add_feature(s, p, FeatureKind::Unstitch { body, faces })
|
||||
}
|
||||
|
||||
fn reverse_normal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (body, faces) = body_faces(s, p, "surface.reverse_normal")?;
|
||||
add_feature(s, p, FeatureKind::ReverseNormal { body, faces })
|
||||
}
|
||||
|
||||
fn bodies_param(s: &Session, p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
let bodies = string_list(p, "bodies");
|
||||
if bodies.is_empty() {
|
||||
@@ -255,6 +278,66 @@ mod tests {
|
||||
assert!((volume(&s, "Walls") - 180.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// Unstitch a box: six surface bodies whose faces keep the names they had on the box,
|
||||
/// stitched back into a solid of the same volume; two faces taken out leave the other four
|
||||
/// under the box's name; undo, redo and reopening.
|
||||
#[test]
|
||||
fn unstitch_a_box_and_stitch_it_back() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Box"}));
|
||||
let names_before: Vec<String> = solidcraft_doc::naming::face_names(s.model.state().body("Box").unwrap()).to_vec();
|
||||
run(&mut s, "surface.unstitch", json!({"body": "Box"}));
|
||||
let st = s.world_state();
|
||||
assert!(st.body("Box").is_none());
|
||||
let pieces: Vec<String> = st.bodies.iter().map(|b| b.name.clone()).collect();
|
||||
assert_eq!(pieces.len(), 6);
|
||||
let mut names_after: Vec<String> = Vec::new();
|
||||
for n in &pieces {
|
||||
let b = st.body(n).unwrap();
|
||||
assert!(b.body.is_surface() && b.body.face_count() == 1);
|
||||
names_after.extend(solidcraft_doc::naming::face_names(s.model.state().body(n).unwrap()).iter().cloned());
|
||||
}
|
||||
let (mut a, mut b) = (names_before.clone(), names_after);
|
||||
a.sort();
|
||||
b.sort();
|
||||
assert_eq!(a, b, "the faces keep their names");
|
||||
run(&mut s, "surface.stitch", json!({"bodies": pieces}));
|
||||
let st = s.world_state();
|
||||
let back = st.bodies.first().unwrap();
|
||||
assert!(!back.body.is_surface());
|
||||
assert!((solidcraft_kernel::measure(&back.body).unwrap().volume - 1000.0).abs() < 1e-9);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.world_state().body("Box").is_some());
|
||||
run(&mut s, "surface.unstitch", json!({"body": "Box", "faces": [[5, 5, 10], [5, 5, 0]]}));
|
||||
let st = s.world_state();
|
||||
assert_eq!(st.bodies.len(), 3);
|
||||
assert_eq!(st.body("Box").unwrap().body.face_count(), 4);
|
||||
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert_eq!(cold.world_state().bodies.len(), 3);
|
||||
assert!(s.execute("surface.unstitch", &json!({"body": "Nope"})).is_err());
|
||||
assert!(s.execute("surface.unstitch", &json!({})).is_err());
|
||||
}
|
||||
|
||||
/// A patch turned over thickens the other way; a solid is not turned over.
|
||||
#[test]
|
||||
fn reverse_normal_turns_the_thicken_side() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "surface.patch", json!({"sketch": "S", "body_name": "Sheet"}));
|
||||
run(&mut s, "surface.thicken", json!({"bodies": ["Sheet"], "thickness": 2, "body_name": "Up"}));
|
||||
let up = solidcraft_kernel::measure(&s.world_state().body("Up").unwrap().body).unwrap().bbox;
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "surface.reverse_normal", json!({"body": "Sheet"}));
|
||||
run(&mut s, "surface.thicken", json!({"bodies": ["Sheet"], "thickness": 2, "body_name": "Down"}));
|
||||
let down = solidcraft_kernel::measure(&s.world_state().body("Down").unwrap().body).unwrap().bbox;
|
||||
assert!((up.max.z - down.max.z - 2.0).abs() < 1e-9 && (up.min.z - down.min.z - 2.0).abs() < 1e-9, "{up:?} vs {down:?}");
|
||||
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "corner": [20, 0, 0], "body_name": "Cube"}));
|
||||
assert!(s.execute("surface.reverse_normal", &json!({"body": "Cube"})).is_err());
|
||||
}
|
||||
|
||||
/// The planned surface commands are registered but refuse to run: disabled with the
|
||||
/// issue that tracks them, an error (never a panic) from every caller, and the design left
|
||||
/// as it was.
|
||||
@@ -269,8 +352,6 @@ mod tests {
|
||||
"surface.boundary",
|
||||
"surface.blend",
|
||||
"surface.untrim",
|
||||
"surface.unstitch",
|
||||
"surface.reverse_normal",
|
||||
"surface.fillet",
|
||||
"inspect.continuity",
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user