A round mitred to one that overflows goes first with it (fork 9ba2b12); flags() and warnings

overflow_first takes the mitred pairs of the stopped call (solidcore#2045's mitred_edges) and
grows the rounds that go first through every mitre, as the capi's. The flag test reads
UpstreamFixes::flags (solidcore#2024), and the check's own filter of SMLib's warning gives way to
AssertReport::is_warning (solidcore#2023).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
brett19
2026-10-31 09:37:58 -07:00
parent 15e3a61dab
commit 6a0166853f
4 changed files with 51 additions and 25 deletions
+4 -4
View File
@@ -501,12 +501,12 @@ mod tests {
#[test]
fn every_upstream_fix_is_on() {
let f = format!("{:?}", fixes());
let off: Vec<&str> =
f.split([',', '{', '}']).filter_map(|kv| kv.trim().strip_suffix(": false")).filter(|name| !HELD_BACK.contains(name)).collect();
let f = fixes();
let flags: Vec<(&str, bool)> = f.flags().collect();
let off: Vec<&str> = flags.iter().filter(|(name, on)| !on && !HELD_BACK.contains(name)).map(|(name, _)| *name).collect();
assert!(off.is_empty(), "UpstreamFixes flags are off: {off:?}");
for name in HELD_BACK {
assert!(f.contains(&format!("{name}: false")), "{name} is on or gone: drop it from HELD_BACK");
assert!(flags.contains(&(*name, false)), "{name} is on or gone: drop it from HELD_BACK");
}
}
+42 -9
View File
@@ -285,10 +285,13 @@ fn blend_failure(s: &FilletSession<'_>, e: SolidError, corner: bool) -> Error {
/// `round_edges`' `overflow_first` argument: with it the session stops, nothing built, when blends
/// of several edges overflow their faces across cliffs (`SetOverflowFirst`) or (`classify_only`)
/// always (`SetClassifyOnly`), and `stopped` gets the edges whose blends overflow
/// (`GetOverflowEdges`; the capi's status `OVERFLOW_FIRST`, the call's failure then meaning only that).
/// (`GetOverflowEdges`; the capi's status `OVERFLOW_FIRST`, the call's failure then meaning only that)
/// and `mitred` the edges of each two rounds mitred at a face's concave corner, two by two
/// (`GetMitredEdges`), when the caller asks.
struct OverflowFirst<'a> {
classify_only: bool,
stopped: &'a mut Option<Vec<EdgeId>>,
mitred: Option<&'a mut Vec<EdgeId>>,
}
/// `round_edges` and `chamfer_offsets` (with equal offsets): `edges` of `b` blended in place,
@@ -365,6 +368,9 @@ fn blend_edges_stopping(
&& s.error_info().code == FilletErrorType::OverflowFirst as i32
{
*o.stopped = Some(s.overflow_edges());
if let Some(m) = o.mitred {
*m = s.mitred_edges();
}
}
Ok(Err(Err(blend_failure(&s, e, corner))))
}
@@ -553,8 +559,11 @@ const OVERFLOW_GOES_FIRST: bool = true;
/// nothing was built in) go first: on a fresh copy of `body`, the picked edges tagged with their
/// places ([`Picked`]), those are rounded, and each other picked edge is then what is left of it,
/// the edges carrying its tag, rounded at its size. A picked edge with nothing left is gone: its
/// points are flagged in `dropped`. Where none overflows on its own, or all do, the edges are
/// rounded together as before. The result is the new body. Unlike the capi, the tags come off it
/// points are flagged in `dropped`. Two rounds mitred at a face's concave corner (`mitred`, the edges
/// of each such pair two by two, as the stopped call found them in `w`) meet in their mitre, each run
/// on past the corner: one goes first with the other, so a round mitred to one that overflows goes
/// first too (fork 9ba2b12). Where none overflows on its own, or all go first, the edges are rounded
/// together as before. The result is the new body. Unlike the capi, the tags come off it
/// (nothing reads them after the call, and a later call's copy would carry them on).
#[allow(clippy::too_many_arguments)] // the capi's overflow_first
fn overflow_first(
@@ -564,6 +573,7 @@ fn overflow_first(
tolerance: f64,
section: Section<'_>,
picked: &[EdgeId],
mitred: &[EdgeId],
each: &[f64],
w: &mut Brep,
dropped: &mut [bool],
@@ -581,10 +591,10 @@ fn overflow_first(
Ok(_) => return Ok(Err(Error::NoMatch { point: 0 })),
Err(m) => return Ok(Err(edge_miss(m))),
};
let (mut lead, mut lead_sizes, mut rest) = (Vec::new(), Vec::new(), Vec::new());
let mut goes_first = vec![false; picked.len()];
for (i, ((&e, &to), &size)) in picked.iter().zip(&again).zip(each).enumerate() {
let mut stopped = None;
let alone = OverflowFirst { classify_only: true, stopped: &mut stopped };
let alone = OverflowFirst { classify_only: true, stopped: &mut stopped, mitred: None };
match blend_edges_stopping(k, w, &[e], &[size], section, None, &[], None, Some(alone))? {
Err(Err(why)) if stopped.is_none() => return Ok(Err(why)),
Err(Ok(_)) => return Ok(Err(Error::Unsupported)),
@@ -592,7 +602,30 @@ fn overflow_first(
Err(Err(_)) => {}
}
fresh.add_attribute(EntityRef::Edge(to), Attribute::application(PICKED_ID, Behavior::Copy, Picked(i)))?;
if stopped.is_some_and(|over| !over.is_empty()) {
if let Some(flag) = goes_first.get_mut(i) {
*flag = stopped.is_some_and(|over| !over.is_empty());
}
}
// A round mitred to one that goes first goes with it, through every mitre.
let place = |e: &EdgeId| picked.iter().position(|p| p == e);
let pairs: Vec<(usize, usize)> = mitred.as_chunks::<2>().0.iter().filter_map(|[a, b]| Some((place(a)?, place(b)?))).collect();
let mut grew = true;
while grew {
grew = false;
for &(a, b) in &pairs {
if goes_first.get(a) != goes_first.get(b) {
for i in [a, b] {
if let Some(flag) = goes_first.get_mut(i) {
*flag = true;
}
}
grew = true;
}
}
}
let (mut lead, mut lead_sizes, mut rest) = (Vec::new(), Vec::new(), Vec::new());
for (i, ((&to, &size), &first)) in again.iter().zip(each).zip(&goes_first).enumerate() {
if first {
lead.push(to);
lead_sizes.push(size);
} else {
@@ -766,11 +799,11 @@ fn blend_call(
// first, on a fresh copy, and the others then round what is left of their edges. An edge
// the overflow removes whole is gone (`dropped`); one it cuts keeps the rest.
let may_go_first = OVERFLOW_GOES_FIRST && back.is_empty() && open == Corners::Filled && picked.len() > 1;
let mut stopped = None;
let first = may_go_first.then_some(OverflowFirst { classify_only: false, stopped: &mut stopped });
let (mut stopped, mut mitred) = (None, Vec::new());
let first = may_go_first.then_some(OverflowFirst { classify_only: false, stopped: &mut stopped, mitred: Some(&mut mitred) });
let done = blend_edges_stopping(k, &mut w, &picked, &each, section, None, &back, (open == Corners::Open).then_some(&mut patches), first)?;
if stopped.is_some() {
return Ok(match overflow_first(k, body, points, tolerance, section, &picked, &each, &mut w, dropped)? {
return Ok(match overflow_first(k, body, points, tolerance, section, &picked, &mitred, &each, &mut w, dropped)? {
Ok(fresh) => finish(fresh, solid_in)?,
Err(e) => Err(e),
});
+4 -11
View File
@@ -6,7 +6,7 @@
//! [`check`] fails with "not supported yet" while a test it reaches is unported in solidcore.
use solidcore::error::EntityRef;
use solidcore::geom::assert_array::{AssertArray, AssertReport, labels, obj_type_string};
use solidcore::geom::assert_array::{AssertArray, AssertReport, obj_type_string};
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::geom::tol::{SmallTopoType, brep_zone_tol_3d, context_xsect_tol_3d};
use solidcore::heal::gap::GapFunction;
@@ -261,18 +261,11 @@ fn report_line(r: &AssertReport) -> String {
s
}
/// The capi's `advisory()`: `SmBrep` test 21, "Suspicious Shell Count" (a body of several faces
/// with one shell each), is a warning. `SmBrep::AssertValid` reports it without failing, and
/// faces that meet nowhere (a sheet's faces offset apart across a sharp edge) rightly make such a
/// body.
fn advisory(r: &AssertReport) -> bool {
r.reporting_type == labels::BREP_TYPE && r.list_index == 0 && r.test_index == 21
}
/// The capi's `collect()`: appends the failed reports of `list`, one line each, and counts them;
/// SMLib's warnings ([`advisory`]) are neither.
/// SMLib's warnings ([`AssertReport::is_warning`]: `SmBrep` test 21, "Suspicious Shell Count", a
/// body of several faces with one shell each, is one) are neither.
fn collect(list: &AssertArray, out: &mut String, failures: &mut usize) {
for r in list.reports().iter().filter(|r| !r.ok && !advisory(r)) {
for r in list.reports().iter().filter(|r| !r.ok && !r.is_warning()) {
*failures += 1;
out.push_str(&report_line(r));
out.push('\n');
+1 -1
View File
@@ -397,7 +397,7 @@ fn a_sealed_void_measures_its_volume() {
/// Two 10 mm squares apart in z = 0, one body of two faces with a shell each (as the top and
/// front of a box offset apart across their edge make, kernel case
/// `surfaces/cov-offset-two-faces-across-a-sharp-edge-come-apart`): `SmBrep` test 21,
/// "Suspicious Shell Count", warns of it, and the capi's check (`advisory`) counts no warning.
/// "Suspicious Shell Count", warns of it, and the capi's check counts no warning.
#[test]
fn faces_apart_with_a_shell_each_check_valid() {
use solidcore::geom::axis2_placement::Axis2Placement;