From 7820ee405bf4b6bf666c1a5c2bcb1ab05ff5e979 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 16:49:41 +0000 Subject: [PATCH] Chamfer dialog: Chamfer Type (Equal Distance, Two Distances, Distance and Angle) with the second distance or the angle and Flip, sent as the engine's distance2 / angle / flip; Edit Feature restores them Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 83 +++++++++++++++++++++++---- crates/ui-egui/src/preselect_tests.rs | 29 ++++++++++ 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d0005805..cf2c6eb9 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -53,6 +53,12 @@ pub enum Kind { radius: String, chamfer: bool, chain: bool, + /// Chamfer type: 0 equal distance, 1 two distances, 2 distance and angle; with the + /// second distance, the angle and which face the first distance is on. + ctype: usize, + distance2: String, + angle: String, + flip: bool, }, Shell { thickness: String, @@ -300,12 +306,8 @@ impl Dialog { Kind::Revolve { angle: "360 deg".into(), operation: 0 }, vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Axis", AXES, false)], ), - "FusionFilletEdgesCommand" => { - Dialog::new(Kind::Fillet { radius: "2 mm".into(), chamfer: false, chain: true }, vec![SelInput::new("Edges", EDGES | FACES, true)]) - } - "FusionChamferCommand" => { - Dialog::new(Kind::Fillet { radius: "1 mm".into(), chamfer: true, chain: true }, vec![SelInput::new("Edges", EDGES | FACES, true)]) - } + "FusionFilletEdgesCommand" => Dialog::new(fillet_kind("2 mm", false), vec![SelInput::new("Edges", EDGES | FACES, true)]), + "FusionChamferCommand" => Dialog::new(fillet_kind("1 mm", true), vec![SelInput::new("Edges", EDGES | FACES, true)]), "FusionShellBodyCommand" => Dialog::new(Kind::Shell { thickness: "2 mm".into() }, vec![SelInput::new("Faces/Body", FACES, true)]), "FusionDraftCommand" => Dialog::new( Kind::Draft { angle: "5 deg".into() }, @@ -1248,10 +1250,39 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { combo(ui, "rv_op", &OP_LABELS, operation); ui.end_row(); } - Kind::Fillet { radius, chamfer, chain } => { - row_label(ui, if *chamfer { "Distance" } else { "Radius" }); + Kind::Fillet { radius, chamfer, chain, ctype, distance2, angle, flip } => { + if *chamfer { + row_label(ui, "Chamfer Type"); + combo(ui, "ch_type", &CHAMFER_TYPES, ctype); + ui.end_row(); + } + row_label( + ui, + if !*chamfer { + "Radius" + } else if *ctype == 0 { + "Distance" + } else { + "Distance 1" + }, + ); enter |= field(ui, radius); ui.end_row(); + if *chamfer && *ctype == 1 { + row_label(ui, "Distance 2"); + enter |= field(ui, distance2); + ui.end_row(); + } + if *chamfer && *ctype == 2 { + row_label(ui, "Angle"); + enter |= field(ui, angle); + ui.end_row(); + } + if *chamfer && *ctype > 0 { + row_label(ui, "Flip"); + ui.checkbox(flip, ""); + ui.end_row(); + } row_label(ui, "Tangent Chain"); ui.checkbox(chain, ""); ui.end_row(); @@ -1613,6 +1644,13 @@ pub fn cancel(app: &mut SolveApp) { } } +const CHAMFER_TYPES: [&str; 3] = ["Equal Distance", "Two Distances", "Distance and Angle"]; + +/// A new Fillet or Chamfer dialog's values. +fn fillet_kind(size: &str, chamfer: bool) -> Kind { + Kind::Fillet { radius: size.into(), chamfer, chain: true, ctype: 0, distance2: size.into(), angle: "45 deg".into(), flip: false } +} + fn hole_defaults() -> Kind { Kind::Hole { diameter: "5 mm".into(), @@ -1801,7 +1839,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S json!({"sketch": sketch, "profiles": idx, "axis": axis, "angle": angle, "operation": OPS.get(*operation).copied().unwrap_or("new")}), ) } - Kind::Fillet { radius, chamfer, .. } => { + Kind::Fillet { radius, chamfer, ctype, distance2, angle, flip, .. } => { need(0, "edges")?; // Faces stand for all their edges. let st = s.world_state(); @@ -1832,7 +1870,16 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } let edges: Vec = uniq.into_iter().map(pt).collect(); if *chamfer { - ("FusionChamferCommand", json!({"edges": edges, "distance": radius})) + let mut p = json!({"edges": edges, "distance": radius}); + match *ctype { + 1 => p["distance2"] = json!(distance2), + 2 => p["angle"] = json!(angle), + _ => {} + } + if *ctype > 0 && *flip { + p["flip"] = json!(true); + } + ("FusionChamferCommand", p) } else { ("FusionFilletEdgesCommand", json!({"edges": edges, "radius": radius})) } @@ -2390,7 +2437,21 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { let chamfer = matches!(f.kind, FeatureKind::Chamfer { .. }); let mut d = start(if chamfer { "FusionChamferCommand" } else { "FusionFilletEdgesCommand" })?; - d.kind = Kind::Fillet { radius: radius.clone(), chamfer, chain: false }; + d.kind = fillet_kind(radius, chamfer); + if let Kind::Fillet { chain, ctype, distance2, angle, flip, .. } = &mut d.kind { + *chain = false; + // An unequal chamfer: two distances, or a distance and an angle. + if let FeatureKind::Chamfer { distance2: d2, angle: a, flip: f, .. } = &f.kind { + if let Some(v) = d2 { + *ctype = 1; + *distance2 = v.clone(); + } else if let Some(v) = a { + *ctype = 2; + *angle = v.clone(); + } + *flip = *f; + } + } if let Some(inp) = d.inputs.first_mut() { inp.items = edges.iter().filter_map(|p| edge_sel(s, *p)).collect(); } diff --git a/crates/ui-egui/src/preselect_tests.rs b/crates/ui-egui/src/preselect_tests.rs index 6a67b193..380b2e09 100644 --- a/crates/ui-egui/src/preselect_tests.rs +++ b/crates/ui-egui/src/preselect_tests.rs @@ -190,3 +190,32 @@ fn a_hole_is_mirrored_as_a_feature() { let cmds = apply_commands(&app, &d).unwrap(); assert_eq!(cmds[0].1["features"], json!(["Hole1"])); } + +#[test] +fn chamfer_types_send_their_values() { + let mut app = boxed(); + let e = edge(&app, Vec3::new(20.0, 0.0, 20.0)); + let mut d = start(&mut app, vec![e], "FusionChamferCommand"); + let mut with = |t: usize| { + if let Kind::Fillet { ctype, distance2, angle, flip, .. } = &mut d.kind { + *ctype = t; + *distance2 = "3 mm".into(); + *angle = "30 deg".into(); + *flip = true; + } + apply_commands(&app, &d).unwrap().remove(0).1 + }; + let p = with(0); + assert!(p.get("distance2").is_none() && p.get("angle").is_none() && p.get("flip").is_none()); + let p = with(1); + assert_eq!((p["distance2"].clone(), p["flip"].clone()), (json!("3 mm"), json!(true))); + let p = with(2); + assert_eq!(p["angle"], json!("30 deg")); + assert!(p.get("distance2").is_none()); + // Applied and edited again, the chamfer keeps its type, angle and flip. + let (id, p) = apply_commands(&app, &d).unwrap().remove(0); + app.session.execute(&id, &p).unwrap(); + let ch = app.session.doc.features.last().unwrap().id; + let e = crate::dialogs::for_feature(&app, ch, None).unwrap(); + assert!(matches!(&e.kind, Kind::Fillet { chamfer: true, ctype: 2, angle, flip: true, .. } if angle == "30 deg"), "{:?}", e.kind); +}