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 <jyviko@gmail.com>
This commit is contained in:
1 parent
6d5df4b9b3
commit
0f0bb09858
9 files changed
+252
-35
No files matched your search
+48
-15
@@ -264,23 +264,38 @@ fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<soli
|
||||
}
|
||||
|
||||
/// A constant fillet of `edges`; with [`FilletFit::Reduce`](crate::FilletFit::Reduce), a radius too
|
||||
/// large for the geometry around them is reduced to the largest that works for all of them, which
|
||||
/// is returned.
|
||||
/// large for the geometry around them is reduced where it doesn't fit, to the largest that works
|
||||
/// for each group of edges sharing a face; the radius per edge is returned.
|
||||
fn fillet_fitted(
|
||||
body: &kernel::Body,
|
||||
edges: &[Vec3],
|
||||
r: f64,
|
||||
fit: crate::FilletFit,
|
||||
) -> std::result::Result<(kernel::Body, Option<f64>), kernel::KernelError> {
|
||||
) -> std::result::Result<(kernel::Body, Option<Vec<f64>>), 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<f64> = 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::<Vec<_>>().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<String>, 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<f64> {
|
||||
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<Vec<(Vec3, f64)>> {
|
||||
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<u64, Vec<Vec3>>,
|
||||
/// The radius a fillet used when it reduced a radius too large for its edges (by feature id,
|
||||
/// see `FilletFit::Reduce`).
|
||||
fitted: BTreeMap<u64, f64>,
|
||||
/// 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<u64, Vec<f64>>,
|
||||
}
|
||||
|
||||
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<String, Value>, 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)
|
||||
}
|
||||
|
||||
@@ -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<Value> {
|
||||
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<Value> =
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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>, 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
|
||||
|
||||
+123
-7
@@ -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<Vec<mt::EdgeID>> {
|
||||
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<Option<mt::EdgeID>> {
|
||||
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<Option<Vec<Vec<Vec3>>>> {
|
||||
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<f64>, 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<Vec3> = 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<Body> {
|
||||
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<Vec3>)> = 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<Vec<Vec<usize>>> {
|
||||
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<mt::FaceID>, Vec<mt::VertexID>, Vec<usize>)> = 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<mt::FaceID> = 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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -41,8 +41,9 @@ pub struct Built {
|
||||
pub error: Option<String>,
|
||||
/// 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<f64>,
|
||||
/// 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<Built, String>;
|
||||
@@ -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<f64>,
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user