Box, Cylinder, Sphere, Torus and Coil go where a face or vertex was picked before starting them: centred on the point, standing on a planar face (a box on an axis-aligned face sits on it; cylinders and coils take the face normal as axis). The dialog shows the position with a button to place at the origin instead. Fixes #580. Co-authored-by: lasselian <[email protected]>
This commit is contained in:
1 parent
04cfb74c5e
commit
bf962a9989
2 files changed
+158
-7
No files matched your search
@@ -204,6 +204,10 @@ pub enum Kind {
|
||||
cmd: &'static str,
|
||||
fields: Vec<(&'static str, String)>,
|
||||
operation: usize,
|
||||
/// Where it goes: a picked point on a face or vertex, with the face's normal when the
|
||||
/// face is planar. `None` keeps the command's own placement (the origin, or the edited
|
||||
/// feature's).
|
||||
at: Option<(Vec3, Option<Vec3>)>,
|
||||
},
|
||||
Combine {
|
||||
operation: usize,
|
||||
@@ -438,22 +442,27 @@ impl Dialog {
|
||||
cmd: "solid.box",
|
||||
fields: vec![("length", "20".into()), ("width", "20".into()), ("height", "20".into())],
|
||||
operation: 0,
|
||||
at: None,
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
"solid.cylinder" => Dialog::new(
|
||||
Kind::Primitive { cmd: "solid.cylinder", fields: vec![("diameter", "20".into()), ("height", "20".into())], operation: 0 },
|
||||
Kind::Primitive { cmd: "solid.cylinder", fields: vec![("diameter", "20".into()), ("height", "20".into())], operation: 0, at: None },
|
||||
vec![],
|
||||
),
|
||||
"solid.sphere" => Dialog::new(Kind::Primitive { cmd: "solid.sphere", fields: vec![("diameter", "20".into())], operation: 0 }, vec![]),
|
||||
"solid.torus" => {
|
||||
Dialog::new(Kind::Primitive { cmd: "solid.torus", fields: vec![("major", "20".into()), ("minor", "5".into())], operation: 0 }, vec![])
|
||||
"solid.sphere" => {
|
||||
Dialog::new(Kind::Primitive { cmd: "solid.sphere", fields: vec![("diameter", "20".into())], operation: 0, at: None }, vec![])
|
||||
}
|
||||
"solid.torus" => Dialog::new(
|
||||
Kind::Primitive { cmd: "solid.torus", fields: vec![("major", "20".into()), ("minor", "5".into())], operation: 0, at: None },
|
||||
vec![],
|
||||
),
|
||||
"solid.coil" => Dialog::new(
|
||||
Kind::Primitive {
|
||||
cmd: "solid.coil",
|
||||
fields: vec![("diameter", "40".into()), ("revolutions", "4".into()), ("pitch", "8".into()), ("section_size", "3".into())],
|
||||
operation: 0,
|
||||
at: None,
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
@@ -531,6 +540,14 @@ impl Dialog {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A primitive goes where a face or vertex is picked, standing on a planar face.
|
||||
if let Kind::Primitive { at, .. } = &mut d.kind {
|
||||
*at = s.selection.iter().find_map(|sel| match sel {
|
||||
Sel::Face { body, index, point } => Some((*point, planar_face(s, body, *index).map(|(_, n)| n))),
|
||||
Sel::Vertex { point, .. } => Some((*point, None)),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
// With nothing pre-selected, a single profile in the sketch being used is taken.
|
||||
if matches!(d.kind, Kind::Extrude { .. } | Kind::Revolve { .. })
|
||||
&& let Some(inp) = d.inputs.first_mut()
|
||||
@@ -1928,12 +1945,22 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
Kind::Primitive { fields, operation, .. } => {
|
||||
Kind::Primitive { fields, operation, at, .. } => {
|
||||
for (k, v) in fields.iter_mut() {
|
||||
row_label(ui, k);
|
||||
enter |= field(ui, v);
|
||||
ui.end_row();
|
||||
}
|
||||
if let Some((p, _)) = *at {
|
||||
row_label(ui, "Position");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("{:.2}, {:.2}, {:.2}", p.x, p.y, p.z));
|
||||
if ui.small_button("×").on_hover_text("Place at the origin instead").clicked() {
|
||||
*at = None;
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
row_label(ui, "Operation");
|
||||
combo(ui, "pr_op", &OP_LABELS, operation);
|
||||
ui.end_row();
|
||||
@@ -2931,11 +2958,27 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
Kind::Primitive { cmd, fields, operation } => {
|
||||
Kind::Primitive { cmd, fields, operation, at } => {
|
||||
let mut p = serde_json::Map::new();
|
||||
for (k, v) in fields {
|
||||
p.insert((*k).into(), json!(v));
|
||||
}
|
||||
if let Some((point, normal)) = *at {
|
||||
match *cmd {
|
||||
"solid.box" => {
|
||||
p.insert("corner".into(), box_corner(s, fields, point, normal));
|
||||
}
|
||||
"solid.cylinder" | "solid.coil" => {
|
||||
p.insert("base".into(), pt(point));
|
||||
if let Some(n) = normal {
|
||||
p.insert("axis".into(), pt(n));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
p.insert("center".into(), pt(point));
|
||||
}
|
||||
}
|
||||
}
|
||||
p.insert("operation".into(), json!(OPS.get(*operation).copied().unwrap_or("new")));
|
||||
(cmd, Value::Object(p))
|
||||
}
|
||||
@@ -2979,6 +3022,28 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
Ok(vec![(cmd.to_string(), params)])
|
||||
}
|
||||
|
||||
/// The corner of a box placed at `p`: standing on a face whose normal is along an axis (centred
|
||||
/// on `p` across the face), otherwise centred on `p`. Sizes that are expressions stay
|
||||
/// expressions, so the box keeps standing on the face when they change.
|
||||
fn box_corner(s: &Session, fields: &[(&'static str, String)], p: Vec3, normal: Option<Vec3>) -> Value {
|
||||
let size = |k: &str| fields.iter().find(|f| f.0 == k).map(|f| s.doc.with_design_unit(&f.1, ValueKind::Length)).unwrap_or_default();
|
||||
let along = |axis: Vec3| normal.map(|n| n.dot(axis)).filter(|d| d.abs() > 1.0 - 1e-9);
|
||||
let coord = |c: f64, size: String, axis: Vec3| {
|
||||
// How much of the size lies behind `p`: none (or all) along the face normal, half across it.
|
||||
let back = match along(axis) {
|
||||
Some(d) if d > 0.0 => return json!(c),
|
||||
Some(_) => 1.0,
|
||||
None => 0.5,
|
||||
};
|
||||
match size.trim().parse::<f64>() {
|
||||
Ok(v) => json!(c - back * v),
|
||||
Err(_) if back < 1.0 => json!(format!("{c} mm - ({size}) / 2")),
|
||||
Err(_) => json!(format!("{c} mm - ({size})")),
|
||||
}
|
||||
};
|
||||
json!([coord(p.x, size("length"), Vec3::X), coord(p.y, size("width"), Vec3::Y), coord(p.z, size("height"), Vec3::Z)])
|
||||
}
|
||||
|
||||
/// Sketch curve ids picked in an input.
|
||||
fn curve_ids(d: &Dialog, i: usize) -> Vec<String> {
|
||||
sels(d, i).iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect()
|
||||
@@ -3452,6 +3517,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
cmd: "solid.box",
|
||||
fields: vec![("length", length.clone()), ("width", width.clone()), ("height", height.clone())],
|
||||
operation: op_index(operation),
|
||||
at: None,
|
||||
};
|
||||
d.extra.insert("corner".into(), pt_expr(corner));
|
||||
d
|
||||
@@ -3462,6 +3528,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
cmd: "solid.cylinder",
|
||||
fields: vec![("radius", radius.clone()), ("height", height.clone())],
|
||||
operation: op_index(operation),
|
||||
at: None,
|
||||
};
|
||||
d.extra.insert("base".into(), pt_expr(base));
|
||||
d.extra.insert("axis".into(), pt3(*axis));
|
||||
@@ -3469,7 +3536,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
FeatureKind::Sphere { center, radius, operation } => {
|
||||
let mut d = start("solid.sphere")?;
|
||||
d.kind = Kind::Primitive { cmd: "solid.sphere", fields: vec![("radius", radius.clone())], operation: op_index(operation) };
|
||||
d.kind = Kind::Primitive { cmd: "solid.sphere", fields: vec![("radius", radius.clone())], operation: op_index(operation), at: None };
|
||||
d.extra.insert("center".into(), pt_expr(center));
|
||||
d
|
||||
}
|
||||
@@ -3479,6 +3546,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
cmd: "solid.torus",
|
||||
fields: vec![("major", major.clone()), ("minor", minor.clone())],
|
||||
operation: op_index(operation),
|
||||
at: None,
|
||||
};
|
||||
d.extra.insert("center".into(), pt_expr(center));
|
||||
d
|
||||
|
||||
@@ -754,3 +754,86 @@ fn split_body_by_a_curved_face_sends_it_as_a_tool() {
|
||||
let p = apply_commands(&app, &d).unwrap().remove(0).1;
|
||||
assert!(p["tool"]["point"].is_array() && p.get("plane").is_none(), "{p}");
|
||||
}
|
||||
|
||||
/// Runs a primitive's dialog with `sels` pre-selected and returns the new body's centroid.
|
||||
fn primitive_on(app: &mut SolveApp, sels: Vec<Sel>, id: &str, edit: impl FnOnce(&mut Dialog)) -> (serde_json::Value, Vec3) {
|
||||
let mut d = start(app, sels, id);
|
||||
edit(&mut d);
|
||||
let (cmd, p) = apply_commands(app, &d).unwrap().remove(0);
|
||||
let name = app.session.execute(&cmd, &p).unwrap()["created_bodies"][0].as_str().unwrap().to_string();
|
||||
let st = app.session.model.state();
|
||||
(p, st.body(&name).unwrap().mesh().measure().centroid)
|
||||
}
|
||||
|
||||
/// The face of Body1 whose centre is at `c`, picked at `p`.
|
||||
fn picked(app: &SolveApp, c: Vec3, p: Vec3) -> Sel {
|
||||
let Sel::Face { body, index, .. } = face(app, c) else { panic!("a face") };
|
||||
Sel::Face { body, index, point: p }
|
||||
}
|
||||
|
||||
fn near(a: Vec3, b: Vec3) -> bool {
|
||||
a.dist(b) < 0.05
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primitives_go_on_the_picked_face() {
|
||||
// #580: a sphere (or any primitive) made with a face selected went to the origin.
|
||||
let mut app = boxed();
|
||||
let top = picked(&app, Vec3::new(20.0, 15.0, 20.0), Vec3::new(8.0, 10.0, 20.0));
|
||||
let (p, c) = primitive_on(&mut app, vec![top.clone()], "solid.sphere", |_| {});
|
||||
assert_eq!(p["center"], json!([8.0, 10.0, 20.0]), "{p}");
|
||||
assert!(near(c, Vec3::new(8.0, 10.0, 20.0)), "{c:?}");
|
||||
|
||||
let (p, c) = primitive_on(&mut app, vec![top.clone()], "solid.cylinder", |_| {});
|
||||
assert_eq!(p["axis"], json!([0.0, 0.0, 1.0]), "standing on the top face: {p}");
|
||||
assert!(near(c, Vec3::new(8.0, 10.0, 30.0)), "{c:?}");
|
||||
|
||||
// A box stands on the face, centred on the picked point across it.
|
||||
let (_, c) = primitive_on(&mut app, vec![top.clone()], "solid.box", |_| {});
|
||||
assert!(near(c, Vec3::new(8.0, 10.0, 30.0)), "{c:?}");
|
||||
|
||||
let (p, c) = primitive_on(&mut app, vec![top], "solid.torus", |_| {});
|
||||
assert_eq!(p["center"], json!([8.0, 10.0, 20.0]), "{p}");
|
||||
assert!(near(c, Vec3::new(8.0, 10.0, 20.0)), "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_box_stands_out_of_a_face_that_faces_down_an_axis() {
|
||||
let mut app = boxed();
|
||||
// The x = 0 side faces -X: the box goes the other way, away from the body.
|
||||
let side = picked(&app, Vec3::new(0.0, 15.0, 10.0), Vec3::new(0.0, 12.0, 8.0));
|
||||
let (_, c) = primitive_on(&mut app, vec![side.clone()], "solid.box", |_| {});
|
||||
assert!(near(c, Vec3::new(-10.0, 12.0, 8.0)), "{c:?}");
|
||||
// Sizes that are expressions stay expressions in the corner.
|
||||
app.session.execute("parameters.add", &json!({"name": "s", "expression": "6 mm"})).unwrap();
|
||||
let (p, c) = primitive_on(&mut app, vec![side.clone()], "solid.box", |d| {
|
||||
if let Kind::Primitive { fields, .. } = &mut d.kind {
|
||||
fields.iter_mut().for_each(|f| f.1 = "s".into());
|
||||
}
|
||||
});
|
||||
assert_eq!(p["corner"][0], json!("0 mm - (s)"), "{p}");
|
||||
assert!(near(c, Vec3::new(-3.0, 12.0, 8.0)), "{c:?}");
|
||||
// The cylinder's axis points out of the face.
|
||||
let (p, c) = primitive_on(&mut app, vec![side], "solid.cylinder", |_| {});
|
||||
assert_eq!(p["axis"], json!([-1.0, 0.0, 0.0]), "{p}");
|
||||
assert!(near(c, Vec3::new(-10.0, 12.0, 8.0)), "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_primitive_without_a_pick_or_with_its_position_cleared_stays_at_the_origin() {
|
||||
let mut app = boxed();
|
||||
let (p, c) = primitive_on(&mut app, vec![], "solid.sphere", |_| {});
|
||||
assert!(p.get("center").is_none(), "{p}");
|
||||
assert!(near(c, Vec3::ZERO), "{c:?}");
|
||||
let top = face(&app, Vec3::new(20.0, 15.0, 20.0));
|
||||
let (p, _) = primitive_on(&mut app, vec![top], "solid.sphere", |d| {
|
||||
if let Kind::Primitive { at, .. } = &mut d.kind {
|
||||
*at = None;
|
||||
}
|
||||
});
|
||||
assert!(p.get("center").is_none(), "{p}");
|
||||
// A vertex places it too.
|
||||
let v = Sel::Vertex { body: "Body1".into(), point: Vec3::new(40.0, 30.0, 20.0) };
|
||||
let (p, _) = primitive_on(&mut app, vec![v], "solid.sphere", |_| {});
|
||||
assert_eq!(p["center"], json!([40.0, 30.0, 20.0]), "{p}");
|
||||
}
|
||||
Reference in new issue
Block a user