Midplane asks for the two planes (#1845) (#1846)

Thanks @thodoristsagko.
This commit is contained in:
Thodoris Tsagkogeorgas authored and GitHub committed 2026-10-17 04:07:23 -04:00
1 parent f350c35414
commit 337564190b
2 files changed
+68 -8

No files matched your search

+45 -8
View File
@@ -114,6 +114,8 @@ pub enum Kind {
OffsetPlane {
offset: String,
},
/// Halfway between two planes or planar faces.
Midplane,
/// A construction plane through an axis at an angle to a plane.
AnglePlane {
angle: String,
@@ -521,6 +523,10 @@ impl Dialog {
"construct.plane.offset" => {
Dialog::new(Kind::OffsetPlane { offset: "10 mm".into() }, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)])
}
"construct.plane.midplane" => Dialog::new(
Kind::Midplane,
vec![SelInput::new("First plane", PLANES | PLANAR_FACES, false), SelInput::new("Second plane", PLANES | PLANAR_FACES, false)],
),
"construct.plane.along_path" => {
Dialog::new(Kind::PathPlane { t: "0.5".into(), sketch: None }, vec![SelInput::new("Path", CURVES | EDGES, false)])
}
@@ -790,6 +796,7 @@ impl Dialog {
| Kind::Loft { .. }
| Kind::Sweep { .. }
| Kind::PathPlane { .. }
| Kind::Midplane
| Kind::Move { .. }
| Kind::Hole { .. }
| Kind::Primitive { .. }
@@ -1379,6 +1386,7 @@ fn title(k: &Kind) -> &'static str {
Kind::Measure { .. } => "MEASURE",
Kind::Material { .. } => "PHYSICAL MATERIAL",
Kind::OffsetPlane { .. } => "OFFSET PLANE",
Kind::Midplane => "MIDPLANE",
Kind::PathPlane { .. } => "PLANE ALONG PATH",
Kind::Rib { web: false, .. } => "RIB",
Kind::Rib { web: true, .. } => "WEB",
@@ -2686,7 +2694,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
enter |= field(ui, t);
ui.end_row();
}
Kind::Split | Kind::ReplaceFace | Kind::Remove => {}
Kind::Split | Kind::ReplaceFace | Kind::Remove | Kind::Midplane => {}
Kind::Rib { thickness, depth, flip, .. } => {
row_label(ui, "Thickness");
enter |= field(ui, thickness);
@@ -3897,15 +3905,16 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
}
Kind::OffsetPlane { offset } => {
need(0, "a plane")?;
let base = match sels(d, 0).first() {
Some(Sel::Plane { name }) => json!(name),
Some(face @ Sel::Face { .. }) => {
crate::sketch_attachment_ui::face_plane(app, face).ok_or("the picked face is no longer in the model")?
}
_ => return Err("pick an origin or construction plane, or a planar face".into()),
};
let base = plane_arg(app, sels(d, 0).first())?;
("construct.plane.offset", json!({"base": base, "offset": offset}))
}
Kind::Midplane => {
need(0, "the first plane")?;
need(1, "the second plane")?;
let a = plane_arg(app, sels(d, 0).first())?;
let b = plane_arg(app, sels(d, 1).first())?;
("construct.plane.midplane", json!({"a": a, "b": b}))
}
Kind::PathPlane { t, sketch } => {
need(0, "a path")?;
let path = match sels(d, 0).first() {
@@ -4495,6 +4504,24 @@ fn emboss_face_sel(s: &Session, component: u64, face: &solidcraft_engine::doc::E
Some(Sel::Face { body: world_name, index, point })
}
/// A picked plane or planar face as `construct.plane.*` takes it.
fn plane_arg(app: &SolveApp, sel: Option<&Sel>) -> Result<Value, String> {
match sel {
Some(Sel::Plane { name }) => Ok(json!(name)),
Some(face @ Sel::Face { .. }) => crate::sketch_attachment_ui::face_plane(app, face).ok_or("the picked face is no longer in the model".into()),
_ => Err("pick an origin or construction plane, or a planar face".into()),
}
}
/// A stored construction reference back into a dialog pick.
fn geo_sel(s: &Session, g: &solidcraft_engine::doc::construct::GeoRef) -> Option<Sel> {
match g {
solidcraft_engine::doc::construct::GeoRef::Plane { plane } => plane_sel(s, plane),
solidcraft_engine::doc::construct::GeoRef::Face { at, .. } => face_sel(s, *at),
_ => None,
}
}
pub(crate) fn plane_sel(s: &Session, pl: &PlaneRef) -> Option<Sel> {
match pl {
PlaneRef::Origin { name } => Some(Sel::Plane { name: name.clone() }),
@@ -5022,6 +5049,16 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
d
}
FeatureKind::ConstructionPlane { plane: PlaneRef::Midplane { a, b } } => {
let mut d = start("construct.plane.midplane")?;
if let Some(inp) = d.inputs.first_mut() {
inp.items = geo_sel(s, a).into_iter().collect();
}
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = geo_sel(s, b).into_iter().collect();
}
d
}
FeatureKind::ConstructionPlane { plane: PlaneRef::AtAngle { base, axis_origin, axis_dir, angle } } => {
let mut d = start("construct.plane.at_angle")?;
d.kind = Kind::AnglePlane { angle: angle.to_string() };
+23
View File
@@ -1649,3 +1649,26 @@ fn patch_dialog_rows_are_translated() {
assert!(h.widget(through).is_some(), "{lang}: the points input must show {through:?}");
}
}
/// Midplane opens a two-plane dialog. Starting it with nothing selected used to run the
/// command and report that `a` is required.
#[test]
fn midplane_asks_for_two_planes_then_builds_the_one_between_them() {
let mut h = crate::scenario::Harness::new();
h.app.run("construct.plane.offset", json!({"base":"XY","offset":"20 mm","name":"Shelf"})).unwrap();
h.app.start("construct.plane.midplane");
h.frames(2);
let d = h.app.dialog.as_ref().unwrap();
assert!(matches!(d.kind, Kind::Midplane));
assert_eq!(d.inputs.len(), 2);
let status = h.app.status.as_ref().map(|s| s.0.clone()).unwrap_or_default();
assert!(!status.contains("is required"), "{status}");
h.app.dialog.as_mut().unwrap().inputs[0].items = vec![Sel::Plane { name: "XY".into() }];
h.app.dialog.as_mut().unwrap().inputs[1].items = vec![Sel::Plane { name: "Shelf".into() }];
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog.as_ref().and_then(|d| d.error.clone()));
let (_, _, pl) =
solidcraft_engine::view::construction_planes(&h.app.session).into_iter().find(|(_, name, _)| name != "Shelf").expect("the midplane");
assert!(pl.normal().cross(Vec3::Z).len() < 1e-6, "{pl:?}");
assert!((pl.origin.z - 10.0).abs() < 1e-6, "{pl:?}");
}