Compare commits

...

1 Commits

Author SHA1 Message Date
Brennen Herbruck 6867d8778f Fillet and chamfer failures say why, not just "status 1001"
solidcore states most blend failures in its own words (BlendFailure), but
crates/smlib reduced them to the bare status they replaced, so a failed fillet
read "SMLib failed (status 1001)". solidcore-ops now types them
(Error::BlendFailed: the kernel's class, the status, its statement), as it
types offset refusals, and SolidCraft carries them as Error::Refused:

  the blend failed (status 1001): the faces the rails enclose reach past the rails

The blend routines leave the set of failures passed on as a bare status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 06:40:10 -05:00
4 changed files with 66 additions and 16 deletions
+34 -15
View File
@@ -75,8 +75,12 @@ const INVALID_INPUT: i32 = 1007;
/// An SMLib status as the capi passes it on (`status`, smlib_shim.h): solidcore's upstream
/// status codes as they are; its refusals of code it has not ported as not supported; a refusal
/// solidcore-ops types as that refusal ([`Error::Refused`]); the rest as solidcore's text.
/// or a blend failure solidcore-ops types as that ([`Error::Refused`]: an offset refused with
/// its reason, a blend failed with the kernel's statement of why); the rest as solidcore's text.
pub(super) fn status(e: SolidError) -> Error {
if let Some(failed) = solidcore_ops::blend::failure(&e) {
return Error::Refused(failed);
}
match e {
SolidError::Upstream { code, .. } => Error::Status(code),
e if stands_for_status(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
@@ -96,9 +100,9 @@ pub(super) fn status(e: SolidError) -> Error {
/// returned (`Upstream` before solidcore described it), whether or not it names an entity. A
/// B-spline method's (`NurbsFailure::BSpline`); an intersection or classification, primitive
/// (extrusion, revolution, skin, taper), mass-property, healing, geometry-utility, query,
/// topology or surface routine's; and a sweep, curve, offset or blend routine's that replaced a
/// status (a railed loft's, a length bracket's and an offset's refusals replaced none and pass on
/// otherwise). `tests::status_sites_only_shrink` counts the sites that make a status, a set that only shrinks.
/// topology or surface routine's; and a sweep, curve or offset routine's that replaced a status
/// (a railed loft's, a length bracket's and an offset's refusals replaced none and pass on
/// otherwise). A blend routine's is solidcore-ops' blend failure ([`status`]). `tests::status_sites_only_shrink` counts the sites that make a status, a set that only shrinks.
fn stands_for_status(e: &SolidError) -> bool {
matches!(e.nurbs_failure(), Some(NurbsFailure::BSpline(_)))
|| e.intersect_failure().is_some()
@@ -112,7 +116,6 @@ fn stands_for_status(e: &SolidError) -> bool {
|| e.curve_failure().is_some_and(|c| c.replaces_upstream_status())
|| e.surface_failure().is_some()
|| e.offset_failure().is_some_and(|o| o.replaces_upstream_status())
|| e.blend_failure().is_some_and(|b| b.replaces_upstream_status())
}
/// A failure of the Boolean engine (which slice, sweep and the other users of the merge engine
@@ -1524,22 +1527,38 @@ mod tests {
}
/// A blend routine's failure passes on as the status it replaced, as the `Upstream` it
/// replaced did, whether or not it names an edge.
/// replaced did, whether or not it names an edge, and keeps solidcore's reason for it
/// ([`Error::BlendFailed`]): a bare status names no cause the user or a report can act on.
#[test]
fn blend_failures_pass_on_as_their_status() {
fn blend_failures_pass_on_as_their_status_with_their_reason() {
use solidcore::error::Failure;
use solidcore::error::blend::BlendFailure;
let blend = |b| Failure::new("blend::feature_executive", b).into_error();
assert!(matches!(status(blend(BlendFailure::Failed("x"))), Error::Status(1001)));
assert!(matches!(status(blend(BlendFailure::Missing("the edge use's edge"))), Error::Status(1006)));
assert!(matches!(status(blend(BlendFailure::InvalidValue("x"))), Error::Status(1007)));
assert!(matches!(status(blend(BlendFailure::Inconsistent("x"))), Error::Status(1005)));
assert!(matches!(status(blend(BlendFailure::BadIntersections("x"))), Error::Status(1027)));
assert!(matches!(status(blend(BlendFailure::NotConverged("x"))), Error::Status(1022)));
assert!(matches!(status(blend(BlendFailure::PassedOn { what: "x", status: 1020 })), Error::Status(1020)));
// the status and the reason the failure carries, and its class (the kernel's own)
let carried = |e: Error| match e {
Error::Refused(solidcore_ops::Error::BlendFailed { class, status, phrase }) => (status, phrase, class),
e => panic!("not a typed blend failure: {e:?}"),
};
let x = |status: i32, phrase: &str| (status, phrase.to_owned());
let cases = [
(BlendFailure::Failed("x"), x(1001, "x")),
(BlendFailure::Missing("the edge use's edge"), x(1006, "the edge use's edge is missing")),
(BlendFailure::InvalidValue("x"), x(1007, "x")),
(BlendFailure::Inconsistent("x"), x(1005, "a consistency check failed: x")),
(BlendFailure::BadIntersections("x"), x(1027, "x")),
(BlendFailure::NotConverged("x"), x(1022, "x")),
(BlendFailure::PassedOn { what: "x", status: 1020 }, x(1020, "x")),
];
for (failure, (want_status, want_phrase)) in cases {
let (status, phrase, class) = carried(status(blend(failure)));
assert_eq!((status, phrase.as_str(), class), (want_status, want_phrase.as_str(), failure.kind()), "{failure:?}");
}
let at = Failure::new("blend::fillet_executive::p3", BlendFailure::Failed("x"))
.at(solidcore::EntityRef::Edge(solidcore::topology::EdgeId::from_index(2)))
.into_error();
assert!(matches!(status(at), Error::Status(1001)));
assert_eq!(carried(status(at)).0, 1001);
// the trim of a blend whose faces leak past its rails, as the message says it
let leak = blend(BlendFailure::Failed("the faces the rails enclose reach past the rails"));
assert_eq!(status(leak).to_string(), "the blend failed (status 1001): the faces the rails enclose reach past the rails");
}
}
+2 -1
View File
@@ -93,7 +93,8 @@ pub enum Error {
/// solidcore reported an error (the call, then its message).
Solidcore(String),
/// A refusal solidcore-ops types, carried as it is: the app words it from its fields
/// (an offset refused with its reason, [`solidcore_ops::Error::OffsetRefused`]).
/// (an offset refused with its reason, [`solidcore_ops::Error::OffsetRefused`]; a fillet or
/// chamfer failed with the kernel's statement of why, [`solidcore_ops::Error::BlendFailed`]).
Refused(solidcore_ops::Error),
/// solidcore panicked; the guard caught it (the call, then the panic's message).
Internal(String),
+14
View File
@@ -1142,6 +1142,20 @@ fn overflow_first(
Ok(Ok(fresh))
}
/// A kernel error as ops' blend failure, for a caller that runs the kernel's blend itself: a
/// failure the kernel states in words where it replaced a bare status (its `BlendFailure`), as
/// [`Error::BlendFailed`] with its class, its status and the kernel's statement; `None` for any
/// other error.
#[must_use]
pub fn failure(e: &SolidError) -> Option<Error> {
let b = e.blend_failure().filter(|b| b.replaces_upstream_status())?;
Some(Error::BlendFailed {
class: b.kind(),
status: e.sm_status().unwrap_or(solidcore::error::SM_ERR),
phrase: b.to_string(),
})
}
/// The edges of `body` at native-list indices `edges` blended by `kind`, edge i at `sizes[i]` (or
/// `sizes[0]` for every edge), the corners at `setbacks` set back (not for a chamfer, whose
/// corners are bevelled) and, with [`Corners::Open`], left open. When some of the rounds
+16
View File
@@ -314,6 +314,19 @@ pub enum Error {
/// Why.
why: OffsetRefusal,
},
/// A fillet or chamfer the kernel fails with a reason it states in words (its
/// `BlendFailure`, which replaced a bare status): the kernel's class of the failure, the
/// status it stands for, and the kernel's statement of it. Transitional, as
/// [`OffsetRefusal::Phrase`] is: each reason gets a type of its own once the kernel gives
/// it a cause of its own.
BlendFailed {
/// The kernel's class of the failure.
class: solidcore::error::ErrorKind,
/// The status the failure stands for (1001 `SM_ERR`, ...).
status: i32,
/// The kernel's statement of it.
phrase: String,
},
}
/// Why the kernel refuses an offset or a draft ([`Error::OffsetRefused`]), as its
@@ -704,6 +717,9 @@ impl std::fmt::Display for Error {
Some(face) => write!(f, "the offset is refused at face {face}: {why}"),
None => write!(f, "the offset is refused: {why}"),
},
Error::BlendFailed { status, phrase, .. } => {
write!(f, "the blend failed (status {status}): {phrase}")
}
}
}
}