mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-14 12:40:43 +00:00
40e95b706d
solidcore's Setback refuses a setback at which its fill is not certified convex (or, beside a curved face, no more concave than its faces), and it knows the least setback that is certified. It said so only in the refusal's comment. BlendRefusal::SetbackTooShort now carries a `why`, mirroring SetbackTooLong's: RailsCross (the existing refusal, a setback short of where the rails cross), ConvexFill and ConcaveFill. The two fill refusals are typed with the least setback where it lies within the corner's rooms. Where none fits they stay untyped, since there is no size to name. Comments, codes and fit_setback are unchanged.
SolidCraft maps the two fill reasons to smlib's new Error::SetbackTooShort { least, at, builds }:
- Where solidcore's fill is the corner (fillet_setback), the message names the smallest setback that builds, in the wording the fillet dialogs read (SETBACK_AT_LEAST).
- Where SolidCraft's G2 patch replaces that fill (fillet_g2_corners), it names only what solidcore's corner needs ("needs a setback of at least"). At that setback the patch is not shown to fit (tangency-labs/solidcraft#14427).
- RailsCross is not mapped: its setback passes that one test only.
Tests:
- solidcore: the 40 x 30 x 20 box rounded r3 at (40, 30, 20), set back 3.5, is refused with SetbackTooShort { ConvexFill, 3.5595 }, and set back 3.5595 it builds. The rails-cross refusal asserts RailsCross.
- SolidCraft: the plain path's 3.5 is refused naming the smallest that builds, 3.5595 mm, and that one builds a valid body. The G2 path names only the lower bound. The kernel case every-box-corner-three-edges-r3-curvature-setback-3-5 expects that text.
- fillet_setback_tests::a_setback_not_past_the_radius_is_refused_naming_the_least now passes, so its known-failing line goes.
Fixes tangency-labs/solidcraft#13913.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
5.5 KiB
Rust
96 lines
5.5 KiB
Rust
use solidcore_record::Vec3;
|
|
use solidcraft_geom::prelude::*;
|
|
|
|
use crate::*;
|
|
|
|
/// The 60 x 40 x 20 box with r5 rounds on the three edges at (60, 40, 20).
|
|
fn corner_case(setback: Option<f64>) -> Result<Body> {
|
|
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
|
|
let edges = [Vec3::new(30.0, 40.0, 20.0), Vec3::new(60.0, 20.0, 20.0), Vec3::new(60.0, 40.0, 10.0)];
|
|
match setback {
|
|
Some(s) => fillet_setback(&b, &edges, 5.0, &[(Vec3::new(60.0, 40.0, 20.0), s)]),
|
|
None => fillet(&b, &edges, 5.0),
|
|
}
|
|
}
|
|
|
|
/// Set back 8 along each edge: outside the corner cube [52, 60] x [32, 40] x [12, 20] the body
|
|
/// is the sphere corner's, and inside it the convex body holds the hull of its points on the
|
|
/// cube's faces (299.0201) and lies within the three rounds' prisms (512 - 225 (1 - pi/4) -
|
|
/// 125 (sqrt 2 - 1)); see capi/tests/ops_kernel.cpp, corner_setback. The patch is SMLib's two
|
|
/// quadrilaterals per side: 15 faces, 36 edges, 23 vertices; the box stays the box.
|
|
#[test]
|
|
fn a_corner_set_back_8_blends_through_a_larger_patch() {
|
|
let pi = std::f64::consts::PI;
|
|
let sphere = measure(&corner_case(None).unwrap()).unwrap().volume;
|
|
let out = corner_case(Some(8.0)).unwrap();
|
|
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
|
let m = measure(&out).unwrap();
|
|
let in_c_sphere = 512.0 - 225.0 * (1.0 - pi / 4.0) - 125.0 * (1.0 - pi / 6.0);
|
|
let prisms = 512.0 - 225.0 * (1.0 - pi / 4.0) - 125.0 * (2f64.sqrt() - 1.0);
|
|
let outside = sphere - in_c_sphere;
|
|
assert!(m.volume > outside + 299.0201 && m.volume < outside + prisms, "{} not in [{}, {}]", m.volume, outside + 299.0201, outside + prisms);
|
|
assert_eq!((m.faces, m.edges, m.vertices), (15, 36, 23));
|
|
assert!((m.bbox.min - Vec3::new(0.0, 0.0, 0.0)).len() < 1e-6 && (m.bbox.max - Vec3::new(60.0, 40.0, 20.0)).len() < 1e-6, "{:?}", m.bbox);
|
|
}
|
|
|
|
/// A setback point at no vertex, at a corner where no edge is rounded, or as long as an edge
|
|
/// there, is refused.
|
|
#[test]
|
|
fn a_setback_off_the_rounded_corner_is_refused() {
|
|
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
|
|
let edges = [Vec3::new(30.0, 40.0, 20.0), Vec3::new(60.0, 20.0, 20.0), Vec3::new(60.0, 40.0, 10.0)];
|
|
assert!(fillet_setback(&b, &edges, 5.0, &[(Vec3::new(30.0, 20.0, 10.0), 8.0)]).is_err());
|
|
assert!(fillet_setback(&b, &edges, 5.0, &[(Vec3::new(0.0, 0.0, 0.0), 8.0)]).is_err());
|
|
assert!(matches!(corner_case(Some(20.0)), Err(KernelError::TooLarge(_))));
|
|
}
|
|
|
|
/// A setback that does not reach past where the corner's rounds meet without one (on faces square
|
|
/// to each other, the radius) is refused before SMLib runs, naming that least setback. Past it, a
|
|
/// setback still too short for the corner fill (certified convex only from a setback on, as SMLib
|
|
/// f916a8d's setback corner does) is refused naming the smallest that builds,
|
|
/// at most twice the one asked for (the certificate scales the setbacks up to 2); that one builds.
|
|
#[test]
|
|
fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
|
|
for s in [2.0, 5.0] {
|
|
match corner_case(Some(s)) {
|
|
Err(KernelError::Invalid(why)) => assert!(why.contains("setback must be more than the radius (5 mm)"), "{why}"),
|
|
other => panic!("setback {s}: {:?}", other.map(|b| b.face_count())),
|
|
}
|
|
}
|
|
let least = match corner_case(Some(5.5)) {
|
|
Err(KernelError::Invalid(why)) => {
|
|
let named = why.split_once(SETBACK_AT_LEAST).and_then(|(_, rest)| rest.split_once(" mm")).and_then(|(n, _)| n.parse::<f64>().ok());
|
|
named.unwrap_or_else(|| panic!("no least setback named: {why}"))
|
|
}
|
|
other => panic!("setback 5.5: {:?}", other.map(|b| b.face_count())),
|
|
};
|
|
eprintln!("setback 5.5 refused: the smallest that builds is {least}");
|
|
assert!(least > 5.5 && least <= 11.0, "{least}");
|
|
let out = corner_case(Some(least)).unwrap();
|
|
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
|
// The least the refusal names, for a form to show before it runs; none at no vertex.
|
|
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
|
|
let least = crate::fillet_least_setback(&b, Vec3::new(60.0, 40.0, 20.0), 5.0).unwrap();
|
|
assert!(least.is_some_and(|l| (l - 5.0).abs() < 1e-9), "{least:?}");
|
|
assert_eq!(crate::fillet_least_setback(&b, Vec3::new(30.0, 20.0, 10.0), 5.0).unwrap(), None);
|
|
}
|
|
|
|
/// tangency-labs/solidcraft#13913: the 40 x 30 x 20 box's corner (40, 30, 20), its three edges
|
|
/// rounded r3, set back 3.5. The corner's fill (solidcore's own here, no patch replaces it) is
|
|
/// certified convex only from a setback of 3.5595 (solidcore's `SetbackTooShort`, from the fill's
|
|
/// convex test), so 3.5 is refused naming it as the smallest that builds; set back that far, it
|
|
/// builds, a valid body.
|
|
#[test]
|
|
fn a_setback_too_short_for_the_fill_is_refused_naming_the_one_that_builds() {
|
|
let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap();
|
|
let edges = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(40.0, 30.0, 10.0)];
|
|
let corner = Vec3::new(40.0, 30.0, 20.0);
|
|
let why = match fillet_setback(&b, &edges, 3.0, &[(corner, 3.5)]) {
|
|
Err(KernelError::Invalid(why)) => why,
|
|
other => panic!("setback 3.5: {:?}", other.map(|b| b.face_count())),
|
|
};
|
|
assert!(why.contains(&format!("{SETBACK_AT_LEAST}3.5595 mm")), "{why}");
|
|
let out = fillet_setback(&b, &edges, 3.0, &[(corner, 3.5595)]).unwrap();
|
|
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
|
}
|