Offsets picked faces along their normals into a new surface body: planes and cylinders move exactly, other faces get a fitted B-spline within tolerance, and offsets that would fold over are refused. Adds the SurfaceOffset feature (noted under format 18), the surface.offset engine command and an Offset Surface dialog. Closes #758. Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
This commit is contained in:
1 parent
15b5b51bf5
commit
2b70ed7b72
15 files changed
+574
-19
No files matched your search
@@ -30,6 +30,10 @@ pub enum Surf {
|
||||
Patch,
|
||||
Unstitch,
|
||||
ReverseNormal,
|
||||
Offset {
|
||||
distance: String,
|
||||
chain: bool,
|
||||
},
|
||||
Stitch {
|
||||
tolerance: String,
|
||||
},
|
||||
@@ -58,6 +62,7 @@ impl Surf {
|
||||
Surf::Patch => "PATCH",
|
||||
Surf::Unstitch => "UNSTITCH",
|
||||
Surf::ReverseNormal => "REVERSE NORMAL",
|
||||
Surf::Offset { .. } => "OFFSET SURFACE",
|
||||
Surf::Stitch { .. } => "STITCH",
|
||||
Surf::ConvertMesh { .. } => "CONVERT MESH",
|
||||
Surf::Thicken { .. } => "THICKEN",
|
||||
@@ -71,7 +76,7 @@ impl Surf {
|
||||
pub fn primary(&mut self) -> Option<(&'static str, ValueKind, &mut String)> {
|
||||
Some(match self {
|
||||
Surf::Thicken { thickness, .. } => ("Thickness", ValueKind::Length, thickness),
|
||||
Surf::Extend { distance } | Surf::Extrude { distance, .. } => ("Distance", ValueKind::Length, distance),
|
||||
Surf::Extend { distance } | Surf::Extrude { distance, .. } | Surf::Offset { distance, .. } => ("Distance", ValueKind::Length, distance),
|
||||
Surf::Revolve { angle, .. } => ("Angle", ValueKind::Angle, angle),
|
||||
_ => return None,
|
||||
})
|
||||
@@ -90,6 +95,7 @@ fn inputs(k: &Surf) -> Vec<SelInput> {
|
||||
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::Offset { .. } => vec![SelInput::new("Faces", 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)],
|
||||
@@ -113,6 +119,7 @@ pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
"surface.patch" => Surf::Patch,
|
||||
"surface.unstitch" => Surf::Unstitch,
|
||||
"surface.reverse_normal" => Surf::ReverseNormal,
|
||||
"surface.offset" => Surf::Offset { distance: "2 mm".into(), chain: false },
|
||||
"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 },
|
||||
@@ -164,6 +171,14 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Surf) -> bool {
|
||||
*symmetric = dir == 1;
|
||||
ui.end_row();
|
||||
}
|
||||
Surf::Offset { distance, chain } => {
|
||||
row_label(ui, "Distance");
|
||||
enter |= field(ui, distance);
|
||||
ui.end_row();
|
||||
row_label(ui, "Tangent chain");
|
||||
ui.checkbox(chain, "");
|
||||
ui.end_row();
|
||||
}
|
||||
Surf::Extrude { distance, direction } => {
|
||||
row_label(ui, "Distance");
|
||||
enter |= field(ui, distance);
|
||||
@@ -327,6 +342,18 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
|
||||
("surface.patch", json!({"edges": edges, "body": body}))
|
||||
}
|
||||
}
|
||||
Surf::Offset { distance, chain } => {
|
||||
need(0, "the faces to offset")?;
|
||||
let (body, faces) = body_and_faces(items(inputs, 0))?;
|
||||
if faces.is_empty() {
|
||||
return Err("pick faces to offset".into());
|
||||
}
|
||||
let mut p = json!({"body": body, "faces": faces, "distance": distance});
|
||||
if *chain {
|
||||
p["chain"] = json!(true);
|
||||
}
|
||||
("surface.offset", p)
|
||||
}
|
||||
Surf::Unstitch | Surf::ReverseNormal => {
|
||||
need(0, "a body or some of its faces")?;
|
||||
let (body, faces) = body_and_faces(items(inputs, 0))?;
|
||||
@@ -459,6 +486,9 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
|
||||
}
|
||||
(Surf::Patch, vec![sels])
|
||||
}
|
||||
FeatureKind::SurfaceOffset { faces, distance, chain, .. } => {
|
||||
(Surf::Offset { distance: distance.clone(), chain: *chain }, vec![faces.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect()])
|
||||
}
|
||||
FeatureKind::Unstitch { body, faces } | FeatureKind::ReverseNormal { body, faces } => {
|
||||
let sels: Vec<Sel> = if faces.is_empty() {
|
||||
vec![Sel::Body { name: body.clone() }]
|
||||
|
||||
@@ -939,6 +939,31 @@ fn surface_loft_dialog_makes_a_section_per_sketch() {
|
||||
assert_eq!(back.inputs[0].items, vec![Sel::SketchCurve { id: "a".into() }, Sel::SketchCurve { id: "b".into() }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_surface_dialog_takes_faces_a_distance_and_the_chain() {
|
||||
// A box top picked, moved up 5: a 10 × 10 square at z = 15; Edit Feature brings it back.
|
||||
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.offset");
|
||||
assert!(apply_commands(&app, &d).is_err(), "no faces yet");
|
||||
d.inputs[0].items = vec![Sel::Face { body: "Box".into(), index: 0, point: Vec3::new(5.0, 5.0, 10.0) }];
|
||||
if let Kind::Surface(crate::dialogs_surface::Surf::Offset { distance, chain }) = &mut d.kind {
|
||||
*distance = "5 mm".into();
|
||||
*chain = true;
|
||||
}
|
||||
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
|
||||
assert_eq!(
|
||||
(id.as_str(), &p["body"], &p["faces"], &p["chain"]),
|
||||
("surface.offset", &json!("Box"), &json!([[5.0, 5.0, 10.0]]), &json!(true)),
|
||||
"{p}"
|
||||
);
|
||||
app.session.execute(&id, &p).unwrap();
|
||||
let m = app.session.model.state().bodies.last().unwrap().mesh().measure();
|
||||
assert!((m.area - 100.0).abs() < 1e-6, "{}", m.area);
|
||||
let fid = app.session.doc.features.last().unwrap().id;
|
||||
let back = crate::dialogs::for_feature(&app, fid, None).unwrap();
|
||||
assert_eq!(back.inputs[0].items.len(), 1);
|
||||
}
|
||||
|
||||
#[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
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
FeatureKind::SurfaceRevolve { .. } => "surface_revolve",
|
||||
FeatureKind::SurfaceSweep { .. } => "surface_sweep",
|
||||
FeatureKind::SurfaceLoft { .. } => "surface_loft",
|
||||
FeatureKind::SurfaceOffset { .. } => "surface_offset",
|
||||
FeatureKind::Revolve { .. } => "revolve",
|
||||
FeatureKind::Fillet { .. } => "fillet",
|
||||
FeatureKind::Chamfer { .. } => "chamfer",
|
||||
|
||||
Reference in new issue
Block a user