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:
1 parent
8535d229ff
commit
ee9b4b9fb1
3 files changed
+49
-1
No files matched your search
+2
-1
@@ -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;
|
||||
|
||||
@@ -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"))?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user