Fix lazy classifier target-cache tolerance and preserve valid closed Hermite inputs

Co-Authored-By: OpenAI GPT-6.1 <noreply@openai.com>
This commit is contained in:
Alex Lytvynenko
2026-10-28 14:16:09 +01:00
parent fbd30bebb7
commit ec3d974864
5 changed files with 123 additions and 23 deletions
@@ -110,11 +110,7 @@ pub fn validate(input: &[f64]) -> Result<(), FfiError> {
if e[0] != 0. || header[8] != 1. {
return Err(invalid());
}
} else if e[0] == 1. {
if e[11..14].iter().zip(&e[17..20]).all(|(a, b)| a == b) {
return Err(invalid());
}
} else if e[1..4].iter().zip(&e[4..7]).all(|(a, b)| a == b) {
} else if e[0] == 0. && e[1..4].iter().zip(&e[4..7]).all(|(a, b)| a == b) {
return Err(invalid());
}
}
@@ -192,10 +188,10 @@ mod tests {
let mut same = i.clone();
same[22 + 9] = 1.;
assert!(validate(&same).is_err());
// The cubic needs two distinct endpoints.
let mut collapsed = i.clone();
collapsed[22 + 17..22 + 20].copy_from_slice(&[0.3, 0.6, -0.4]);
assert!(validate(&collapsed).is_err());
// A nonconstant cubic may close on itself; the wire constructor glues its ends.
let mut closed = i.clone();
closed[22 + 17..22 + 20].copy_from_slice(&[0.3, 0.6, -0.4]);
assert!(validate(&closed).is_ok());
// A line record still needs distinct endpoints, and unknown kinds are rejected.
let mut repeated = i.clone();
repeated[22] = 0.;
+86 -10
View File
@@ -25,8 +25,9 @@
//! itself then names stand-ins for the four curves (`operand_handles`) reporting
//! their natural intervals, so `IdenticalSolutions` merges within 1/10000th of
//! them and `sm_RefineCCSolutions` resolves each curve, as upstream.
//! - **Caches.** The caches are built for the call with [`make_curve_cache`] (see
//! unit `curve.curve_p5`), or kept across calls in slots the caller lends
//! - **Caches.** The caches are built for the call with
//! [`make_curve_cache`](crate::curve::curve::p5::make_curve_cache) (see unit
//! `curve.curve_p5`), or kept across calls in slots the caller lends
//! (`global_curve_intersect_cached`, as upstream keeps them on the curves; the
//! composite curve offsetter uses it for its offset pairs, and
//! `global_curve_intersect_unnamed_cached` serves callers that name no handles,
@@ -34,6 +35,8 @@
//! identity, [`core::ptr::eq`]) shares one cache and so one tree, which makes the
//! intersection a self intersection, as upstream's shared object cache. A cache that cannot be built is
//! `SM_ERR_NULL_POINTER` (upstream `NER`).
//! Callers holding topology may supply each curve owner's zone tolerance to the
//! lazy build without changing intersection tolerance or copying geometry.
//! - **Leaf pair solve.** Upstream calls `LocalSolve` on the intersection test
//! solver directly, which appends to the solution array the solver still points
//! at; the port lends the array to the solver for that call.
@@ -57,7 +60,7 @@
use crate::curve::curve::p1::refine_cc_solutions;
use crate::curve::curve::p2::{at, CciGlobalSolver, Curve};
use crate::curve::curve::p4::{analytic_curve_intersect, point_or_undef, AnalyticOperand};
use crate::curve::curve::p5::make_curve_cache;
use crate::curve::curve::p5::make_curve_cache_with_zone_tol;
use crate::curve::curve_cache::CurveCache;
use crate::error::{ignored, Result, SolidError};
use crate::geom::extent1d::Extent1d;
@@ -285,13 +288,24 @@ fn cache_tree<O: SolutionObject>(cache: &CurveCache<O>) -> Result<&Tree<O>> {
cache.tree().ok_or(SolidError::null_pointer(OP_GCI))
}
/// `GetOrCreateObjectCache` for a curve (`NER` on failure).
/// `GetOrCreateObjectCache` for a curve (`NER` on failure). `zone_tol_3d` is the zone
/// tolerance the caller resolved for the curve's owner: an edge curve's cache
/// tessellates at half that tolerance ([`make_curve_cache_with_zone_tol`]), as
/// upstream's `SmTol::GetApproxTol3d(&m_crCurve)` reads it from the curve's edge
/// (`SmCurveCache.cpp:1496`). `None` preserves the curve's recorded owner tolerance,
/// falling back to the system tolerance when the curve has none.
fn build_cache<O: SolutionObject>(
k: &Kernel,
curve: &Curve,
handle: Option<O>,
zone_tol_3d: Option<f64>,
) -> Result<CurveCache<O>> {
match make_curve_cache::<O>(k, curve, handle) {
match make_curve_cache_with_zone_tol::<O>(
k,
curve,
handle,
zone_tol_3d.or_else(|| curve.owner_zone_tol_3d()),
) {
Ok(c) => Ok(c),
Err(e) if e.is_unported() => Err(e),
Err(_) => Err(SolidError::null_pointer(OP_GCI)),
@@ -375,6 +389,7 @@ impl Curve {
opts,
&mut None,
&mut None,
[None, None],
)
}
@@ -402,9 +417,63 @@ impl Curve {
opts: &CurveIntersectOptions<O>,
cache1: &mut Option<CurveCache<O>>,
cache2: &mut Option<CurveCache<O>>,
) -> Result<SolutionArray<O>> {
self.global_curve_intersect_cached_with_zone_tols(
k,
interval,
other,
other_interval,
dist_tol,
opts,
cache1,
cache2,
[None, None],
)
}
/// [`Curve::global_curve_intersect_cached`] with the zone tolerances the caller
/// resolved for the curves' owners. Upstream builds each curve's cache on the curve
/// object itself, so an edge curve tessellates at half the edge's tolerance
/// (`SmTol::GetApproxTol3d(&m_crCurve)`, `SmCurveCache.cpp:1496`); a caller whose
/// handle geometry is not that owned object supplies the tolerance here. A slot is
/// used exactly as in [`Curve::global_curve_intersect_cached`]: nothing is built
/// before the body reaches it, and a `None` tolerance builds as that method does.
///
/// # Errors
///
/// As [`Curve::global_curve_intersect`].
pub(crate) fn global_curve_intersect_cached_with_zone_tols<O: SolutionObject>(
&self,
k: &Kernel,
interval: &Extent1d,
other: &Self,
other_interval: &Extent1d,
dist_tol: f64,
opts: &CurveIntersectOptions<O>,
cache1: &mut Option<CurveCache<O>>,
cache2: &mut Option<CurveCache<O>>,
zone_tols: [Option<f64>; 2],
) -> Result<SolutionArray<O>> {
if opts.handles == [None, None] && opts.analytic_handles == [None, None] {
return self.global_curve_intersect(k, interval, other, other_interval, dist_tol, opts);
let own = operand_handles(self, other);
let inner = CurveIntersectOptions {
skip_global_coincidence_check: opts.skip_global_coincidence_check,
handles: [Some(own[0]), Some(own[1])],
analytic_handles: [Some(own[2]), Some(own[3])],
};
return Ok(self
.global_curve_intersect_cached_with_zone_tols(
k,
interval,
other,
other_interval,
dist_tol,
&inner,
&mut None,
&mut None,
zone_tols,
)?
.without_objects());
}
self.global_curve_intersect_body(
k,
@@ -415,6 +484,7 @@ impl Curve {
opts,
cache1,
cache2,
zone_tols,
)
}
@@ -456,12 +526,16 @@ impl Curve {
&inner,
cache1,
cache2,
[None, None],
)?
.without_objects())
}
/// The body of [`Curve::global_curve_intersect`] for a caller giving handles, with
/// the cache slots of [`Curve::global_curve_intersect_cached`].
/// the cache slots of [`Curve::global_curve_intersect_cached`]. `zone_tols` are the
/// zone tolerances the caller resolved for the curves' owners, used only where a
/// slot's cache is built ([`build_cache`]); `None` preserves the corresponding
/// curve's recorded owner tolerance.
fn global_curve_intersect_body<O: SolutionObject>(
&self,
k: &Kernel,
@@ -472,6 +546,7 @@ impl Curve {
opts: &CurveIntersectOptions<O>,
cache1_slot: &mut Option<CurveCache<O>>,
cache2_slot: &mut Option<CurveCache<O>>,
zone_tols: [Option<f64>; 2],
) -> Result<SolutionArray<O>> {
let [h_this, h_other] = opts.handles;
let same_curve = core::ptr::eq(self, other);
@@ -518,12 +593,13 @@ impl Curve {
}
// get target curves' caches (kept in the caller's slots, see
// `global_curve_intersect_cached`)
// `global_curve_intersect_cached`), each at its owner's zone tolerance when the
// caller resolved one
if cache1_slot.is_none() {
*cache1_slot = Some(build_cache(k, self, h_this)?);
*cache1_slot = Some(build_cache(k, self, h_this, zone_tols[0])?);
}
if !same_curve && cache2_slot.is_none() {
*cache2_slot = Some(build_cache(k, other, h_other)?);
*cache2_slot = Some(build_cache(k, other, h_other, zone_tols[1])?);
}
let cache1 = cache1_slot
.as_mut()
@@ -17,7 +17,11 @@
//! clearing is represented by a private copy for a lifted 2D source. Concrete
//! curve-on-surface dispatch is preserved; [`CurveClassification::intersect_edges`]
//! inserts through a read-only adapter (its mutable edits defer) and
//! [`CurveClassification::intersect_edges_mut`] through a mutable one. Ignored native evaluation
//! [`CurveClassification::intersect_edges_mut`] through a mutable one. Each target
//! edge's curve cache is built at that edge's live zone tolerance, as upstream's
//! `SmTol::GetApproxTol3d(&m_crCurve)` reads it from the curve's owning edge; the
//! classification curve's cache has no owner in reach through the caller's geometry and
//! still builds on the system tolerance. Ignored native evaluation
//! and drop statuses preserve initialized outputs; unported errors propagate. The
//! native checked-drop guess aliases its output and is reset to zero before solving;
//! the Rust call explicitly uses that zero guess. A 3D trim-boundary source returns
@@ -371,7 +375,12 @@ impl<O: ClassObject> CurveClassification<O> {
solutions.push(sol);
}
} else {
let found = working.global_curve_intersect(
// `GlobalCurveIntersect` caches the edge's own curve object, whose cache is
// built at `SmTol::GetApproxTol3d(&m_crCurve)`: half the edge's tolerance
// (`SmCurveCache.cpp:1496`, through the curve's owner). The caller's handle
// geometry is not that owned object, so the live edge tolerance is supplied
// to the lazy build; a cache is still built only where the solve needs it.
let found = working.global_curve_intersect_cached_with_zone_tols(
k,
&self.interval(),
target,
@@ -384,6 +393,9 @@ impl<O: ClassObject> CurveClassification<O> {
],
..CurveIntersectOptions::default()
},
&mut None,
&mut None,
[None, Some(edge_zone)],
)?;
solutions.extend(
found
@@ -321,6 +321,23 @@
}
]
}
},
{
"name": "hermite_target_closed_disjoint",
"upstream_ref": "source/SMLib/src/SmBrep.cpp:2264-2293: CreateWireEdgeFromCurve glues coincident endpoints; a nonconstant closed Hermite is a valid disjoint target, not an invalid line",
"inputs": {
"op": 0,
"dim3": true,
"zone": 1e-06,
"edges": [
{
"a": [0.5, 2.0, 0.0],
"b": [0.5, 2.0, 0.0],
"zone": 1e-06,
"hermite": [[0.5, 2.0, 0.0], [1.0, 0.0, 1.0], [0.5, 2.0, 0.0], [1.0, 0.0, -1.0]]
}
]
}
}
]
}
@@ -144,8 +144,7 @@ SC_EXPORT int32_t sc_intersect_curve_class_p9_run(const double* in,int64_t n,dou
for(int k=11;k<23;k++) if(!std::isfinite(p[k]*in[10])) return SC_ERR_ARG;
// A Hermite edge carries its own curve, never the source curve.
if(p[9]) { ++shared; if(p[0]!=0 || in[8]!=1) return SC_ERR_ARG; }
else if(p[0]==1) { if(p[11]==p[17] && p[12]==p[18] && p[13]==p[19]) return SC_ERR_ARG; }
else if(p[1]==p[4] && p[2]==p[5] && p[3]==p[6]) return SC_ERR_ARG;
else if(p[0]==0 && p[1]==p[4] && p[2]==p[5] && p[3]==p[6]) return SC_ERR_ARG;
}
if(shared>1) return SC_ERR_ARG;
SC_TRY