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
@@ -20,6 +20,8 @@ pub enum Surf {
|
||||
direction: usize,
|
||||
},
|
||||
Patch,
|
||||
Unstitch,
|
||||
ReverseNormal,
|
||||
Stitch {
|
||||
tolerance: String,
|
||||
},
|
||||
@@ -43,6 +45,8 @@ impl Surf {
|
||||
match self {
|
||||
Surf::Extrude { .. } => "SURFACE EXTRUDE",
|
||||
Surf::Patch => "PATCH",
|
||||
Surf::Unstitch => "UNSTITCH",
|
||||
Surf::ReverseNormal => "REVERSE NORMAL",
|
||||
Surf::Stitch { .. } => "STITCH",
|
||||
Surf::ConvertMesh { .. } => "CONVERT MESH",
|
||||
Surf::Thicken { .. } => "THICKEN",
|
||||
@@ -69,6 +73,8 @@ fn inputs(k: &Surf) -> Vec<SelInput> {
|
||||
match k {
|
||||
Surf::Extrude { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true)],
|
||||
Surf::Patch => vec![SelInput::new("Boundary", PROFILES | EDGES, true)],
|
||||
Surf::Unstitch => vec![SelInput::new("Body, or faces to take out", BODIES | FACES, true)],
|
||||
Surf::ReverseNormal => vec![SelInput::new("Surface, or faces to turn over", BODIES | FACES, true)],
|
||||
Surf::Stitch { .. } => vec![SelInput::new("Surfaces", BODIES, true)],
|
||||
Surf::ConvertMesh { .. } => vec![SelInput::new("Mesh body", BODIES, false)],
|
||||
Surf::Thicken { .. } => vec![SelInput::new("Surfaces", BODIES, true)],
|
||||
@@ -87,6 +93,8 @@ pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
let k = match id {
|
||||
"surface.extrude" => Surf::Extrude { distance: "10 mm".into(), direction: 0 },
|
||||
"surface.patch" => Surf::Patch,
|
||||
"surface.unstitch" => Surf::Unstitch,
|
||||
"surface.reverse_normal" => Surf::ReverseNormal,
|
||||
"surface.stitch" => Surf::Stitch { tolerance: String::new() },
|
||||
"mesh.convert" => Surf::ConvertMesh { tolerance: "0.05 mm".into() },
|
||||
"surface.thicken" => Surf::Thicken { thickness: "2 mm".into(), symmetric: false, operation: 0 },
|
||||
@@ -136,7 +144,7 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Surf) -> bool {
|
||||
combo(ui, "sx_dir", &EXTRUDE_SIDES.map(|(label, _)| label), direction);
|
||||
ui.end_row();
|
||||
}
|
||||
Surf::Patch | Surf::Trim | Surf::SplitFace => {}
|
||||
Surf::Patch | Surf::Trim | Surf::SplitFace | Surf::Unstitch | Surf::ReverseNormal => {}
|
||||
}
|
||||
enter
|
||||
}
|
||||
@@ -153,6 +161,20 @@ fn bodies(inputs: &[SelInput], i: usize) -> Vec<String> {
|
||||
items(inputs, i).iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// One body and points on the picked faces of it (none: a whole body picked).
|
||||
fn body_and_faces(picks: &[Sel]) -> Result<(String, Vec<Value>), String> {
|
||||
let body_of = |x: &Sel| match x {
|
||||
Sel::Body { name } | Sel::Face { body: name, .. } => Some(name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let body = picks.iter().find_map(body_of).ok_or("pick a body or some of its faces")?;
|
||||
if picks.iter().any(|x| body_of(x).as_ref() != Some(&body)) {
|
||||
return Err("pick faces of one body".into());
|
||||
}
|
||||
let faces = picks.iter().filter_map(|x| if let Sel::Face { point, .. } = x { Some(pt(*point)) } else { None }).collect();
|
||||
Ok((body, faces))
|
||||
}
|
||||
|
||||
/// A splitting or trimming tool: a plane by name, a planar face as its plane, or any other
|
||||
/// face as {body, point} (its surface extended).
|
||||
fn tool(app: &SolveApp, sel: Option<&Sel>) -> Result<(&'static str, Value), String> {
|
||||
@@ -210,6 +232,12 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
|
||||
("surface.patch", json!({"edges": edges, "body": body}))
|
||||
}
|
||||
}
|
||||
Surf::Unstitch | Surf::ReverseNormal => {
|
||||
need(0, "a body or some of its faces")?;
|
||||
let (body, faces) = body_and_faces(items(inputs, 0))?;
|
||||
let cmd = if matches!(k, Surf::Unstitch) { "surface.unstitch" } else { "surface.reverse_normal" };
|
||||
(cmd, json!({"body": body, "faces": faces}))
|
||||
}
|
||||
Surf::Stitch { tolerance } => {
|
||||
need(0, "the surfaces to stitch")?;
|
||||
let mut p = json!({ "bodies": bodies(inputs, 0) });
|
||||
@@ -315,6 +343,14 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
}
|
||||
(Surf::Patch, vec![sels])
|
||||
}
|
||||
FeatureKind::Unstitch { body, faces } | FeatureKind::ReverseNormal { body, faces } => {
|
||||
let sels: Vec<Sel> = if faces.is_empty() {
|
||||
vec![Sel::Body { name: body.clone() }]
|
||||
} else {
|
||||
faces.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect()
|
||||
};
|
||||
(if matches!(kind, FeatureKind::Unstitch { .. }) { Surf::Unstitch } else { Surf::ReverseNormal }, vec![sels])
|
||||
}
|
||||
FeatureKind::Stitch { bodies, tolerance } => (
|
||||
Surf::Stitch { tolerance: if tolerance.trim() == "0" { String::new() } else { tolerance.clone() } },
|
||||
vec![bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect()],
|
||||
|
||||
@@ -829,6 +829,31 @@ fn surface_extrude_dialog_takes_profiles_and_curves() {
|
||||
assert_eq!(back.inputs[0].items.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unstitch_and_reverse_normal_dialogs_take_a_body_or_faces() {
|
||||
// A whole box picked: Unstitch sends the body alone; two of its faces: their points.
|
||||
let mut app = app_with(json!([{"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Box"}}]));
|
||||
let mut d = start(&mut app, vec![], "surface.unstitch");
|
||||
d.inputs[0].items = vec![Sel::Body { name: "Box".into() }];
|
||||
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
|
||||
assert_eq!((id.as_str(), &p["body"], &p["faces"]), ("surface.unstitch", &json!("Box"), &json!([])), "{p}");
|
||||
let top = Vec3::new(5.0, 5.0, 10.0);
|
||||
d.inputs[0].items = vec![Sel::Face { body: "Box".into(), index: 0, point: top }];
|
||||
let (_, p) = apply_commands(&app, &d).unwrap().remove(0);
|
||||
assert_eq!(p["faces"], json!([[5.0, 5.0, 10.0]]), "{p}");
|
||||
app.session.execute("surface.unstitch", &p).unwrap();
|
||||
assert_eq!(app.session.model.state().bodies.len(), 2);
|
||||
let fid = app.session.doc.features.last().unwrap().id;
|
||||
assert!(matches!(crate::dialogs::for_feature(&app, fid, None).unwrap().kind, Kind::Surface(_)));
|
||||
app.dialog = None;
|
||||
let sheet = app.session.model.state().bodies.last().unwrap().name.clone();
|
||||
let mut d = start(&mut app, vec![], "surface.reverse_normal");
|
||||
d.inputs[0].items = vec![Sel::Body { name: sheet.clone() }];
|
||||
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
|
||||
assert_eq!((id.as_str(), &p["body"]), ("surface.reverse_normal", &json!(sheet)), "{p}");
|
||||
app.session.execute(&id, &p).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_dialog_fills_a_loop_of_edges_that_is_not_planar() {
|
||||
// A box with its top cut away by a sideways cylinder: its rim is a closed loop of four
|
||||
|
||||
@@ -12,6 +12,8 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
FeatureKind::Sketch { .. } => "sketch",
|
||||
FeatureKind::Extrude { .. } => "extrude",
|
||||
FeatureKind::SurfaceExtrude { .. } => "surface_extrude",
|
||||
FeatureKind::Unstitch { .. } => "unstitch",
|
||||
FeatureKind::ReverseNormal { .. } => "reverse_normal",
|
||||
FeatureKind::Revolve { .. } => "revolve",
|
||||
FeatureKind::Fillet { .. } => "fillet",
|
||||
FeatureKind::Chamfer { .. } => "chamfer",
|
||||
|
||||
Reference in new issue
Block a user