diff --git a/crates/solidcore-ffi/src/units/intersect_curve_class_p9.rs b/crates/solidcore-ffi/src/units/intersect_curve_class_p9.rs index c7972698fd..f198f497c9 100644 --- a/crates/solidcore-ffi/src/units/intersect_curve_class_p9.rs +++ b/crates/solidcore-ffi/src/units/intersect_curve_class_p9.rs @@ -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.; diff --git a/crates/solidcore/src/curve/curve/p8.rs b/crates/solidcore/src/curve/curve/p8.rs index 703fffa135..a1881a03ee 100644 --- a/crates/solidcore/src/curve/curve/p8.rs +++ b/crates/solidcore/src/curve/curve/p8.rs @@ -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(cache: &CurveCache) -> Result<&Tree> { 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( k: &Kernel, curve: &Curve, handle: Option, + zone_tol_3d: Option, ) -> Result> { - match make_curve_cache::(k, curve, handle) { + match make_curve_cache_with_zone_tol::( + 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, cache1: &mut Option>, cache2: &mut Option>, + ) -> Result> { + 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( + &self, + k: &Kernel, + interval: &Extent1d, + other: &Self, + other_interval: &Extent1d, + dist_tol: f64, + opts: &CurveIntersectOptions, + cache1: &mut Option>, + cache2: &mut Option>, + zone_tols: [Option; 2], ) -> Result> { 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( &self, k: &Kernel, @@ -472,6 +546,7 @@ impl Curve { opts: &CurveIntersectOptions, cache1_slot: &mut Option>, cache2_slot: &mut Option>, + zone_tols: [Option; 2], ) -> Result> { 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() diff --git a/crates/solidcore/src/intersect/curve_class/p9.rs b/crates/solidcore/src/intersect/curve_class/p9.rs index 3109b639ff..074a0229ea 100644 --- a/crates/solidcore/src/intersect/curve_class/p9.rs +++ b/crates/solidcore/src/intersect/curve_class/p9.rs @@ -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 CurveClassification { 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 CurveClassification { ], ..CurveIntersectOptions::default() }, + &mut None, + &mut None, + [None, Some(edge_zone)], )?; solutions.extend( found diff --git a/fixtures/intersect.curve_class_p9/hermite_edge_targets.json b/fixtures/intersect.curve_class_p9/hermite_edge_targets.json index e86cb83961..a8430ecdae 100644 --- a/fixtures/intersect.curve_class_p9/hermite_edge_targets.json +++ b/fixtures/intersect.curve_class_p9/hermite_edge_targets.json @@ -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]] + } + ] + } } ] } diff --git a/harness/cpp/shim/intersect_curve_class_p9.cpp b/harness/cpp/shim/intersect_curve_class_p9.cpp index 9fb97c5006..287249f0ba 100644 --- a/harness/cpp/shim/intersect_curve_class_p9.cpp +++ b/harness/cpp/shim/intersect_curve_class_p9.cpp @@ -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