Kernel: refuse arc blends that collapse a side face (#1281)

A fillet or chamfer that consumes both seams of a side, leaving its old boundary and the fitted contact on the same curve, now returns TooLarge ("the blend would consume a neighbouring face") instead of a zero-area face that doesn't mesh. Genuine two-edge faces are kept. Fixes #1113.

Co-authored-by: Karl-VonRichter <[email protected]>
This commit is contained in:
Karl Von RichterandKarl-VonRichter authored and GitHub committed 2026-10-15 01:19:48 -04:00
1 parent 8535d229ff
commit ee9b4b9fb1
3 files changed
+49 -1

No files matched your search

+2 -1
View File
@@ -301,7 +301,8 @@ retain their saved two-finger scroll preference; camera motion is available thro
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
other, except a round after a round of the edge between the wall and a flat face that it
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
piece of sphere.
piece of sphere. A limiting blend that collapses a side wall into coincident boundary
curves is refused as too large rather than returning an unmeshable face (#1113).
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
+20
View File
@@ -768,6 +768,23 @@ fn straight(e: &mt::Edge, tol: f64) -> bool {
a.dist(b) > tol && (0..=16).all(|i| from_p3(c.subs(t0 + (t1 - t0) * i as f64 / 16.0)).dist_to_segment(a, b) <= tol)
}
/// Consuming both seams of a side can leave its old boundary and a fitted contact on the
/// same curve (#1113). They are distinct topological edges but enclose no usable face. Keep
/// genuine two-edge faces (a lens or lune); refuse only coincident traces, in both directions
/// since the curves need not have the same parameterisation.
fn collapsed_boundary(edges: &[mt::Edge], tolerance: f64) -> bool {
use mt::{BoundedCurve, ParametricCurve};
let [a, b] = edges else { return false };
[(a, b), (b, a)].into_iter().all(|(edge, other)| {
let curve = edge.curve();
let (lo, hi) = curve.range_tuple();
(0..=16).all(|k| {
let p = from_p3(curve.subs(lo + (hi - lo) * f64::from(k) / 16.0));
crate::blend::on_edge(other, p, tolerance).is_some()
})
})
}
/// A curved seam (an arc of an end face) shortened to the blend's contacts: the part of its
/// curve between the new ends `a` and `b` (`None` keeps that end), which must lie on it, in
/// order, short of each other.
@@ -1659,6 +1676,9 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
};
es.push(ne);
}
if collapsed_boundary(&es, fit_tolerance) {
return Err(too_large("the blend would consume a neighbouring face"));
}
wires.push(mt::Wire::from(es));
}
let rebuilt = crate::heal::absolute_face(f, wires).ok_or_else(|| fail("a side face could not be rebuilt"))?;
+27
View File
@@ -231,6 +231,33 @@ fn arc_top_edge_sizes_never_panic() {
}
}
/// At the full wall height the fitted contact coincides with the bottom arc: retaining that
/// wall produces a zero-area face, not a successful blend (#1113). Just below the limit the
/// wall still has area and must remain supported. Face order and mirrored edge directions
/// do not change either outcome.
#[test]
fn arc_top_blend_refuses_a_collapsed_wall() {
for body in [part(), part_other_order(), part_cut_from_a_box()] {
for z in [1.0, -1.0] {
let matrix = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, z, 0.0], [0.0, 0.0, 0.0, 1.0]];
let body = transform_matrix(&body, matrix).unwrap();
let at = arc_top();
let pick = [Vec3::new(at.x, at.y, at.z * z)];
for round in [true, false] {
let blend = |r| if round { fillet(&body, &pick, r) } else { chamfer(&body, &pick, r) };
let error = blend(HEIGHT).unwrap_err();
assert!(matches!(error, KernelError::TooLarge(_)), "round={round}, mirror={z}: {error}");
let made = blend(HEIGHT - 0.5).unwrap();
assert!(made.validity().is_empty(), "round={round}, mirror={z}: {:?}", made.validity());
let before = planned_volume(&body);
let removed = before - planned_volume(&made);
let expected = arc_top_removed(HEIGHT - 0.5, round);
assert!((removed - expected).abs() < 0.01 * expected + 0.01, "round={round}, mirror={z}: removed {removed}, expected {expected}");
}
}
}
}
fn tf() -> Vec3 {
Vec3::new(20.0, -R_WALL, HEIGHT)
}