Fix boss-bottom blends and merged arc-corner surfaces (#1294)
A fillet or chamfer on a cylindrical boss's bottom arc now builds with a closed display mesh: the seam-extension containment ray counts a shared curved-boundary endpoint once. Merging the split faces of a three-edge arc corner rebuilds the face on the original input face's full surface instead of a piece's restricted patch, and runs under the kernel's panic guard. Fixes #1262 and #1263. Co-authored-by: Karl-VonRichter <20079846+Karl-VonRichter@users.noreply.github.com>
This commit is contained in:
1 parent
af2f09fac2
commit
d6f167a88a
4 files changed
+42
-12
No files matched your search
@@ -231,6 +231,11 @@ pub(crate) fn extension_fits(face: &mt::Face, p: Vec3, q: Vec3, tol: f64) -> boo
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Curve endpoints may round to opposite sides of a ray that follows a
|
||||
// straight boundary into the face. Treat points within its clearance as
|
||||
// on the ray, so a shared endpoint contributes one crossing.
|
||||
let ay = if ay.abs() <= tol { 0.0 } else { ay };
|
||||
let by = if by.abs() <= tol { 0.0 } else { by };
|
||||
if (ay > 0.0) != (by > 0.0) {
|
||||
let hit = ax - ay * (bx - ax) / (by - ay);
|
||||
if hit > tol && hit <= len + tol {
|
||||
@@ -2663,12 +2668,20 @@ mod tests {
|
||||
),
|
||||
(Shape::Flat, 0.32, 0.8 / 3.0),
|
||||
] {
|
||||
let out = blend(&body, &[Vec3::new(52.0, 20.0, 0.0)], 0.8, shape, "blend").unwrap();
|
||||
let picks = [Vec3::new(52.0, 20.0, 0.0)];
|
||||
let out = match shape {
|
||||
Shape::Round => fillet(&body, &picks, 0.8),
|
||||
_ => chamfer(&body, &picks, 0.8),
|
||||
}
|
||||
.unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// Pappus: the section turned half round the axis at its centroid's radius.
|
||||
let want = area * std::f64::consts::PI * (12.0 - c);
|
||||
let removed = v0 - vol(&out);
|
||||
assert!((removed - want).abs() < want * 2e-3, "{removed} vs {want}");
|
||||
// The app's planned mesh must also close, with the same analytic removal.
|
||||
let planned = crate::issue_851_tests::planned_volume(&body) - crate::issue_851_tests::planned_volume(&out);
|
||||
assert!((planned - want).abs() < want * 2e-3, "display mesh: {planned} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ fn round_vertex(cur: &Body, at: Vec3, r: f64) -> Result<Body> {
|
||||
let notched = crate::ops::boolean(cur, &cutter, BoolOp::Cut)?.ok_or_else(|| unsupported("a corner this small"))?;
|
||||
// The cut splits a curved face along the trace of its plane (the whole way across, not just
|
||||
// where the plane cuts): the pieces are one face again.
|
||||
let notched = merge_split_faces(¬ched, size)?;
|
||||
let notched = crate::guard("merge split faces", || merge_split_faces(¬ched, cur, size))?;
|
||||
// A point on what is left of each edge, half way from where its plane crosses it to its far end.
|
||||
let mut picks: Vec<Vec3> = Vec::with_capacity(3);
|
||||
for (e, m) in edges.iter().zip(&normals) {
|
||||
@@ -320,7 +320,7 @@ fn close_with_sphere(step: &Body, ball: &Ball, planes: [Vec3; 3], r: f64, v: Vec
|
||||
|
||||
/// `body` with the curved faces that are pieces of one input face (as the cut that made `body`
|
||||
/// recorded them) joined again: the edges two pieces share go, the rest make the one boundary.
|
||||
fn merge_split_faces(body: &Body, size: f64) -> Result<Body> {
|
||||
fn merge_split_faces(body: &Body, source: &Body, size: f64) -> Result<Body> {
|
||||
let tol = (size * 1e-6).max(1e-9);
|
||||
let Some(history) = body.history() else { return Ok(body.clone()) };
|
||||
let solid = body.deep_copy();
|
||||
@@ -328,7 +328,7 @@ fn merge_split_faces(body: &Body, size: f64) -> Result<Body> {
|
||||
if history.faces.len() != faces.len() {
|
||||
return Ok(body.clone());
|
||||
}
|
||||
let on = |a: &mt::Face, p: Vec3| project(a, p, None).is_some_and(|(q, _, _)| q.dist(p) <= tol * 10.0);
|
||||
let on = |a: &mt::Face, p: Vec3| project(a, p, None).is_some_and(|(q, _, uv)| crate::curveblend::in_range(a, uv) && q.dist(p) <= tol * 10.0);
|
||||
// Pieces by the input face they come from.
|
||||
let mut groups: Vec<Vec<usize>> = Vec::new();
|
||||
for (i, f) in history.faces.iter().enumerate() {
|
||||
@@ -349,10 +349,8 @@ fn merge_split_faces(body: &Body, size: f64) -> Result<Body> {
|
||||
continue;
|
||||
}
|
||||
// Edges used once, as the pieces walk them.
|
||||
let all: Vec<mt::Edge> = pieces
|
||||
.iter()
|
||||
.flat_map(|f| f.absolute_boundaries().iter().flat_map(|w| w.edge_iter().cloned().collect::<Vec<_>>()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
let all: Vec<mt::Edge> =
|
||||
pieces.iter().flat_map(|f| f.boundaries().iter().flat_map(|w| w.edge_iter().cloned().collect::<Vec<_>>()).collect::<Vec<_>>()).collect();
|
||||
let mut uses: HashMap<mt::EdgeID, usize> = HashMap::new();
|
||||
for e in &all {
|
||||
*uses.entry(e.id()).or_default() += 1;
|
||||
@@ -371,12 +369,18 @@ fn merge_split_faces(body: &Body, size: f64) -> Result<Body> {
|
||||
if !left.is_empty() || wire.first().map(|e| e.front().clone()) != wire.last().map(|e| e.back().clone()) {
|
||||
continue;
|
||||
}
|
||||
// On the surface of the piece whose patch reaches every corner of all.
|
||||
// A boolean may restrict a piece's surface to its own parameter box. The
|
||||
// joined boundary needs the original face's full patch, not an extrapolation
|
||||
// of one piece (which the bounded display planner correctly refuses).
|
||||
let all_on = |f: &mt::Face| {
|
||||
pieces.iter().all(|p| p.vertex_iter().all(|v| on(f, vtx(&v))) && p.edge_iter().all(|e| edge_mid(&e).is_none_or(|m| on(f, m))))
|
||||
};
|
||||
let Some(like) = pieces.iter().copied().find(|f| all_on(f)) else { continue };
|
||||
let Some(joined) = crate::heal::absolute_face(like, vec![mt::Wire::from(wire)]) else { continue };
|
||||
let original = group.first().and_then(|i| history.faces.get(*i)).and_then(|h| match h.origins.as_slice() {
|
||||
[crate::history::FaceOrigin::Face { operand: 0, face }] => source.solid.face_iter().nth(*face),
|
||||
_ => None,
|
||||
});
|
||||
let Some(like) = original.filter(|f| all_on(f)).or_else(|| pieces.iter().copied().find(|f| all_on(f))) else { continue };
|
||||
let Ok(joined) = mt::Face::try_new(vec![mt::Wire::from(wire)], like.oriented_surface()) else { continue };
|
||||
let Some(first) = group.first() else { continue };
|
||||
for i in group {
|
||||
if let Some(slot) = out.get_mut(*i) {
|
||||
|
||||
@@ -455,7 +455,7 @@ pub(crate) fn project(f: &mt::Face, p: Vec3, hint: Option<(f64, f64)>) -> Option
|
||||
}
|
||||
|
||||
/// Whether `uv` lies within the parameters of the face's surface (unbounded directions pass).
|
||||
fn in_range(f: &mt::Face, uv: (f64, f64)) -> bool {
|
||||
pub(crate) fn in_range(f: &mt::Face, uv: (f64, f64)) -> bool {
|
||||
use mt::ParametricSurface;
|
||||
use std::ops::Bound;
|
||||
let inside = |range: (Bound<f64>, Bound<f64>), x: f64| {
|
||||
|
||||
@@ -393,6 +393,19 @@ fn fillet_the_three_edges_at_the_arcs_front_corner() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #1263: check the unfilleted input separately from the three-edge result.
|
||||
/// The circular wall's tessellation differs from its analytic volume by at most
|
||||
/// its arc length times the chord tolerance times the height.
|
||||
#[test]
|
||||
fn unfilleted_knife_edge_input_has_a_closed_display_plan() {
|
||||
let want = HEIGHT * ((TIP_X - R_WALL) * R_WALL + R_WALL * R_WALL * (1.0 - PI / 4.0));
|
||||
for (name, body) in [("plate first", part()), ("corner first", part_other_order()), ("cut from a box", part_cut_from_a_box())] {
|
||||
let volume = planned_volume(&body);
|
||||
let tolerance = 0.5 * PI * R_WALL * 2e-4 * HEIGHT;
|
||||
assert!((volume - want).abs() < tolerance, "{name}: volume {volume}, expected {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The construction does not depend on the faces being curved: at a box's corner it gives the
|
||||
/// body the planar corner builder does (a sphere's octant where the three rounds meet).
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user