From e46bbde814816bfdb1068e36bc8d5e18045fce01 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 22:47:46 -0700 Subject: [PATCH] query.face_p4: call GetLoopCrossings for the loop bisector in ClassifyLoops SmFace::ClassifyLoops still returned Unported for a target loop that crosses no seam, although SmCurveClassification::GetLoopCrossings is ported. Call it with the target loops, the face props and the bisector's classification, as upstream does; its status stays ignored. The query.face_p4 runner now also exports the face props the bisector and GetLoopCrossings read (zone tolerance, no-vertex poles, pole points, missing seam direction). Faces whose surface or loop edge curves the importer cannot reproduce exactly report inexact_geometry on both sides when ClassifyLoops may evaluate them, decided from the export as unit query.face_p3 does. Co-Authored-By: Claude Opus 5.5 --- crates/difftest/src/units/query_face_p4.rs | 145 ++++++++++++++++-- .../solidcore-ffi/src/units/query_face_p4.rs | 3 +- crates/solidcore/src/query/face/p4.rs | 46 +++--- harness/cpp/shim/query_face_p4.cpp | 11 ++ 4 files changed, 167 insertions(+), 38 deletions(-) diff --git a/crates/difftest/src/units/query_face_p4.rs b/crates/difftest/src/units/query_face_p4.rs index d7f82a89ce..3fd2e42d4d 100644 --- a/crates/difftest/src/units/query_face_p4.rs +++ b/crates/difftest/src/units/query_face_p4.rs @@ -17,16 +17,22 @@ //! does (`SetProps_Stage2`, `ReSet(SM_PROPSTAGE_3)`, the loop props) and exports the //! values `ClassifyLoops` reads (stage, closed flags, poles, the loops' seam crossing //! and winding counts, the seam classifications' end point classes and vertices, the -//! seam done-touch lists); both sides then run `ClassifyLoops` on those same props (the +//! seam done-touch lists, and for the loop bisector and `GetLoopCrossings` the zone +//! tolerance, no-vertex poles, pole points and missing seam direction); both sides then run `ClassifyLoops` on those same props (the //! Rust side imports them; the seam classifications' non-vertex point classifications //! carry no object, which `ClassifyLoops` never reads). The target loops are exported //! too. With `props` `0` the props are fresh and `ClassifyLoops` computes them itself //! (`SetProps_Stage2`; the Rust side runs the port of unit `heal.face_props`). //! -//! **Deferred.** A target loop that crosses no seam the face props classified needs -//! `sms_FindAndClassifyLoopBisector` (`query.face_p3`) and `GetLoopCrossings` -//! (`intersect.curve_class_p4`); fresh props of a closed surface need -//! `SmFace::HasSeamProblem` (`topology.face_p4`). +//! **Inexact geometry.** Faces whose surface, and edges whose curve, the importer cannot +//! reproduce exactly (unit `topology.face_p2`'s `Exact`) are not compared when +//! `ClassifyLoops` may evaluate them: with fresh props (`SetProps_Stage2`), or when a +//! target loop's imported loop props show no crossing of a closed seam (the loop +//! bisector and `GetLoopCrossings` evaluate the edge curves). Both sides then report +//! `inexact_geometry`, decided from the export alone. +//! +//! **Deferred.** Fresh props of a closed surface need `SmFace::HasSeamProblem` +//! (`topology.face_p4`), which reaches its unported callees. //! //! **Rejected** (both sides report `rejected`): the inputs the FFI wrapper documents, //! where `ClassifyLoops` would read outside its arrays. @@ -50,13 +56,16 @@ use solidcore::intersect::point_class::{ClassObject, ClassObjectKind}; use solidcore::intersect::curve_class::p1::{CrvDirOnFaceType, CurveClassification, TouchData}; use solidcore::intersect::point_class::PointClassType; use solidcore::kernel::Kernel; +use solidcore::math::Point3; use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType; +use solidcore::nurbs::b_spline_surface::SurfParamType; use solidcore::query::face::p4 as qf4; -use solidcore::topology::{EdgeId, EdgeuseId, FaceId, LoopId, VertexId}; +use solidcore::topology::face::p2 as face_p2; +use solidcore::topology::{edgeuse::p1 as eu1, Brep, EdgeId, EdgeuseId, FaceId, LoopId, VertexId}; use solidcore_ffi::units::query_face_p4::{self as ffi, Op}; use super::topology_edgeuse_p1::{domain, flag, gen_surface, push_surface, SrfIn, FILES}; -use super::topology_face_p2::import; +use super::topology_face_p2::{import, Exact}; use crate::case::CaseContext; use crate::runner::default_cases; use crate::{Case, HarnessError, Output, Rng, UnitDiff}; @@ -483,6 +492,10 @@ fn result_output( } fn cpp(i: &Inputs) -> Result { + // the same rejections as the Rust side (failed build, rejected, inexact geometry) + if let Err(out) = export(&Kernel::default(), i)? { + return Ok(out); + } let o = run_ffi(Op::Classify, i)?; let mut rd = Rd { v: &o, at: 0 }; if rd.num()? != 0.0 { @@ -585,21 +598,81 @@ fn tri(code: i64) -> Result { }) } -fn rust(i: &Inputs) -> Result { - let k = Kernel::default(); +/// What the export of a case gives the Rust side. +struct Exported { + b: Brep, + face: FaceId, + props: FaceProps, + loops: Vec, +} + +/// Whether the surface of `face` or an edge curve of its loops or of `loops` was not +/// imported exactly ([`Exact`]). +fn face_or_edges_inexact(b: &Brep, exact: &Exact, face: FaceId, loops: &[LoopId]) -> bool { + if !exact.faces.get(face.index() as usize).copied().unwrap_or(true) { + return true; + } + let mut all = face_p2::loops(b, face, false, None).unwrap_or_default(); + all.extend_from_slice(loops); + all.into_iter().any(|l| { + let Some(lu) = b.loop_(l).ok().and_then(|l| l.loopuse) else { + return false; + }; + eu1::loopuse_edgeuses(b, lu).is_ok_and(|eus| { + eus.into_iter().any(|eu| { + eu1::edge(b, eu).ok().flatten().is_some_and(|e| { + !exact.edges.get(e.index() as usize).copied().unwrap_or(true) + }) + }) + }) + }) +} + +/// Whether `ClassifyLoops` may evaluate geometry on this input: fresh props (`props` `0`, +/// computed by `SetProps_Stage2`), or a pass over a target loop whose imported loop props +/// show no crossing of a closed seam (the loop bisector and `GetLoopCrossings`). With +/// imported props a seam-crossing pass reads only the exported touch lists. +fn evaluates_geometry( + i: &Inputs, + b: &Brep, + face: FaceId, + props: &FaceProps, + loops: &[LoopId], +) -> bool { + if i.props != 1 { + return true; + } + let n = if loops.is_empty() { + face_p2::loops(b, face, false, None).map_or(0, |l| l.len()) + } else { + loops.len() + }; + let closed = |t: Tri| !matches!(t, Tri::False); + (0..n).any(|ii| { + props.loop_props.get(ii).is_some_and(|lp| { + !(closed(props.closed_u) && lp.seam_crossing_cnt_u > 0) + && !(closed(props.closed_v) && lp.seam_crossing_cnt_v > 0) + }) + }) +} + +/// The export of `i`: the imported B-rep, face, props and target loops; `Err(output)` for +/// a failed build, a rejected case or inexact geometry the call may evaluate (the same +/// output on both sides). +fn export(k: &Kernel, i: &Inputs) -> Result, HarnessError> { let o = run_ffi(Op::Export, i)?; if o.first() != Some(&0.0) { - return Ok(Output::err(format!( + return Ok(Err(Output::err(format!( "build_failed: {}", o.get(1).copied().unwrap_or(-9.0) - ))); + )))); } let (graph, block) = ffi::split_export(&o).ok_or_else(|| harness("bad export"))?; let mut rd = Rd { v: block, at: 0 }; if rd.num()? != 0.0 { - return Ok(Output::err("rejected")); + return Ok(Err(Output::err("rejected"))); } - let (b, _exact) = import(graph)?; + let (b, exact) = import(graph)?; let face_index = rd.int()?; let face = FaceId::from_index(u32::try_from(face_index).map_err(|_| harness("bad face"))?); @@ -620,12 +693,20 @@ fn rust(i: &Inputs) -> Result { lp.seam_winding_cnt_v = rd.int()? as u64; loop_props.push(lp); } - let crv_class_u = classification(&k, &mut rd)?; - let crv_class_v = classification(&k, &mut rd)?; + let crv_class_u = classification(k, &mut rd)?; + let crv_class_v = classification(k, &mut rd)?; let done_u = touches(&mut rd)?; let done_v = touches(&mut rd)?; + let zone_tol = rd.num()?; + let no_vertex_poles = rd.int()?; + let bad_missing_seam = rd.int()?; + let n_pp = rd.count()?; + let mut pole_points = Vec::new(); + for _ in 0..n_pp { + pole_points.push(Point3::new(rd.num()?, rd.num()?, rd.num()?)); + } let n_loops = rd.count()?; - let mut loops = rd + let loops = rd .ints(n_loops)? .into_iter() .map(loop_id) @@ -644,8 +725,40 @@ fn rust(i: &Inputs) -> Result { props.crv_class_v = crv_class_v; props.done_touch_list_u = done_u; props.done_touch_list_v = done_v; + props.zone_tol_3d = zone_tol; + props.no_vertex_poles = + u64::try_from(no_vertex_poles).map_err(|_| harness("bad no-vertex poles"))?; + props.bad_missing_seam_dir = u64::try_from(bad_missing_seam) + .ok() + .and_then(SurfParamType::from_code) + .ok_or_else(|| harness("bad SmSurfParamType"))?; + props.pole_points = pole_points; } + if face_or_edges_inexact(&b, &exact, face, &loops) + && evaluates_geometry(i, &b, face, &props, &loops) + { + return Ok(Err(Output::err("inexact_geometry"))); + } + Ok(Ok(Exported { + b, + face, + props, + loops, + })) +} + +fn rust(i: &Inputs) -> Result { + let k = Kernel::default(); + let Exported { + b, + face, + mut props, + mut loops, + } = match export(&k, i)? { + Ok(e) => e, + Err(out) => return Ok(out), + }; let mut out = initial_out(); let mut scratch: CurveClassification = CurveClassification::new(&k); let r = qf4::classify_loops(&k, &b, face, &mut loops, &mut out, &mut scratch, &mut props); diff --git a/crates/solidcore-ffi/src/units/query_face_p4.rs b/crates/solidcore-ffi/src/units/query_face_p4.rs index d5e292aded..f621972371 100644 --- a/crates/solidcore-ffi/src/units/query_face_p4.rs +++ b/crates/solidcore-ffi/src/units/query_face_p4.rs @@ -37,7 +37,8 @@ //! | `Classify` | `rejected [status loops classifiable not_yet n_io (loop type orient degen area containment)* start stop n_no_area idx* tree scratch_size]` | //! //! `props` is `stage face_index closed_u closed_v poles not_yet n_lp (cross_u wind_u -//! cross_v wind_v)* class_u class_v done_u done_v`; a classification is `0` (none) or `1 +//! cross_v wind_v)* class_u class_v done_u done_v zone_tol no_vertex_poles +//! bad_missing_seam n_pp (x y z)*` (`n_pp` pole points); a classification is `0` (none) or `1 //! n (start_class start_vertex end_class end_vertex)*` (the vertex's export index for a //! vertex classification, else `-1`); a touch list `n (loop dir loop_index)*` (`loop`: the //! loop's export index, `-1` none, `-2` not a loop); `loops` `n idx*`; `tree` `n (pole diff --git a/crates/solidcore/src/query/face/p4.rs b/crates/solidcore/src/query/face/p4.rs index cc3239b8c1..a1738ccd29 100644 --- a/crates/solidcore/src/query/face/p4.rs +++ b/crates/solidcore/src/query/face/p4.rs @@ -49,10 +49,10 @@ //! [`LoopObject::from_classify_curve`]; `EntityRef` cannot name curves) cannot run //! the bisector: `ClassifyLoops` then returns [`SolidError::InvalidInput`] //! ([`qf3::NO_CURVE_HANDLES`]) instead of ignoring it as a status. -//! - **Unported callees** (`UNPORTED` stubs): `SmCurveClassification::GetLoopCrossings` -//! (`intersect.curve_class_p4`), reached only for a target loop that crosses no seam -//! the face props already classified. Faces whose loops all cross a periodic seam -//! (no-area loops of a missing seam) are classified entirely by the port. +//! - **Ignored `GetLoopCrossings` status.** A failing call leaves the touch lists it +//! reset at entry; an `SM_ERR` return keeps what it filled. Errors for upstream's +//! undefined behaviour ([`SolidError::InvalidInput`], [`SolidError::NotFound`]) and +//! unported callees propagate. //! - **Undefined behaviour upstream** is [`SolidError::InvalidInput`]: an index read //! outside `rClassifyLoopIOs`, the face props' loop props or a touch list (odd length), //! an empty seam classification read at its first or last interval, a loop without @@ -263,19 +263,24 @@ fn find_and_classify_loop_bisector( } /// `rCrvClassification.GetLoopCrossings(rLoops, &rFaceProps, &sCrossingCounts, -/// &sRawTouchDirList, &sDoneTouchDirList)`: the done touch list. +/// &sRawTouchDirList, &sDoneTouchDirList)`: the done touch list (the counts and the raw +/// list are not read by `ClassifyLoops`). `curves` are the bisector curves +/// `classification` names. The status is not checked upstream: a failing call leaves +/// the lists it reset at entry empty, except where upstream's behaviour is undefined or +/// a callee is unported (propagated). fn get_loop_crossings( - _k: &Kernel, - _b: &Brep, - _classification: &CurveClassification, - _loops: &[LoopId], - _props: &FaceProps, + k: &Kernel, + curves: &qf3::LoopObjectCurves<'_, O>, + classification: &CurveClassification, + loops: &[LoopId], + props: &mut FaceProps, ) -> Result>> { - // UNPORTED(intersect.curve_class_p4): SmCurveClassification::GetLoopCrossings - Err(SolidError::unported( - "SmCurveClassification::GetLoopCrossings", - "intersect.curve_class_p4", - )) + let handles: Vec = loops.iter().map(|&l| O::from_loop(l)).collect(); + match classification.get_loop_crossings(k, curves, &handles, Some(props)) { + Ok(c) => Ok(c.done_touches), + Err(e @ (SolidError::InvalidInput { .. } | SolidError::NotFound { .. })) => Err(e), + Err(e) => ignored(Err(e)).map(|_: Option<()>| Vec::new()), + } } /// The orientation of an entering or pairing touch: `SM_OT_SAME` for an arrive-out / @@ -310,10 +315,10 @@ fn touch_orient(done: &[TouchData], j1: usize) -> Result /// `SM_ERR` ([`SolidError::sm_err`]) for a face without loops or a classification curve /// without touches to walk; [`SolidError::InvalidInput`] ([`NOT_THIS_FACE`], /// [`NO_SURFACE`]) when the first loop is not on `face` or the face has no surface, and -/// where upstream's behaviour is undefined (module docs); [`SolidError::Unported`] for a -/// target loop that crosses no seam (the bisector's callees, `intersect.curve_class_p4`); -/// [`SolidError::InvalidInput`] ([`qf3::NO_CURVE_HANDLES`]) when such a loop meets a -/// handle type that cannot name the bisector's curves; the +/// where upstream's behaviour is undefined (module docs); [`SolidError::InvalidInput`] +/// ([`qf3::NO_CURVE_HANDLES`]) when a target loop that crosses no seam meets a handle +/// type that cannot name the bisector's curves; the errors of the bisector and of +/// [`CurveClassification::get_loop_crossings`] other than its ignored statuses; the /// errors of the topology queries and of [`FaceProps::set_props_stage2`]'s /// [`SolidError::Unported`]. #[allow(clippy::too_many_lines)] @@ -463,8 +468,7 @@ pub fn classify_loops( // UVTrimLine Vertex and Edge TgtLoop_Crossings (the status is not checked) crv = ClassifyCrv::Bisector; - bisector_done = - ignored(get_loop_crossings(k, b, scratch, loops, props))?.unwrap_or_default(); + bisector_done = get_loop_crossings(k, &bisector_curves, scratch, loops, props)?; } let done: &[TouchData] = match crv { ClassifyCrv::SeamU => &props.done_touch_list_u, diff --git a/harness/cpp/shim/query_face_p4.cpp b/harness/cpp/shim/query_face_p4.cpp index c11cc1f24b..1f11f93448 100644 --- a/harness/cpp/shim/query_face_p4.cpp +++ b/harness/cpp/shim/query_face_p4.cpp @@ -1083,6 +1083,17 @@ void put_props(std::vector& o, Graph& g, const SmFaceProps& p) put_classification(o, g, p.m_pCrvClassV); put_touches(o, g, p.m_sDoneTouchListU); put_touches(o, g, p.m_sDoneTouchListV); + // what the loop bisector and GetLoopCrossings read + o.push_back((double)p.m_sZoneTol3d); + o.push_back((double)p.m_lNoVertexPoles); + o.push_back((double)p.m_eBadMissingSeam); + o.push_back((double)p.m_sPolePoints.GetSize()); + for (ULONG i = 0; i < p.m_sPolePoints.GetSize(); i++) + { + o.push_back(p.m_sPolePoints[i].x); + o.push_back(p.m_sPolePoints[i].y); + o.push_back(p.m_sPolePoints[i].z); + } } void put_loops(std::vector& o, Graph& g, const SmTArray& l)