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 <noreply@anthropic.com>
This commit is contained in:
Brett Lawson
2026-10-23 22:47:46 -07:00
parent bd7d25ad97
commit e46bbde814
4 changed files with 167 additions and 38 deletions
+129 -16
View File
@@ -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<Output, HarnessError> {
// 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<Tri, HarnessError> {
})
}
fn rust(i: &Inputs) -> Result<Output, HarnessError> {
let k = Kernel::default();
/// What the export of a case gives the Rust side.
struct Exported {
b: Brep,
face: FaceId,
props: FaceProps<H>,
loops: Vec<LoopId>,
}
/// 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<H>,
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<Result<Exported, Output>, 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<Output, HarnessError> {
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<Output, HarnessError> {
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<Output, HarnessError> {
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<H> = CurveClassification::new(&k);
let r = qf4::classify_loops(&k, &b, face, &mut loops, &mut out, &mut scratch, &mut props);
@@ -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
+25 -21
View File
@@ -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<O: LoopObject>(
}
/// `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<O: LoopObject>(
_k: &Kernel,
_b: &Brep,
_classification: &CurveClassification<O>,
_loops: &[LoopId],
_props: &FaceProps<O>,
k: &Kernel,
curves: &qf3::LoopObjectCurves<'_, O>,
classification: &CurveClassification<O>,
loops: &[LoopId],
props: &mut FaceProps<O>,
) -> Result<Vec<TouchData<O>>> {
// UNPORTED(intersect.curve_class_p4): SmCurveClassification::GetLoopCrossings
Err(SolidError::unported(
"SmCurveClassification::GetLoopCrossings",
"intersect.curve_class_p4",
))
let handles: Vec<O> = 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<O: Copy>(done: &[TouchData<O>], j1: usize) -> Result<OrientType>
/// `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<O: LoopObject>(
// 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<O>] = match crv {
ClassifyCrv::SeamU => &props.done_touch_list_u,
+11
View File
@@ -1083,6 +1083,17 @@ void put_props(std::vector<double>& 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<double>& o, Graph& g, const SmTArray<SmLoop*>& l)