Action matrix regressions of #1255: knuckle rounds, thin holes, a 45° flange hole, an axis after an edit (#1280)
* Action matrix regressions of #1255: knuckle rounds, thin holes, a 45° flange hole, an axis after an edit - Curve blends: past the end of a curved edge the run of sections along the end tangent wrapped round the ring of centres; it is shortened while it still crosses the end face (the hinge knuckle's cap rounds at 0.728 mm). - Boolean retry guard: each body's volume is measured finer than its smallest radius, and a volume out of bounds is measured again ten times finer before it refuses (a 0.133 mm drill measured -30272 mm³; a plastic part's coarse mesh was off by more than the slack). - Sheet metal: a drill cap's certificate allows the evaluation noise its fitted tilt shows (a hole square to a 45° flange). - Construction geometry on a named face uses the face found by name, not a second pick at its nearest point (an axis through a corner round after a parameter moved it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Retry guard: measure the target, tool and result at one tolerance Measured at different tolerances, the chords' error on the faces they share didn't cancel: a blind hole in a long D-profile bar measured 20 mm³ more than the bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Action matrix baseline from this branch's full run: 37 actions pass again, none fewer Blessed with cargo xtask action-matrix --from DIR --bless from the 16 shards of run 38673859774. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
464a6dfad4
commit
5c2fa3e9ca
7 files changed
+858
-174
No files matched your search
@@ -107,6 +107,19 @@ fn stray_point(from: &Mesh, onto: &Near, region: &Aabb3) -> Option<Vec3> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The smallest radius of a body's cylinders, spheres and tori (their tubes).
|
||||
fn smallest_radius(body: &Body) -> Option<f64> {
|
||||
use crate::surface_kind::{FaceSurface, face_surface};
|
||||
(0..body.face_count())
|
||||
.filter_map(|i| match face_surface(body, i)? {
|
||||
FaceSurface::Cylinder { radius, .. } | FaceSurface::Sphere { radius, .. } => Some(radius),
|
||||
FaceSurface::Torus { minor, .. } => Some(minor),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|r| r.is_finite() && *r > 0.0)
|
||||
.min_by(f64::total_cmp)
|
||||
}
|
||||
|
||||
fn bad(op: BoolOp, what: String) -> KernelError {
|
||||
let verb = match op {
|
||||
BoolOp::Union => "join",
|
||||
@@ -148,14 +161,36 @@ pub(crate) fn check_retry(a: &Body, b: &Body, op: BoolOp, result: Option<&Body>)
|
||||
let (Ok(ma), Ok(mb), Ok(mr)) = (a.tessellate_with(t, true), b.tessellate_with(t, true), r.tessellate_with(t, true)) else {
|
||||
return Err(bad(op, "the result could not be checked".into()));
|
||||
};
|
||||
let (va, vb, vr) = (ma.measure().volume, mb.measure().volume, mr.measure().volume);
|
||||
let slack = 2e-4 * (va.abs() + vb.abs()) + 1e-9;
|
||||
let volume_ok = vr.is_finite()
|
||||
&& match op {
|
||||
BoolOp::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
|
||||
BoolOp::Cut => vr <= va + slack && vr >= va - vb - slack,
|
||||
BoolOp::Intersect => true,
|
||||
};
|
||||
// A mesh as coarse as `t` on a round smaller than it has no inside to speak of (a through
|
||||
// hole 0.13 mm across in a 900 mm slab measured −30272 mm³, #1255): the volumes are measured
|
||||
// at a tenth of the bodies' smallest radius, where that is finer. All three at the same
|
||||
// tolerance, so the chords' error on the faces they share cancels. `scale` refines it all.
|
||||
let finest = [a, b, r].iter().filter_map(|x| smallest_radius(x)).fold(f64::INFINITY, f64::min);
|
||||
let fine = if finest * 0.1 < t { (finest * 0.1).max(t * 1e-2) } else { t };
|
||||
let volume = |body: &Body, mesh: &Mesh, scale: f64| -> Result<f64> {
|
||||
if fine == t && scale == 1.0 {
|
||||
return Ok(mesh.measure().volume);
|
||||
}
|
||||
body.tessellate_with(fine * scale, true).map(|m| m.measure().volume).map_err(|_| bad(op, "the result could not be checked".into()))
|
||||
};
|
||||
let judge = |scale: f64| -> Result<(bool, f64, f64, f64)> {
|
||||
let (va, vb, vr) = (volume(a, &ma, scale)?, volume(b, &mb, scale)?, volume(r, &mr, scale)?);
|
||||
let slack = 2e-4 * (va.abs() + vb.abs()) + 1e-9;
|
||||
let ok = vr.is_finite()
|
||||
&& match op {
|
||||
BoolOp::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
|
||||
BoolOp::Cut => vr <= va + slack && vr >= va - vb - slack,
|
||||
BoolOp::Intersect => true,
|
||||
};
|
||||
Ok((ok, va, vb, vr))
|
||||
};
|
||||
// A volume out of bounds is measured again ten times finer before it refuses the result:
|
||||
// the coarse meshes of a part with many rounds can be off by more than the slack (a 0.8 mm
|
||||
// hole through a plastic part's wall measured 6.8 mm³ more than the part, #1255).
|
||||
let (mut volume_ok, mut va, mut vb, mut vr) = judge(1.0)?;
|
||||
if !volume_ok {
|
||||
(volume_ok, va, vb, vr) = judge(0.1)?;
|
||||
}
|
||||
if !volume_ok {
|
||||
let what = match op {
|
||||
BoolOp::Union => format!("the joined body's volume would be {vr:.4} mm³ (the target is {va:.4} mm³, the tool {vb:.4} mm³)"),
|
||||
|
||||
@@ -31,8 +31,13 @@ const EXTENSION: usize = 8;
|
||||
const KNIFE: f64 = 1e-4;
|
||||
/// An end face this close to square-on-to-its-sides' normal counts as tangent to a side.
|
||||
const TANGENT_END: f64 = 1e-6;
|
||||
/// Times the run past a tangent end is halved when the ball does not fit along it.
|
||||
/// Times the run past a tangent end is halved when the ball does not fit along it (and the run
|
||||
/// past a curved end is shortened when its sections wrap round).
|
||||
const TANGENT_RETRIES: usize = 8;
|
||||
/// The longest step between the centres of neighbouring sections past an open chain's end, as a
|
||||
/// multiple of the step along the end tangent: longer, and the sections have wrapped round a
|
||||
/// curved edge's ring of centres.
|
||||
const MAX_STEP_STRETCH: f64 = 3.0;
|
||||
/// Slides of the ball's centre along a knife edge's faces, each 1.6 times the last.
|
||||
const KNIFE_STEPS: usize = 40;
|
||||
|
||||
@@ -1170,20 +1175,44 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
// the ball no longer fits, the run is shortened (it only gives the fit its context).
|
||||
let mut list = Vec::with_capacity(EXTENSION);
|
||||
let mut reach = reach;
|
||||
for attempt in 0..(if tangent.is_some() { TANGENT_RETRIES } else { 1 }) {
|
||||
// Past the end of a curved edge the planes square to its end tangent cut the ring of
|
||||
// centres further and further round it, until a step jumps across the ring (#1255: a
|
||||
// knuckle's cap circle with a round as wide as 0.7 of its radius). The run is shortened
|
||||
// then, as long as one of its sections still lies wholly past the end face.
|
||||
let ng_out = if ng.dot(out) < 0.0 { -ng } else { ng };
|
||||
let past = |sec: &Section| [sec.c, sec.q1, sec.q2].iter().all(|x| (*x - *p).dot(ng_out) > tol);
|
||||
let mut fallback: Option<Vec<Section>> = None;
|
||||
for attempt in 0..TANGENT_RETRIES {
|
||||
list.clear();
|
||||
let step = reach / EXTENSION as f64;
|
||||
let sections = (1..=EXTENSION).map(|j| {
|
||||
let anchor = *p + out * (reach * j as f64 / EXTENSION as f64);
|
||||
let anchor = *p + out * (step * j as f64);
|
||||
solve(&e1, &e2, anchor, *t, r, sigma, cusp, None, tol * 100.0)
|
||||
});
|
||||
if let Some(all) = sections.collect::<Option<Vec<_>>>() {
|
||||
list = all;
|
||||
break;
|
||||
match sections.collect::<Option<Vec<_>>>() {
|
||||
Some(all) => {
|
||||
let smooth = tangent.is_some()
|
||||
|| std::iter::once(at.c).chain(all.iter().map(|s| s.c)).collect::<Vec<_>>().windows(2).all(|w| match w {
|
||||
[a, b] => a.dist(*b) <= MAX_STEP_STRETCH * step,
|
||||
_ => true,
|
||||
});
|
||||
if smooth {
|
||||
// A shortened run must still cross the end face; else the first,
|
||||
// full-length run stands.
|
||||
if fallback.is_none() || all.iter().any(past) {
|
||||
list = all;
|
||||
}
|
||||
break;
|
||||
}
|
||||
fallback.get_or_insert(all);
|
||||
}
|
||||
None if tangent.is_none() || fallback.is_some() || attempt + 1 == TANGENT_RETRIES => break,
|
||||
None => {}
|
||||
}
|
||||
if attempt + 1 == TANGENT_RETRIES || tangent.is_none() {
|
||||
return Err(unsupported("the ball doesn't fit past the end of the edge"));
|
||||
}
|
||||
reach *= 0.5;
|
||||
reach *= if tangent.is_some() { 0.5 } else { 0.75 };
|
||||
}
|
||||
if list.is_empty() {
|
||||
list = fallback.ok_or_else(|| unsupported("the ball doesn't fit past the end of the edge"))?;
|
||||
}
|
||||
if *k == 0 {
|
||||
list.reverse();
|
||||
|
||||
Reference in new issue
Block a user