From 3b4adeb73b317ae3dae9db756f71aa975900d372 Mon Sep 17 00:00:00 2001 From: brett19 Date: Sun, 1 Nov 2026 18:32:30 -0700 Subject: [PATCH] solidcore: a variable round is built again, as the union of its balls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With #2262 and fillet_gn_section_generator on, solidcore builds a variable round as the union of its balls, so fillet_variable's refusal from sync-2 (9f64cce54, solidcore#2250) comes off. Laws with a corner stay refused (solidcore#430); a law whose balls fold is refused by solidcore (NoRoomReason::BallsFold) and arrives as Error::NoRoom. Against closed forms, solidcore and SMLib d01d7b0 agreeing to 1e-11: - 1 to 3 along 30 mm removes 27.858907 against the balls' union's 27.857434 (great circles 27.898239); a law of one radius 2 removes 25.753574 against (1 - π/4) 4 · 30 = 25.752220: both 5.3e-5 over, the solver's surface fit, as on the C++ kernel. - the rig's 1 to 5 cone: 11956.21991 against 11956.22243 (2.1e-7). - kernel tests: a_linear_radius_runs_from_the_start_end (18.5378), variable_radius_on_a_box_edge, down_to_zero (22.5949), taper_follows_the_room (the wedge 5245.68 against the balls' union) pass. The refused test becomes a_variable_round_is_the_union_of_its_balls (the two closed forms, either direction, a folding law refused, a pick miss named first). the_wedge_round_keeps_its_volume held 5223.0386, measured before the fork's balls' union; it now takes its closed form from removed_by_balls. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz --- crates/kernel/src/taper_mesh_tests.rs | 17 ++++++++---- crates/smlib/src/core/blend.rs | 29 ++++++++++----------- crates/smlib/src/core/blend_tests.rs | 37 +++++++++++++++------------ 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/crates/kernel/src/taper_mesh_tests.rs b/crates/kernel/src/taper_mesh_tests.rs index 410a4c3521..12287b7c86 100644 --- a/crates/kernel/src/taper_mesh_tests.rs +++ b/crates/kernel/src/taper_mesh_tests.rs @@ -1,9 +1,14 @@ use super::*; -fn wedge() -> Body { +/// A wedge 40 x 20, 15 high, its top front edge along x from 0 to 40. +fn wedge_body() -> Body { use solidcraft_geom::{Loop2, Plane, Region2, Vec2}; let outer = Loop2::polygon(&[Vec2::new(0., 0.), Vec2::new(40., 0.), Vec2::new(0., 20.)]); - let body = crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap(); + crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap() +} + +fn wedge() -> Body { + let body = wedge_body(); let at = Vec3::new(20., 0., 15.); // The room's taper, closing to zero at the wedge's tip, given as a spline fillet: R16 itself // is built over the wedge's cliffs (the blend-overflow decision), so a tapered fillet no @@ -12,12 +17,14 @@ fn wedge() -> Body { fillet_variable_spline(&body, at, t.start, &t.spline).unwrap() } -/// The wedge's variable round: V 5223.0386, on whichever kernel the test runs (the fork's variable -/// round follows the fillet between its traced points, 4438859, and solidcore's with it). +/// The wedge's variable round takes off the union of its balls (smlib#78, solidcore#2262): +/// `removed_by_balls` along the 40 mm edge, about 754.28, on whichever kernel the test runs. #[test] fn the_wedge_round_keeps_its_volume() { let v = crate::measure(&wedge()).unwrap().volume; - assert!((v - 5223.0386).abs() < 1e-3, "{v}"); + let law = taper(&wedge_body(), Vec3::new(20., 0., 15.), 16.).unwrap().spline; + let want = 6000.0 - super::tests::removed_by_balls(40.0, &|u| law.eval(u), &|_, _| f64::INFINITY, &[]); + assert!(((v - want) / want).abs() < 1e-4, "{v} vs {want}"); } /// The display mesh of a fillet tapering to zero at a wedge's tip converges to the B-rep: each diff --git a/crates/smlib/src/core/blend.rs b/crates/smlib/src/core/blend.rs index a6a497078c..a5294e35ea 100644 --- a/crates/smlib/src/core/blend.rs +++ b/crates/smlib/src/core/blend.rs @@ -660,14 +660,6 @@ fn untagged(mut b: Brep) -> Out { pub(crate) const CURVATURE_SECTION: &str = "a curvature-continuous fillet on solidcore: the curvature section's equal-area Gⁿ \ generator is not in solidcore yet (solidcore#2250, port in progress as #2260)"; -/// Why a variable round whose radius changes is refused ([`fillet_variable`]). The capi builds it -/// as the union of its balls (fork 6b346f6, 5db8feb, fb5c060); solidcore's variable round takes -/// the balls' great circles, a body every check passes but not that round, so it is not built in -/// its place. The refusal comes off when the solidcore pin carries #2262 (on #2260) and its -/// `fillet_gn_section_generator` flag is on. -pub(crate) const VARIABLE_ROUND: &str = "a variable-radius fillet on solidcore: a round whose radius changes is the union of its \ -balls, which solidcore does not build yet (solidcore#2250, port in progress as #2262)"; - /// [`blend_holes`] without its holes. #[allow(clippy::too_many_arguments)] // the capi's blend_call pub(crate) fn blend( @@ -1216,24 +1208,31 @@ pub(crate) fn fillet_variable( if law_pinches(degree, knots, radii) { return Err(Error::Unsupported); } - on_copy("smlib_fillet_variable", body, |k, w| { + on_copy("smlib_fillet_variable", body, |k, mut w| { let picked = match pick_edges(k, &w, &[point], tolerance)? { Ok((p, _)) => p, Err(m) => return Ok(Err(edge_miss(m))), }; let [e] = picked[..] else { return Ok(Err(Error::NoMatch { point: 0 })) }; - if law_along(k, &w, e, start, degree, knots, radii)?.is_none() { + let Some(law) = law_along(k, &w, e, start, degree, knots, radii)? else { return Ok(Err(Error::Status(INVALID_INPUT))); - } + }; // A law with a corner splits the round's face there (`SplitFilletFacesAtRadiusCorners`, // fork 6eecf04), which solidcore does not have. if law_has_corner(degree, knots, radii) { return Ok(Err(Error::Unsupported)); } - // The round is the union of its balls (smlib#78): each ball touches it along the circle - // shifted -r r'/|c'| from its great circle, which solidcore's variable round does not - // take yet: the great-circle round is not built in its place. - Ok(Err(Error::Unported(VARIABLE_ROUND))) + // The round is the union of its balls (smlib#78, solidcore#2262 with + // `fillet_gn_section_generator`): each ball touches it along the circle shifted + // -r r'/|c'| from its great circle. A law whose balls fold over one another is refused + // (`NoRoomReason::BallsFold`), [`Error::NoRoom`]. + let solid_in = brep_p2::is_manifold_solid(&w)?; + let (r0, r1) = (radii.first().copied().unwrap_or(0.0), radii.last().copied().unwrap_or(0.0)); + Ok(match blend_edges(k, &mut w, &[e], &[r0.max(r1)], Section::Round, Some(law), &[], None)? { + Ok(()) => finish(w, solid_in)?, + Err(Ok(_)) => Err(Error::Unsupported), + Err(Err(e)) => Err(e), + }) }) } diff --git a/crates/smlib/src/core/blend_tests.rs b/crates/smlib/src/core/blend_tests.rs index 2f79bf5664..b7abee6334 100644 --- a/crates/smlib/src/core/blend_tests.rs +++ b/crates/smlib/src/core/blend_tests.rs @@ -678,26 +678,31 @@ fn a_curvature_round_is_refused_until_solidcore_has_the_equal_area_section() { assert_eq!(blend(&b, Blend::Curvature, &[[90.0; 3]], &[2.0], TOL, &[], &[], Corners::Filled).err(), Some(Error::NoMatch { point: 0 })); } -/// A variable round whose radius changes is refused on solidcore: the round is the union of its -/// balls, and solidcore's takes their great circles (1 to 3 along 30 mm: 27.8982 off, not -/// 27.8574), until its pin has that round (solidcore#2250). A constant round is untouched. +/// A variable round is the union of its balls (solidcore#2262, `fillet_gn_section_generator`). +/// Along the 30 mm top front edge: a law of one radius 2 takes the constant round's +/// (1 - π/4) 4 · 30 = 25.752220; 1 to 3, the balls' union's 27.857434 (the great circles would take +/// 27.898239), either way along the edge. Both come out 5.3e-5 of that over, as on the C++ +/// kernel: the solver's surface fit. A law whose radius climbs faster than a rolling ball can +/// follow (1 to 4 within a mm) folds its balls over one another: refused, not built. #[test] -fn a_variable_round_is_refused_until_solidcore_has_the_balls_union() { +fn a_variable_round_is_the_union_of_its_balls() { let b = BLOCK.core(); - let linear = fillet_variable(&b, TOP_FRONT, TOL, [0.0, 0.0, 10.0], 1, &[0.0, 0.0, 1.0, 1.0], &[1.0, 3.0]).err(); - assert_eq!(linear, Some(Error::Unported(super::blend::VARIABLE_ROUND))); - assert!(linear.is_some_and(|e| e.to_string().contains("solidcore#2250"))); - let cubic = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]; - let bulge = fillet_variable(&b, TOP_FRONT, TOL, [0.0, 0.0, 10.0], 3, &cubic, &[1.0, 2.0, 2.0, 1.0]).err(); - assert_eq!(bulge, Some(Error::Unported(super::blend::VARIABLE_ROUND))); - // A law of one radius too: the variable round's own surface, not the constant round's. - let constant = fillet_variable(&b, TOP_FRONT, TOL, [0.0, 0.0, 10.0], 1, &[0.0, 0.0, 1.0, 1.0], &[2.0, 2.0]).err(); - assert_eq!(constant, Some(Error::Unported(super::blend::VARIABLE_ROUND))); + let removed = |radii: &[f64], start: [f64; 3]| { + let r = fillet_variable(&b, TOP_FRONT, TOL, start, 1, &[0.0, 0.0, 1.0, 1.0], radii).unwrap(); + let (rec, _) = native(&to_arrays(&r).unwrap()).unwrap(); + rec.validate().unwrap(); + ABC - volume(&r, 1e-8).unwrap() + }; + let constant = removed(&[2.0, 2.0], [0.0, 0.0, 10.0]); + assert!(rel(constant, (1.0 - PI / 4.0) * 4.0 * 30.0) < 1e-4, "{constant}"); + let linear = removed(&[1.0, 3.0], [0.0, 0.0, 10.0]); + assert!(rel(linear, 27.857434) < 1e-4, "{linear}"); + assert!(rel(removed(&[1.0, 3.0], [30.0, 0.0, 10.0]), linear) < 1e-8); + let steep = [0.0, 0.0, 0.0, 0.0, 0.02, 0.04, 1.0, 1.0, 1.0, 1.0]; + let folds = fillet_variable(&b, TOP_FRONT, TOL, [0.0, 0.0, 10.0], 3, &steep, &[1.0, 1.0, 4.0, 4.0, 4.0, 4.0]).err(); + assert_eq!(folds, Some(Error::NoRoom)); // A pick that misses is still named first. assert_eq!(fillet_variable(&b, [90.0; 3], TOL, [0.0; 3], 1, &[0.0, 0.0, 1.0, 1.0], &[1.0, 3.0]).err(), Some(Error::NoMatch { point: 0 })); - // A plain round is untouched. - let round = blend(&b, Blend::Round, &[TOP_FRONT], &[2.0], TOL, &[], &[], Corners::Filled).unwrap(); - assert!((ABC - volume(&round, 1e-8).unwrap() - (1.0 - PI / 4.0) * 4.0 * 30.0).abs() < 1e-6); } /// The capi's argument checks, before any kernel call.