From 0f0bb09858c7d4aa94e16538d4d976b6c2165f42 Mon Sep 17 00:00:00 2001 From: Irakli Kourtis Date: Tue, 13 Oct 2026 05:27:03 -0400 Subject: [PATCH] feat: fit reduce lowers the radius only where it doesn't fit (#496) Fillet fit: reduce lowers the radius only where it doesn't fit: picked edges are grouped where they can limit each other (sharing a face or vertex), and each group gets the requested radius where it fits, otherwise the largest that does. Refs #293. Co-authored-by: Iraklis Kourtis --- crates/doc/src/eval.rs | 63 ++++++++++--- crates/engine/src/cmd/features.rs | 10 +- crates/engine/src/tests.rs | 27 ++++++ crates/kernel/src/blend.rs | 130 ++++++++++++++++++++++++-- crates/kernel/src/lib.rs | 2 +- crates/kernel/src/tests.rs | 30 ++++++ crates/ui-egui/src/dialogs.rs | 10 +- crates/ui-egui/src/preselect_tests.rs | 4 +- crates/ui-egui/src/preview.rs | 11 ++- 9 files changed, 252 insertions(+), 35 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 9d0bf366..972ed7d3 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -264,23 +264,38 @@ fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result std::result::Result<(kernel::Body, Option), kernel::KernelError> { +) -> std::result::Result<(kernel::Body, Option>), kernel::KernelError> { match kernel::fillet(body, edges, r) { - Err(kernel::KernelError::TooLarge(why)) if fit == crate::FilletFit::Reduce => match kernel::fillet_max(body, edges, r) { - Some((used, b)) => Ok((b, Some(used))), + Err(kernel::KernelError::TooLarge(why)) if fit == crate::FilletFit::Reduce => match kernel::fillet_max_each(body, edges, r) { + // Built at the radius asked for after all (the groups apart): nothing reduced. + Some((used, b)) => Ok((b, used.iter().any(|u| *u < r).then_some(used))), None => Err(kernel::KernelError::TooLarge(why)), }, other => other.map(|b| (b, None)), } } +/// The warning of a fillet whose radius `asked` was reduced to `used` (per edge). +fn reduced_note(used: &[f64], asked: f64) -> String { + let mut values: Vec = used.iter().copied().filter(|r| *r < asked).collect(); + values.sort_by(f64::total_cmp); + values.dedup(); + let shown = values.iter().map(|v| show_mm(*v)).collect::>().join(", "); + let n = used.iter().filter(|r| **r < asked).count(); + if n == used.len() && values.len() == 1 { + format!("the radius is reduced to {shown} mm, the largest that fits these edges") + } else { + format!("the radius is reduced to {shown} mm on {n} of {} edges, the largest that fits them", used.len()) + } +} + /// Add `note` to a feature's warning. fn append_warning(warning: &mut Option, note: String) { *warning = Some(match warning.take() { @@ -296,9 +311,23 @@ fn show_mm(v: f64) -> String { } impl ModelState { - /// The radius fillet `feature` used when it reduced a radius too large for its edges. + /// The smallest radius fillet `feature` used when it reduced a radius too large for its edges. pub fn fitted_radius(&self, feature: u64) -> Option { - self.fitted.get(&feature).copied() + self.fitted_range(feature).map(|(lo, _)| lo) + } + + /// The smallest and largest radius fillet `feature` used when it reduced a radius too large + /// for some of its edges. + pub fn fitted_range(&self, feature: u64) -> Option<(f64, f64)> { + self.fitted.get(&feature)?.iter().fold(None, |acc, r| Some(acc.map_or((*r, *r), |(lo, hi): (f64, f64)| (lo.min(*r), hi.max(*r))))) + } + + /// Each edge fillet `feature` blended (a point on it) with the radius it used, when it + /// reduced a radius too large for some of its edges. + pub fn fitted_edges(&self, feature: u64) -> Option> { + let radii = self.fitted.get(&feature)?; + let edges = self.blend_edges.get(&feature)?; + Some(edges.iter().copied().zip(radii.iter().copied()).collect()) } } @@ -321,9 +350,9 @@ pub struct ModelState { /// a pattern of the feature repeats these, not the points picked when it was made, which a /// parameter change may have moved off the edges. blend_edges: BTreeMap>, - /// The radius a fillet used when it reduced a radius too large for its edges (by feature id, - /// see `FilletFit::Reduce`). - fitted: BTreeMap, + /// The radius per edge a fillet used when it reduced a radius too large for some of its edges + /// (by feature id, see `FilletFit::Reduce`). + fitted: BTreeMap>, } impl ModelState { @@ -349,9 +378,11 @@ impl ModelState { [p.x, p.y, p.z].map(f64::to_bits).hash(&mut h); } } - for (feature, r) in &self.fitted { + for (feature, radii) in &self.fitted { feature.hash(&mut h); - r.to_bits().hash(&mut h); + for r in radii { + r.to_bits().hash(&mut h); + } } serde_json::to_string(&self.threads).unwrap_or_default().hash(&mut h); serde_json::to_string(&self.sheets).unwrap_or_default().hash(&mut h); @@ -2955,8 +2986,10 @@ fn replay( let nb = match &src.kind { FeatureKind::Chamfer { distance2, angle, flip, .. } => chamfer_with(vals, &mb.body, &moved, r, distance2, angle, *flip)?, // A copy is congruent to the original: the radius it was reduced to first. - FeatureKind::Fillet { fit, .. } => match st.fitted.get(&src.id).copied() { - Some(used) => kernel::fillet(&mb.body, &moved, used).or_else(|_| fillet_fitted(&mb.body, &moved, r, *fit).map(|x| x.0))?, + FeatureKind::Fillet { fit, .. } => match st.fitted.get(&src.id) { + Some(used) => { + kernel::fillet_each(&mb.body, &moved, used).or_else(|_| fillet_fitted(&mb.body, &moved, r, *fit).map(|x| x.0))? + } None => fillet_fitted(&mb.body, &moved, r, *fit)?.0, }, _ => kernel::fillet(&mb.body, &moved, r)?, @@ -3731,8 +3764,8 @@ fn eval_feature(doc: &Document, vals: &BTreeMap, f: &Feature, st: FeatureKind::Fillet { fit, .. } => { let (b, used) = fillet_fitted(&mb.body, &edges, r, *fit)?; if let Some(used) = used { + append_warning(warning, reduced_note(&used, r)); st.fitted.insert(f.id, used); - append_warning(warning, format!("the radius is reduced to {} mm, the largest that fits these edges", show_mm(used))); } (b, input) } diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 3b60e20b..72af07f5 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -31,7 +31,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .at("SOLID", "MODIFY") .icon("fillet") .key("F") - .params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other); radius2?; start?: [x,y,z]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); fit?: exact|reduce (exact, the default: a radius too large for the geometry around the edges is refused; reduce: it is reduced to the largest that fits all of them, returned as `radius_applied` and noted in the feature's warning)"), + .params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other); radius2?; start?: [x,y,z]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); fit?: exact|reduce (exact, the default: a radius too large for the geometry around the edges is refused; reduce: where it doesn't fit it is reduced to the largest that does, one radius for edges sharing a face; the smallest used is returned as `radius_applied`, each edge's as `radii_applied: [{edge: [x,y,z], radius}]`, and noted in the feature's warning)"), CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"), CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool; new_component?: bool (the result goes into a new component; component_name?)"), CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect) @@ -807,11 +807,15 @@ fn fillet(s: &mut Session, p: &Value) -> Result { return Err(bad(cmd, "`fit: reduce` takes a constant radius")); } let mut out = add_feature(s, p, FeatureKind::Fillet { edges, radius, body: str_(p, "body").map(str::to_string), style, fit })?; - // The radius the fillet used, when `fit: reduce` reduced it. + // The radii the fillet used, when `fit: reduce` reduced it. if let Some(id) = out.get("feature").and_then(Value::as_u64) - && let Some(r) = s.model.result(id).and_then(|res| res.state.fitted_radius(id)) + && let Some(state) = s.model.result(id).map(|res| &res.state) + && let Some(r) = state.fitted_radius(id) { out["radius_applied"] = serde_json::json!(r); + let each: Vec = + state.fitted_edges(id).unwrap_or_default().into_iter().map(|(p, r)| serde_json::json!({"edge": [p.x, p.y, p.z], "radius": r})).collect(); + out["radii_applied"] = Value::Array(each); } Ok(out) } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ecff5abc..24821eca 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2909,6 +2909,33 @@ fn fillet_fit_reduce_uses_the_largest_radius_that_fits() { assert!(s.execute("solid.fillet", &json!({"edges": mids, "radius": 1, "fit": "reduce", "type": "chord"})).is_err()); } +#[test] +fn fillet_fit_reduce_keeps_the_radius_where_it_fits() { + // A 2 mm fin on a block: its top edge can't take 4, a corner of the block far from it can. + // Only the fin's edge is reduced; the reply gives each edge's radius. + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 2, "width": 30, "height": 10, "corner": [19, 0, 20], "operation": "join"})); + assert_eq!(s.model.state().bodies.len(), 1); + let edges = json!([[21, 15, 30], [40, 0, 10]]); + let out = run(&mut s, "solid.fillet", json!({"edges": edges, "radius": 4, "fit": "reduce"})); + let fin = out["radius_applied"].as_f64().unwrap(); + assert!(fin < 4.0, "{out}"); + let each: Vec<(Vec, f64)> = out["radii_applied"] + .as_array() + .unwrap() + .iter() + .map(|x| (x["edge"].as_array().unwrap().iter().filter_map(Value::as_f64).collect(), x["radius"].as_f64().unwrap())) + .collect(); + assert_eq!(each, [(vec![21.0, 15.0, 30.0], fin), (vec![40.0, 0.0, 10.0], 4.0)], "{out}"); + let removed = (1.0 - PI / 4.0) * (fin * fin * 30.0 + 16.0 * 20.0); + let want = 40.0 * 30.0 * 20.0 + 2.0 * 30.0 * 10.0 - removed; + assert!(rel(volume(&mut s), want) < 2e-4, "{} vs {want}", volume(&mut s)); + let id = out["feature"].as_u64().unwrap(); + let warning = s.model.result(id).and_then(|r| r.warning.clone()).unwrap_or_default(); + assert!(warning.contains("on 1 of 2 edges"), "{warning}"); +} + #[test] fn a_pattern_of_a_reduced_fillet_rounds_each_copy_alike() { // #293: a 6 mm wide post on a plate, its four vertical edges at 3.5 reduced to 3, patterned diff --git a/crates/kernel/src/blend.rs b/crates/kernel/src/blend.rs index c0683f2b..86e6ace5 100644 --- a/crates/kernel/src/blend.rs +++ b/crates/kernel/src/blend.rs @@ -828,22 +828,28 @@ fn smooth_loop(w: &mt::Wire) -> bool { /// The B-rep edges nearest the points (each once). fn edge_ids(cur: &Body, solid: &Solid, pts: &[Vec3]) -> Result> { - let size = cur.size(); - let tol = (size * 2e-3).max(1e-3); let all = Body::unique_edges(solid); let mut ids = Vec::new(); for p in pts { - if let Some((idx, dist)) = cur.nearest_edge(*p, tol)? - && dist <= size * 0.05 + 1e-3 - && let Some(e) = all.get(idx) - && !ids.contains(&e.id()) + if let Some(id) = nearest_edge_id(cur, &all, *p)? + && !ids.contains(&id) { - ids.push(e.id()); + ids.push(id); } } Ok(ids) } +/// The edge of `all` (`cur`'s edges, in their order) a point picks: the nearest, if near enough. +fn nearest_edge_id(cur: &Body, all: &[mt::Edge], p: Vec3) -> Result> { + let size = cur.size(); + let tol = (size * 2e-3).max(1e-3); + Ok(match cur.nearest_edge(p, tol)? { + Some((idx, dist)) if dist <= size * 0.05 + 1e-3 => all.get(idx).map(|e| e.id()), + _ => None, + }) +} + /// When the edges make up whole boundary loops of planar faces: the edge points per loop. fn loop_groups(cur: &Body, pts: &[Vec3]) -> Result>>> { Ok(loop_split(cur, pts)?.and_then(|(g, rest)| rest.is_empty().then_some(g))) @@ -955,6 +961,116 @@ pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Option<(f64, Body best.map(|b| (lo as f64 * q, b)) } +/// [`fillet_max`] for each group of the edges that may limit each other's radius (sharing a +/// face or a vertex, directly or through other edges in the group), so a round that fits isn't +/// reduced for a tight edge elsewhere. The radius per edge (in `edges`' order) and the result; +/// one radius for all (as [`fillet_max`]) when a group is refused for another reason than its +/// size, the groups don't build together, or there are too many to search (each search is up +/// to 16 fillets, and this runs in the live preview). +pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Option<(Vec, Body)> { + const MAX_GROUPS: usize = 8; + const MAX_SEARCHES: usize = 2; + let one = || fillet_max(body, edges, radius).map(|(r, b)| (vec![r; edges.len()], b)); + let Some(groups) = interacting_groups(body, edges).filter(|g| g.len() > 1 && g.len() <= MAX_GROUPS) else { return one() }; + let mut radii = vec![radius; edges.len()]; + let mut searches = 0; + for g in &groups { + let pts: Vec = g.iter().filter_map(|i| edges.get(*i).copied()).collect(); + let r = match fillet(body, &pts, radius) { + Ok(_) => continue, + Err(KernelError::TooLarge(_)) if searches < MAX_SEARCHES => { + searches += 1; + match fillet_max(body, &pts, radius) { + Some((r, _)) => r, + None => return one(), + } + } + Err(_) => return one(), + }; + for i in g { + if let Some(x) = radii.get_mut(*i) { + *x = r; + } + } + } + match fillet_each(body, edges, &radii) { + Ok(b) => Some((radii, b)), + Err(_) => one(), + } +} + +/// The fillet of each edge at its own radius (`radii` in `edges`' order): built together where +/// the corner build takes it, else the edges of each radius together, the largest first. +pub fn fillet_each(body: &Body, edges: &[Vec3], radii: &[f64]) -> Result { + if radii.len() != edges.len() { + return Err(KernelError::Invalid("one radius per edge".into())); + } + if let Some(r) = radii.first().filter(|r| radii.iter().all(|x| x.to_bits() == r.to_bits())) { + return fillet(body, edges, *r); + } + let sel: Vec<(Vec3, f64)> = edges.iter().copied().zip(radii.iter().copied()).collect(); + if let Ok(b) = crate::blend_corner::fillet_radii(body, &sel) { + return Ok(b); + } + let mut by_radius: Vec<(f64, Vec)> = Vec::new(); + for (p, r) in sel { + match by_radius.iter_mut().find(|(x, _)| x.to_bits() == r.to_bits()) { + Some((_, pts)) => pts.push(p), + None => by_radius.push((r, vec![p])), + } + } + by_radius.sort_by(|a, b| b.0.total_cmp(&a.0)); + let mut cur = body.clone(); + for (r, pts) in &by_radius { + cur = fillet(&cur, pts, *r)?; + } + Ok(cur) +} + +/// The points' edges (found as [`edge_ids`] finds them, on the body as the blends see it) grouped +/// where they share a face or a vertex, directly or through other points' edges: indices into +/// `pts`. `None` when a point has no edge near it. +fn interacting_groups(body: &Body, pts: &[Vec3]) -> Option>> { + let cur = crate::boolean_record::healed(body).ok()?; + let all = Body::unique_edges(&cur.solid); + // What each point's edge touches: its faces and its ends. + let mut groups: Vec<(Vec, Vec, Vec)> = Vec::new(); + for (i, p) in pts.iter().enumerate() { + let id = nearest_edge_id(&cur, &all, *p).ok()??; + let edge = all.iter().find(|e| e.id() == id)?; + let mut faces: Vec = cur.solid.face_iter().filter(|f| f.edge_iter().any(|e| e.id() == id)).map(|f| f.id()).collect(); + let mut ends = vec![edge.front().id(), edge.back().id()]; + let mut members = vec![i]; + // Join every group touching one of these faces or ends. + let (meet, rest): (Vec<_>, Vec<_>) = + groups.into_iter().partition(|(fs, vs, _)| fs.iter().any(|f| faces.contains(f)) || vs.iter().any(|v| ends.contains(v))); + for (fs, vs, ms) in meet { + for f in fs { + if !faces.contains(&f) { + faces.push(f); + } + } + for v in vs { + if !ends.contains(&v) { + ends.push(v); + } + } + members.extend(ms); + } + groups = rest; + groups.push((faces, ends, members)); + } + Some( + groups + .into_iter() + .map(|(_, _, mut ms)| { + ms.sort_unstable(); + ms + }) + .collect(), + ) +} + /// Equal-distance chamfer of the edges nearest to the given points. Each edge is cut with a /// prism whose only face inside the body is the chamfer plane (so neighbouring chamfers meet in /// a mitre); the local operation is the fallback. diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 3b97e2b9..f88f6ed3 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -70,7 +70,7 @@ mod thread_tests; mod topo; mod tspline; -pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_max}; +pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_max, fillet_max_each}; pub use blend_corner::fillet_radii; pub use blend_variable::{fillet_chord, fillet_variable, fillet_variable_points}; pub use body::{Body, CapSignature, DISPLAY_ANGLE, EdgeInfo, FaceInfo, FacePaint, FaceSource, Paint}; diff --git a/crates/kernel/src/tests.rs b/crates/kernel/src/tests.rs index f7d93bb2..ec988484 100644 --- a/crates/kernel/src/tests.rs +++ b/crates/kernel/src/tests.rs @@ -382,6 +382,36 @@ fn fillet_max_finds_the_largest_radius_that_works() { assert!(fillet_max(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).is_none()); } +/// Edges that share no face get their own largest radius: a 2 mm fin's top edge takes at most +/// its thickness, while a corner of the block it stands on, far from it, keeps the 4 asked for. +/// Edges that limit each other (the post's four, sharing faces) still share one. +#[test] +fn fillet_max_each_reduces_only_where_the_radius_does_not_fit() { + let (block, fin) = (40.0 * 30.0 * 20.0, 2.0 * 30.0 * 10.0); + let body = boolean( + &box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(), + &box_solid(Vec3::new(19.0, 0.0, 20.0), Vec3::new(21.0, 30.0, 30.0)).unwrap(), + BoolOp::Union, + ) + .unwrap() + .unwrap(); + let edges = [Vec3::new(21.0, 15.0, 30.0), Vec3::new(40.0, 0.0, 10.0)]; + assert!(matches!(fillet(&body, &edges, 4.0), Err(KernelError::TooLarge(_)))); + let (one, _) = fillet_max(&body, &edges, 4.0).unwrap(); + let (radii, b) = fillet_max_each(&body, &edges, 4.0).unwrap(); + assert_eq!(radii.get(1), Some(&4.0), "{radii:?}"); + let fin_r = radii.first().copied().unwrap(); + assert!(fin_r < 4.0 && (fin_r - one).abs() < 1e-9, "{radii:?}, one radius {one}"); + assert!(b.validity().is_empty(), "{:?}", b.validity()); + // Each round takes (1 − π/4)·r² along its edge: the fin's 30 long, the corner's 20. + let removed = (1.0 - PI / 4.0) * (fin_r * fin_r * 30.0 + 16.0 * 20.0); + assert!(rel(measure(&b).unwrap().volume, block + fin - removed) < 2e-4); + // The post's four edges limit each other: one radius, as `fillet_max`. + let post = box_solid(Vec3::new(14.0, 0.0, 0.0), Vec3::new(28.0, 6.0, 10.0)).unwrap(); + let mids = [Vec3::new(14.0, 0.0, 5.0), Vec3::new(28.0, 0.0, 5.0), Vec3::new(14.0, 6.0, 5.0), Vec3::new(28.0, 6.0, 5.0)]; + assert_eq!(fillet_max_each(&post, &mids, 3.5).map(|x| x.0), Some(vec![3.0; 4])); +} + /// A corner's rounds too large for its faces are refused as too large, so a fit can reduce /// them, also where only the corner build finds it (the edge-by-edge blend failing otherwise). #[test] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fd2f7153..31b190e8 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1604,10 +1604,16 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { let fit = extra_choice(ui, &mut d.extra, "fi_fit", "fit", &[("Exact", "exact"), ("Reduce to Fit", "reduce")]); ui.end_row(); if fit == 1 - && let Some(r) = app.preview.applied + && let Some((lo, hi)) = app.preview.applied { + // A range where edges apart from each other fit different radii. row_label(ui, "Applied Radius"); - ui.label(RichText::new(crate::prefs::show_mm(app, r, 1)).color(t.warning)); + let shown = if hi > lo { + format!("{} – {}", crate::prefs::show_mm_bare(app, lo), crate::prefs::show_mm(app, hi, 1)) + } else { + crate::prefs::show_mm(app, lo, 1) + }; + ui.label(RichText::new(shown).color(t.warning)); ui.end_row(); } } else if !*chamfer { diff --git a/crates/ui-egui/src/preselect_tests.rs b/crates/ui-egui/src/preselect_tests.rs index 90178142..5444b772 100644 --- a/crates/ui-egui/src/preselect_tests.rs +++ b/crates/ui-egui/src/preselect_tests.rs @@ -537,7 +537,7 @@ fn fillet_radius_fit_reduces_and_shows_the_applied_radius() { assert_eq!(p["fit"], json!("reduce")); let job = crate::preview::compute(&app.session, &[(id.clone(), p.clone())], crate::preview::Colors::from_tokens(&crate::theme::Tokens::dark())) .unwrap(); - assert_eq!(job.applied, Some(3.0)); + assert_eq!(job.applied, Some((3.0, 3.0))); // Applied and edited again, the fillet keeps its fit. app.session.execute(&id, &p).unwrap(); let fillet = app.session.doc.features.last().unwrap().id; @@ -546,7 +546,7 @@ fn fillet_radius_fit_reduces_and_shows_the_applied_radius() { // Its edit preview shows the radius it uses too. let cmds = apply_commands(&app, &e).unwrap(); let job = crate::preview::compute(&app.session, &cmds, crate::preview::Colors::from_tokens(&crate::theme::Tokens::dark())).unwrap(); - assert_eq!(job.applied, Some(3.0)); + assert_eq!(job.applied, Some((3.0, 3.0))); } /// Apply a dialog's commands to the session; the new feature's id. diff --git a/crates/ui-egui/src/preview.rs b/crates/ui-egui/src/preview.rs index 3ff37813..f4b0b579 100644 --- a/crates/ui-egui/src/preview.rs +++ b/crates/ui-egui/src/preview.rs @@ -41,8 +41,9 @@ pub struct Built { pub error: Option, /// Extent of what the preview shows (the view's depth range must cover it). pub bounds: Aabb3, - /// The radius a fillet used when it reduced one too large for its edges (`fit: reduce`). - pub applied: Option, + /// The smallest and largest radius a fillet used when it reduced one too large for its edges + /// (`fit: reduce`). + pub applied: Option<(f64, f64)>, } type Job = Result; @@ -68,8 +69,8 @@ pub struct PreviewState { /// Time the last preview took (ms), for `ui.inspect`. pub ms: f64, started: f64, - /// The radius the previewed fillet used when it reduced the one asked for. - pub applied: Option, + /// The smallest and largest radius the previewed fillet used when it reduced the one asked for. + pub applied: Option<(f64, f64)>, } impl PreviewState { @@ -231,7 +232,7 @@ pub fn compute(s: &Session, cmds: &[(String, Value)], colors: Colors) -> Job { b.bounds = state_bounds(&p.after); // The radius a previewed fillet used when it reduced the one asked for (a new // fillet or an edited one: the feature the replies name). - b.applied = p.replies.iter().filter_map(|r| r.get("feature").and_then(Value::as_u64)).find_map(|id| p.after.fitted_radius(id)); + b.applied = p.replies.iter().filter_map(|r| r.get("feature").and_then(Value::as_u64)).find_map(|id| p.after.fitted_range(id)); Ok(b) } }