Fillet dialog: middle radii for variable fillets (add, remove, position and radius) and the Smooth toggle
Variable type shows a Mid Radius row per point (position along the edge from the start end, 0.01 to 0.99, and a radius expression, with a remove button), an Add Mid Radius button and, with points present, Smooth. They go to solid.fillet as mid and smooth and come back on Edit Feature. Test: the round trip and removing every point. Refs #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
1 parent
be18bd6573
commit
62746045af
2 files changed
+84
-1
No files matched your search
@@ -1422,6 +1422,37 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
row_label(ui, "Swap Ends");
|
row_label(ui, "Swap Ends");
|
||||||
extra_check(ui, &mut d.extra, "swap_ends");
|
extra_check(ui, &mut d.extra, "swap_ends");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
// Radii between the ends: position along the edge (from the start end)
|
||||||
|
// and radius.
|
||||||
|
let mut mids = mid_points(&d.extra);
|
||||||
|
let mut remove = None;
|
||||||
|
for (k, (t, r)) in mids.iter_mut().enumerate() {
|
||||||
|
row_label(ui, "Mid Radius");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(egui::DragValue::new(t).speed(0.01).range(0.01..=0.99).fixed_decimals(2))
|
||||||
|
.on_hover_text("Position along the edge, 0 at the start end, 1 at the other");
|
||||||
|
enter |= field(ui, r);
|
||||||
|
if ui.small_button("×").on_hover_text("Remove this point").clicked() {
|
||||||
|
remove = Some(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
if let Some(k) = remove {
|
||||||
|
mids.remove(k);
|
||||||
|
}
|
||||||
|
row_label(ui, "");
|
||||||
|
if ui.button("Add Mid Radius").clicked() && mids.len() < 20 {
|
||||||
|
let t = if mids.is_empty() { 0.5 } else { (mids.iter().map(|m| m.0).fold(0.0, f64::max) + 1.0) * 0.5 };
|
||||||
|
mids.push((t.min(0.99), radius.clone()));
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
d.extra.insert("mid".into(), json!(mids.iter().map(|(t, r)| json!([t, r])).collect::<Vec<_>>()));
|
||||||
|
if !mids.is_empty() {
|
||||||
|
row_label(ui, "Smooth");
|
||||||
|
extra_check(ui, &mut d.extra, "smooth");
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
row_label(ui, "Tangent Chain");
|
row_label(ui, "Tangent Chain");
|
||||||
ui.checkbox(chain, "");
|
ui.checkbox(chain, "");
|
||||||
@@ -1913,6 +1944,25 @@ fn extra_field(ui: &mut egui::Ui, extra: &mut serde_json::Map<String, Value>, ke
|
|||||||
enter
|
enter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A variable fillet's middle radii as (position, radius expression).
|
||||||
|
fn mid_points(extra: &serde_json::Map<String, Value>) -> Vec<(f64, String)> {
|
||||||
|
extra
|
||||||
|
.get("mid")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|m| {
|
||||||
|
let t = m.get(0)?.as_f64()?;
|
||||||
|
let r = match m.get(1)? {
|
||||||
|
Value::String(x) => x.clone(),
|
||||||
|
Value::Number(n) => n.to_string(),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((t, r))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
const CHAMFER_TYPES: [&str; 3] = ["Equal Distance", "Two Distances", "Distance and Angle"];
|
const CHAMFER_TYPES: [&str; 3] = ["Equal Distance", "Two Distances", "Distance and Angle"];
|
||||||
|
|
||||||
/// A new Fillet or Chamfer dialog's values.
|
/// A new Fillet or Chamfer dialog's values.
|
||||||
@@ -2170,6 +2220,13 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
if let Some((a, b)) = ends {
|
if let Some((a, b)) = ends {
|
||||||
p["start"] = pt(if swap { b } else { a });
|
p["start"] = pt(if swap { b } else { a });
|
||||||
}
|
}
|
||||||
|
let mids = mid_points(&d.extra);
|
||||||
|
if !mids.is_empty() {
|
||||||
|
p["mid"] = json!(mids.iter().map(|(t, r)| json!([t, r])).collect::<Vec<_>>());
|
||||||
|
if d.extra.get("smooth").and_then(Value::as_bool).unwrap_or(false) {
|
||||||
|
p["smooth"] = json!(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
("solid.fillet", p)
|
("solid.fillet", p)
|
||||||
}
|
}
|
||||||
@@ -2790,9 +2847,11 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
|||||||
FilletStyle::Chord => {
|
FilletStyle::Chord => {
|
||||||
d.extra.insert("type".into(), json!("chord"));
|
d.extra.insert("type".into(), json!("chord"));
|
||||||
}
|
}
|
||||||
FilletStyle::Variable { radius2, start, .. } => {
|
FilletStyle::Variable { radius2, start, mid, smooth } => {
|
||||||
d.extra.insert("type".into(), json!("variable"));
|
d.extra.insert("type".into(), json!("variable"));
|
||||||
d.extra.insert("radius2".into(), json!(radius2));
|
d.extra.insert("radius2".into(), json!(radius2));
|
||||||
|
d.extra.insert("mid".into(), json!(mid.iter().map(|(t, r)| json!([t, r])).collect::<Vec<_>>()));
|
||||||
|
d.extra.insert("smooth".into(), json!(smooth));
|
||||||
// The start was the first edge's last point: ends swapped.
|
// The start was the first edge's last point: ends swapped.
|
||||||
let first_end = edges.first().and_then(|p| edge_sel(s, *p)).and_then(|x| match x {
|
let first_end = edges.first().and_then(|p| edge_sel(s, *p)).and_then(|x| match x {
|
||||||
Sel::Edge { body, index, .. } => {
|
Sel::Edge { body, index, .. } => {
|
||||||
|
|||||||
@@ -261,6 +261,30 @@ fn variable_fillet_round_trip() {
|
|||||||
assert_eq!(e.extra.get("radius2"), Some(&json!("4 mm")));
|
assert_eq!(e.extra.get("radius2"), Some(&json!("4 mm")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_fillet_mid_radii_round_trip() {
|
||||||
|
let mut app = boxed();
|
||||||
|
let e0 = edge(&app, Vec3::new(40.0, 15.0, 20.0));
|
||||||
|
let mut d = start(&mut app, vec![e0], "solid.fillet");
|
||||||
|
d.inputs[0].items.truncate(1);
|
||||||
|
d.extra.insert("type".into(), json!("variable"));
|
||||||
|
d.extra.insert("radius2".into(), json!("2 mm"));
|
||||||
|
d.extra.insert("mid".into(), json!([[0.5, "4 mm"]]));
|
||||||
|
d.extra.insert("smooth".into(), json!(true));
|
||||||
|
let p = apply_commands(&app, &d).unwrap().remove(0).1;
|
||||||
|
assert_eq!((p["mid"].clone(), p["smooth"].clone()), (json!([[0.5, "4 mm"]]), json!(true)), "{p}");
|
||||||
|
let id = apply(&mut app, &d);
|
||||||
|
let e = crate::dialogs::for_feature(&app, id, None).unwrap();
|
||||||
|
assert_eq!(e.extra.get("mid"), Some(&json!([[0.5, "4 mm"]])));
|
||||||
|
assert_eq!(e.extra.get("smooth"), Some(&json!(true)));
|
||||||
|
// All middle radii removed: a plain two-radius fillet again.
|
||||||
|
let mut d = e;
|
||||||
|
d.extra.insert("mid".into(), json!([]));
|
||||||
|
let p = apply_commands(&app, &d).unwrap().remove(0).1;
|
||||||
|
// (an edit: the feature's new parameters)
|
||||||
|
assert_eq!(p["params"]["mid"], json!([]), "{p}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn newer_options_reach_their_commands() {
|
fn newer_options_reach_their_commands() {
|
||||||
let mut app = boxed();
|
let mut app = boxed();
|
||||||
|
|||||||
Reference in new issue
Block a user