Offset Plane (with plane preview), Plane at Angle, Split Body, Scale, Offset Face and Thread dialogs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 01:47:33 +00:00
1 parent 441b36ce74
commit 015bc56c93
2 files changed
+164 -3

No files matched your search

+20 -3
View File
@@ -31,13 +31,17 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
Sel::Vertex { point, .. } => (*point, None),
Sel::Body { name } => (st.body(name)?.mesh().bounds().center(), None),
Sel::Plane { name } => {
let pl = solvecraft_engine::geom::Plane::named(name)?;
let pl = solvecraft_engine::geom::Plane::named(name).or_else(|| {
solvecraft_engine::view::construction_planes(&app.session).into_iter().find(|(_, n, _)| n == name).map(|(_, _, pl)| pl)
})?;
(pl.origin, Some(pl.normal()))
}
_ => return None,
};
Some(match d.kind {
Kind::Extrude { .. } | Kind::Fillet { .. } | Kind::Section { .. } | Kind::Hole { .. } => (at, normal),
Kind::Extrude { .. } | Kind::Fillet { .. } | Kind::Section { .. } | Kind::Hole { .. } | Kind::OffsetPlane { .. } | Kind::OffsetFaces { .. } => {
(at, normal)
}
// Shell thickness grows into the body.
Kind::Shell { .. } => (at, normal.map(|n| -n)),
Kind::Move { .. } => (at, Some(Vec3::Z)),
@@ -122,7 +126,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
};
let symmetric = matches!(d.kind, Kind::Extrude { direction: 2, .. });
// Extrudes and moves go either way; radii and thicknesses stay positive.
let signed = matches!(d.kind, Kind::Extrude { .. } | Kind::Move { .. } | Kind::Section { .. });
let signed = matches!(d.kind, Kind::Extrude { .. } | Kind::Move { .. } | Kind::Section { .. } | Kind::OffsetPlane { .. } | Kind::OffsetFaces { .. });
let focus = std::mem::take(&mut d.focus);
let half_height = app.cam.half_height();
let axis = revolve_axis(app, d);
@@ -132,6 +136,19 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
{
drag_arrow(ui, painter, proj, base, bs, -n, l2, false, egui::Id::new("sc_manipulator2"), half_height, distance2);
}
// An offset plane shows where it will be.
if let (Kind::OffsetPlane { offset }, Some(n)) = (&d.kind, dir)
&& let Ok(off) = app.session.doc.eval(offset, ValueKind::Length)
&& let Some(u) = n.cross(if n.z.abs() < 0.9 { Vec3::Z } else { Vec3::X }).normalized()
{
let v = n.cross(u);
let h = half_height * 0.25;
let c = base + n * off;
let q: Vec<Pos2> = [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)].iter().filter_map(|(a, b)| proj.to_screen(c + u * (a * h) + v * (b * h))).collect();
if q.len() == 4 {
painter.add(egui::Shape::convex_polygon(q, t.construction_plane, Stroke::new(1.2, t.origin_plane_edge)));
}
}
// Holes: the depth arrow points into the material (empty depth: through all); the
// diameter is typed in the box.
let mut dir = dir;
+144
View File
@@ -78,6 +78,29 @@ pub enum Kind {
Sweep {
operation: usize,
},
/// A construction plane offset from a plane.
OffsetPlane {
offset: String,
},
/// A construction plane through an axis at an angle to a plane.
AnglePlane {
angle: String,
},
/// Split a body by a plane.
Split,
/// Scale bodies uniformly.
Scale {
factor: String,
},
/// Move planar faces along their normals.
OffsetFaces {
distance: String,
},
/// A cosmetic thread on a cylindrical face.
Thread {
designation: String,
length: String,
},
/// Application preferences (applied as they change; kept between runs).
Preferences,
/// Physical material (and with it the appearance) of bodies.
@@ -208,6 +231,25 @@ impl Dialog {
"MirrorCommand" => {
Dialog::new(Kind::Mirror, vec![SelInput::new("Bodies", BODIES, true), SelInput::new("Mirror plane", PLANES | PLANAR_FACES, false)])
}
"ConstructionPlaneOffsetFromPlaneCommand" => {
Dialog::new(Kind::OffsetPlane { offset: "10 mm".into() }, vec![SelInput::new("Plane", PLANES, false)])
}
"ConstructionPlaneAtAngleCommand" => Dialog::new(
Kind::AnglePlane { angle: "45 deg".into() },
vec![SelInput::new("Plane", PLANES, false), SelInput::new("Axis", AXES, false)],
),
"FusionSplitBodyCommand" => Dialog::new(
Kind::Split,
vec![SelInput::new("Body to split", BODIES, false), SelInput::new("Splitting plane", PLANES | PLANAR_FACES, false)],
),
"ModifyScale" => Dialog::new(Kind::Scale { factor: "2".into() }, vec![SelInput::new("Bodies", BODIES, true)]),
"FusionOffsetFacesCommand" => {
Dialog::new(Kind::OffsetFaces { distance: "2 mm".into() }, vec![SelInput::new("Faces", PLANAR_FACES, true)])
}
"FusionThreadCommand" => Dialog::new(
Kind::Thread { designation: String::new(), length: String::new() },
vec![SelInput::new("Cylindrical face", FACES, false)],
),
"PhysicalMaterialCommand" => Dialog::new(Kind::Material { index: 1 }, vec![SelInput::new("Bodies", BODIES, true)]),
"FusionHalfSectionViewCommand" => {
Dialog::new(Kind::Section { offset: "0 mm".into(), flip: false }, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)])
@@ -323,6 +365,9 @@ impl Dialog {
| Kind::Shell { .. }
| Kind::Draft { .. }
| Kind::Mirror
| Kind::Split
| Kind::Scale { .. }
| Kind::OffsetFaces { .. }
| Kind::PatternRect { .. }
| Kind::PatternCirc { .. }
| Kind::Loft { .. }
@@ -347,6 +392,10 @@ impl Dialog {
Kind::PatternRect { spacing, .. } => ("Spacing", ValueKind::Length, spacing),
Kind::PatternCirc { angle, .. } => ("Angle", ValueKind::Angle, angle),
Kind::Section { offset, .. } => ("Distance", ValueKind::Length, offset),
Kind::OffsetPlane { offset } => ("Distance", ValueKind::Length, offset),
Kind::AnglePlane { angle } => ("Angle", ValueKind::Angle, angle),
Kind::Scale { factor } => ("Scale", ValueKind::Unitless, factor),
Kind::OffsetFaces { distance } => ("Distance", ValueKind::Length, distance),
_ => return None,
})
}
@@ -502,6 +551,12 @@ fn title(k: &Kind) -> &'static str {
Kind::Measure { .. } => "MEASURE",
Kind::Material { .. } => "PHYSICAL MATERIAL",
Kind::Preferences => "PREFERENCES",
Kind::OffsetPlane { .. } => "OFFSET PLANE",
Kind::AnglePlane { .. } => "PLANE AT ANGLE",
Kind::Split => "SPLIT BODY",
Kind::Scale { .. } => "SCALE",
Kind::OffsetFaces { .. } => "OFFSET FACE",
Kind::Thread { .. } => "THREAD",
Kind::Section { .. } => "SECTION ANALYSIS",
Kind::PatternRect { .. } => "RECTANGULAR PATTERN",
Kind::PatternCirc { .. } => "CIRCULAR PATTERN",
@@ -787,6 +842,37 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
Kind::Mirror => {}
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
Kind::Preferences => preferences_rows(app, ui),
Kind::OffsetPlane { offset } => {
ui.label("Distance");
enter |= field(ui, offset);
ui.end_row();
}
Kind::AnglePlane { angle } => {
ui.label("Angle");
enter |= field(ui, angle);
ui.end_row();
}
Kind::Split => {}
Kind::Scale { factor } => {
ui.label("Scale factor");
enter |= field(ui, factor);
ui.end_row();
}
Kind::OffsetFaces { distance } => {
ui.label("Distance");
enter |= field(ui, distance);
ui.end_row();
}
Kind::Thread { designation, length } => {
ui.label("Size");
let r = ui.add(egui::TextEdit::singleline(designation).hint_text("fits the face, e.g. M8"));
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
ui.label("Length");
let r = ui.add(egui::TextEdit::singleline(length).hint_text("whole face"));
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
}
Kind::Material { index } => {
let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
ui.label("Material");
@@ -1257,6 +1343,64 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")}),
)
}
Kind::OffsetPlane { offset } => {
need(0, "a plane")?;
let base = match sels(d, 0).first() {
Some(Sel::Plane { name }) => name.clone(),
_ => return Err("pick an origin or construction plane".into()),
};
("ConstructionPlaneOffsetFromPlaneCommand", json!({"base": base, "offset": offset}))
}
Kind::AnglePlane { angle } => {
need(0, "a plane")?;
need(1, "an axis")?;
let base = match sels(d, 0).first() {
Some(Sel::Plane { name }) => name.clone(),
_ => return Err("pick an origin or construction plane".into()),
};
let axis = match sels(d, 1).first() {
Some(Sel::Axis { name }) => json!(name),
Some(x) => {
let (o, dir) = axis_of(app, x).ok_or("the axis must be an origin axis or a sketch line")?;
json!({"origin": pt(o), "dir": pt(dir)})
}
None => return Err("pick an axis".into()),
};
("ConstructionPlaneAtAngleCommand", json!({"base": base, "axis": axis, "angle": angle}))
}
Kind::Split => {
need(0, "the body to split")?;
need(1, "the splitting plane")?;
let body = body_names(0).into_iter().next().unwrap_or_default();
let plane = match sels(d, 1).first() {
Some(Sel::Plane { name }) => json!(name),
Some(Sel::Face { body, index, point }) => {
let (_, n) = planar_face(s, body, *index).ok_or("the face must be planar")?;
json!({"origin": pt(*point), "normal": pt(n)})
}
_ => return Err("pick a plane or a planar face".into()),
};
("FusionSplitBodyCommand", json!({"body": body, "plane": plane}))
}
Kind::Scale { factor } => {
need(0, "bodies")?;
("ModifyScale", json!({"bodies": body_names(0), "factor": factor}))
}
Kind::OffsetFaces { distance } => {
need(0, "planar faces")?;
("FusionOffsetFacesCommand", json!({"faces": face_points(0), "distance": distance}))
}
Kind::Thread { designation, length } => {
need(0, "a cylindrical face")?;
let mut p = json!({"face": face_points(0).into_iter().next().unwrap_or(Value::Null)});
if !designation.trim().is_empty() {
p["designation"] = json!(designation.trim());
}
if !length.trim().is_empty() {
p["length"] = json!(length);
}
("FusionThreadCommand", p)
}
Kind::Material { index } => {
need(0, "bodies")?;
let name = solvecraft_engine::doc::MATERIALS.get(*index).map(|(n, _)| *n).unwrap_or("Default");