Merge pull request #2037 from brett19/fix/2009d-query-statuses

fix: query statuses pass on (#2009 group D, WR-663)
This commit is contained in:
bherbruck
2026-10-31 12:28:57 -04:00
committed by GitHub
16 changed files with 969 additions and 383 deletions
+57 -12
View File
@@ -162,6 +162,9 @@
//! infallible) when `num_derivs` exceeds [`crate::curve::MAX_EVAL_DERIVS`]; the other
//! kinds' evaluators check the bound themselves or fail earlier (B-splines at
//! `NL_MAXDER`).
//! - **No length (WR-663).** With the opt-in `Kernel::fixes.query_statuses_pass_on`,
//! `ApproximateLength` is `SM_UNDEF_DOUBLE` (no turn angle) when an evaluation or a
//! turn angle fails, as the fork (tangency-labs/smlib 8311f8e).
use crate::curve::composite_curve::CompositeCurve;
use crate::curve::conic::Conic;
@@ -2279,6 +2282,11 @@ pub fn approximate_length<C: CurveEvaluate + ?Sized>(
// init output
let mut ret = 0.0;
// with the opt-in fix a curve that cannot be evaluated has no length:
// SM_UNDEF_DOUBLE (tangency-labs/smlib 8311f8e)
let no_length = k.fixes.query_statuses_pass_on;
let undef = Ok((crate::math::UNDEF_DOUBLE, None));
// switch for turning angles
if curve.dim() == 2 && want_turn_angle {
let z = Vec3::new(0.0, 0.0, 1.0);
@@ -2287,15 +2295,22 @@ pub fn approximate_length<C: CurveEvaluate + ?Sized>(
let mut turn_ang_rad = 0.0;
// Get Start and end Points
if let Some(v) = eval_ignored(k, curve, interval.min(), 1, true)? {
last = [at(&v, 0), at(&v, 1)];
match eval_ignored(k, curve, interval.min(), 1, true)? {
Some(v) => last = [at(&v, 0), at(&v, 1)],
None if no_length => return undef,
None => (),
}
if let Some(v) = eval_ignored(k, curve, interval.max(), 1, true)? {
curr = [at(&v, 0), at(&v, 1)];
match eval_ignored(k, curve, interval.max(), 1, true)? {
Some(v) => curr = [at(&v, 0), at(&v, 1)],
None if no_length => return undef,
None => (),
}
// Get base of ActTurnAngRad = BaseTurnAngRad +- N*2PI
let base_turn_ang_rad = z.ccw_angle_between(last[1], curr[1])?;
let base_turn_ang_rad = match z.ccw_angle_between(last[1], curr[1]) {
Err(e) if no_length && !e.is_unported() => return undef,
r => r?,
};
// for every sample point - assume enough sample points that every turn is less than 180 degrees
for ii in 1..=samples {
@@ -2307,13 +2322,18 @@ pub fn approximate_length<C: CurveEvaluate + ?Sized>(
};
// Evaluate and accumulate distance
if let Some(v) = eval_ignored(k, curve, param, 1, true)? {
curr = [at(&v, 0), at(&v, 1)];
match eval_ignored(k, curve, param, 1, true)? {
Some(v) => curr = [at(&v, 0), at(&v, 1)],
None if no_length => return undef,
None => (),
}
ret += curr[0].distance(last[0]);
// accumulate turn angle
let this_turn_ang_rad = z.ccw_angle_between(last[1], curr[1])?;
let this_turn_ang_rad = match z.ccw_angle_between(last[1], curr[1]) {
Err(e) if no_length && !e.is_unported() => return undef,
r => r?,
};
turn_ang_rad += this_turn_ang_rad;
// for next iter
@@ -2327,12 +2347,17 @@ pub fn approximate_length<C: CurveEvaluate + ?Sized>(
let turn_deg = (base_turn_ang_rad + n * 2.0 * PI) * (180.0 / PI);
Ok((ret, Some(turn_deg)))
} else {
let point = |t: f64, prev: Point3| -> Result<Point3> {
Ok(ignored(curve.evaluate_point(k, t))?.unwrap_or(prev))
let point = |t: f64, prev: Point3| -> Result<Option<Point3>> {
match ignored(curve.evaluate_point(k, t))? {
None if no_length => Ok(None),
p => Ok(Some(p.unwrap_or(prev))),
}
};
// Get Start Point
let mut last = point(interval.min(), Vec3::UNDEF)?;
let Some(mut last) = point(interval.min(), Vec3::UNDEF)? else {
return undef;
};
let mut curr = Vec3::UNDEF;
// for every sample point
@@ -2345,7 +2370,10 @@ pub fn approximate_length<C: CurveEvaluate + ?Sized>(
};
// evaluate and accumulate distance
curr = point(param, curr)?;
let Some(p) = point(param, curr)? else {
return undef;
};
curr = p;
ret += curr.distance(last);
// next iter
@@ -4668,6 +4696,23 @@ mod tests {
assert!(g.edge(|_| Some(owner_curve)).is_err());
}
#[test]
fn issue_2009_unevaluable_curve_has_no_length() {
// tangency-labs/smlib 8311f8e (SmCurve::ApproximateLength): a curve that cannot
// be evaluated has no length (SM_UNDEF_DOUBLE); upstream sums distances
// between unset points.
let empty = Curve::from(BSplineCurve::new(3));
let ivl = Extent1d::from_min_max(0.0, 1.0);
let off = Kernel::default();
let mut on = Kernel::default();
on.fixes.query_statuses_pass_on = true;
let (len_off, _) = approximate_length(&off, &empty, &ivl, 5, false).unwrap();
assert_ne!(len_off, crate::math::UNDEF_DOUBLE);
let (len_on, turn) = approximate_length(&on, &empty, &ivl, 5, false).unwrap();
assert_eq!(len_on, crate::math::UNDEF_DOUBLE);
assert!(turn.is_none());
}
#[test]
fn length_and_step_off() {
let k = &Kernel::default();
+75 -15
View File
@@ -67,6 +67,9 @@
//! `SmFindCCNormalENFO` (unit `curve.curve_p5`) and `SmFindPCExtremaEFO`
//! (`curve.curve_p3`) are not ported here: their structs carry only the
//! constructors' data, and those units add the trait implementations.
//! - **No evidence (WR-663).** With the opt-in `Kernel::fixes.query_statuses_pass_on`,
//! `IsClosed` and `IsDegenerate` are `false` when a point or tangent cannot be
//! evaluated, as the fork (tangency-labs/smlib 8311f8e).
#[cfg(doc)]
use crate::curve::curve::p2::FindCurveDistanceExtrema;
@@ -114,8 +117,23 @@ pub(crate) fn point_or_undef<C: CurveEvaluate + ?Sized>(
Ok(ignored(curve.evaluate_point(k, t))?.unwrap_or(Vec3::UNDEF))
}
/// [`point_or_undef`], or `None` when, with the opt-in
/// [`crate::kernel::UpstreamFixes::query_statuses_pass_on`], the point cannot be
/// evaluated (the fork's predicates then show nothing, tangency-labs/smlib 8311f8e).
fn point_or_none<C: CurveEvaluate + ?Sized>(
k: &Kernel,
curve: &C,
t: f64,
) -> Result<Option<Point3>> {
match ignored(curve.evaluate_point(k, t))? {
None if k.fixes.query_statuses_pass_on => Ok(None),
p => Ok(Some(p.unwrap_or(Vec3::UNDEF))),
}
}
/// `Evaluate` whose status upstream ignores into `pv` (entries not written keep
/// their values); [`SolidError::Unported`] is propagated.
/// their values); [`SolidError::Unported`] is propagated. `false` when, with the
/// opt-in [`crate::kernel::UpstreamFixes::query_statuses_pass_on`], it fails.
fn eval_into<C: CurveEvaluate + ?Sized>(
k: &Kernel,
curve: &C,
@@ -123,13 +141,17 @@ fn eval_into<C: CurveEvaluate + ?Sized>(
num_derivs: usize,
non_zero_tangents: bool,
pv: &mut [Vec3],
) -> Result<()> {
if let Some(v) = ignored(curve.evaluate(k, t, num_derivs, true, non_zero_tangents))? {
for (d, s) in pv.iter_mut().zip(v) {
*d = s;
) -> Result<bool> {
match ignored(curve.evaluate(k, t, num_derivs, true, non_zero_tangents))? {
Some(v) => {
for (d, s) in pv.iter_mut().zip(v) {
*d = s;
}
}
None if k.fixes.query_statuses_pass_on => return Ok(false),
None => (),
}
Ok(())
Ok(true)
}
// ---- chaining ---------------------------------------------------------------------------
@@ -802,10 +824,17 @@ pub fn is_closed_base<C: CurveEvaluate + ?Sized>(
interval: &Extent1d,
tol: f64,
) -> Result<bool> {
// get curve start, end, and mid 3d Points
let p1 = point_or_undef(k, curve, interval.min())?;
let p2 = point_or_undef(k, curve, interval.max())?;
let p_mid = point_or_undef(k, curve, interval.evaluate(0.5))?;
// get curve start, end, and mid 3d Points (with the opt-in fix, ends that cannot be
// evaluated do not show it closed, tangency-labs/smlib 8311f8e)
let Some(p1) = point_or_none(k, curve, interval.min())? else {
return Ok(false);
};
let Some(p2) = point_or_none(k, curve, interval.max())? else {
return Ok(false);
};
let Some(p_mid) = point_or_none(k, curve, interval.evaluate(0.5))? else {
return Ok(false);
};
// let dTol = dTolerance (when given) or dScaledZero
let tol = if tol == 0.0 {
@@ -843,8 +872,14 @@ pub fn is_degenerate_base<C: CurveEvaluate + ?Sized>(
if ivl.min() == ivl.max() {
return Ok(true);
}
let p1 = point_or_undef(k, curve, ivl.min())?;
let p2 = point_or_undef(k, curve, ivl.max())?;
// (with the opt-in fix a curve that cannot be evaluated is not shown degenerate,
// tangency-labs/smlib 8311f8e)
let Some(p1) = point_or_none(k, curve, ivl.min())? else {
return Ok(false);
};
let Some(p2) = point_or_none(k, curve, ivl.max())? else {
return Ok(false);
};
// select a tolerance - scale it to p1 location
let scaled_zero = EFF_ZERO * (1.0 + p1.max_dimension());
@@ -856,18 +891,24 @@ pub fn is_degenerate_base<C: CurveEvaluate + ?Sized>(
let lsq = ivl.length() * ivl.length();
// if start, mid, or end tangents are nonZero - the curve is not degenerate
eval_into(k, curve, ivl.min(), 1, false, &mut pv)?; // exact tangent lengths
if !eval_into(k, curve, ivl.min(), 1, false, &mut pv)? {
return Ok(false);
}
if at(&pv, 1).length_squared() * lsq > tol2 {
return Ok(false);
}
eval_into(k, curve, ivl.max(), 1, false, &mut pv)?;
if !eval_into(k, curve, ivl.max(), 1, false, &mut pv)? {
return Ok(false);
}
if at(&pv, 1).length_squared() * lsq > tol2 {
return Ok(false);
}
// not the exact midpoint (a projected circle seen edge-on) [091023]
eval_into(k, curve, ivl.evaluate(0.4567), 1, false, &mut pv)?;
if !eval_into(k, curve, ivl.evaluate(0.4567), 1, false, &mut pv)? {
return Ok(false);
}
if at(&pv, 1).length_squared() * lsq > tol2 {
return Ok(false);
}
@@ -2173,6 +2214,25 @@ mod tests {
assert_eq!(r.origin, vec![0]);
}
#[test]
fn issue_2009_unevaluable_curve_is_not_shown_degenerate_or_closed() {
// tangency-labs/smlib 8311f8e (SmCurve::IsDegenerate, SmCurve::IsClosed): a
// curve that cannot be evaluated is shown neither degenerate nor closed;
// upstream compares the unset points, which coincide.
let empty = Curve::from(crate::nurbs::b_spline_curve::BSplineCurve::new(3));
let ivl = Extent1d::from_min_max(0.0, 1.0);
assert!(empty.evaluate_point(&Kernel::default(), 0.0).is_err());
let off = Kernel::default();
let mut on = Kernel::default();
on.fixes.query_statuses_pass_on = true;
// IsDegenerate: discriminating
assert!(is_degenerate_base(&off, &empty, 0.0, Some(&ivl)).unwrap());
assert!(!is_degenerate_base(&on, &empty, 0.0, Some(&ivl)).unwrap());
// IsClosed: coverage only (the coincident unset points fail the mid test too)
assert!(!is_closed_base(&off, &empty, &ivl, 0.0).unwrap());
assert!(!is_closed_base(&on, &empty, &ivl, 0.0).unwrap());
}
#[test]
fn closed_and_degenerate() {
let k = &Kernel::default();
+8 -1
View File
@@ -514,6 +514,13 @@ pub struct UpstreamFixes {
/// instead of the operation carrying on and reporting success (WR-658, #2009 group A;
/// tangency-labs/smlib 60a8a51, 4803d6d, e7ebc8d). Off by default for pinned parity.
pub brep_edit_statuses_pass_on: bool,
/// The loop, vertex, edge use, face, curve and surface queries pass on the statuses
/// SMLib drops where the call returns a status, and the classifying queries answer
/// only from what they computed: a value that could not be evaluated is no evidence
/// (`SM_POC_UNKNOWN`, no G1 meeting, no parallel pair, no dog-leg, not fresh) instead
/// of one read unset (WR-663, #2009 group D; tangency-labs/smlib 7a35f33, 6997962,
/// 666242b, 8843967, 8311f8e, 21dcf69). Off by default for pinned parity.
pub query_statuses_pass_on: bool,
/// Extrusions, revolutions, sweeps, lofts and the station sweeps (`SmPrimitiveCreation`,
/// `SmTopologySweep`, the translational and rotational sweep geometry and `SM_API`'s
/// primitive wrappers) pass on the statuses SMLib drops: a failed evaluation, copy,
@@ -559,7 +566,7 @@ pub struct UpstreamFixes {
/// `plan/upstream-issues/WR-30--addendum-2026-10-31-uv-iso-line-both-params.md`).
/// Off by default for pinned C++ parity.
pub iso_curve_uv_line_both_params: bool,
/// `GetUVCornerBoundaries`, `GetStartPointUVLoopGap` and `sm_CalcDistAndDev` return failed curve/surface evaluations and point drops instead of reading unwritten or earlier outputs (WR-262, WR-263, WR-264, #521).
/// `GetUVCornerBoundaries`, `GetStartPointUVLoopGap` and `sm_CalcDistAndDev` answer only from what they computed instead of reading unwritten or earlier outputs (WR-262, WR-263, WR-264, #521), as the fork does (changed by #2009 group D to tangency-labs/smlib 7a35f33 and 666242b): in the corner walk an edge use whose trim curve cannot be made has its 3D ends dropped and one whose ends cannot be evaluated adds no boundary (failed drops stay ignored, as upstream); a loop gap that cannot be measured (a failed evaluation or drop) is `SM_BIG_DOUBLE`; a distance without the surface's derivatives or normal is none (`-1`).
pub topology_failed_evaluations_propagate: bool,
/// `SetProps_Stage3` returns a failed `ClassifyLoops` status before reading its outputs, and a failed stage drops its in-progress marker so a retry reruns it (WR-268, WR-266, #521).
pub heal_stage3_failures_propagate: bool,
+57 -27
View File
@@ -41,6 +41,9 @@
//! `AssertValid`'s evaluations and bounding box), the port does too, except that
//! [`SolidError::Unported`] is always passed on. Ignored evaluations leave
//! default-constructed `SmVector3d`s ([`Vec3::UNDEF`]), as upstream.
//! With the opt-in `Kernel::fixes.query_statuses_pass_on` (WR-663) `TrimGap` returns
//! `false` (gap not fixed) at the first of those that fails, as the fork
//! (tangency-labs/smlib 8843967).
//! - **Crashes.** Where upstream dereferences a `NULL` (a curve without NURB in
//! `GetControlPoint` / `IsSegmentLine`, an extension `TrimGap` did not make, an
//! unclamped knot vector indexing before the control points in `HasInternalPole`)
@@ -406,12 +409,6 @@ fn invalid(operation: &'static str, reason: &'static str) -> SolidError {
SolidError::InvalidInput { operation, reason }
}
/// An `EvaluatePoint` whose status is ignored: [`Vec3::UNDEF`] (the
/// default-constructed point) on failure.
fn point_or_undef(c: &BSplineCurve, t: f64) -> Result<Point3> {
Ok(ignored(c.evaluate_point(t))?.unwrap_or(Vec3::UNDEF))
}
/// The `SM_ASSERT_*_REPORT` macros of `AssertValid` (`SM_LEVEL_0` tests, so the
/// predicate always runs): `true` when `passed`; otherwise, with a list, a report for
/// test `test_index` (tolerance `tol`, value `val`, then `set` applied) is added
@@ -966,17 +963,30 @@ impl BSplineCurve {
// only close gap if less than dMin
let d_min_sq = d_min * d_min;
// with the opt-in fix each failed step leaves the gap unfixed (returns 0), as the
// fork has it (tangency-labs/smlib 8843967); upstream goes on with unset values
let soft = k.fixes.query_statuses_pass_on;
macro_rules! or_unfixed {
($r:expr, $default:expr) => {
match ignored($r)? {
Some(v) => v,
None if soft => return Ok(false),
None => $default,
}
};
}
// curve 0 start and end points
let mut extent0 = self.natural_interval();
let t0size = extent0.max() - extent0.min();
let s0 = point_or_undef(self, extent0.min())?;
let e0 = point_or_undef(self, extent0.max())?;
let s0 = or_unfixed!(self.evaluate_point(extent0.min()), Vec3::UNDEF);
let e0 = or_unfixed!(self.evaluate_point(extent0.max()), Vec3::UNDEF);
// curve 1 start and end points
let mut extent1 = other.natural_interval();
let t1size = extent1.max() - extent1.min();
let s1 = point_or_undef(other, extent1.min())?;
let e1 = point_or_undef(other, extent1.max())?;
let s1 = or_unfixed!(other.evaluate_point(extent1.min()), Vec3::UNDEF);
let e1 = or_unfixed!(other.evaluate_point(extent1.max()), Vec3::UNDEF);
// Extend curves based on closest endpoints by domain * 0.3
let d_s0s1 = (s0 - s1).length_squared();
@@ -1011,22 +1021,20 @@ impl BSplineCurve {
let end1 = ext_case == 2 || ext_case == 4;
// extend the input curves past the ends with the smallest gaps
let extend = |c: &Self, t: f64| -> Result<Option<Self>> {
Ok(ignored(c.create_extended_curve(t, ContinuityType::G1, true))?.flatten())
};
let extend = |c: &Self, t: f64| c.create_extended_curve(t, ContinuityType::G1, true);
let mut curve0e = None;
let mut curve1e = None;
if start0 {
curve0e = extend(self, extent0.min() - 0.03 * t0size)?;
curve0e = or_unfixed!(extend(self, extent0.min() - 0.03 * t0size), None);
}
if end0 {
curve0e = extend(self, extent0.max() + 0.03 * t0size)?;
curve0e = or_unfixed!(extend(self, extent0.max() + 0.03 * t0size), None);
}
if start1 {
curve1e = extend(other, extent1.min() - 0.03 * t1size)?;
curve1e = or_unfixed!(extend(other, extent1.min() - 0.03 * t1size), None);
}
if end1 {
curve1e = extend(other, extent1.max() + 0.03 * t1size)?;
curve1e = or_unfixed!(extend(other, extent1.max() + 0.03 * t1size), None);
}
let (Some(mut curve0e), Some(mut curve1e)) = (curve0e, curve1e) else {
return Err(invalid(OP_GAP, "curve extension missing"));
@@ -1063,8 +1071,8 @@ impl BSplineCurve {
let mut ext0 = Extent1d::from_min_max(tmin0, tmax0);
let mut ext1 = Extent1d::from_min_max(tmin1, tmax1);
ignored(curve0e.trim(k, &mut ext0, TrimMode::Notify))?;
ignored(curve1e.trim(k, &mut ext1, TrimMode::Notify))?;
or_unfixed!(curve0e.trim(k, &mut ext0, TrimMode::Notify), ());
or_unfixed!(curve1e.trim(k, &mut ext1, TrimMode::Notify), ());
// only do this once
break;
@@ -1078,16 +1086,16 @@ impl BSplineCurve {
let mut p0 = Vec3::UNDEF;
let mut p1 = Vec3::UNDEF;
if start0 {
p0 = point_or_undef(&curve0e, extent0.min())?;
p0 = or_unfixed!(curve0e.evaluate_point(extent0.min()), Vec3::UNDEF);
}
if end0 {
p0 = point_or_undef(&curve0e, extent0.max())?;
p0 = or_unfixed!(curve0e.evaluate_point(extent0.max()), Vec3::UNDEF);
}
if start1 {
p1 = point_or_undef(&curve1e, extent1.min())?;
p1 = or_unfixed!(curve1e.evaluate_point(extent1.min()), Vec3::UNDEF);
}
if end1 {
p1 = point_or_undef(&curve1e, extent1.max())?;
p1 = or_unfixed!(curve1e.evaluate_point(extent1.max()), Vec3::UNDEF);
}
// the new common end point
@@ -1112,18 +1120,18 @@ impl BSplineCurve {
// replace the endpoint of the original curves
let form = ControlPointForm::NonRational;
if start0 {
ignored(self.set_control_point(form, 0, mid, 1.0))?;
or_unfixed!(self.set_control_point(form, 0, mid, 1.0), ());
}
if end0 {
let last = self.number_control_points()?.wrapping_sub(1);
ignored(self.set_control_point(form, last, mid, 1.0))?;
or_unfixed!(self.set_control_point(form, last, mid, 1.0), ());
}
if start1 {
ignored(other.set_control_point(form, 0, mid, 1.0))?;
or_unfixed!(other.set_control_point(form, 0, mid, 1.0), ());
}
if end1 {
let last = other.number_control_points()?.wrapping_sub(1);
ignored(other.set_control_point(form, last, mid, 1.0))?;
or_unfixed!(other.set_control_point(form, last, mid, 1.0), ());
}
Ok(true) // success...we have modified the curves
@@ -1760,6 +1768,28 @@ mod tests {
assert!(on.on_plane);
}
#[test]
fn issue_2009_trim_gap_unfixed_without_end_points() {
// tangency-labs/smlib 8843967 (SmBSplineCurve::TrimGap): an end point that
// cannot be evaluated leaves the gap unfixed (0); upstream measures the gap
// between unset points, finds none and reports the curves joined.
let mut on = Kernel::default();
on.fixes.query_statuses_pass_on = true;
let off = Kernel::default();
let mut a = BSplineCurve::new(3);
assert!(a.evaluate_point(0.0).is_err());
let mut b = curve(
1,
&[[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
&[0.0, 0.0, 1.0, 1.0],
);
assert!(a
.clone()
.trim_gap(&off, &mut b.clone(), GapFix::Both, 0.1)
.unwrap());
assert!(!a.trim_gap(&on, &mut b, GapFix::Both, 0.1).unwrap());
}
#[test]
fn trim_gap_early_exits() {
let k = Kernel::default();
+69 -13
View File
@@ -35,7 +35,9 @@
//! - **Ignored statuses.** Upstream ignores the status of `EvaluatePoint` /
//! `Evaluate` of the surface and the trim curves and then reads the outputs they did
//! not write (`SM_UNDEF_DOUBLE` defaults, or the previous step's derivatives); the
//! port returns their error instead (no valid input reaches it).
//! port returns their error instead (no valid input reaches it). With the opt-in
//! `Kernel::fixes.query_statuses_pass_on` (WR-663) a failed evaluation or local solve
//! answers `SM_POC_UNKNOWN`, as the fork (tangency-labs/smlib 7a35f33).
//! `DropPoint` and `LocalPointSolve` failures are "no answer" (both reset their
//! success flag first upstream). [`SolidError::Unported`] from any callee is
//! returned.
@@ -146,9 +148,21 @@ fn drop_on_edge(
}
}
/// The value of a call whose failure the port returns, or `None` when, with the opt-in
/// [`crate::kernel::UpstreamFixes::query_statuses_pass_on`], the point is not classified
/// (`SM_POC_UNKNOWN`, tangency-labs/smlib 7a35f33). [`SolidError::Unported`] propagates.
fn or_unknown<T>(k: &Kernel, r: Result<T>) -> Result<Option<T>> {
match r {
Err(e) if k.fixes.query_statuses_pass_on && !e.is_unported() => Ok(None),
r => r.map(Some),
}
}
/// `LocalPointSolve(SM_SO_MINIMIZE)` of the test point on a UV trim curve: whether it
/// converged and the solution. A failing solve has not converged (its status is
/// ignored upstream and the flag is reset first).
/// ignored upstream and the flag is reset first); with the opt-in
/// [`crate::kernel::UpstreamFixes::query_statuses_pass_on`] it classifies nothing
/// (`Err(())`, the fork's `SM_POC_UNKNOWN`).
fn local_minimize(
k: &Kernel,
trim: &Curve,
@@ -156,16 +170,17 @@ fn local_minimize(
point: Point3,
tol_uv: f64,
guess: f64,
) -> Result<Option<Solution<()>>> {
) -> Result<std::result::Result<Option<Solution<()>>, ()>> {
let opts = PointSolveOptions {
distance_tolerance: Some(tol_uv),
..PointSolveOptions::default()
};
match trim.local_point_solve::<()>(k, interval, SolverOperation::Minimize, point, &opts, guess)
{
Ok((true, sol)) => Ok(Some(sol)),
Ok((true, sol)) => Ok(Ok(Some(sol))),
Err(e @ SolidError::Unported { .. }) => Err(e),
Ok((false, _)) | Err(_) => Ok(None),
Err(_) if k.fixes.query_statuses_pass_on => Ok(Err(())),
Ok((false, _)) | Err(_) => Ok(Ok(None)),
}
}
@@ -280,7 +295,9 @@ pub(crate) fn contains_uv_point_with_topology(
let zone_tol = face_rec.tolerance;
let srf_uv_domain = surface.natural_uv_domain_with_nurb(k)?;
let point_3d = surface.evaluate_point(k, uv)?;
let Some(point_3d) = or_unknown(k, surface.evaluate_point(k, uv))? else {
return Ok(UvContainment::of(PointContainment::Unknown));
};
let uv_pt = Point3::new(uv.x, uv.y, 0.0);
// Get a tolerance in uv space.
@@ -365,7 +382,10 @@ pub(crate) fn contains_uv_point_with_topology(
let last_trim = eu1::uv_trim_curve(b, last_eu)?
.0
.ok_or_else(|| null("ContainsUVPoint: edge use without UV trim curve"))?;
last_trim.evaluate_point(k, last_end_param)?
let Some(p) = or_unknown(k, last_trim.evaluate_point(k, last_end_param))? else {
return Ok(UvContainment::of(PointContainment::Unknown));
};
p
};
let mut last_end_quad_num = calc_quad_num(uv_pt, last_end_uv);
@@ -418,15 +438,21 @@ pub(crate) fn contains_uv_point_with_topology(
let curve_is_linear = trim.is_linear(k, tol_uv, false)?.linear;
// Eval info for start point of this EU.
let (mut prev_uv, mut prev_uv_tan, mut prev_uv_tan_len) =
eval_with_tangent(k, trim, prev_param, orient)?;
let Some((mut prev_uv, mut prev_uv_tan, mut prev_uv_tan_len)) =
or_unknown(k, eval_with_tangent(k, trim, prev_param, orient))?
else {
return Ok(UvContainment::of(PointContainment::Unknown));
};
let (prev_vec, mut prev_dist) = unit_from(uv_pt, prev_uv);
let mut prev_cos = prev_vec.dot(prev_uv_tan);
// Note: if dPrevDist < tol, then this uv pt is on the edge.
if prev_dist < tol_uv {
// We should refine the point.
if let Some(sol) = local_minimize(k, trim, &domain, uv_pt, tol_uv, prev_param)? {
let Ok(sol) = local_minimize(k, trim, &domain, uv_pt, tol_uv, prev_param)? else {
return Ok(UvContainment::of(PointContainment::Unknown));
};
if let Some(sol) = sol {
// since the guess was good enough - we expect the refined solution to
// be as good or better
if sol.start.solution_value <= tol_uv {
@@ -533,8 +559,11 @@ pub(crate) fn contains_uv_point_with_topology(
return Ok(UvContainment::of(PointContainment::Unknown));
}
let (uv_at, this_uv_tan, this_uv_tan_len) =
eval_with_tangent(k, trim, this_param, orient)?;
let Some((uv_at, this_uv_tan, this_uv_tan_len)) =
or_unknown(k, eval_with_tangent(k, trim, this_param, orient))?
else {
return Ok(UvContainment::of(PointContainment::Unknown));
};
this_uv = uv_at;
let (this_vec, this_dist) = unit_from(uv_pt, this_uv);
let this_cos = this_vec.dot(this_uv_tan);
@@ -616,7 +645,10 @@ pub(crate) fn contains_uv_point_with_topology(
crv_ivl.add_value(this_param);
crv_ivl.expand_absolute(EFF_ZERO);
if let Some(sol) = local_minimize(k, trim, &crv_ivl, uv_pt, tol_uv, guess_t)? {
let Ok(sol) = local_minimize(k, trim, &crv_ivl, uv_pt, tol_uv, guess_t)? else {
return Ok(UvContainment::of(PointContainment::Unknown));
};
if let Some(sol) = sol {
if sol.start.solution_value <= tol_uv {
return Ok(UvContainment::on(this_edge, sol.start.param(0)?));
}
@@ -837,6 +869,30 @@ mod tests {
)
}
#[test]
fn issue_2009_unevaluable_trim_is_unknown() {
// tangency-labs/smlib 7a35f33 (SmLoop::ContainsUVPoint): a trim point that
// cannot be evaluated is not classified (SM_POC_UNKNOWN); the port without the
// flag returns the evaluation's error (upstream reads it unset).
let mut b = square(plane(), true, &[]);
b.edgeuse_mut(EdgeuseId::from_index(3))
.unwrap()
.uv_trim_curve = Some(Curve::from(BSplineCurve::new(2)));
assert!(contains(&mut b.clone(), [0.0, 0.0]).is_err());
let mut k = Kernel::default();
k.fixes.query_statuses_pass_on = true;
let r = contains_uv_point(
&k,
&mut b,
LoopId::from_index(0),
None,
Point2::new(0.0, 0.0),
0.0,
)
.unwrap();
assert_eq!(r, UvContainment::of(PointContainment::Unknown));
}
#[test]
fn square_inside_outside_on() {
let mut b = square(plane(), true, &[]);
+74 -17
View File
@@ -99,6 +99,10 @@
//! `EvaluateSTEP`, `GetFace` and the base `CalculateBoundingBox`
//! (`surface.surface_p1`; the solver constructors also hand over the caches holding
//! the patch data of tree nodes).
//! - **No evidence (WR-663).** With the opt-in `Kernel::fixes.query_statuses_pass_on`,
//! `IsClosed` is `false` when an end, the continuity, the mid derivatives or the
//! Frenet frame cannot be evaluated, and the base `IsPlanar` is `false` without its
//! box, as the fork (tangency-labs/smlib 21dcf69).
use crate::curve::curve::{Curve, CurveKind};
use crate::error::{ignore_status, ignored, Result, SolidError};
@@ -539,6 +543,9 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
want_continuity: bool,
) -> Result<(bool, Option<ContinuityType>)> {
let discontinuous = want_continuity.then_some(ContinuityType::Discontinuous);
// with the opt-in fix a surface that cannot be evaluated is not shown closed
// (tangency-labs/smlib 21dcf69)
let not_shown = k.fixes.query_statuses_pass_on;
// locals
let count = if param == SurfParamType::Both { 2 } else { 1 };
@@ -601,32 +608,40 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
// evaluate isoParameter endPoints
if want_continuity {
if let Some(d) = ignored(evaluate_2nd_derivatives(
match ignored(evaluate_2nd_derivatives(
k,
surface,
uv_min,
Side::Left,
Side::Left,
))? {
d2_min = d;
Some(d) => d2_min = d,
None if not_shown => return Ok((false, discontinuous)),
None => (),
}
if let Some(d) = ignored(evaluate_2nd_derivatives(
match ignored(evaluate_2nd_derivatives(
k,
surface,
uv_max,
Side::Left,
Side::Left,
))? {
d2_max = d;
Some(d) => d2_max = d,
None if not_shown => return Ok((false, discontinuous)),
None => (),
}
p_min = d2_min.point;
p_max = d2_max.point;
} else {
if let Some(p) = ignored(surface.evaluate_point(k, uv_min))? {
p_min = p;
match ignored(surface.evaluate_point(k, uv_min))? {
Some(p) => p_min = p,
None if not_shown => return Ok((false, discontinuous)),
None => (),
}
if let Some(p) = ignored(surface.evaluate_point(k, uv_max))? {
p_max = p;
match ignored(surface.evaluate_point(k, uv_max))? {
Some(p) => p_max = p,
None if not_shown => return Ok((false, discontinuous)),
None => (),
}
}
@@ -648,7 +663,7 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
} else {
(d2_min.dv, d2_min.dvv, d2_max.dv, d2_max.dvv)
};
let curve_continuity = evaluate_curve_continuity_with(
let curve_continuity = match evaluate_curve_continuity_with(
&CurveEval {
position: p_min,
first: first_min,
@@ -665,7 +680,10 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
&ContinuityOptions {
geometric_g2: k.fixes.geometric_g2_continuity,
},
)?;
) {
Err(e) if not_shown && !e.is_unported() => return Ok((false, discontinuous)),
r => r?,
};
// save lowest found continuity
if (min_continuity as i32) > (curve_continuity as i32) {
@@ -676,7 +694,7 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
// extra test
if (min_continuity as i32) > (ContinuityType::Discontinuous as i32) {
// filter out very short curve passing as closed curves
if let Some(d) = ignored(evaluate_directional_derivs(
match ignored(evaluate_directional_derivs(
k,
surface,
uv_mid,
@@ -685,9 +703,13 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
Side::Left,
Side::Left,
))? {
for (dst, src) in p_mid.iter_mut().zip(d) {
*dst = src;
Some(d) => {
for (dst, src) in p_mid.iter_mut().zip(d) {
*dst = src;
}
}
None if not_shown => return Ok((false, discontinuous)),
None => (),
}
let p_mid0 = p_mid.first().copied().unwrap_or(Vec3::UNDEF);
@@ -704,6 +726,8 @@ pub fn base_is_closed<S: SurfaceEvaluate + ?Sized>(
for (dst, src) in g_mid.iter_mut().zip(g) {
*dst = src;
}
} else if not_shown {
return Ok((false, discontinuous));
} else {
for (dst, src) in g_mid.iter_mut().zip(p_mid.iter().take(2)) {
*dst = *src;
@@ -921,10 +945,18 @@ impl Surface {
&self.natural_uv_domain_with_nurb(k)?,
[false, true, false],
false,
))?
.and_then(|b| b.pseudo_box)
.unwrap_or_else(PseudoBox::new);
Ok(pseudo.is_planar(tol_3d))
))?;
// with the opt-in fix a surface that cannot be boxed is not shown planar
// (tangency-labs/smlib 21dcf69)
let not_shown = pseudo.is_none() && k.fixes.query_statuses_pass_on;
let pseudo = pseudo
.and_then(|b| b.pseudo_box)
.unwrap_or_else(PseudoBox::new);
let mut planar = pseudo.is_planar(tol_3d);
if not_shown {
planar.is_planar = false;
}
Ok(planar)
}
}
}
@@ -3085,6 +3117,31 @@ mod tests {
use crate::curve::hermite_curve::HermiteCurve;
use crate::surface::plane::Plane;
#[test]
fn issue_2009_unevaluable_surface_is_not_shown_closed() {
// tangency-labs/smlib 21dcf69 (SmSurface::IsClosed): a surface that cannot be
// evaluated is not shown closed. Coverage only: upstream's unset ends coincide
// but its short-curve test then rejects them too.
let s = Surface::from(BSplineSurface::default());
let dom = Extent2d::from_bounds(0.0, 0.0, 1.0, 1.0);
assert!(s
.evaluate_point(&Kernel::default(), Point2::new(0.0, 0.0))
.is_err());
let off = Kernel::default();
let mut on = Kernel::default();
on.fixes.query_statuses_pass_on = true;
let closed_off = s
.is_closed(&off, &dom, SurfParamType::U, None, false)
.unwrap()
.0;
let closed_on = s
.is_closed(&on, &dom, SurfParamType::U, None, false)
.unwrap()
.0;
assert!(!closed_on);
assert!(!closed_off);
}
/// tangency-labs/solidcore#525 (WR-384): with the corner points wanted, upstream's
/// `if (lRtn &= bit)` clears the other corners' bits (4 flat corners come back as
/// none, with only the first point); the opt-in tests each bit.
+78 -45
View File
@@ -80,8 +80,10 @@
//! the statuses upstream ignores ([`SolidError::Unported`] still propagates),
//! leaving the outputs of a failed evaluation at their `SM_UNDEF_DOUBLE` defaults.
//! With the opt-in `Kernel::fixes.topology_failed_evaluations_propagate` (WR-263,
//! WR-264, #521) they propagate evaluation failures instead of reading unwritten
//! outputs, and an unsuccessful point drop returns `SM_ERR`. `sm_AreSeamCurves`'
//! WR-264, #521) they answer as the fork (tangency-labs/smlib 666242b) instead of
//! reading unwritten outputs: a gap that cannot be measured (a failed evaluation or
//! drop status) is `SM_BIG_DOUBLE`, a distance without the derivatives or normal is
//! none. `sm_AreSeamCurves`'
//! unused edge use argument is dropped.
//! - **Unported callees.** `SmEdgeuse::EvaluateBinormal` is wired
//! (`topology.edgeuse_p3`); its deferred callees return [`SolidError::Unported`]. The gap computations are [`crate::heal::gap`];
@@ -103,7 +105,7 @@ use crate::geom::solution_array::{ObjectKind, Solution, SolutionObject};
use crate::geom::tol::{context_xsect_tol_3d, SmallTopoType, ZONE_TOL_3D};
use crate::heal::gap::{EdgeEdgeGap, EdgeFaceGap, GapFunction};
use crate::kernel::Kernel;
use crate::math::{Point2, Point3, Vec3, EFF_ZERO, EFF_ZERO_SQ};
use crate::math::{Point2, Point3, Vec3, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQ};
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
use crate::nurbs::knots::Side;
use crate::surface::surface::p1::evaluate_1st_derivatives;
@@ -1108,13 +1110,15 @@ pub fn calc_edge_ccw_edge_gap(
// ---- UV loop gap ------------------------------------------------------------------------
/// A curve point at `t`, `SM_UNDEF_DOUBLE` where upstream ignores a failed
/// evaluation (only [`SolidError::Unported`] propagates; with the opt-in
/// [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`] every error).
fn point_or_undef(k: &Kernel, r: Result<Point3>) -> Result<Point3> {
/// evaluation (only [`SolidError::Unported`] propagates); `None` with the opt-in
/// [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`], whose gap
/// cannot then be measured (tangency-labs/smlib 666242b).
fn point_or_undef(k: &Kernel, r: Result<Point3>) -> Result<Option<Point3>> {
let p = crate::error::ignored(r)?;
if k.fixes.topology_failed_evaluations_propagate {
return r;
return Ok(p);
}
Ok(crate::error::ignored(r)?.unwrap_or(Vec3::UNDEF))
Ok(Some(p.unwrap_or(Vec3::UNDEF)))
}
/// The edge interval of `eu`'s edge.
@@ -1133,8 +1137,8 @@ fn edge_interval(b: &Brep, eu: EdgeuseId, what: &'static str) -> Result<Extent1d
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] where
/// upstream dereferences null; [`SolidError::Unported`] for the point drop
/// (`SmSurface::DropPoint`, unit `surface.surface_p5`). With the opt-in
/// `Kernel::fixes.topology_failed_evaluations_propagate`, also curve and point-drop
/// errors, including `SM_ERR` when a drop does not converge.
/// `Kernel::fixes.topology_failed_evaluations_propagate`, a gap that cannot be measured
/// (a failed curve evaluation or point drop) is `SM_BIG_DOUBLE`, as the fork has it.
pub fn start_point_uv_loop_gap(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<f64> {
// check state - only Loopuse Edgeuses have End_to_End gaps
let r = b.edgeuse(eu)?;
@@ -1177,11 +1181,17 @@ pub fn start_point_uv_loop_gap(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<f6
.as_ref()
.ok_or_else(|| null("GetStartPointUVLoopGap: edge without curve"))
};
let start_3d = point_or_undef(k, curve_of(eu)?.evaluate_point(k, start_param))?;
let cw_end_3d = point_or_undef(k, curve_of(cw)?.evaluate_point(k, cw_end_param))?;
let Some(start_3d) = point_or_undef(k, curve_of(eu)?.evaluate_point(k, start_param))?
else {
return Ok(BIG_DOUBLE);
};
let Some(cw_end_3d) = point_or_undef(k, curve_of(cw)?.evaluate_point(k, cw_end_param))?
else {
return Ok(BIG_DOUBLE);
};
// drop point to Surface (statuses ignored, as upstream; with the opt-in fix a
// failed drop supplies neither a valid point nor the next guess)
// failed drop status is a gap that cannot be measured, `bSuccess` stays unread)
let fix = k.fixes.topology_failed_evaluations_propagate;
let domain = surface.natural_uv_domain_with_nurb(k)?;
let start_opts = DropPointOptions {
@@ -1190,13 +1200,10 @@ pub fn start_point_uv_loop_gap(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<f6
};
let start_drop = surface.drop_point::<NoObject>(k, start_3d, &domain, &start_opts);
let start_2d = if fix {
let d = start_drop?;
if !d.success {
return Err(SolidError::sm_err(
"topology::edgeuse::p1::start_point_uv_loop_gap",
));
match crate::error::ignored(start_drop)? {
Some(d) => d.result_uv,
None => return Ok(BIG_DOUBLE),
}
d.result_uv
} else {
// `sStart2d` stays default-constructed when the call fails
crate::error::ignored(start_drop)?.map_or(Point2::UNDEF, |d| d.result_uv)
@@ -1208,21 +1215,22 @@ pub fn start_point_uv_loop_gap(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<f6
};
let cw_drop = surface.drop_point::<NoObject>(k, cw_end_3d, &domain, &cw_opts);
let cw_end_2d = if fix {
let d = cw_drop?;
if !d.success {
return Err(SolidError::sm_err(
"topology::edgeuse::p1::start_point_uv_loop_gap",
));
match crate::error::ignored(cw_drop)? {
Some(d) => d.result_uv,
None => return Ok(BIG_DOUBLE),
}
d.result_uv
} else {
// upstream's DropPoint sets rResultUV to the guess before anything can fail
crate::error::ignored(cw_drop)?.map_or(start_2d, |d| d.result_uv)
};
return Ok(start_2d.distance(cw_end_2d));
};
let start_uv = point_or_undef(k, tc.evaluate_point(k, start_param))?;
let cw_end_uv = point_or_undef(k, cc.evaluate_point(k, cw_end_param))?;
let Some(start_uv) = point_or_undef(k, tc.evaluate_point(k, start_param))? else {
return Ok(BIG_DOUBLE);
};
let Some(cw_end_uv) = point_or_undef(k, cc.evaluate_point(k, cw_end_param))? else {
return Ok(BIG_DOUBLE);
};
// compute and return the EdgeEnd to EdgeEnd gap
Ok(start_uv.distance(cw_end_uv))
@@ -1481,8 +1489,8 @@ pub fn faceuse_collect_adjacent(
///
/// [`SolidError::Unported`] from the surface evaluations (their other failures are
/// ignored, as upstream). With the opt-in
/// `Kernel::fixes.topology_failed_evaluations_propagate` (WR-264), every error of the
/// surface evaluations: failed derivatives cannot supply a distance.
/// `Kernel::fixes.topology_failed_evaluations_propagate` (WR-264), failed derivatives or
/// a failed normal give no distance (`-1`, as the fork, tangency-labs/smlib 666242b).
pub fn calc_dist_and_dev<'s, O: SolutionObject>(
k: &Kernel,
pt: Point3,
@@ -1514,7 +1522,11 @@ pub fn calc_dist_and_dev<'s, O: SolutionObject>(
let uv = Point2::new(u, v);
let derivs = evaluate_1st_derivatives(k, srf, uv, Side::Left, Side::Left);
let (s_pt, s_su, s_sv) = if k.fixes.topology_failed_evaluations_propagate {
derivs?
// tangency-labs/smlib 666242b: no distance without the derivatives
match crate::error::ignored(derivs)? {
Some(d) => d,
None => return Ok((dist, Some(dev))),
}
} else {
crate::error::ignored(derivs)?.unwrap_or((Vec3::UNDEF, Vec3::UNDEF, Vec3::UNDEF))
};
@@ -1524,7 +1536,16 @@ pub fn calc_dist_and_dev<'s, O: SolutionObject>(
if mag > EFF_ZERO {
norm /= mag;
} else {
norm = srf.evaluate_normal(k, uv, Side::Left, Side::Left)?;
let n = srf.evaluate_normal(k, uv, Side::Left, Side::Left);
norm = if k.fixes.topology_failed_evaluations_propagate {
// tangency-labs/smlib 666242b: no distance without the normal
match crate::error::ignored(n)? {
Some(n) => n,
None => return Ok((dist, Some(dev))),
}
} else {
n?
};
if norm.length() < 0.5 {
return Ok((dist, Some(dev)));
}
@@ -2005,8 +2026,12 @@ mod tests {
}
#[test]
fn issue_521_loop_gap_refuses_failed_trim_and_edge_evaluations() {
fn issue_521_loop_gap_is_big_without_trim_and_edge_evaluations() {
// tangency-labs/smlib 666242b (SmEdgeuse::GetStartPointUVLoopGap): a gap that
// cannot be measured is SM_BIG_DOUBLE, no closure; upstream measures it from
// unset points.
use crate::nurbs::b_spline_curve::BSplineCurve;
let off = Kernel::default();
let mut k = Kernel::default();
k.fixes.topology_failed_evaluations_propagate = true;
let a0 = EdgeuseId::from_index(0);
@@ -2016,12 +2041,11 @@ mod tests {
b.edgeuse_mut(EdgeuseId::from_index(6))
.unwrap()
.uv_trim_curve = Some(uv_line([0.0, 1.0], [0.0, 0.0]));
let empty = Curve::from(BSplineCurve::new(2));
let expected = empty.evaluate_point(&k, 0.0).unwrap_err();
b.edgeuse_mut(EdgeuseId::from_index(bad))
.unwrap()
.uv_trim_curve = Some(empty);
assert_eq!(start_point_uv_loop_gap(&k, &b, a0), Err(expected));
.uv_trim_curve = Some(Curve::from(BSplineCurve::new(2)));
assert_eq!(start_point_uv_loop_gap(&k, &b, a0).unwrap(), BIG_DOUBLE);
assert_ne!(start_point_uv_loop_gap(&off, &b, a0).unwrap(), BIG_DOUBLE);
}
for bad in [0, 3] {
let mut b = square();
@@ -2030,10 +2054,10 @@ mod tests {
Some(Curve::from(crate::curve::line::Line::from_points(a, z, 3)));
}
assert_eq!(start_point_uv_loop_gap(&k, &b, a0).unwrap(), 0.0);
let empty = Curve::from(BSplineCurve::new(3));
let expected = empty.evaluate_point(&k, 0.0).unwrap_err();
b.edge_mut(EdgeId::from_index(bad)).unwrap().curve = Some(empty);
assert_eq!(start_point_uv_loop_gap(&k, &b, a0), Err(expected));
b.edge_mut(EdgeId::from_index(bad)).unwrap().curve =
Some(Curve::from(BSplineCurve::new(3)));
assert_eq!(start_point_uv_loop_gap(&k, &b, a0).unwrap(), BIG_DOUBLE);
assert_ne!(start_point_uv_loop_gap(&off, &b, a0).unwrap(), BIG_DOUBLE);
}
}
@@ -2142,19 +2166,28 @@ mod tests {
}
#[test]
fn issue_521_dist_and_dev_propagates_surface_evaluation_failure() {
fn issue_521_dist_and_dev_is_none_without_surface_derivatives() {
// tangency-labs/smlib 666242b (sm_CalcDistAndDev): no distance without the
// derivatives (rDist stays -1); upstream measures from unset ones.
let mut k = Kernel::default();
k.fixes.topology_failed_evaluations_propagate = true;
let s = Surface::from(crate::nurbs::b_spline_surface::BSplineSurface::default());
let mut sol = Solution::<H>::new();
sol.objects[0] = Some(H(ObjectKind::Surface));
sol.start.solution_value = 0.5;
let expected =
assert!(
evaluate_1st_derivatives(&k, &s, Point2::new(0.0, 0.0), Side::Left, Side::Left)
.unwrap_err();
.is_err()
);
assert_eq!(
calc_dist_and_dev(&k, Vec3::Z, &sol, |_| Some(&s), true),
Err(expected)
calc_dist_and_dev(&k, Vec3::Z, &sol, |_| Some(&s), true).unwrap(),
(-1.0, Some(-1.0))
);
// coverage only: upstream reaches -1 here as well (the unset derivatives
// vanish and the empty surface's normal is short)
assert_eq!(
calc_dist_and_dev(&Kernel::default(), Vec3::Z, &sol, |_| Some(&s), true).ok(),
Some((-1.0, Some(-1.0)))
);
assert_eq!(
calc_dist_and_dev(&k, Vec3::Z, &sol, |_| Some(&s), false).unwrap(),
+74 -10
View File
@@ -61,6 +61,10 @@
//! [`topo::notify`] (`SmTopology::Notify`) with the caller's records, attribute
//! store and context; it propagates errors upstream does not report (`Notify`
//! returns `void`).
//! - **No evidence (WR-663).** With the opt-in `Kernel::fixes.query_statuses_pass_on`,
//! `IsEdgeuseSideInSurface` answers `true` at a sample whose point or drop fails and
//! `HasDogLegNearPole` shows no dog-leg without the polygon or its angles, as the
//! fork (tangency-labs/smlib 666242b).
use crate::curve::crv_on_surf::CrvOnSurf;
use crate::curve::curve::Curve;
@@ -916,7 +920,9 @@ pub struct EdgeuseSide {
/// without curve (upstream dereferences null); those of `IsPeriodic`, the curve and
/// surface evaluations and `MapTo2d` that upstream does not ignore. The statuses of
/// the evaluations and of `DropPoint` are ignored, as upstream ([`SolidError::Unported`]
/// still propagates).
/// still propagates); with the opt-in
/// [`crate::kernel::UpstreamFixes::query_statuses_pass_on`] a sample whose point or drop
/// fails answers `true` (its "cannot tell", tangency-labs/smlib 666242b).
pub fn is_edgeuse_side_in_surface(
k: &Kernel,
b: &Brep,
@@ -1008,16 +1014,23 @@ pub fn is_edgeuse_side_in_surface(
};
let param = edge_interval.evaluate(norm_param);
// Edge->3d Point (status ignored: a failed evaluation leaves the last point)
if let Some(p) = ignored(edge_curve.evaluate_point(k, param))? {
edge_point = p;
// Edge->3d Point (status ignored: a failed evaluation leaves the last point;
// with the opt-in fix a sample that cannot be evaluated or dropped gives no
// evidence, TRUE, as the fork has it: tangency-labs/smlib 666242b)
let no_evidence = k.fixes.query_statuses_pass_on;
match ignored(edge_curve.evaluate_point(k, param))? {
Some(p) => edge_point = p,
None if no_evidence => return Ok(out),
None => (),
}
// next Drop GuessUV = UVTrimCurve(dParam) or last Drop point
let mut guess = None;
if let Some(trim) = uv_trim_curve {
if let Some(p) = ignored(trim.evaluate_point(k, param))? {
uv_trim_curve_3d = p;
match ignored(trim.evaluate_point(k, param))? {
Some(p) => uv_trim_curve_3d = p,
None if no_evidence => return Ok(out),
None => (),
}
guess = Some(Point2::new(uv_trim_curve_3d.x, uv_trim_curve_3d.y));
} else if ii > 0 {
@@ -1037,6 +1050,7 @@ pub fn is_edgeuse_side_in_surface(
// only by a successful drop
Some(d) if d.success || guess.is_some() => uv_drop_point = d.result_uv,
Some(_) => (),
None if no_evidence => return Ok(out),
None => {
if let Some(g) = guess {
uv_drop_point = g;
@@ -1196,7 +1210,13 @@ pub fn has_dog_leg_near_pole(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<DogL
))
}
};
let control_points = base.polygon(&base.natural_interval())?;
// with the opt-in fix, without the polygon or the angles the edge use is not shown
// to dog-leg (tangency-labs/smlib 666242b); upstream's polygon error is the port's
let no_evidence = k.fixes.query_statuses_pass_on;
let control_points = match base.polygon(&base.natural_interval()) {
Err(e) if no_evidence && !e.is_unported() => return Ok(out),
r => r?,
};
let num = control_points.len();
let uv = |i: usize| -> Result<Vec2> {
control_points
@@ -1292,13 +1312,22 @@ pub fn has_dog_leg_near_pole(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<DogL
// Angle in Control Polygon at target ControlPoint (a zero vector
// gives 0, as upstream)
let ang_vec_rad = last_vec.angle_between(next_vec).unwrap_or(0.0);
let ang_vec_rad = match last_vec.angle_between(next_vec) {
Err(_) if no_evidence => return Ok(out),
r => r.unwrap_or(0.0),
};
// Angles between ControlPolygon Next/Last Vecs and the
// singularity direction (upstream sets [1 0 0] for both checks)
let sing_vec = Vec3::new(1.0, 0.0, 0.0);
let ang_last_pole_rad = last_vec.angle_between(sing_vec).unwrap_or(0.0);
let ang_next_pole_rad = next_vec.angle_between(sing_vec).unwrap_or(0.0);
let ang_last_pole_rad = match last_vec.angle_between(sing_vec) {
Err(_) if no_evidence => return Ok(out),
r => r.unwrap_or(0.0),
};
let ang_next_pole_rad = match next_vec.angle_between(sing_vec) {
Err(_) if no_evidence => return Ok(out),
r => r.unwrap_or(0.0),
};
let ang_last_pole_deg = rad_to_deg(ang_last_pole_rad);
let ang_next_pole_deg = rad_to_deg(ang_next_pole_rad);
@@ -1373,6 +1402,41 @@ mod tests {
(b, id)
}
// tangency-labs/smlib 666242b (SmEdgeuse::IsEdgeuseSideInSurface): a sample whose
// point cannot be evaluated gives no evidence, `TRUE`, as for what it cannot
// classify; upstream drops the unset point and classifies it. Coverage only: a
// box's planes have no natural boundary, so upstream answers `TRUE` as well.
#[test]
fn issue_2009_side_in_surface_without_sample_points_is_true() {
let mut k = Kernel::default();
let b0 = crate::primitive::api_primitives::create_box(&mut k, Vec3::ZERO, 1.0, 2.0, 3.0)
.unwrap();
let mut on_k = Kernel::default();
on_k.fixes.query_statuses_pass_on = true;
for eu in b0.edgeuses().ids() {
if face(&b0, eu).unwrap().is_none() {
continue;
}
let mut b = b0.clone();
let e = edge(&b, eu).unwrap().unwrap();
b.edge_mut(e).unwrap().curve = Some(Curve::from(
crate::nurbs::b_spline_curve::BSplineCurve::new(3),
));
let on = is_edgeuse_side_in_surface(&on_k, &b, eu, 5, None, None).unwrap();
assert_eq!(
on,
EdgeuseSide {
in_surface: true,
on_natural_boundary: true
}
);
assert_eq!(
is_edgeuse_side_in_surface(&k, &b, eu, 5, None, None).unwrap(),
on
);
}
}
#[test]
fn tangent_sector_without_edge_is_false() {
let mut b = Brep::new();
+83 -47
View File
@@ -78,6 +78,13 @@
//! - **Recursion.** None: `EvaluateBinormal` calls `CreateUVTrimCurve` and
//! `CreateUVTrimCurve` calls `EvaluateBinormal` only with `bUse3DOnly = TRUE`, so
//! the depth is bounded by one level (as upstream comments).
//! - **Passed-on statuses (WR-663).** With the opt-in
//! `Kernel::fixes.query_statuses_pass_on`, the statuses the fork passes on
//! (tangency-labs/smlib 666242b: `CreateUVTrimCurve`'s box, seam transforms, 2D
//! conversions, trims, end evaluations and drops, `NormalizedEvaluate`;
//! `EvaluateBinormal`'s `GetOrCreateUVTrimCurve`, derivatives, normal and
//! `ComputeParamDirection`; `NormalizedEvaluate`'s derivatives) are returned instead of
//! ignored. `GetEnds` stays ignored, as in the fork.
use crate::curve::crv_on_surf::CrvOnSurf;
use crate::curve::curve::{Curve, CurveKind};
@@ -124,6 +131,13 @@ fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
/// A status SMLib drops and the fork passes on (`SER`, tangency-labs/smlib 666242b):
/// returned with the opt-in [`crate::kernel::UpstreamFixes::query_statuses_pass_on`],
/// handed on to the site's own ignore otherwise.
fn pass<T>(k: &Kernel, r: Result<T>) -> Result<Result<T>> {
crate::error::passed_on(r, k.fixes.query_statuses_pass_on)
}
/// The status of `SmVector3d::AngleBetween` of a zero-length vector, which upstream
/// checks with `SER`: `SM_ERR_INVALID_INPUT` (1007), a failing status, not a null
/// dereference.
@@ -341,12 +355,15 @@ fn crv_on_surf_length(k: &Kernel, surface: &Surface, uv_curve: &Curve) -> Result
fn seam_curves(k: &Kernel, uv_curve: &Curve, delta: Vec3) -> Result<Vec<Curve>> {
let mut other = uv_curve.clone();
let mut axis = Axis2Placement::default();
let _ = ignored(axis.set_canonical(delta, Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)))?;
let _ = ignored(other.transform(k, &axis, None))?;
let _ = ignored(other.convert_to_2d(k))?;
let _ = ignored(pass(
k,
axis.set_canonical(delta, Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)),
)?)?;
let _ = ignored(pass(k, other.transform(k, &axis, None))?)?;
let _ = ignored(pass(k, other.convert_to_2d(k))?)?;
let mut copy = uv_curve.clone();
let _ = ignored(copy.convert_to_2d(k))?;
let _ = ignored(pass(k, copy.convert_to_2d(k))?)?;
Ok(vec![other, copy])
}
@@ -442,16 +459,19 @@ pub fn create_uv_trim_curve(
None => (Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)),
};
let uv_box = ignored(uv_curve.calculate_bounding_box(
let uv_box = ignored(pass(
k,
&uv_curve.natural_interval(),
BoundingBoxRequest {
normal: true,
pseudo: false,
polar: false,
},
true,
))?
uv_curve.calculate_bounding_box(
k,
&uv_curve.natural_interval(),
BoundingBoxRequest {
normal: true,
pseudo: false,
polar: false,
},
true,
),
)?)?
.and_then(|r| r.normal)
.unwrap_or_default();
@@ -501,7 +521,7 @@ pub fn create_uv_trim_curve(
} else {
// Not a seam, so just copy the curve
let mut copy = uv_curve.clone();
let _ = ignored(copy.convert_to_2d(k))?;
let _ = ignored(pass(k, copy.convert_to_2d(k))?)?;
// SM_SET_CREATE_UVTRIMCURVE_FAIL(SM_SUCCESS, SM_CTC_FROM_CRVONSURF)
set_create_fail(
k,
@@ -626,7 +646,10 @@ pub fn create_uv_trim_curve(
// Have to trim this, otherwise the reversals mess things up. [081031]
// (may snap sIvl by tol to existing knots; status ignored)
let _ = ignored(rev_curve.trim(k, &mut edge_domain, TrimMode::Notify))?;
let _ = ignored(pass(
k,
rev_curve.trim(k, &mut edge_domain, TrimMode::Notify),
)?)?;
let new_ivl = rev_curve.reverse_parameterization(k, &edge_domain)?;
// drop reverse curve to surface (DropCurve's default keep-all TRUE)
@@ -875,9 +898,9 @@ pub fn create_uv_trim_curve(
// Get the 3D end points of the Drop Curve
let drop_curve_start_3d =
ignored(surface.evaluate_point(k, uv_start_2d))?.unwrap_or(Vec3::UNDEF);
ignored(pass(k, surface.evaluate_point(k, uv_start_2d))?)?.unwrap_or(Vec3::UNDEF);
let drop_curve_end_3d =
ignored(surface.evaluate_point(k, uv_end_2d))?.unwrap_or(Vec3::UNDEF);
ignored(pass(k, surface.evaluate_point(k, uv_end_2d))?)?.unwrap_or(Vec3::UNDEF);
// Get the local XSect Tolerances
// (`SmTol::GetXSectTol3d(pEdge, pVertex)` takes the system zone tolerance for a
@@ -931,7 +954,7 @@ pub fn create_uv_trim_curve(
let mut new_ivl = Extent1d::from_min_max(edge_domain.min(), uv_ivl.max());
if let Some(c) = uv_curves.first_mut() {
// may snap sIvl by tol to existing knots
let _ = ignored(c.trim(k, &mut new_ivl, TrimMode::Notify))?;
let _ = ignored(pass(k, c.trim(k, &mut new_ivl, TrimMode::Notify))?)?;
}
fail.create_path_type =
CreateUvTrimCurvePathType::OnePercentUvTrimCurveTrimRecovery1;
@@ -955,7 +978,7 @@ pub fn create_uv_trim_curve(
let mut new_ivl = Extent1d::from_min_max(uv_ivl.min(), edge_domain.max());
if let Some(c) = uv_curves.first_mut() {
// may snap sIvl by tol to existing knots
let _ = ignored(c.trim(k, &mut new_ivl, TrimMode::Notify))?;
let _ = ignored(pass(k, c.trim(k, &mut new_ivl, TrimMode::Notify))?)?;
}
fail.create_path_type =
CreateUvTrimCurvePathType::OnePercentUvTrimCurveTrimRecovery2;
@@ -1137,7 +1160,7 @@ fn trim_other_uv_curves(
}
if let Some(c) = b.edgeuse_mut(x)?.uv_trim_curve.as_mut() {
// may snap sIvl by tol to existing knots
let _ = ignored(c.trim(&tk, new_ivl, TrimMode::Notify))?;
let _ = ignored(pass(k, c.trim(&tk, new_ivl, TrimMode::Notify))?)?;
}
}
Ok(())
@@ -1167,25 +1190,23 @@ fn two_singular_end_points(
let surface_uv_domain = surface.natural_uv_domain_with_nurb(k)?;
// Curve EndPoints
let start_point = ignored(curve.evaluate_point(k, edge_domain.min()))?.unwrap_or(Vec3::UNDEF);
let end_point = ignored(curve.evaluate_point(k, edge_domain.max()))?.unwrap_or(Vec3::UNDEF);
let start_point =
ignored(pass(k, curve.evaluate_point(k, edge_domain.min()))?)?.unwrap_or(Vec3::UNDEF);
let end_point =
ignored(pass(k, curve.evaluate_point(k, edge_domain.max()))?)?.unwrap_or(Vec3::UNDEF);
// drop curve endpoints onto surface
let start_drop = ignored(surface_drop_point(
let start_drop = ignored(pass(
k,
surface,
start_point,
&surface_uv_domain,
))?;
surface_drop_point(k, surface, start_point, &surface_uv_domain),
)?)?;
if let Some(d) = start_drop.filter(|d| d.found) {
start_singularity = surface_is_singularity(k, surface, d.uv)?.0;
if start_singularity {
let end_drop = ignored(surface_drop_point(
let end_drop = ignored(pass(
k,
surface,
end_point,
&surface_uv_domain,
))?;
surface_drop_point(k, surface, end_point, &surface_uv_domain),
)?)?;
if let Some(d) = end_drop.filter(|d| d.found) {
end_singularity = surface_is_singularity(k, surface, d.uv)?.0;
}
@@ -1207,8 +1228,14 @@ fn two_singular_end_points(
// trim curves to their half of the original curve (may snap sIvl by tol to
// existing knots)
let _ = ignored(first_curve.trim(k, &mut first_half, TrimMode::Notify))?;
let _ = ignored(second_curve.trim(k, &mut second_half, TrimMode::Notify))?;
let _ = ignored(pass(
k,
first_curve.trim(k, &mut first_half, TrimMode::Notify),
)?)?;
let _ = ignored(pass(
k,
second_curve.trim(k, &mut second_half, TrimMode::Notify),
)?)?;
// reverse the parameterization of the 1st half
let new_interval = first_curve.reverse_parameterization(k, &first_half)?;
@@ -1301,7 +1328,10 @@ fn pick_seam_curve(
} else {
1.0 - 0.48765
};
let _ = ignored(normalized_evaluate(k, b, eu, mid_param, false, false, true))?;
let _ = ignored(pass(
k,
normalized_evaluate(k, b, eu, mid_param, false, false, true),
)?)?;
// evaluate NearMid 1st UVCurve Point clamped to face domain (for seams: coincident
// to sMidPoint, for crossing curves: not). Use the uv curve's domain, not the
@@ -1545,7 +1575,7 @@ fn brute_force(
false,
ParamMethod::ChordLength,
)?;
let _ = ignored(new_curve.convert_to_2d())?;
let _ = ignored(pass(k, new_curve.convert_to_2d())?)?;
// clean up Surface Cache side-effect of attaching 2nd UVTrimCurve
if !uv_trim_at_start && uv_trim_curve(b, eu)?.0.is_some() {
@@ -1916,7 +1946,8 @@ pub fn evaluate_binormal_uv(
// (EvaluateBinormal), which would seem to invite infinite recursion. However,
// that method calls this one only with bUse3DOnly set to TRUE, so this will
// never happen. (status ignored)
let _ = get_or_create_uv_trim_curve(k, b, eu, false, false, true)?;
let fetch = get_or_create_uv_trim_curve(k, b, eu, false, false, true)?;
let _ = ignored(pass(k, fetch.status)?)?;
}
let uv_trim: Option<Curve> = uv_trim_curve(b, eu)?.0.cloned();
@@ -1945,9 +1976,10 @@ pub fn evaluate_binormal_uv(
// Allow for the 3d curve point being off the surface. (It happens.) Check
// the dist from the 3d curve point to the surface normal.
// (status ignored)
let (this_srf_pt, this_su, this_sv) = ignored(
let (this_srf_pt, this_su, this_sv) = ignored(pass(
k,
surface.evaluate_1st_derivatives(k, uv, false, false),
)?
)?)?
.unwrap_or((Vec3::UNDEF, Vec3::UNDEF, Vec3::UNDEF));
// try a cheap eval for surface normal
@@ -1958,12 +1990,15 @@ pub fn evaluate_binormal_uv(
} else {
// when cheap is degenerate - try expensive normal eval (status ignored;
// sThisNorm is returned unitized)
if let Some(n) = ignored(surface.evaluate_normal(
if let Some(n) = ignored(pass(
k,
uv,
crate::nurbs::knots::Side::Right,
crate::nurbs::knots::Side::Right,
))? {
surface.evaluate_normal(
k,
uv,
crate::nurbs::knots::Side::Right,
crate::nurbs::knots::Side::Right,
),
)?)? {
this_norm = n;
}
}
@@ -2192,7 +2227,7 @@ pub fn evaluate_binormal_uv(
// store optional output values (ComputeParamDirection: status ignored)
let uv_vector = if options.want_uv_vector {
ignored(surface.compute_param_direction(k, uv, binormal))?
ignored(pass(k, surface.compute_param_direction(k, uv, binormal))?)?
} else {
None
};
@@ -2332,8 +2367,9 @@ pub fn normalized_evaluate(
let mut first_deriv = None;
if let Some(d3) = three_d.first_deriv {
// (status ignored)
let (s_su, s_sv) = ignored(surf.evaluate_1st_derivatives(k, uv, false, false))?
.map_or((Vec3::UNDEF, Vec3::UNDEF), |(_, su, sv)| (su, sv));
let (s_su, s_sv) =
ignored(pass(k, surf.evaluate_1st_derivatives(k, uv, false, false))?)?
.map_or((Vec3::UNDEF, Vec3::UNDEF), |(_, su, sv)| (su, sv));
first_deriv = Some(match vec_lin_comb_two_vectors(s_su, s_sv, d3) {
Ok(d) => Vec3::new(d.x, d.y, 0.0),
Err(e) if e.is_unported() => return Err(e),
+62 -46
View File
@@ -201,6 +201,13 @@ use crate::error::SM_ERR_ASSERT_FAILURE;
/// merge into the V merges of the faces it made).
pub const MAX_SPLIT_AT_SEAM_DEPTH: usize = 8;
/// A status SMLib drops and the fork passes on (`SER`, tangency-labs/smlib 6997962):
/// returned with the opt-in [`crate::kernel::UpstreamFixes::query_statuses_pass_on`],
/// handed on to the site's own ignore otherwise.
fn pass<T>(k: &Kernel, r: Result<T>) -> Result<Result<T>> {
crate::error::passed_on(r, k.fixes.query_statuses_pass_on)
}
fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
@@ -1466,7 +1473,10 @@ fn swap_uv_notifying(
let x_axis = Vec3::new(1.0, 1.0, 0.0).unitize()?;
let mut mirror = Axis2Placement::default();
// x:[1 1 0] y:[0 0 1] z=cross(x,y):[1 -1 0] (status ignored)
let _ = mirror.set_canonical(Point3::new(0.0, 0.0, 0.0), x_axis, Vec3::new(0.0, 0.0, 1.0));
let _ = pass(
k,
mirror.set_canonical(Point3::new(0.0, 0.0, 0.0), x_axis, Vec3::new(0.0, 0.0, 1.0)),
)?;
// Mirror the UV trims and swap the surface (the steps that can fail) before changing
// the face, so that a failure leaves the face unchanged.
@@ -1674,11 +1684,12 @@ fn shrink_geometry_notifying(
// Trim the surface (status ignored)
if let Some(s) = b.face_mut(f)?.surface.as_mut() {
ignore_status(s.trim_with_domain_with(k, &mut new_domain, trim))?;
ignore_status(pass(k, s.trim_with_domain_with(k, &mut new_domain, trim))?)?;
}
// remove all associated UVTrimCurves (status ignored)
ignore_status(remove_uv_trim_curves_notifying(b, f, notify))?;
// remove all associated UVTrimCurves (status ignored; passed on with the opt-in fix,
// tangency-labs/smlib 6997962, which keeps the curve trims below ignored)
ignore_status(pass(k, remove_uv_trim_curves_notifying(b, f, notify))?)?;
// Now Trim all Curves to the face edges
for e in face_edges(b, f)? {
@@ -1774,26 +1785,23 @@ fn sample_uv(
param: f64,
pv: &mut [Vec3; 2],
) -> Result<Point2> {
if let Some(v) = ignored(evaluate_geometric(k, curve, param, 1, true))? {
if let Some(v) = ignored(pass(k, evaluate_geometric(k, curve, param, 1, true))?)? {
if let [p, d, ..] = v.as_slice() {
*pv = [*p, *d];
}
}
let mut uv = Vec2::UNDEF;
if let Some(r) =
ignored(surface.drop_point::<NoObject>(k, pv[0], uv_domain, &DropPointOptions::default()))?
{
if let Some(r) = ignored(pass(
k,
surface.drop_point::<NoObject>(k, pv[0], uv_domain, &DropPointOptions::default()),
)?)? {
uv = r.result_uv;
}
// DropVectors of the tangent (its output is never read)
ignore_status(drop_vectors_at(
ignore_status(pass(
k,
surface,
uv,
Side::Left,
Side::Left,
&pv[1..],
))?;
drop_vectors_at(k, surface, uv, Side::Left, Side::Left, &pv[1..]),
)?)?;
Ok(uv)
}
@@ -1803,7 +1811,7 @@ fn vertexuse_uv(k: &Kernel, b: &mut Brep, vu: VertexuseId, prev: Point2) -> Resu
if let Some(Some(gap)) = ignored(vu1::vertex_face_gap(k, b, vu, false))? {
return Ok(gap.face_uv);
}
Ok(ignored(vu1::compute_uv_point(k, b, vu, false))?.unwrap_or(prev))
Ok(ignored(pass(k, vu1::compute_uv_point(k, b, vu, false))?)?.unwrap_or(prev))
}
/// `MoveSeam`: on a periodic B-spline surface whose seam the face's edges cross (or
@@ -1903,9 +1911,9 @@ pub fn move_seam_notified<O: LoopObject>(
let ivl_u = uv_domain.u_interval();
let ivl_v = uv_domain.v_interval();
let mut periodic_u = PeriodicExtent1d::full(ivl_u.length());
let _ = periodic_u.set_min_max(0.0, 0.0, None, true);
let _ = pass(k, periodic_u.set_min_max(0.0, 0.0, None, true))?;
let mut periodic_v = PeriodicExtent1d::full(ivl_v.length());
let _ = periodic_v.set_min_max(0.0, 0.0, None, true);
let _ = pass(k, periodic_v.set_min_max(0.0, 0.0, None, true))?;
// check State - SmBSplineSurfaces only
if !surface0.is_kind_of(B_SPLINE_SURFACE_TYPE)? {
@@ -1994,7 +2002,7 @@ pub fn move_seam_notified<O: LoopObject>(
&mut periodic_v
};
let s = comp(uv_prev, ii) - ivl.min();
let _ = periodic.set_min_max(s, s, None, true);
let _ = pass(k, periodic.set_min_max(s, s, None, true))?;
}
'loops: for (j0, &lu) in loopuses.iter().enumerate() {
@@ -2183,7 +2191,7 @@ pub fn move_seam_notified<O: LoopObject>(
let mut dist = BIG_DOUBLE;
let mut this_ivl = periodic;
this_ivl.set_full_period(false);
let knots = ignored(surface.knots_with_nurb(k, surf_param, None))?
let knots = ignored(pass(k, surface.knots_with_nurb(k, surf_param, None))?)?
.map(|(k, _)| k)
.unwrap_or_default();
let k0 = knots.first().copied().unwrap_or(0.0);
@@ -2193,7 +2201,7 @@ pub fn move_seam_notified<O: LoopObject>(
if this_dist < dist {
let lo = periodic.map_value(this_knot - ivl.min() - far);
let hi = periodic.map_value(this_knot - ivl.min() + far);
let _ = this_ivl.set_min_max(lo, hi, None, false);
let _ = pass(k, this_ivl.set_min_max(lo, hi, None, false))?;
if periodic.are_disjoint(&this_ivl) {
best = Some(jj);
dist = this_dist;
@@ -2225,7 +2233,7 @@ pub fn move_seam_notified<O: LoopObject>(
let mut joined = Surface::from(joined);
if surface_is_analytic {
if let SurfaceKind::BSpline(j) = joined.kind() {
if let Some(a) = ignored(j.copy_and_add_analytics(k))? {
if let Some(a) = ignored(pass(k, j.copy_and_add_analytics(k))?)? {
if a.is_kind_of(B_SPLINE_SURFACE_TYPE)? {
joined = a;
}
@@ -2287,18 +2295,22 @@ pub fn move_seam_notified<O: LoopObject>(
.unwrap_or(false)
{
// status ignored upstream
if let Some(s) = ignored(split_at_seam_depth(
k,
b,
env,
f,
crv_class_u,
crv_class_v,
problem,
props,
resolve,
0,
))? {
let pass_on = k.fixes.query_statuses_pass_on;
if let Some(s) = ignored(crate::error::passed_on(
split_at_seam_depth(
k,
b,
env,
f,
crv_class_u,
crv_class_v,
problem,
props,
resolve,
0,
),
pass_on,
)?)? {
out.split = s;
}
}
@@ -2578,18 +2590,22 @@ fn split_at_seam_merges<O: LoopObject>(
))?
.unwrap_or(false);
if has {
let s = ignored(split_at_seam_depth(
k,
b,
env.as_deref_mut(),
f,
&mut cu,
&mut cv,
this_problem,
props,
resolve,
depth + 1,
))?
let pass_on = k.fixes.query_statuses_pass_on;
let s = ignored(crate::error::passed_on(
split_at_seam_depth(
k,
b,
env.as_deref_mut(),
f,
&mut cu,
&mut cv,
this_problem,
props,
resolve,
depth + 1,
),
pass_on,
)?)?
.unwrap_or_default();
out.faces.extend_from_slice(&s.faces);
if tgt != f && !out.faces.contains(&tgt) {
+90 -49
View File
@@ -60,6 +60,9 @@
//! leaves the caller's default point ([`Point2::UNDEF`]); a failed `DropPoint` is
//! no drop point; a failed evaluation keeps the previous value (initially
//! [`Vec3::UNDEF`]).
//! With the opt-in `Kernel::fixes.query_statuses_pass_on` (WR-663) `sm_AreConnected`
//! shows ends whose UV points cannot be computed not connected, as the fork
//! (tangency-labs/smlib 6997962).
//! - **`sm_FixUVTrimCurves` opt-in fix** (WR-255, tangency-labs/solidcore#959/#570, the
//! fork's tangency-labs/smlib@5af7070). Upstream drops the vertex's UV point onto
//! each of the two curves, `Trim`s the curve there and `EditParameterization`s it back
@@ -210,6 +213,13 @@ pub const fn is_kind_of(t: i64) -> bool {
}
}
/// A status SMLib drops and the fork passes on (`SER`, tangency-labs/smlib 6997962):
/// returned with the opt-in [`crate::kernel::UpstreamFixes::query_statuses_pass_on`],
/// handed on to the site's own ignore otherwise.
fn pass<T>(k: &Kernel, r: Result<T>) -> Result<Result<T>> {
crate::error::passed_on(r, k.fixes.query_statuses_pass_on)
}
fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
@@ -932,7 +942,8 @@ fn normalized_evaluate_uv(k: &Kernel, b: &mut Brep, eu: EdgeuseId, t: f64) -> Re
/// `pEU->GetOrCreateUVTrimCurve(rpUVCurve)` (its status is ignored; the curve is then
/// dereferenced).
fn get_or_create_uv_trim_curve(k: &Kernel, b: &mut Brep, eu: EdgeuseId) -> Result<Curve> {
let _ = eu3::get_or_create_uv_trim_curve(k, b, eu, false, false, true)?;
let fetch = eu3::get_or_create_uv_trim_curve(k, b, eu, false, false, true)?;
let _ = pass(k, fetch.status)?;
eu1::uv_trim_curve(b, eu)?
.0
.cloned()
@@ -1147,9 +1158,19 @@ pub fn are_connected(
let vu2 =
eu1::vertexuse(b, eu2)?.ok_or_else(|| null("AreConnected: edge use without vertex use"))?;
// FALSE = don't make UVTrimCurve, if missing use GlobalSolve (statuses ignored)
let uv1 = ignored(vu1::compute_uv_point(k, b, vu1, false))?.unwrap_or(Point2::UNDEF);
let uv2 = ignored(vu1::compute_uv_point(k, b, vu2, false))?.unwrap_or(Point2::UNDEF);
// FALSE = don't make UVTrimCurve, if missing use GlobalSolve (statuses ignored; with
// the opt-in fix ends whose UV points cannot be computed are not shown connected,
// tangency-labs/smlib 6997962)
let uv1 = ignored(vu1::compute_uv_point(k, b, vu1, false))?;
if uv1.is_none() && k.fixes.query_statuses_pass_on {
return Ok(out);
}
let uv2 = ignored(vu1::compute_uv_point(k, b, vu2, false))?;
if uv2.is_none() && k.fixes.query_statuses_pass_on {
return Ok(out);
}
let uv1 = uv1.unwrap_or(Point2::UNDEF);
let uv2 = uv2.unwrap_or(Point2::UNDEF);
let gap_u = (uv2.x - uv1.x).abs();
let gap_v = (uv2.y - uv1.y).abs();
@@ -1439,16 +1460,20 @@ fn internal_point<B: QueryBrep>(k: &Kernel, b: B, f: FaceId) -> Result<Point3> {
(Point2::new(u, v_min), Point2::new(u, v_max))
};
// classify the target UVLine (status ignored)
ignore_status(B::classify_uv_line(
// classify the target UVLine (status ignored; passed on with the opt-in fix,
// tangency-labs/smlib 6997962)
ignore_status(pass(
k,
f,
&mut uv_objects,
start,
end,
src_zone_tol_3d,
&mut classification,
))?;
B::classify_uv_line(
k,
f,
&mut uv_objects,
start,
end,
src_zone_tol_3d,
&mut classification,
),
)?)?;
// when intervals were found in the face
for ivl in classification.intervals() {
@@ -1599,18 +1624,17 @@ pub fn points_in_face(
// set search range to face boundary domain
// (status ignored upstream: on a failure the output is still the face's UV
// domain, which CalculateUVDomainFromUVTrimCurves sets first)
let trim_uv_domain = match calculate_uv_domain_from_uv_trim_curves(k, b, f) {
let trim_uv_domain = match pass(k, calculate_uv_domain_from_uv_trim_curves(k, b, f))? {
Ok(d) => d,
Err(e) if e.is_unported() => return Err(e),
Err(_) => b.face(f)?.uv_domain,
};
let srf_dom = surf.natural_uv_domain_with_nurb(k)?;
// (SmExtent2d::Intersect leaves the output unchanged when disjoint)
let uv_dom = srf_dom
.intersect(&trim_uv_domain)
.unwrap_or_else(|_| Extent2d::new());
let uv_dom =
pass(k, srf_dom.intersect(&trim_uv_domain))?.unwrap_or_else(|_| Extent2d::new());
// SetMinMax status ignored
let _ = u_interval.set_min_max(uv_dom.min().x, uv_dom.max().x);
let _ = pass(k, u_interval.set_min_max(uv_dom.min().x, uv_dom.max().x))?;
// create isoParameter UVTrimCurve - we'll keep reusing it until done
let uv_start = dom.evaluate(u_inc, 0.0);
@@ -1681,8 +1705,7 @@ pub fn points_in_face(
let v_dither = if point_count > 1 { rand_dither(k) } else { 0.0 };
let v_norm = ((kk + 1) as f64 + v_dither) / ((smp_cnt + 1) as f64);
// status ignored: the point keeps its default value
let uv_point = crv
.evaluate_point(k, ivl.interval.evaluate(v_norm))
let uv_point = pass(k, crv.evaluate_point(k, ivl.interval.evaluate(v_norm)))?
.unwrap_or(Vec3::UNDEF);
out.uv.push(Point2::new(*u_param, uv_point.y));
}
@@ -1694,10 +1717,13 @@ pub fn points_in_face(
let mut pnt = Vec3::UNDEF;
let mut nrm = Vec3::UNDEF;
for &uv in &out.uv {
if let Some(p) = ignored(surf.evaluate_point(k, uv))? {
if let Some(p) = ignored(pass(k, surf.evaluate_point(k, uv))?)? {
pnt = p;
}
if let Some(n) = ignored(surf.evaluate_normal(k, uv, Side::Left, Side::Left))? {
if let Some(n) = ignored(pass(
k,
surf.evaluate_normal(k, uv, Side::Left, Side::Left),
)?)? {
nrm = n;
}
out.pv.push(pnt);
@@ -2944,24 +2970,33 @@ pub fn fix_uv_trim_curves_notified(
.ok_or_else(|| null("FixUVTrimCurves: edge use without vertex use"))?;
let uv_vertex = match vu1::vertex_face_gap(k, b, vu, false)? {
Some(gap) => gap.face_uv,
None => ignored(vu1::compute_uv_point(k, b, vu, true))?.unwrap_or(Point2::UNDEF),
None => ignored(pass(k, vu1::compute_uv_point(k, b, vu, true))?)?.unwrap_or(Point2::UNDEF),
};
let uv_vertex_3d = Point3::new(uv_vertex.x, uv_vertex.y, 0.0);
// project Vertex drop point to 3d through surface (statuses ignored)
let surf = surface_deref(b, curr_face)?;
let surf_vertex = ignored(surf.evaluate_point(k, uv_vertex))?.unwrap_or(Vec3::UNDEF);
let _ = ignored(surf.evaluate_normal(k, uv_vertex, Side::Left, Side::Left))?;
let surf_vertex = ignored(pass(k, surf.evaluate_point(k, uv_vertex))?)?.unwrap_or(Vec3::UNDEF);
let _ = ignored(pass(
k,
surf.evaluate_normal(k, uv_vertex, Side::Left, Side::Left),
)?)?;
// UVGap sizes
let uv_curr_edge_gap = uv_vertex_3d.distance(uv_curr);
let uv_prev_edge_gap = uv_vertex_3d.distance(uv_prev);
// edge end points (statuses ignored)
let curr_edge_point =
ignored(edge_curve(b, curr_edge)?.evaluate_point(k, t_curr))?.unwrap_or(Vec3::UNDEF);
let prev_edge_point =
ignored(edge_curve(b, prev_edge)?.evaluate_point(k, t_prev))?.unwrap_or(Vec3::UNDEF);
let curr_edge_point = ignored(pass(
k,
edge_curve(b, curr_edge)?.evaluate_point(k, t_curr),
)?)?
.unwrap_or(Vec3::UNDEF);
let prev_edge_point = ignored(pass(
k,
edge_curve(b, prev_edge)?.evaluate_point(k, t_prev),
)?)?
.unwrap_or(Vec3::UNDEF);
// Vertex/SurfVertex and EdgeEnd/SurfVertex gaps (the out-of-tolerance branch only
// warns upstream)
@@ -2973,32 +3008,38 @@ pub fn fix_uv_trim_curves_notified(
// drop sUVVertex to each UVTrimCurve, trim there and stretch back (upstream). The
// opt-in WR-255 fix (#959/#570) skips this and only moves the end points below.
if !k.fixes.uv_trim_corner_end_points_only {
let curr_drop = ignored(held_curve(b, curr_holder)?.drop_point(
let curr_drop = ignored(pass(
k,
&curr_ivl,
uv_vertex_3d,
None,
1.1 * (EFF_ZERO + uv_curr_edge_gap),
Some(t_curr),
SolverOperation::Intersect,
))?;
let prev_drop = ignored(held_curve(b, prev_holder)?.drop_point(
held_curve(b, curr_holder)?.drop_point(
k,
&curr_ivl,
uv_vertex_3d,
None,
1.1 * (EFF_ZERO + uv_curr_edge_gap),
Some(t_curr),
SolverOperation::Intersect,
),
)?)?;
let prev_drop = ignored(pass(
k,
&prev_ivl,
uv_vertex_3d,
None,
1.1 * (EFF_ZERO + uv_prev_edge_gap),
Some(t_prev),
SolverOperation::Intersect,
))?;
held_curve(b, prev_holder)?.drop_point(
k,
&prev_ivl,
uv_vertex_3d,
None,
1.1 * (EFF_ZERO + uv_prev_edge_gap),
Some(t_prev),
SolverOperation::Intersect,
),
)?)?;
if let Some(d) = curr_drop.filter(|d| d.success) {
let t_curr_new = d.parameter;
if !are_same(t_curr, t_curr_new) {
let mut trim_ivl = Extent1d::new();
if eu1::orientation(b, eu_current)? == OrientType::Same {
let _ = trim_ivl.set_min_max(t_curr_new, curr_ivl.max());
let _ = pass(k, trim_ivl.set_min_max(t_curr_new, curr_ivl.max()))?;
} else {
let _ = trim_ivl.set_min_max(curr_ivl.min(), t_curr_new);
let _ = pass(k, trim_ivl.set_min_max(curr_ivl.min(), t_curr_new))?;
}
if !are_same(trim_ivl.min(), trim_ivl.max()) {
// SmTol::GetZoneTol3d(curve) follows its actual holder's edge.
@@ -3024,9 +3065,9 @@ pub fn fix_uv_trim_curves_notified(
if !are_same(t_prev, t_prev_new) {
let mut trim_ivl = Extent1d::new();
if eu1::orientation(b, eu_previous)? == OrientType::Same {
let _ = trim_ivl.set_min_max(prev_ivl.min(), t_prev_new);
let _ = pass(k, trim_ivl.set_min_max(prev_ivl.min(), t_prev_new))?;
} else {
let _ = trim_ivl.set_min_max(t_prev_new, prev_ivl.max());
let _ = pass(k, trim_ivl.set_min_max(t_prev_new, prev_ivl.max()))?;
}
if !are_same(trim_ivl.min(), trim_ivl.max()) {
// The previous curve can likewise be held by another edge use.
+34 -21
View File
@@ -141,6 +141,13 @@ pub use crate::topology::topology_traverser::has_lamina_edge;
const OP: &str = "topology::face::p3";
/// A status SMLib drops and the fork passes on (`SER`, tangency-labs/smlib 6997962):
/// returned with the opt-in [`crate::kernel::UpstreamFixes::query_statuses_pass_on`],
/// handed on to the site's own ignore otherwise.
fn pass<T>(k: &Kernel, r: Result<T>) -> Result<Result<T>> {
crate::error::passed_on(r, k.fixes.query_statuses_pass_on)
}
fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
@@ -253,16 +260,19 @@ fn point_3d_classify(
tol: f64,
) -> Result<PointClassType> {
let mut pc = PointClassification::<EntityRef>::new(tol);
ignore_status(crate::query::face::p1::point_3d_classify(
ignore_status(pass(
k,
b,
f,
point,
tol,
true,
&mut pc,
crate::query::face::p1::Point3dClassifyOptions::default(),
))?;
crate::query::face::p1::point_3d_classify(
k,
b,
f,
point,
tol,
true,
&mut pc,
crate::query::face::p1::Point3dClassifyOptions::default(),
),
)?)?;
Ok(pc.point_class())
}
@@ -1304,17 +1314,20 @@ fn create_uv_trim_curves_body(
);
// status ignored upstream
let fixed = ignored(face2::fix_uv_trim_curves_notified(
let fixed = ignored(pass(
k,
b,
eu,
cw,
v,
tol_3d,
uv_gap_size,
&uv_domain,
env.as_deref_mut(),
))?
face2::fix_uv_trim_curves_notified(
k,
b,
eu,
cw,
v,
tol_3d,
uv_gap_size,
&uv_domain,
env.as_deref_mut(),
),
)?)?
.unwrap_or(false);
if fixed {
@@ -2379,7 +2392,7 @@ pub fn face_intersect_breps_cached(
};
let mut interval = fb.edge(e)?.interval;
// may snap sIvl by tol to existing knots (status ignored)
ignored(new_crv.trim(k, &mut interval, TrimMode::Notify))?;
ignored(pass(k, new_crv.trim(k, &mut interval, TrimMode::Notify))?)?;
curves_3d.push(new_crv);
uv_curves1.push(None);
uv_curves2.push(None);
@@ -2454,7 +2467,7 @@ pub fn face_intersect_breps_cached(
continue;
};
// status ignored upstream
let point = ignored(curve.evaluate_point(k, 0.5))?.unwrap_or(Vec3::UNDEF);
let point = ignored(pass(k, curve.evaluate_point(k, 0.5))?)?.unwrap_or(Vec3::UNDEF);
// `Point3DClassify` on both faces, in place on their own B-reps
let pct1 = point_3d_classify(k, this_brep, f, point, this_zone_tol)?;
let ob = other_brep.as_deref_mut().unwrap_or(&mut *this_brep);
+65 -42
View File
@@ -90,8 +90,10 @@
//! - **UV-corner failures (WR-262, #521).** By default `GetUVCornerBoundaries` ignores
//! failed endpoint evaluations and point drops, as upstream (an edge use can reuse
//! the preceding one's endpoints). With the opt-in
//! `Kernel::fixes.topology_failed_evaluations_propagate` they propagate and an
//! unsuccessful drop returns `SM_ERR`.
//! `Kernel::fixes.topology_failed_evaluations_propagate` it does as the fork
//! (tangency-labs/smlib 7a35f33): a trim curve that cannot be made is none (the 3D
//! ends are dropped), an edge use whose ends cannot be evaluated adds no boundary,
//! and failed drops stay ignored.
//! - The `std::unordered_set` overloads of `GetEdges` / `GetVertices`,
//! `GetEdgeusesAndVertexuses`, `CalculateBoundingBox`, `GetTurningAngDeg`,
//! `SmLoop::GetMaxSameDimGap3d`, `IsLamina`, `IsManifold`,
@@ -1283,13 +1285,19 @@ pub struct UvCorners {
}
/// `pEdgeuse->GetOrCreateUVTrimCurve(pUVTrimCurve)`: the curve after the call (its status
/// is ignored upstream; `None` when it could not be made).
/// is ignored upstream; `None` when it could not be made). With the opt-in
/// [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`] a failing
/// status is no curve, as the fork has it (tangency-labs/smlib 7a35f33: the pointer kept
/// the previous edge use's curve), so the walk drops the 3D ends instead.
fn get_or_create_uv_trim_curve(
k: &Kernel,
b: &mut QueryTopology<'_>,
eu: EdgeuseId,
) -> Result<Option<Curve>> {
let _ = b.ensure_trim_curve(k, eu)?;
let made = b.ensure_trim_curve(k, eu)?;
if k.fixes.topology_failed_evaluations_propagate && !made {
return Ok(None);
}
Ok(crate::topology::edgeuse::p1::uv_trim_curve(b, eu)?
.0
.cloned())
@@ -1303,22 +1311,12 @@ fn get_or_create_uv_trim_curve(
/// With no guess and no solution array, `DropPoint` writes `rResultUV` only when it
/// ends with `rbSuccess` set (its failed `GlobalPointSolve` returns before any write).
///
/// With the opt-in [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`]
/// (WR-262) only a successful drop supplies a corner: a failed one is an error, and an
/// unsuccessful one `SM_ERR`.
/// The fork keeps these drops' statuses ignored (`SE`, tangency-labs/smlib 7a35f33), so
/// [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`] does not change
/// them.
fn drop_point(k: &Kernel, s: &Surface, dom: &Extent2d, p: Point3) -> Result<Option<Point2>> {
let opts = crate::surface::surface::p5::DropPointOptions::default();
let r = s.drop_point::<crate::surface::surface::p4::NoObject>(k, p, dom, &opts);
if k.fixes.topology_failed_evaluations_propagate {
let r = r?;
return if r.success {
Ok(Some(r.result_uv))
} else {
Err(SolidError::sm_err(
"topology::loopuse::uv_corner_boundaries",
))
};
}
let r = ignored(r)?;
Ok(r.filter(|d| d.success).map(|d| d.result_uv))
}
@@ -1332,13 +1330,22 @@ fn ignored<T>(r: Result<T>) -> Result<Option<T>> {
}
}
/// [`ignored`], or with the opt-in
/// [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`] every error.
fn evaluated<T>(k: &Kernel, r: Result<T>) -> Result<Option<T>> {
if k.fixes.topology_failed_evaluations_propagate {
r.map(Some)
} else {
ignored(r)
/// An end point of the corner walk.
enum CornerEnd<T> {
/// Evaluated, or `None` where upstream ignores the failure and carries the previous
/// value on.
Value(Option<T>),
/// With the opt-in
/// [`crate::kernel::UpstreamFixes::topology_failed_evaluations_propagate`], the edge
/// use adds no boundary (the fork's `continue`, tangency-labs/smlib 7a35f33).
Skip,
}
/// The [`CornerEnd`] of an evaluation.
fn evaluated<T>(k: &Kernel, r: Result<T>) -> Result<CornerEnd<T>> {
match ignored(r)? {
None if k.fixes.topology_failed_evaluations_propagate => Ok(CornerEnd::Skip),
v => Ok(CornerEnd::Value(v)),
}
}
@@ -1350,9 +1357,7 @@ fn evaluated<T>(k: &Kernel, r: Result<T>) -> Result<Option<T>> {
/// # Errors
///
/// [`SolidError::Unported`] for a loop with edge uses (`GetOrCreateUVTrimCurve`);
/// those of [`edgeuses`]; [`SolidError::NotFound`] for a dangling id. With the opt-in
/// `Kernel::fixes.topology_failed_evaluations_propagate`, also those of the curve
/// evaluations and point drops, and `SM_ERR` when a point drop does not converge.
/// those of [`edgeuses`]; [`SolidError::NotFound`] for a dangling id.
pub fn uv_corner_boundaries(
k: &Kernel,
b: &mut Brep,
@@ -1383,7 +1388,7 @@ pub(crate) fn uv_corner_boundaries_with_topology(
};
// locals (carried from one edge use to the next where upstream ignores a failed
// evaluation; with the opt-in fix a failure returns instead)
// evaluation; with the opt-in fix that edge use adds no boundary instead)
let mut uv_pnt_beg = Point2::UNDEF;
let mut uv_pnt_end = Point2::UNDEF;
let mut uv_beg = Vec3::UNDEF;
@@ -1398,10 +1403,18 @@ pub(crate) fn uv_corner_boundaries_with_topology(
if let Some(trim) = trim {
// end points
ivl = trim.natural_interval();
if let Some(p) = evaluated(k, trim.evaluate_point(k, ivl.evaluate(0.0)))? {
let CornerEnd::Value(beg) = evaluated(k, trim.evaluate_point(k, ivl.evaluate(0.0)))?
else {
continue;
};
if let Some(p) = beg {
uv_beg = p;
}
if let Some(p) = evaluated(k, trim.evaluate_point(k, ivl.evaluate(1.0)))? {
let CornerEnd::Value(end) = evaluated(k, trim.evaluate_point(k, ivl.evaluate(1.0)))?
else {
continue;
};
if let Some(p) = end {
uv_end = p;
}
uv_pnt_beg = Point2::new(uv_beg.x, uv_beg.y);
@@ -1422,8 +1435,14 @@ pub(crate) fn uv_corner_boundaries_with_topology(
.as_ref()
.ok_or_else(|| null("GetUVCornerBoundaries: no curve"))?;
ivl = edge.interval;
let beg = evaluated(k, c.evaluate_point(k, ivl.min()))?.unwrap_or(Vec3::UNDEF);
let end = evaluated(k, c.evaluate_point(k, ivl.max()))?.unwrap_or(Vec3::UNDEF);
let CornerEnd::Value(beg) = evaluated(k, c.evaluate_point(k, ivl.min()))? else {
continue;
};
let beg = beg.unwrap_or(Vec3::UNDEF);
let CornerEnd::Value(end) = evaluated(k, c.evaluate_point(k, ivl.max()))? else {
continue;
};
let end = end.unwrap_or(Vec3::UNDEF);
if let Some(p) = drop_point(k, s, &dom, beg)? {
uv_pnt_beg = p;
}
@@ -1974,9 +1993,10 @@ mod tests {
}
#[test]
fn issue_521_uv_corners_refuse_failed_trim_evaluation_after_valid_edge() {
let mut k = Kernel::default();
k.fixes.topology_failed_evaluations_propagate = true;
fn issue_521_uv_corners_skip_an_edgeuse_whose_ends_fail() {
// tangency-labs/smlib 7a35f33 (SmLoop::GetUVCornerBoundaries): an edge use whose
// trim curve's ends cannot be evaluated adds no boundary; upstream carries the
// previous edge use's ends on (or unset ones) and classifies those.
let dom = Extent2d::from_bounds(0.0, 0.0, 1.0, 1.0);
for bad in [0, 2] {
let mut b = square(0.0);
@@ -1984,13 +2004,16 @@ mod tests {
b.edgeuse_mut(eu(2 * i)).unwrap().uv_trim_curve =
b.edge(EdgeId::from_index(i)).unwrap().curve.clone();
}
let empty = Curve::from(BSplineCurve::new(2));
let expected = empty.evaluate_point(&k, 0.0).unwrap_err();
b.edgeuse_mut(eu(bad)).unwrap().uv_trim_curve = Some(empty);
assert_eq!(
uv_corner_boundaries(&k, &mut b, LoopId::from_index(0), &dom, 1e-6),
Err(expected),
);
b.edgeuse_mut(eu(bad)).unwrap().uv_trim_curve = Some(Curve::from(BSplineCurve::new(2)));
let bad_edge = eu1::edge(&b, eu(bad)).unwrap();
let mut k = Kernel::default();
let off = uv_corner_boundaries(&k, &mut b.clone(), LoopId::from_index(0), &dom, 1e-6)
.unwrap();
k.fixes.topology_failed_evaluations_propagate = true;
let on = uv_corner_boundaries(&k, &mut b, LoopId::from_index(0), &dom, 1e-6).unwrap();
assert!(!on.on_edges.contains(&bad_edge), "bad={bad}: {on:?}");
assert_eq!(on.on_edges.len(), 6, "bad={bad}: {on:?}");
assert_ne!(off.on_edges, on.on_edges, "bad={bad}");
}
}
+23 -2
View File
@@ -933,8 +933,13 @@ pub fn is_topological_vertex(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<bo
// a tangent nothing wrote is the default SmVector3d (SM_UNDEF_DOUBLE)
let t1 = tan1.unwrap_or(Vec3::UNDEF);
let t2 = tan2.unwrap_or(Vec3::UNDEF);
// AngleBetween sets the angle to 0 on failure (status ignored)
t1.angle_between(t2).unwrap_or(0.0)
// AngleBetween sets the angle to 0 on failure (status ignored); with the opt-in
// fix the curves are not shown to meet G1 without it, so the vertex stays
// (tangency-labs/smlib 7a35f33)
match t1.angle_between(t2) {
Err(_) if k.fixes.query_statuses_pass_on => return Ok(false),
r => r.unwrap_or(0.0),
}
} else {
0.0
};
@@ -1848,6 +1853,22 @@ mod tests {
assert!(is_topological_vertex(&k, &mut b, v[1]).unwrap());
}
// tangency-labs/smlib 7a35f33 (SmVertex::IsTopologicalVertex): without the angle
// between the two tangents (the default zero tangent of a failed end evaluation)
// the curves are not shown to meet G1, so the vertex is kept.
#[test]
fn issue_2009_vertex_without_tangent_angle_is_kept() {
use crate::curve::composite_curve::CompositeCurve;
let (mut b, v, e) = chain(true, Vec3::X, Vec3::Y);
let e0 = b.edge_mut(e[0]).unwrap();
e0.curve = Some(Curve::from(CompositeCurve::new(3)));
e0.interval = Extent1d::from_min_max(-1.0, 0.0);
let mut k = Kernel::default();
assert!(is_topological_vertex(&k, &mut b, v[1]).unwrap());
k.fixes.query_statuses_pass_on = true;
assert!(!is_topological_vertex(&k, &mut b, v[1]).unwrap());
}
#[test]
fn gaps_and_notify() {
let k = Kernel::default();
+109 -36
View File
@@ -38,6 +38,11 @@
//! point with undefined UVs (it sets its found flag before the failing local solve),
//! as `intersect::curve_class::p2::refine_on_pair` ports it; a failed `DropPoint` or
//! `LocalSurfaceSurfaceIntersect` reports none found.
//! With the opt-in `Kernel::fixes.query_statuses_pass_on` (WR-663) these statuses are
//! passed on as the fork does (tangency-labs/smlib 7a35f33; `ComputeUVPoint` after the
//! surface check), `HasParallelFaces` skips a pair whose point, normal or angle cannot
//! be evaluated, and `IsOnFaceCorner` takes a failed `CalcCornerFromFaces` as no best
//! point.
//! - **Null pointers.** A face without surface where upstream dereferences it, and
//! [`has_parallel_faces`] on a vertex without faces (upstream reads past an empty
//! array), return [`SolidError::InvalidInput`]. `SM_DBG_WARN`, `SM_ASSERT_MSG` and the
@@ -58,6 +63,13 @@ use crate::topology::{vertexuse, Brep, FaceId, VertexId, VertexuseId};
const OP: &str = "topology::vertex::corner";
/// A status SMLib drops and the fork passes on (`SER`, tangency-labs/smlib 7a35f33):
/// returned with the opt-in [`crate::kernel::UpstreamFixes::query_statuses_pass_on`],
/// handed on to the site's own ignore otherwise.
fn pass<T>(k: &Kernel, r: Result<T>) -> Result<Result<T>> {
crate::error::passed_on(r, k.fixes.query_statuses_pass_on)
}
fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
@@ -180,15 +192,22 @@ pub struct ParallelFaces {
}
/// The surface point and normal at a vertex use's gap UV, both statuses ignored (the
/// previous values stay on failure).
fn gap_point_normal(k: &Kernel, surface: &Surface, uv: Point2, pn: &mut [Vec3; 2]) -> Result<()> {
if let Some(p) = ignored(surface.evaluate_point(k, uv))? {
pn[0] = p;
/// previous values stay on failure). `false` when, with the opt-in
/// [`crate::kernel::UpstreamFixes::query_statuses_pass_on`], one cannot be evaluated: the
/// fork compares no such pair (tangency-labs/smlib 7a35f33).
fn gap_point_normal(k: &Kernel, surface: &Surface, uv: Point2, pn: &mut [Vec3; 2]) -> Result<bool> {
let skip = k.fixes.query_statuses_pass_on;
match ignored(surface.evaluate_point(k, uv))? {
Some(p) => pn[0] = p,
None if skip => return Ok(false),
None => (),
}
if let Some(n) = ignored(surface.evaluate_normal(k, uv, Side::Left, Side::Left))? {
pn[1] = n;
match ignored(surface.evaluate_normal(k, uv, Side::Left, Side::Left))? {
Some(n) => pn[1] = n,
None if skip => return Ok(false),
None => (),
}
Ok(())
Ok(true)
}
/// `SmVertex::HasParallelFaces`: whether two of the vertex's faces have normals (at
@@ -241,17 +260,25 @@ pub fn has_parallel_faces(
let Some(gap1) = vertexuse::vertex_face_gap(k, b, vu1, false)? else {
continue;
};
gap_point_normal(k, face_surface(b, face1)?, gap1.face_uv, &mut pn1)?;
if !gap_point_normal(k, face_surface(b, face1)?, gap1.face_uv, &mut pn1)? {
continue;
}
for &vu2 in vertexuses.iter().skip(ii + 1) {
let face2 = vertexuse_face(b, vu2)?.ok_or_else(|| null("vertex use without face"))?;
let Some(gap2) = vertexuse::vertex_face_gap(k, b, vu2, false)? else {
continue;
};
gap_point_normal(k, face_surface(b, face2)?, gap2.face_uv, &mut pn2)?;
if !gap_point_normal(k, face_surface(b, face2)?, gap2.face_uv, &mut pn2)? {
continue;
}
// AngleBetween reports 0 for a zero-length normal (status ignored)
let dihedral_ang_rad = pn1[1].angle_between(pn2[1]).unwrap_or(0.0);
// AngleBetween reports 0 for a zero-length normal (status ignored; with the
// opt-in fix the pair is not compared, tangency-labs/smlib 7a35f33)
let dihedral_ang_rad = match pn1[1].angle_between(pn2[1]) {
Err(_) if k.fixes.query_statuses_pass_on => continue,
r => r.unwrap_or(0.0),
};
let dihedral_ang_deg = rad_to_deg(dihedral_ang_rad);
if out.min_dihedral_ang_deg > dihedral_ang_deg {
out.min_dihedral_ang_deg = dihedral_ang_deg;
@@ -338,10 +365,16 @@ pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<F
.ok_or_else(|| null("parallel faces without face2"))?;
let mut pt1 = Vec3::UNDEF;
let mut pt2 = Vec3::UNDEF;
if let Some(p) = ignored(face_surface(b, f1)?.evaluate_point(k, parallel.uv1))? {
if let Some(p) = ignored(pass(
k,
face_surface(b, f1)?.evaluate_point(k, parallel.uv1),
)?)? {
pt1 = p;
}
if let Some(p) = ignored(face_surface(b, f2)?.evaluate_point(k, parallel.uv2))? {
if let Some(p) = ignored(pass(
k,
face_surface(b, f2)?.evaluate_point(k, parallel.uv2),
)?)? {
pt2 = p;
}
@@ -376,14 +409,25 @@ pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<F
continue;
};
// FALSE = Use GlobalSolve without touching UVTrimCurve (status ignored)
if let Some(p) = ignored(vertexuse::compute_uv_point(k, b, vu, false))? {
uv = p;
// FALSE = Use GlobalSolve without touching UVTrimCurve (status ignored). With
// the opt-in fix the fork's order: faces without surface are skipped before the
// drop, whose status is passed on (tangency-labs/smlib 7a35f33).
let pass_on = k.fixes.query_statuses_pass_on;
if !pass_on {
if let Some(p) = ignored(vertexuse::compute_uv_point(k, b, vu, false))? {
uv = p;
}
}
let face = b
.faceuse(fu)?
.face
.ok_or_else(|| null("face use without face"))?;
if b.face(face)?.surface.is_none() {
continue;
}
if pass_on {
uv = vertexuse::compute_uv_point(k, b, vu, false)?;
}
let Some(surface) = b.face(face)?.surface.as_ref() else {
continue;
};
@@ -441,15 +485,17 @@ pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<F
..Default::default()
};
let domain = surface.natural_uv_domain_with_nurb(k)?;
let found_answer =
match ignored(surface.drop_point::<NoObject>(k, point, &domain, &opts))? {
Some(d) => {
uv = d.result_uv;
out.max_gap_3d = d.gap;
d.success
}
None => false,
};
let found_answer = match ignored(pass(
k,
surface.drop_point::<NoObject>(k, point, &domain, &opts),
)?)? {
Some(d) => {
uv = d.result_uv;
out.max_gap_3d = d.gap;
d.success
}
None => false,
};
if !found_answer {
return Ok(out);
@@ -478,7 +524,7 @@ pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<F
None,
false,
);
let (found_answer, uvs) = match ignored(r)? {
let (found_answer, uvs) = match ignored(pass(k, r)?)? {
Some(Some(uvs)) => (true, uvs),
Some(None) => (false, [Point2::UNDEF; 2]),
None => (true, [Point2::UNDEF; 2]),
@@ -499,9 +545,10 @@ pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<F
let s0 = face_surface(b, *f0)?;
let s1 = face_surface(b, *f1)?;
let s2 = face_surface(b, *f2)?;
let sol = ignored(
let sol = ignored(pass(
k,
s0.local_surface_surface_intersect(k, &d0, s1, &d1, s2, &d2, *uv0, *uv1, *uv2),
)?
)?)?
.flatten();
let Some(s) = sol else {
return Ok(out);
@@ -520,7 +567,7 @@ pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<F
out.found_point = true;
let f0 = *tgt_faces.first().ok_or_else(|| null("no target face"))?;
if let Some(p) = ignored(face_surface(b, f0)?.evaluate_point(k, uv))? {
if let Some(p) = ignored(pass(k, face_surface(b, f0)?.evaluate_point(k, uv))?)? {
out.best_point = p;
}
@@ -542,13 +589,19 @@ pub fn is_on_face_corner(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<(bool,
// vertex (status ignored: the outputs keep their initial values)
let corner = match calc_corner_from_faces(k, b, v) {
Ok(c) => c,
// the SM_ERR of more than three target faces leaves the initial outputs
Err(SolidError::Upstream { operation: OP, .. }) => FaceCorner {
found_point: false,
max_gap_3d: -1.0,
max_gap_face: None,
best_point: Vec3::UNDEF,
},
// the SM_ERR of more than three target faces leaves the initial outputs; with
// the opt-in fix any failure is no best point (tangency-labs/smlib 7a35f33)
Err(e)
if matches!(e, SolidError::Upstream { operation: OP, .. })
|| (k.fixes.query_statuses_pass_on && !e.is_unported()) =>
{
FaceCorner {
found_point: false,
max_gap_3d: -1.0,
max_gap_face: None,
best_point: Vec3::UNDEF,
}
}
Err(e) => return Err(e),
};
@@ -630,6 +683,26 @@ mod tests {
assert!((p.min_dihedral_ang_deg - 90.0).abs() < 1e-9);
}
// tangency-labs/smlib 7a35f33 (SmVertex::HasParallelFaces): a face pair whose
// points or normals cannot be evaluated is not compared. Here one face's surface
// is swapped for one that cannot be evaluated after its gap was cached.
#[test]
fn issue_2009_parallel_faces_skip_an_unevaluable_pair() {
let (k, mut b, v) = box_corner();
let _ = is_within_xsect_tol_3d_of_faces(&k, &mut b, v, false).unwrap();
let f = super::super::faces(&b, v).unwrap()[0];
b.face_mut(f).unwrap().surface = Some(Surface::from(
crate::nurbs::b_spline_surface::BSplineSurface::default(),
));
let off = has_parallel_faces(&k, &mut b, v, None).unwrap();
let mut on_k = Kernel::default();
on_k.fixes.query_statuses_pass_on = true;
let on = has_parallel_faces(&on_k, &mut b, v, None).unwrap();
assert!(!on.parallel, "{on:?}");
assert!((on.min_dihedral_ang_deg - 90.0).abs() < 1e-9, "{on:?}");
assert_ne!(off, on);
}
#[test]
fn vertex_off_its_faces_fails_the_gap_test() {
let (k, mut b, v) = box_corner();
@@ -0,0 +1,11 @@
#### WR-663. The loop, vertex, edge use, face, curve and surface queries drop failed statuses and classify from unset values
- **Severity:** Wrong result
- **Area:** Topology queries, curve and surface predicates
- **Location:** `source/SMLib/src/SmLoop.cpp` (`GetUVCornerBoundaries`, `ContainsUVPoint`), `SmVertex.cpp` (`IsTopologicalVertex`, `HasParallelFaces`, `CalcCornerFromFaces`, `IsOnFaceCorner`), `SmEdgeuse.cpp` (`CreateUVTrimCurve`, `EvaluateBinormal`, `NormalizedEvaluate`, `IsEdgeuseSideInSurface`, `HasDogLegNearPole`, `GetStartPointUVLoopGap`, `sm_CalcDistAndDev`), `SmFace.cpp` (`sm_AreConnected`, `sm_FixUVTrimCurves`, `GetPointsInFace`, `CalculateAnInternalPoint`, `MoveSeam`, `SplitAtSeam`, `SwapUV`, `ShrinkGeometry`, `CreateCurvesFromFace`, `CreateUVTrimCurves`, `FaceIntersect`), `SmBSplineCurve.cpp` (`TrimGap`), `SmCurve.cpp` (`ApproximateLength`, `IsClosed`, `IsDegenerate`), `SmSurface.cpp` (`IsClosed`, `IsPlanar`).
- **What happens:** an evaluation, drop, trim, interval edit or sub-operation fails and its status is not checked. Functions that return `SmStatus` go on and return `SM_SUCCESS`. Predicates and classifications read the output the failed call never wrote, which is either the default `SM_UNDEF_DOUBLE` vector or the previous edge use's value, and answer from it. For example, `IsDegenerate` calls a curve that cannot be evaluated degenerate, `TrimGap` reports two curves joined, `ApproximateLength` returns 0, `IsTopologicalVertex` takes a zero tangent as G1, and `HasParallelFaces` compares a stale normal. No crash.
- **Trigger:** no valid input is known to fail these calls. The tests inject the failure: an empty B-spline curve or surface, or a composite curve, which cannot be evaluated.
- **How found:** code reading. The SolidCraft fork (tangency-labs/smlib) handles these statuses in 7a35f33 (SmLoop, SmVertex), 6997962 (SmFace), 666242b (SmEdgeuse), 8843967 (SmBSplineCurve), 8311f8e (SmCurve) and 21dcf69 (SmSurface). They are tracked as #2009 group D.
- **Confirmed:** in the port, by injected-failure tests with the flag on and off. The pinned C++ drops the statuses, so with the flag off the port matches it.
- **Fix (fork):** `SER` on each dropped status in a function that returns `SmStatus`. A classification answers only from what it computed: `SM_POC_UNKNOWN`, no boundary, not G1, no pair compared, no best point, `TRUE` ("cannot tell") for `IsEdgeuseSideInSurface`, no dog-leg, `SM_BIG_DOUBLE` for an unmeasurable loop gap, `-1` for a distance, `FALSE` for the predicates, `SM_UNDEF_DOUBLE` for a length, and 0 (not fixed) for `TrimGap`. Kept dropped: `GetEnds`, the gap caches' refreshes, `HasSeamProblem` and its callers, `EuclidianStepOff`, `ShrinkGeometry`'s curve trims, `InsertTopologyIntersections` in `IsDegenerate`, the list getters, `AssertValid` and drawing.
- **Rust port:** opt-in `Kernel::fixes.query_statuses_pass_on` (default `false`, upstream's behaviour). The `SER` sites go through `crate::error::passed_on`, so with the flag off each site keeps its own ignore policy. Sites: `topology/edgeuse/{p2,p3}.rs`, `topology/vertex.rs`, `topology/vertex/corner.rs`, `query/loop.rs`, `topology/face/{p1,p2,p3}.rs`, `nurbs/b_spline_curve/p3.rs`, `curve/curve/{p2,p4}.rs`, `surface/surface/p7.rs`. The fork's soft answers in `GetUVCornerBoundaries`, `GetStartPointUVLoopGap` and `sm_CalcDistAndDev` replace what the unshipped `topology_failed_evaluations_propagate` (WR-262/263/264, #521) did there, which was to return an error. The flag now gives the fork's answers, and its failed point drops in `GetUVCornerBoundaries` stay ignored, as in the fork.
- **Not ported here:** most of the fork's `SER` sites in `SmCurve.cpp` (about 205), `SmSurface.cpp` (about 260) and `SmBSplineCurve.cpp` (bounding boxes, frames, knot edits, approximations), and their remaining predicates (`IsArc`, `IsLine`, `IsLinear`, `IsPlanar`, `AreCoPlanar`, `AreOnPlane`, `IsPeriodic`, `IsDomainBoundary`, `HasPlaneCrossings`, the curve-list helpers, `GetEnds`; the surface's `IsDegenerateCurve/Point`, `IsPlanarPoint`, `IsFlatCorner`, `IsOnUVBoundary`, `IsSingularity`, `GetSingularities`, `ApproxDerivativeLengths`, the solvers' `LocalSolve`; the B-spline curve's `HasInternalPole`, `HasNullSegments`, `HasSameParameterization`, `IsLine`, `IsDegenerate`, `AssertNoPeriodicJump`, `GetGrevileAbscissa`, `AssertValid`). Also not ported: the `SmFace.cpp` sites in functions without a port (`ShrinkSurface`, `IdentifyFillet`, `GlobalLineIntersect`, `ClassifyLoops`, `OutputPolygons`, drawing), and the `SmVertex.cpp`/`SmLoop.cpp` sites in `RefineGeometry`, `RebuildFromEdgeCurves` and `FindAndRemoveVertexuseByEdge`, none of which has a port. The same goes for `SmEdgeuse`'s `IsMaxEdgeFaceGapFresh`, `IsEdgeCCWEdgeGapFresh` and `NormalizedEvaluateAndDropPt`.