Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions crates/difftest/known_mismatches/heal.face_props.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# PR #700: eight individually verified incomplete Stage3 marker differences.

[[mismatch]]
unit = "heal.face_props"
case = "gen0345"
why = "WR-266 / #521: failed Stage3 retains in-progress marker 5 in C++; Rust retains the same 1040 error but restores incomplete stage 4 for retry; other compared outputs agree (plan/findings/general--521-topology-evaluation-status.md: Failed topology evaluations and incomplete healing stages)"

[[mismatch]]
unit = "heal.face_props"
case = "gen0821"
why = "WR-266 / #521: failed Stage3 retains in-progress marker 5 in C++; Rust retains the same 1040 error but restores incomplete stage 4 for retry; other compared outputs agree (plan/findings/general--521-topology-evaluation-status.md: Failed topology evaluations and incomplete healing stages)"

[[mismatch]]
unit = "heal.face_props"
case = "gen0957"
why = "WR-266 / #521: failed Stage3 retains in-progress marker 5 in C++; Rust retains the same 1040 error but restores incomplete stage 4 for retry; other compared outputs agree (plan/findings/general--521-topology-evaluation-status.md: Failed topology evaluations and incomplete healing stages)"

[[mismatch]]
unit = "heal.face_props"
case = "stage3"
why = "WR-266 / #521: failed Stage3 retains in-progress marker 5 in C++; Rust retains the same 1040 error but restores incomplete stage 4 for retry; other compared outputs agree (plan/findings/general--521-topology-evaluation-status.md: Failed topology evaluations and incomplete healing stages)"

[[mismatch]]
unit = "heal.face_props"
case = "gen0241"
why = "WR-266 / #521, newly reachable after the cache-wrapper rebase on e0266d5af: values.v[84] is C++ 5 versus Rust 4 after the same 1040 failure; all other outputs agree. Raw outputs checked against both current main and the PR (plan/findings/general--521-topology-evaluation-status--addendum-2026-10-25-cache-wrapper.md)."

[[mismatch]]
unit = "heal.face_props"
case = "gen0662"
why = "WR-266 / #521, newly reachable after the cache-wrapper rebase on e0266d5af: values.v[1], [84] and [167] are C++ 5 versus Rust 4; the initial 1040 failure and subsequent 1000 statuses are identical, and all other outputs agree. Raw outputs checked against both current main and the PR (plan/findings/general--521-topology-evaluation-status--addendum-2026-10-25-cache-wrapper.md)."

[[mismatch]]
unit = "heal.face_props"
case = "gen1105"
why = "WR-266 / #521, newly reachable after the cache-wrapper rebase on e0266d5af: values.v[84] is C++ 5 versus Rust 4 after the same 1040 failure; all other outputs agree. Raw outputs checked against both current main and the PR (plan/findings/general--521-topology-evaluation-status--addendum-2026-10-25-cache-wrapper.md)."

[[mismatch]]
unit = "heal.face_props"
case = "gen1377"
why = "WR-266 / #521, newly reachable after the cache-wrapper rebase on e0266d5af: values.v[167], [250] and [333] are C++ 5 versus Rust 4; the initial 1040 failure and subsequent successful query statuses are identical, and all other outputs agree. Raw outputs checked against both current main and the PR (plan/findings/general--521-topology-evaluation-status--addendum-2026-10-25-cache-wrapper.md)."
124 changes: 108 additions & 16 deletions crates/solidcore/src/heal/face_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@
//! ([`crate::topology::face::p4::BrepSeamObjects`]; otherwise
//! [`SolidError::InvalidInput`]). The `SmLoop` queries (`topology.loopuse`),
//! `SmCurveClassification::ReSet` (`intersect.curve_class_p2`) and
//! `SmFace::ClassifyLoops` ([`crate::query::face::p4::classify_loops`], whose status
//! `SetProps_Stage3` ignores as upstream) are wired.
//! `SmFace::ClassifyLoops` ([`crate::query::face::p4::classify_loops`]) are wired.
//! `SetProps_Stage3` propagates classification failures (#521) before reading the
//! classification records. An error clears its in-progress stage marker, keeping
//! partial diagnostics without certifying the properties or skipping a retry.
//! - **Query caches.** The mutable healer explicitly applies the vertex/face and
//! edge/face gap caches produced by seam queries, even on errors. Read-only props
//! queries still leave the B-rep unchanged; no geometry or topology is copied back.

use crate::error::{ignored, EntityRef, Result, SolidError};
use crate::error::{EntityRef, Result, SolidError};
use crate::geom::tol::{calc_face_zone_tol_3d, xsect_tol_3d_from_zones};
use crate::geom::vector3d::{Point3, Vec3};
use crate::intersect::curve_class::p1::{
Expand Down Expand Up @@ -2081,14 +2083,15 @@ impl<O: LoopObject> FaceProps<O> {
/// `SetProps_Stage3`: sets the loop values (after `Fix_SplitEdgesAtSeam`): the
/// per-loop props, the closed loop count, and from `SmFace::ClassifyLoops` the
/// containment, orientation and the loop problem counts.
/// A failed stage remains incomplete and can be retried.
///
/// # Errors
///
/// `SM_ERR` when `exe_heal_op` is before [`HealerOpType::FixSplitedgeAtseam`];
/// `SM_ERR_NOTYET_HEAL_FACE` ([`SM_ERR_NOTYET_HEAL_FACE`]) for a face
/// `ClassifyLoops` cannot classify; the errors of [`FaceProps::set_props_stage2`]
/// and [`LoopProps::set_props`]; [`SolidError::InvalidInput`] where upstream reads
/// outside an array; [`SolidError::Unported`] from `SmFace::ClassifyLoops`.
/// outside an array; the errors of `SmFace::ClassifyLoops`.
pub fn set_props_stage3(
&mut self,
k: &Kernel,
Expand All @@ -2112,6 +2115,23 @@ impl<O: LoopObject> FaceProps<O> {
face_index: u64,
exe_heal_op: HealerOpType,
cache: &mut CurveOnClassifyCache,
) -> Result<()> {
let result = self.set_props_stage3_inner(k, b, face, face_index, exe_heal_op, cache);
if result.is_err() && self.prop_stage == PropStage::Stage3A {
// Keep diagnostics, but an unfinished stage is not a cached success.
self.prop_stage = PropStage::Stage2;
}
result
}

fn set_props_stage3_inner(
&mut self,
k: &Kernel,
b: &Brep,
face: FaceId,
face_index: u64,
exe_heal_op: HealerOpType,
cache: &mut CurveOnClassifyCache,
) -> Result<()> {
// no work - already done or being done
if self.prop_stage >= PropStage::Stage3A {
Expand Down Expand Up @@ -2159,9 +2179,7 @@ impl<O: LoopObject> FaceProps<O> {
..ClassifyLoopsOut::default()
};
let mut target_loops = closed_loops;
// (the status is only reported upstream; a handle type that cannot name the
// loop bisector's objects is a limitation of the caller, not a status)
let classify_status = match crate::query::face::p4::classify_loops_cached(
let classify_status = crate::query::face::p4::classify_loops_cached(
k,
b,
m_face,
Expand All @@ -2170,14 +2188,7 @@ impl<O: LoopObject> FaceProps<O> {
&mut scratch,
self,
cache,
) {
Err(e @ SolidError::InvalidInput { reason, .. })
if reason == crate::query::face::p3::NO_CURVE_HANDLES =>
{
return Err(e);
}
r => ignored(r)?,
};
);
let ClassifyLoopsOut {
classifiable,
classify_loop_ios,
Expand All @@ -2187,6 +2198,10 @@ impl<O: LoopObject> FaceProps<O> {
looptree,
} = classified;
self.looptree = looptree;
self.not_yet_face_type = classifiable;
// Preserve the owned tree and problem classification on error, but do not
// consume output arrays that the failed classification did not fill.
classify_status?;

// watch out for problem faces that defy ClassifyLoops
if classifiable != HealNotYetFaceType::NoProbs {
Expand Down Expand Up @@ -2216,7 +2231,6 @@ impl<O: LoopObject> FaceProps<O> {
// guard against NULL m_pLooptreeRoot - can happen when ClassifyLoops cannot
// process loops (a failed status is checked upstream by SE_MSG only)
let Some(root) = self.looptree.as_ref().and_then(Looptree::root) else {
let _ = classify_status;
self.not_yet_face_type = HealNotYetFaceType::NoLoopTreeRoot;
return Err(not_yet_heal_face(OP_STAGE3));
};
Expand Down Expand Up @@ -2706,6 +2720,84 @@ mod tests {
assert!(p.has_props(PropStage::Stage0, f));
}

#[test]
fn stage3_propagates_classification_failure() {
let k = Kernel::default();
let (b, f) = plane_without_loops();
let mut p: FaceProps<H> = FaceProps::for_face(f, 0);
p.set_props_stage2(&k, &b, f, 0, HealerOpType::All).unwrap();
let expected = crate::query::face::p4::classify_loops(
&k,
&b,
f,
&mut Vec::new(),
&mut ClassifyLoopsOut::default(),
&mut CurveClassification::new(&k),
&mut p,
)
.unwrap_err();
assert_eq!(
p.set_props_stage3(&k, &b, f, 0, HealerOpType::All),
Err(expected)
);
assert!(!p.has_props_stage3(Some(f)));
}

#[test]
fn stage3_failure_does_not_certify_cached_properties() {
for cached in [false, true] {
let k = Kernel::default();
let (mut b, f) = plane_without_loops();
let mut p: FaceProps<H> = FaceProps::for_face(f, 0);
p.set_props_stage2(&k, &b, f, 0, HealerOpType::All).unwrap();
b.faceuse_mut(crate::topology::FaceuseId::from_index(0))
.unwrap()
.loopuses
.push(crate::topology::LoopuseId::from_index(99));
let mut cache = CurveOnClassifyCache::default();
let mut run = |props: &mut FaceProps<H>| {
if cached {
props.set_props_stage3_cached(&k, &b, f, 0, HealerOpType::All, &mut cache)
} else {
props.set_props_stage3(&k, &b, f, 0, HealerOpType::All)
}
};
let first = run(&mut p).unwrap_err();
assert_eq!(run(&mut p), Err(first));
assert_eq!(p.prop_stage, PropStage::Stage2);
}
}

fn plane_without_loops() -> (Brep, FaceId) {
use crate::topology::{Face, Faceuse, FaceuseId, Shell};
let mut b = Brep::new();
let s = b.add_shell_record(Shell::default()).unwrap();
let f = b
.add_face_record(Face {
surface: Some(crate::surface::surface::Surface::from(
crate::surface::plane::Plane::from_normal(Vec3::ZERO, Vec3::Z),
)),
faceuse: Some(FaceuseId::from_index(0)),
..Face::new(s)
})
.unwrap();
b.shell_mut(s).unwrap().faces.push(f);
for (i, orientation) in [OrientType::Same, OrientType::Opposite]
.into_iter()
.enumerate()
{
b.add_faceuse_record(Faceuse {
shell: Some(s),
face: Some(f),
mate: Some(FaceuseId::from_index(1 - i as u32)),
orientation,
..Faceuse::default()
})
.unwrap();
}
(b, f)
}

#[test]
fn early_stage_statuses() {
let k = Kernel::default();
Expand Down
48 changes: 40 additions & 8 deletions crates/solidcore/src/topology/edge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2116,25 +2116,22 @@ pub fn set_orientation(b: &mut Brep, e: EdgeId, orientation: OrientType) -> Resu
Ok(())
}

/// `ReverseOrientation`: reverses the curve's parameterisation (the edge interval
/// becomes the reversed one; its status is ignored, a failure leaves the default
/// interval), flips every edge use's orientation and deletes their UV trim curves.
/// `ReverseOrientation`: reverses the curve's parameterisation, then installs the
/// reversed interval, flips every edge use's orientation and deletes its UV trim.
/// A failed curve reversal returns before changing the interval or edge uses.
///
/// # Errors
///
/// [`SolidError::InvalidInput`] without edge uses (upstream walks a null list) and
/// those of [`eu1::set_uv_trim_curve`]; [`SolidError::Unported`] from the curve.
/// those of the curve reversal and [`eu1::set_uv_trim_curve`].
pub fn reverse_orientation(k: &Kernel, b: &mut Brep, e: EdgeId) -> Result<()> {
// locals
let old_ivl = b.edge(e)?.interval;
let mut new_ivl = Extent1d::new();

// reverse Curve Parameterization
if let Some(c) = b.edge_mut(e)?.curve.as_mut() {
match c.reverse_parameterization(k, &old_ivl) {
Ok(n) => new_ivl = n,
Err(err) => failed(err)?,
}
new_ivl = c.reverse_parameterization(k, &old_ivl)?;
}
b.edge_mut(e)?.interval = new_ivl;

Expand Down Expand Up @@ -3173,6 +3170,41 @@ mod tests {
}
}

#[test]
fn failed_reversal_preserves_edge_interval_uses_and_trims() {
let k = Kernel::default();
let (mut b, e, eus, _) = wire();
let hermite = crate::curve::hermite_curve::HermiteCurve::new(
Vec3::ZERO,
Vec3::X,
Vec3::X,
Vec3::Y,
3,
);
let mut curve = C::from(hermite);
let old_ivl = b.edge(e).unwrap().interval;
let expected = curve.reverse_parameterization(&k, &old_ivl).unwrap_err();
b.edge_mut(e).unwrap().curve = Some(curve);
for eu in eus {
b.edgeuse_mut(eu).unwrap().uv_trim_curve = Some(C::from(BSplineCurve::from_nurb(
NlCurve::new(
vec![
CPoint::new(0.0, 0.0, 0.0, NOW),
CPoint::new(1.0, 0.0, 0.0, NOW),
],
1,
vec![0.0, 0.0, 1.0, 1.0],
)
.unwrap(),
3,
false,
)));
}
let before = format!("{b:?}");
assert_eq!(reverse_orientation(&k, &mut b, e), Err(expected));
assert_eq!(format!("{b:?}"), before);
}

#[test]
fn reverse_and_reset_primary() {
let (mut b, e, eus, _) = wire();
Expand Down
Loading