From 42eaf5e5f220be22345a1415cef16c2adc52ec20 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Sat, 7 Nov 2026 09:41:05 -0500 Subject: [PATCH] fillet: rounds whose rails only run on past their edges' ends do not cross (#2845) The crossing check read the rails before they are trimmed. A rail runs on past its edge's ends, and the distance to the other round's edge clamps at that edge's end. So two rounds on a stepped face, a wall's edge and a ledge's that share no vertex, were refused with 1001 at R2.5 and R3, though each builds alone. A rail point now counts only when it lies on the shared face and across the other round's strip, not beside its end. Both edges at R2.5 and R3 give the closed form 9520 - (1 - pi/4) r^2 * 52, on every fix and on the default kernel; the case 12342-round-wall-and-ledge-edges-r2-5 passes and leaves known_failing. #2906's second design, the same loft cut from a pocket floor beside a hole, takes the same closed forms; its operand and test are added. Part of #2845. Part of #2906. Co-Authored-By: Claude Opus 5.5 --- .../data/issue_2906_loft_cut_beside_hole.smb | Bin 0 -> 34376 bytes .../tests/issue_2845_stepped_wall.rs | 84 ++++++++++++++++++ .../tests/issue_2906_loft_cut_rim.rs | 64 +++++++++++-- .../session/crossing_rails.rs | 52 ++++++++--- .../12342-round-wall-and-ledge-edges-r2-5.jpg | Bin 25003 -> 10381 bytes ...12342-round-wall-and-ledge-edges-r2-5.json | 16 ++-- .../tests/kernel_cases/known_failing.json | 5 -- 7 files changed, 196 insertions(+), 25 deletions(-) create mode 100644 crates/solidcore-ops/tests/data/issue_2906_loft_cut_beside_hole.smb create mode 100644 crates/solidcore-ops/tests/issue_2845_stepped_wall.rs diff --git a/crates/solidcore-ops/tests/data/issue_2906_loft_cut_beside_hole.smb b/crates/solidcore-ops/tests/data/issue_2906_loft_cut_beside_hole.smb new file mode 100644 index 0000000000000000000000000000000000000000..13c183c0f33dc175eff605ae403cdd1a4c8731a3 GIT binary patch literal 34376 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 Body { + let wall = make_box([16.0, -20.0, -40.0], [4.0, 40.0, 42.0]).unwrap(); + let base = make_box([16.0, -20.0, -50.0], [7.0, 40.0, 10.0]).unwrap(); + boolean(wall, base, BooleanOp::Union).unwrap().unwrap() +} + +/// The two rounds are apart, each between faces square to it: each takes (1 - pi/4) r^2 per +/// length, over 42 + 10 mm. +fn closed_form(r: f64) -> f64 { + 9520.0 - (1.0 - std::f64::consts::FRAC_PI_4) * r * r * 52.0 +} + +#[test] +fn both_rounds_that_fit_build_their_closed_form() { + all_fixes(); + for r in [2.5, 3.0] { + let b = stepped_wall(); + let edges = [edge_at(&b, WALL), edge_at(&b, LEDGE)]; + blend(&b, Blend::Round, &edges, &[r], &[], Corners::Filled).unwrap(); + assert!(b.check(CheckLevel::All).unwrap().is_valid(), "r {r}"); + let v = volume(&b); + assert!((v - closed_form(r)).abs() <= 1e-9 * v, "r {r}: {v}"); + } +} + +/// Past the wall's 4 mm the ball rolls on the face x = 20 and on the wall's far edge (x = 16, +/// y = 20): the arc runs from that edge to its rail, and the section it takes off the wall is +/// the strip less the disc's part under the rail. With every fix on: SolidCraft's set fails here +/// with a bare 1001 (the case `12342-round-wall-edge-r5-past-the-wall-refused`). +fn past_the_wall(r: f64) -> f64 { + let near = r - 4.0; + let height = (r * r - near * near).sqrt(); + let under = |u: f64| u / 2.0 * (r * r - u * u).max(0.0).sqrt() + r * r / 2.0 * (u / r).asin(); + 9520.0 - 42.0 * (4.0 * height - (under(r) - under(near))) +} + +#[test] +fn the_wall_alone_rides_its_far_edge_past_its_width() { + all_fixes(); + for r in [4.5, 5.0, 6.0] { + let b = stepped_wall(); + let e = edge_at(&b, WALL); + blend(&b, Blend::Round, &[e], &[r], &[], Corners::Filled).unwrap(); + assert!(b.check(CheckLevel::All).unwrap().is_valid(), "r {r}"); + let v = volume(&b); + assert!((v - past_the_wall(r)).abs() <= 1e-9 * v, "r {r}: {v}"); + } +} + +#[test] +fn the_same_on_the_default_kernel() { + for r in [2.5, 3.0] { + let body = stepped_wall(); + let edges = [edge_at(&body, WALL), edge_at(&body, LEDGE)]; + let (_, mut b) = kernel_copy(&body); + let ids = edges.map(|i| b.edge_list()[i]); + fillet_edges(&mut Kernel::default(), &mut b, &ids, r, 1, 1, 1.0).unwrap(); + let v = volume(&built(|_| Ok(b))); + assert!((v - closed_form(r)).abs() <= 1e-9 * v, "r {r}: {v}"); + } +} diff --git a/crates/solidcore-ops/tests/issue_2906_loft_cut_rim.rs b/crates/solidcore-ops/tests/issue_2906_loft_cut_rim.rs index d26204cf43..283947671d 100644 --- a/crates/solidcore-ops/tests/issue_2906_loft_cut_rim.rs +++ b/crates/solidcore-ops/tests/issue_2906_loft_cut_rim.rs @@ -27,12 +27,9 @@ fn slab_less_loft() -> f64 { 900.0 * 480.0 * 160.0 - 4.0 / 6.0 * (top + 4.0 * middle + bottom) } -/// SolidCraft's set: every fix but two. +/// SolidCraft's set: every fix but one. fn fixes() { - let off = [ - "fillet_refuse_rounds_breaking_through", - "fillet_round_ends_on_outline", - ]; + let off = ["fillet_round_ends_on_outline"]; let on = solidcore::kernel::UpstreamFixes::NAMES .iter() .copied() @@ -57,6 +54,15 @@ fn blended(kind: Blend, p: [f64; 3]) -> f64 { volume(&b) } +/// SolidCraft's `loft_cut_beside_round_hole`: the same cut from a pocket's floor, a hole beside it. +fn beside_a_hole() -> Body { + Body::from_smb( + include_bytes!("data/issue_2906_loft_cut_beside_hole.smb").to_vec(), + false, + ) + .unwrap() +} + /// The operand's volume to 1e-8, so the closed forms are held to 2e-10 of the body (0.014 mm3). const TOL: f64 = 2e-10; @@ -100,3 +106,51 @@ fn the_rest_of_the_rim_builds() { } } } + +/// The same floor edges from a pocket's floor: the same closed forms on the operand's volume. +#[test] +fn the_cut_from_a_pocket_floor_takes_the_same_closed_forms() { + fixes(); + let v = volume(&beside_a_hole()); + let phi = (4.0_f64 / 9.5).atan(); + let round = 80.0 * 64.0 * ((phi / 2.0).tan() - phi / 2.0); + let chamfer = 80.0 * 32.0 * phi.sin(); + for y in [-61.16, -141.16] { + for (kind, added) in [(Blend::Round, round), (Blend::Chamfer, chamfer)] { + let b = beside_a_hole(); + let e = edge_at(&b, [450.0, y, 726.0]); + blend(&b, kind, &[e], &[8.0], &[], Corners::Filled).unwrap(); + assert!(b.check(CheckLevel::All).unwrap().is_valid(), "{y}"); + let made = volume(&b); + assert!((made - (v + added)).abs() <= TOL * v, "{made}"); + } + } + for p in [[290.83, -56.41, 728.0], [609.17, -145.91, 728.0]] { + let b = beside_a_hole(); + let e = edge_at(&b, p); + blend(&b, Blend::Round, &[e], &[8.0], &[], Corners::Filled).unwrap(); + assert!(b.check(CheckLevel::All).unwrap().is_valid(), "{p:?}"); + } +} + +/// Writes the operand and each blended body for the PR's pictures, when asked. +#[test] +#[ignore = "writes the bodies for the PR's pictures"] +fn bodies_for_the_pictures() { + fixes(); + let dir = std::env::var("ISSUE_2906_OUT").unwrap(); + let write = |name: &str, b: &Body| { + std::fs::write(format!("{dir}/{name}.smb"), b.to_smb(false).unwrap()).unwrap(); + }; + write("before", &operand()); + for (name, kind, p) in [ + ("round", Blend::Round, [450.0, 418.84, 156.0]), + ("chamfer", Blend::Chamfer, [450.0, 418.84, 156.0]), + ("side", Blend::Round, [290.83, 423.59, 158.0]), + ] { + let b = operand(); + let e = edge_at(&b, p); + blend(&b, kind, &[e], &[8.0], &[], Corners::Filled).unwrap(); + write(name, &b); + } +} diff --git a/crates/solidcore/src/blend/fillet_executive/session/crossing_rails.rs b/crates/solidcore/src/blend/fillet_executive/session/crossing_rails.rs index 13f3393d0b..925b999e55 100644 --- a/crates/solidcore/src/blend/fillet_executive/session/crossing_rails.rs +++ b/crates/solidcore/src/blend/fillet_executive/session/crossing_rails.rs @@ -5,7 +5,10 @@ //! face with no room between their rails. `SmFilletExecutive::DoFilleting` builds //! the open body anyway. After `CreateFilletBrep` and before the target is edited, //! each rail of a round is compared with the strip the other round takes from the -//! same face: a rail lying inside that strip means the rounds do not fit. +//! same face: a rail lying inside that strip means the rounds do not fit. Only what is on +//! the face and across the strip counts: the rails are not trimmed yet and run on past their +//! edges' ends, so a sample off the face, or beside the other edge's end, is no crossing +//! (#2845: a wall's edge and a ledge's on a stepped face, whose rounds never meet). //! //! Deviations: not upstream. Rail positions are sampled, and the distance to the other //! round's edge is a 3D distance (exact on planar faces, a lower bound on curved ones @@ -13,18 +16,22 @@ //! solution (`SmEdge::ClosestPoint` finding none, #2050) measures nothing: a rail point //! without one is not inside the other round's strip, and gives its own round no width; //! a rail with no width at all takes no strip it can be tested against. -// ponytail: sampled 3D distances, no face-interior test; a C-shaped face whose two -// edges face each other across its notch would be refused though they fit. Upgrade to -// the surface distance within the face if that shows up. +// ponytail: sampled 3D distances; a C-shaped face whose two edges face each other across +// its notch would be refused though they fit. Upgrade to the surface distance within the +// face if that shows up. use super::{FilletBody, FilletObject, FilletSession}; use crate::error::{EntityRef, Result, SolidError}; +use crate::geom::tol::zone_tol3d; +use crate::intersect::point_class::{PointClassType, PointClassification}; use crate::kernel::Kernel; use crate::math::Point3; use crate::query::edge::closest_point; +use crate::query::face::p1::{point_3d_classify, Point3dClassifyOptions}; use crate::topology::edge; use crate::topology::edgeuse::p1 as eu1; -use crate::topology::{EdgeId, FaceId}; +use crate::topology::face::p2 as fp2; +use crate::topology::{Brep, EdgeId, FaceId}; const OP: &str = "blend::fillet_executive::session::crossing_rails"; /// Rail points compared per rail. @@ -47,18 +54,26 @@ impl FilletSession<'_> { pub(super) fn refuse_crossing_rails(&mut self, k: &Kernel) -> Result<()> { let rails = self.collect_rails(k)?; let tolerance = self.objects.target.tolerance(); - let target = &*self.objects.target; for a in &rails { for b in &rails { - if a.face != b.face || a.edge == b.edge || shares_vertex(target, a.edge, b.edge)? { + if a.face != b.face + || a.edge == b.edge + || shares_vertex(self.objects.target, a.edge, b.edge)? + { continue; } + let along = edge::interval(self.objects.target, b.edge)?; for &p in &a.points { // No closest point on the other round's edge: no crossing found here. - let Some(closest) = closest_point(k, target, b.edge, p)? else { + let Some(closest) = closest_point(k, self.objects.target, b.edge, p)? else { continue; }; - if closest.distance + tolerance < b.width { + // At an end of the edge, the point is beside the strip, not across it. + let across = closest.parameter > along.min() && closest.parameter < along.max(); + if across + && closest.distance + tolerance < b.width + && on_face(k, self.objects.target, a.face, p)? + { self.executive.error.code = 4; // SM_FILERR_EDGE_PROBLEM self.executive.error.status = 2; // SM_FIL_FAILURE self.executive.error.edge = Some(FilletObject::Topology( @@ -139,7 +154,24 @@ impl FilletSession<'_> { } } -fn shares_vertex(target: &crate::topology::Brep, a: EdgeId, b: EdgeId) -> Result { +/// Whether `p` lies on `face`, within the zone of the face's tolerance. +fn on_face(k: &Kernel, target: &mut Brep, face: FaceId, p: Point3) -> Result { + let zone = zone_tol3d(k, fp2::tolerance(target, face)?); + let mut on = PointClassification::::new(zone); + point_3d_classify( + k, + target, + face, + p, + zone, + true, + &mut on, + Point3dClassifyOptions::default(), + )?; + Ok(on.point_class() != PointClassType::Unknown) +} + +fn shares_vertex(target: &Brep, a: EdgeId, b: EdgeId) -> Result { let (a0, a1) = edge::start_end_vertices(target, a)?; let (b0, b1) = edge::start_end_vertices(target, b)?; Ok([a0, a1] diff --git a/crates/solidcore/tests/kernel_cases/fillets/12342-round-wall-and-ledge-edges-r2-5.jpg b/crates/solidcore/tests/kernel_cases/fillets/12342-round-wall-and-ledge-edges-r2-5.jpg index 6cb7d5a8641f41205f1418482c762a690c41c942..3685682a5b93567cb64961e0fd7fa3ae034d39a9 100644 GIT binary patch literal 10381 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 literal 25003 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 E10R~QiU0rr diff --git a/crates/solidcore/tests/kernel_cases/fillets/12342-round-wall-and-ledge-edges-r2-5.json b/crates/solidcore/tests/kernel_cases/fillets/12342-round-wall-and-ledge-edges-r2-5.json index 4dc3d534de..be7eedab13 100644 --- a/crates/solidcore/tests/kernel_cases/fillets/12342-round-wall-and-ledge-edges-r2-5.json +++ b/crates/solidcore/tests/kernel_cases/fillets/12342-round-wall-and-ledge-edges-r2-5.json @@ -8,11 +8,13 @@ }, "calls": [ { - "error": "solidcore failed (status 1001)", "inputs": [ "b1" ], "op": "blend_dropping", + "outputs": [ + "b2" + ], "params": { "edges": { "kind": "edge", @@ -84,18 +86,22 @@ "rel": 1e-09 }, "volume_mm3": 9450.25440310417, - "what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid; it fails with status 1001 (R3 the same), while each edge alone builds" + "what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid (R3 the same), as each edge alone builds" }, "expressible": true, - "gallery_note": "the back face y = 20 and the ledge top highlighted: R2.5 fits each edge alone, but the two together fail with status 1001", + "gallery_note": "R2.5 fits each edge alone and the two together: the rounds end apart on the stepped back face", "operation_expressible": true, "records": "12342-round-wall-and-ledge-edges-r2-5.bodies.json.gz", - "result": [], + "result": [ + { + "made": "b2" + } + ], "sources": [ "tangency-labs/solidcraft#12342", "tangency-labs/solidcore#2845" ], "ungraded": [], - "what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid; it fails with status 1001 (R3 the same), while each edge alone builds", + "what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid (R3 the same), as each edge alone builds", "why_not": [] } diff --git a/crates/solidcore/tests/kernel_cases/known_failing.json b/crates/solidcore/tests/kernel_cases/known_failing.json index e7f868c872..c1bd932c6f 100644 --- a/crates/solidcore/tests/kernel_cases/known_failing.json +++ b/crates/solidcore/tests/kernel_cases/known_failing.json @@ -4,11 +4,6 @@ "issue": "solidcore#2844", "reason": "the round of the loft's circular rim is not a closed solid" }, - { - "case": "fillets/12342-round-wall-and-ledge-edges-r2-5", - "issue": "solidcore#2845", - "reason": "status 1001 instead of a closed solid" - }, { "case": "fillets/12342-round-wall-and-ledge-edges-r5-refused", "issue": "solidcore#2845",