perf(kernel): a refused edge is not rolled again after blends far from it (#777)
A blend no longer re-rolls an edge it already refused unless a later blend came near it, so long curved chains stop paying for the same refusal over and over.
This commit is contained in:
1 parent
25a3dc501b
commit
7b0a1b296c
2 files changed
+78
-5
No files matched your search
@@ -823,6 +823,11 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
// that earlier blends left between selected edges, so corners round off), a single edge,
|
||||
// or a chain of curved edges.
|
||||
let mut todo: Vec<Vec3> = edges.to_vec();
|
||||
// Edges refused since the last blend that could have changed them: (pick, the box around the
|
||||
// edge, the refusal). A blend only changes the body near the edges it rounds, so an edge
|
||||
// refused away from every later blend would be refused the same way again: it is not tried
|
||||
// again (a refusal costs a rolling-ball attempt, seconds each on a long curved edge).
|
||||
let mut refused: Vec<(Vec3, (Vec3, Vec3), KernelError)> = Vec::new();
|
||||
for _ in 0..(2 * edges.len() + 4) {
|
||||
if todo.is_empty() {
|
||||
return Ok(cur);
|
||||
@@ -835,13 +840,21 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
&& let Some(Ok((res, rec))) = crate::loopblend::loop_blend(&solid, &ids, size, r, shape == Shape::Round)
|
||||
&& let Ok(b) = guard(what, || Ok(res)).and_then(|res| record::rebuilt(res, &[&solid], &rec))
|
||||
{
|
||||
let near = edges_box(&solid, &ids, r + tol);
|
||||
refused.retain(|(_, b, _)| !boxes_meet(*b, near));
|
||||
cur = record::after(b, &cur, &counts);
|
||||
todo.retain(|p| !covered.iter().any(|q| q.dist(*p) < 1e-12));
|
||||
continue;
|
||||
}
|
||||
let mut first_err: Option<KernelError> = None;
|
||||
let mut done: Option<(Body, Vec<usize>)> = None;
|
||||
let mut done: Option<(Body, Vec<usize>, (Vec3, Vec3))> = None;
|
||||
for (j, p) in todo.iter().enumerate() {
|
||||
if let Some((_, _, e)) = refused.iter().find(|(q, _, _)| q.dist(*p) < 1e-12) {
|
||||
if first_err.is_none() || (matches!(e, KernelError::TooLarge(_)) && !matches!(first_err, Some(KernelError::TooLarge(_)))) {
|
||||
first_err = Some(e.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some((idx, dist)) = cur.nearest_edge(*p, tol)? else { return Err(KernelError::Invalid("the body has no edges".into())) };
|
||||
if dist > size * 0.05 + 1e-3 {
|
||||
return Err(KernelError::Invalid(format!("no edge near {:?}", [p.x, p.y, p.z])));
|
||||
@@ -861,6 +874,8 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
#[cfg(test)]
|
||||
ROLLS.with(|n| n.set(n.get() + 1));
|
||||
guard(what, || crate::curveblend::curve_blend(&solid, ids, size, r, shape == Shape::Round))
|
||||
.map(|o| (o, if on_chain.is_empty() { vec![j] } else { on_chain }))
|
||||
};
|
||||
@@ -888,22 +903,26 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
},
|
||||
},
|
||||
};
|
||||
// What this attempt could change: the edge and its smooth chain, with room for the blend.
|
||||
let reach = edges_box(&solid, chain.as_deref().unwrap_or(&[edge.id()]), r + tol);
|
||||
match attempt.and_then(|((o, rec), js)| record::rebuilt(o, &[&solid], &rec).map(|b| (b, js))) {
|
||||
Ok(x) => {
|
||||
done = Some(x);
|
||||
Ok((b, js)) => {
|
||||
done = Some((b, js, reach));
|
||||
break;
|
||||
}
|
||||
// Reported: the first refusal, unless a later one is a size a smaller radius may
|
||||
// fit (the others may be refused only until that edge is blended).
|
||||
Err(e) => {
|
||||
if first_err.is_none() || (matches!(e, KernelError::TooLarge(_)) && !matches!(first_err, Some(KernelError::TooLarge(_)))) {
|
||||
first_err = Some(e);
|
||||
first_err = Some(e.clone());
|
||||
}
|
||||
refused.push((*p, reach, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
match done {
|
||||
Some((b, js)) => {
|
||||
Some((b, js, near)) => {
|
||||
refused.retain(|(_, b2, _)| !boxes_meet(*b2, near));
|
||||
cur = record::after(b, &cur, &counts);
|
||||
let mut k = 0;
|
||||
todo.retain(|_| {
|
||||
@@ -918,6 +937,36 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
if todo.is_empty() { Ok(cur) } else { Err(unsupported("blending these edges together")) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Rolling-ball attempts made by [`blend`] on this thread (tests count them).
|
||||
pub(crate) static ROLLS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The box around these edges of `solid` (their ends and middles), grown by `margin`.
|
||||
fn edges_box(solid: &Solid, ids: &[mt::EdgeID], margin: f64) -> (Vec3, Vec3) {
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
||||
let mut hi = -lo;
|
||||
for e in Body::unique_edges(solid).iter().filter(|e| ids.contains(&e.id())) {
|
||||
let c = e.curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
for k in 0..=8 {
|
||||
let q = from_p3(c.subs(t0 + (t1 - t0) * f64::from(k) / 8.0));
|
||||
lo = lo.min(q);
|
||||
hi = hi.max(q);
|
||||
}
|
||||
}
|
||||
let m = Vec3::new(margin, margin, margin);
|
||||
(lo - m, hi + m)
|
||||
}
|
||||
|
||||
/// Whether two boxes overlap (an empty box meets every box: nothing is known about it).
|
||||
fn boxes_meet(a: (Vec3, Vec3), b: (Vec3, Vec3)) -> bool {
|
||||
let empty = |x: (Vec3, Vec3)| !(x.0.x <= x.1.x && x.0.y <= x.1.y && x.0.z <= x.1.z);
|
||||
empty(a) || empty(b) || (a.0.x <= b.1.x && b.0.x <= a.1.x && a.0.y <= b.1.y && b.0.y <= a.1.y && a.0.z <= b.1.z && b.0.z <= a.1.z)
|
||||
}
|
||||
|
||||
/// Neighbouring edges along one boundary loop of a planar face that between them hold at least
|
||||
/// two of the points' edges, joined by arcs that meet them smoothly (left by earlier blends):
|
||||
/// the run's edge ids and the points it covers.
|
||||
|
||||
@@ -327,3 +327,27 @@ fn rolling_ball_past_the_end_seam_is_too_large_and_reduces() {
|
||||
}
|
||||
assert!(worst < 1e-3, "blend surface is {worst} mm off the analytic round at r {r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_refused_edge_is_not_rolled_again_after_blends_far_from_it() {
|
||||
// The crossing cylinders' seam refuses a round of 3 (too large). A bar joined to them has
|
||||
// two straight edges at its far end that round on their own. Each blend there leaves the
|
||||
// seam's surroundings as they were, so the seam is tried once, not once per blend, and the
|
||||
// refusal is the same.
|
||||
let (r1, r2, d, h) = (17.0, 9.5, 12.0, 10.0);
|
||||
let body = boolean(&cylinder(Vec3::ZERO, Vec3::Z, r1, h).unwrap(), &cylinder(Vec3::new(d, 0.0, 0.0), Vec3::Z, r2, h).unwrap(), BoolOp::Union)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let bar = crate::box_solid(Vec3::new(-80.0, -5.0, 0.0), Vec3::new(-10.0, 5.0, h)).unwrap();
|
||||
let body = boolean(&body, &bar, BoolOp::Union).unwrap().unwrap();
|
||||
let along = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
|
||||
let seam = Vec3::new(along, (r1 * r1 - along * along).sqrt(), h / 2.0);
|
||||
let ends = [Vec3::new(-80.0, -5.0, h / 2.0), Vec3::new(-80.0, 5.0, h / 2.0)];
|
||||
crate::blend::ROLLS.with(|n| n.set(0));
|
||||
let e = fillet(&body, &[seam, ends[0], ends[1]], 3.0).unwrap_err();
|
||||
assert!(matches!(e, KernelError::TooLarge(_)), "{e}");
|
||||
let rolls = crate::blend::ROLLS.with(|n| n.get());
|
||||
assert_eq!(rolls, 1, "the seam was rolled {rolls} times");
|
||||
// The far edges alone still round.
|
||||
assert!(fillet(&body, &ends, 3.0).is_ok());
|
||||
}
|
||||
Reference in new issue
Block a user