mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
kernel, kernel-cases: a convex blend overflowing a cliff is built, with its analytic answer
SMLib now builds a round or chamfer whose rail runs off its face across a cliff (an opening's rim, the body's outline): the blend is made as if its faces continued and what it removes is subtracted, so it never bridges an opening (owner decision for rig 5158/5181; fork commit "SMLib, capi: a convex blend overflowing a cliff is subtracted from the body"). The tests that expected a refusal for such blends now expect the body, each answer from the geometry: - variable_radius_on_a_box_edge: 2 -> 50 on the 10x20x30 box's top edge builds at 3546.18767939 (circular sections in y = const, the r x r square less the quarter disc, clipped to the box; rel 1e-4). - variable_radius_from_a_spline: the "big" law builds at 6000 - 726.5334566720 (de Boor law, split at the knot and where r crosses 10). - a_chamfer_meets_sloped_end_planes: chamfer 25 removes 11500 - 4625 * slope (integral of 40 - 2 * slope * y over z >= y - 5). - g2_fillet_refuses_invalid_geometry: G2 at 25 builds at 6138.1606889 (the quintic section with control points at thirds, clipped to 20x20, along 20). - fillet_max_each_reduces_only_where_the_radius_does_not_fit: both edges take 4, 24438.7317761. - hostile_inputs_are_errors: chamfer 30 on the cube builds at 1000 (y + z <= 10 along 20); R50 still fails. - kernel-cases fillets/1080: the 9 mm chamfer into the openings builds at 3350 (method in expected.json). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
This commit is contained in:
@@ -610,7 +610,14 @@ mod tests {
|
||||
assert!(fillet_g2(&body, &[], 2.0).is_err());
|
||||
assert!(fillet_g2(&body, &[Vec3::new(f64::NAN, 0.0, 0.0)], 2.0).is_err());
|
||||
assert!(fillet_g2(&body, &[Vec3::new(1000.0, 0.0, 0.0)], 2.0).is_err());
|
||||
assert!(fillet_g2(&body, &edge, 25.0).is_err());
|
||||
// Wider than the faces (20), over their far edges, cliffs: cut as if they went on. The
|
||||
// section is the quintic from 25 out on each face, its inner control points a third and
|
||||
// two thirds of the way to the corner; it takes the region between it and the corner
|
||||
// inside the 20 x 20 section (clipped polygon of the curve, 2e5 pieces), along 20.
|
||||
let wide = fillet_g2(&body, &edge, 25.0).unwrap();
|
||||
let v = crate::measure(&wide).unwrap().volume;
|
||||
assert!((v - 6138.1606889014).abs() < 1e-6 * 6138.1606889014, "{v}");
|
||||
assert!(wide.validity().is_empty());
|
||||
}
|
||||
|
||||
/// A box with a cylinder joined across one side (#556): the bottom edge of that side runs
|
||||
@@ -813,7 +820,15 @@ mod tests {
|
||||
for point in [Vec3::new(slope * 2.0, 2.0, 20.0), Vec3::new(40.0 - slope * 2.0, 2.0, 20.0)] {
|
||||
assert!(has_vertex(&result, point, 1e-8));
|
||||
}
|
||||
assert!(chamfer(&body, &pick, 25.0).is_err());
|
||||
// Past the faces (20 deep, 20 high), over their far edges, cliffs: cut as if the
|
||||
// faces and the sloped end planes went on. The plane 25 back on each face takes, of
|
||||
// the section, z >= y - 5 (y, z in [0, 20]), each line of it 40 - 2 slope y long
|
||||
// between the end planes: 11500 - 4625 slope.
|
||||
let past = chamfer(&body, &pick, 25.0).unwrap();
|
||||
let removed = 11500.0 - 4625.0 * slope;
|
||||
let actual = crate::measure(&body).unwrap().volume - crate::measure(&past).unwrap().volume;
|
||||
assert!((actual - removed).abs() < 1e-6 * removed, "{actual} vs {removed}");
|
||||
assert!(past.validity().is_empty());
|
||||
// A fillet ends in each sloped end plane along an ellipse: the removed section's
|
||||
// length is taken at its centroid, c from the edge.
|
||||
let rounded = fillet(&body, &pick, 2.0).unwrap();
|
||||
|
||||
@@ -557,8 +557,17 @@ mod tests {
|
||||
let g = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 3.0, 3.0).unwrap();
|
||||
let k = crate::fillet(&b, &[Vec3::new(10.0, 10.0, 30.0)], 3.0).unwrap();
|
||||
assert!(rel(measure(&g).unwrap().volume, measure(&k).unwrap().volume) < 1e-6);
|
||||
// Too big for the faces: refused.
|
||||
assert!(fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 50.0).is_err());
|
||||
// Wider than the faces: past r = 10 the contact on the top leaves it across its far edge
|
||||
// x = 0 (past r = 30 the one on the side across its bottom), cliffs. The round is built as
|
||||
// if the faces went on, and what it removes is cut from the box: at each y the section of
|
||||
// the r x r square at the edge less the quarter disc about (10 - r, 30 - r), inside
|
||||
// [0, 10] x [0, 30], in closed form (a, b from the disc's centre: a, b in [0, r],
|
||||
// a^2 + b^2 >= r^2, a >= r - 10, b >= r - 30), integrated along y by Gauss-Legendre
|
||||
// between the kinks at r = 10 and 30: 2453.81232061 removed, 3546.18767939 left (to 1e-4,
|
||||
// as the variable rounds above).
|
||||
let w = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 50.0).unwrap();
|
||||
let vw = measure(&w).unwrap().volume;
|
||||
assert!(rel(vw, 3546.1876793935) < 1e-4, "{vw} vs 3546.1876793935");
|
||||
}
|
||||
|
||||
/// A radius through a middle point: straight pieces (exact: each piece removes
|
||||
@@ -704,9 +713,15 @@ mod tests {
|
||||
assert!(fillet_variable_spline(&b, at, start, &unclamped).is_err());
|
||||
let dips = RadiusSpline { radii: vec![2.0, 0.0, 0.0, 3.0, 2.0], ..law.clone() };
|
||||
assert!(fillet_variable_spline(&b, at, start, &dips).is_err());
|
||||
// A middle radius beyond the faces: too large.
|
||||
// A middle radius beyond the faces (up to 21.25 on the top 10 wide): the contact on the top
|
||||
// leaves it across its far edge, a cliff, and the round is cut as if the top went on: at
|
||||
// each y the r x r square at the edge less the quarter disc about (10 - r, 30 - r), inside
|
||||
// [0, 10] x [0, 30], in closed form, integrated along y (Gauss-Legendre, split at the
|
||||
// knot and where r crosses 10): 726.5334566720 removed.
|
||||
let big = RadiusSpline { radii: vec![2.0, 2.0, 40.0, 3.0, 2.0], ..law };
|
||||
assert!(matches!(fillet_variable_spline(&b, at, start, &big), Err(KernelError::TooLarge(_))));
|
||||
let wide = fillet_variable_spline(&b, at, start, &big).unwrap();
|
||||
let vw = fine_volume(&wide);
|
||||
assert!(rel(vw, 6000.0 - 726.5334566720) < 1e-4, "{vw} vs {}", 6000.0 - 726.5334566720);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -396,16 +396,19 @@ fn fillet_max_each_reduces_only_where_the_radius_does_not_fit() {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let edges = [Vec3::new(21.0, 15.0, 30.0), Vec3::new(40.0, 0.0, 10.0)];
|
||||
assert!(matches!(fillet(&body, &edges, 4.0), Err(KernelError::TooLarge(_))));
|
||||
let (one, _) = fillet_max(&body, &edges, 4.0).unwrap();
|
||||
// R4 on the fin's top edge runs over the fin's 2 mm top, past its far edge (a cliff): it is
|
||||
// cut as if the top went on, so both edges take 4. The fin's section loses the 4 x 4 square
|
||||
// at its edge less the quarter disc about (17, 26), inside the fin (x >= 19): 2 x 4 less the
|
||||
// disc's part, in closed form; the corner's round (1 − π/4)·16 along 20.
|
||||
let both = fillet(&body, &edges, 4.0).unwrap();
|
||||
let (radii, b) = fillet_max_each(&body, &edges, 4.0).unwrap();
|
||||
assert_eq!(radii.get(1), Some(&4.0), "{radii:?}");
|
||||
let fin_r = radii.first().copied().unwrap();
|
||||
assert!(fin_r < 4.0 && (fin_r - one).abs() < 1e-9, "{radii:?}, one radius {one}");
|
||||
assert_eq!(radii, vec![4.0, 4.0], "{radii:?}");
|
||||
assert!(b.validity().is_empty(), "{:?}", b.validity());
|
||||
// Each round takes (1 − π/4)·r² along its edge: the fin's 30 long, the corner's 20.
|
||||
let removed = (1.0 - PI / 4.0) * (fin_r * fin_r * 30.0 + 16.0 * 20.0);
|
||||
assert!(rel(measure(&b).unwrap().volume, block + fin - removed) < 2e-4);
|
||||
let disc = |a: f64| 0.5 * (a * (16.0 - a * a).max(0.0).sqrt() + 16.0 * (a / 4.0).asin());
|
||||
let fin_section = 2.0 * 4.0 - (disc(4.0) - disc(2.0));
|
||||
let removed = fin_section * 30.0 + (1.0 - PI / 4.0) * 16.0 * 20.0;
|
||||
assert!(rel(measure(&both).unwrap().volume, block + fin - removed) < 1e-6, "{}", measure(&both).unwrap().volume);
|
||||
assert!(rel(measure(&b).unwrap().volume, block + fin - removed) < 1e-6);
|
||||
// The post's four edges limit each other: one radius, as `fillet_max`.
|
||||
let post = box_solid(Vec3::new(14.0, 0.0, 0.0), Vec3::new(28.0, 6.0, 10.0)).unwrap();
|
||||
let mids = [Vec3::new(14.0, 0.0, 5.0), Vec3::new(28.0, 0.0, 5.0), Vec3::new(14.0, 6.0, 5.0), Vec3::new(28.0, 6.0, 5.0)];
|
||||
|
||||
@@ -207,12 +207,15 @@ fn hostile_inputs_are_errors() {
|
||||
assert_eq!(c.shell(&[[10.0, 10.0, 20.0]], 0.1, t).unwrap_err(), Error::Argument, "thickness {t}");
|
||||
}
|
||||
// Sizes SMLib cannot build fail as errors, whatever SMLib says.
|
||||
for (what, r) in [("radius larger than the cube", c.fillet(&mid, 0.1, 50.0)), ("chamfer past the faces", c.chamfer(&mid, 0.1, 30.0))] {
|
||||
match r {
|
||||
Ok(b) => panic!("{what}: SMLib built a body of volume {:?}, counts {:?}", b.volume(ACC), b.counts()),
|
||||
Err(e) => eprintln!("{what}: {e:?}"),
|
||||
}
|
||||
match c.fillet(&mid, 0.1, 50.0) {
|
||||
Ok(b) => panic!("radius larger than the cube: SMLib built a body of volume {:?}, counts {:?}", b.volume(ACC), b.counts()),
|
||||
Err(e) => eprintln!("radius larger than the cube: {e:?}"),
|
||||
}
|
||||
// A chamfer past the faces runs over their far edges, cliffs: it is cut as if the faces went
|
||||
// on (the plane y + z = 10 through the setbacks 30 back from the edge y = z = 20), leaving the
|
||||
// triangle y + z <= 10 of the cube's section along x: 50 x 20.
|
||||
let past = c.chamfer(&mid, 0.1, 30.0).unwrap();
|
||||
assert!((volume(&past) - 1000.0).abs() < 1e-6 * 1000.0, "{}", volume(&past));
|
||||
// A wall with no room inside it never reaches SMLib's offset.
|
||||
assert_eq!(c.shell(&[[10.0, 10.0, 20.0]], 0.1, -15.0).unwrap_err(), Error::NoRoom);
|
||||
assert_eq!(c.offset_faces(&[[10.0, 10.0, 20.0]], 0.1, -20.0).unwrap_err(), Error::NoRoom);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
"tangency-labs/solidcraft#1080",
|
||||
"tangency-labs/solidcraft#1124"
|
||||
],
|
||||
"what": "a 9 mm chamfer on the top front edge of a cube with square openings 5 mm in must be refused: it would cut through the rims of the openings",
|
||||
"source": "invariants",
|
||||
"method": "cube 20 with three orthogonal 10x10 through openings; the top face opening starts 5 mm from the front edge and the front opening 5 mm below the top edge, so a 9 mm setback runs past the face between the edge and the openings and the chamfer plane would cross the openings and leave slivers. The right answer is a clean refusal; nothing is built.",
|
||||
"outcome": "refused"
|
||||
}
|
||||
"what": "a 9 mm chamfer on the top front edge of a cube with square openings 5 mm in runs past the 5 mm between the edge and the openings: built as if the faces went on over the openings, then cut by them (cliff overflow); it never bridges an opening",
|
||||
"source": "analytic",
|
||||
"method": "cube 20 less three orthogonal 10x10 through openings (cross): V0 = 4000. The chamfer plane takes, of the section, the triangle y + (20 - z) <= 9 (y >= 0, z <= 20), area 40.5, along x: 810; less its parts inside the openings: the x opening (y, z in [5, 15]) misses it (z >= 11 + y >= 16 there), the y opening (z in [5, 15], x in [5, 15]) holds y in [0, 4], z in [11 + y, 15]: area 8, times 10 = 80, the z opening (y in [5, 15], x in [5, 15]) holds y in [5, 9], z in [11 + y, 20]: area 8, times 10 = 80. V = 4000 - (810 - 160) = 3350.",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 3350.0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user