diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 9d29fb8a..6bb35e23 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -55,7 +55,7 @@ Generated-in-code assets are original and have no file to list: |---|---|---|---| | vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: parameter searches kept inside the surface domain (vendor/README.md) | | vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) | -| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; trace macro (vendor/README.md) | +| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; trace macro (vendor/README.md) | | vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) | ## Vendored renderer compatibility fix diff --git a/ROADMAP.md b/ROADMAP.md index 196acf0c..ab001dbd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -304,6 +304,9 @@ headline counts in-scope tabs only and lists the deferred ones separately. ## Kernel +- Boolean intersection-curve construction refuses point-only, collapsed and non-finite + polylines without panicking. Direct constructor regressions retain valid open endpoints + and closed spherical intersections; the original interactive trigger remains unidentified. - Shell of a stepped, joined planar body no longer creates horizontal subdivisions across continuous walls. Fresh axis-aligned Cut walls are stitched using their exact construction support and shared topology, preserving real steps and intentional boundaries. The local diff --git a/crates/kernel/src/intersection_curve_tests.rs b/crates/kernel/src/intersection_curve_tests.rs new file mode 100644 index 00000000..c1d8bef1 --- /dev/null +++ b/crates/kernel/src/intersection_curve_tests.rs @@ -0,0 +1,60 @@ +//! Regressions against the actual patched truck constructor, run by workspace CI. +use truck_meshalgo::prelude::PolylineCurve; +use truck_modeling::{BoundedCurve, ParametricCurve, Plane, Point3, Sphere}; +use truck_shapeops::IntersectionCurveWithParameters; + +fn planes() -> (Plane, Plane) { + let o = Point3::new(0.0, 0.0, 0.0); + (Plane::new(o, Point3::new(1.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)), Plane::new(o, Point3::new(1.0, 0.0, 0.0), Point3::new(0.0, 0.0, 1.0))) +} + +#[test] +fn degenerate_intersection_polylines_return_none_without_panicking() { + let origin = Point3::new(0.0, 0.0, 0.0); + for points in [ + vec![], + vec![origin], + vec![origin, origin], + vec![origin; 3], + vec![origin, Point3::new(f64::NAN, 0.0, 0.0)], + vec![origin, Point3::new(f64::INFINITY, 0.0, 0.0)], + ] { + let (a, b) = planes(); + let result = std::panic::catch_unwind(|| IntersectionCurveWithParameters::try_new(a, b, PolylineCurve(points.clone()), 0.01)); + assert!(result.is_ok(), "constructor panicked for {points:?}"); + assert!(result.unwrap().is_none(), "collapsed input must not create a curve: {points:?}"); + } +} + +#[test] +fn closed_intersection_retains_the_closure_and_spherical_supports() { + let a = Sphere::new(Point3::new(0.0, 0.0, 1.0), 2.0_f64.sqrt()); + let b = Sphere::new(Point3::new(0.0, 0.0, -1.0), 2.0_f64.sqrt()); + let poly = PolylineCurve(vec![ + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + Point3::new(-1.0, 0.0, 0.0), + Point3::new(0.0, -1.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + ]); + let curve = IntersectionCurveWithParameters::try_new(a, b, poly, 0.01).unwrap(); + let (lo, hi) = curve.range_tuple(); + assert_eq!(curve.subs(lo), curve.subs(hi)); + for i in 0..=40 { + let p = curve.subs(lo + (hi - lo) * i as f64 / 40.0); + assert!(p.z.abs() < 1e-8, "{p:?}"); + assert!(((p.x * p.x + p.y * p.y).sqrt() - 1.0).abs() <= 0.01, "{p:?}"); + } +} + +#[test] +fn open_intersection_keeps_endpoints_on_both_planes() { + let (a, b) = planes(); + let start = Point3::new(1.0, 0.0, 0.0); + let end = Point3::new(3.0, 0.0, 0.0); + let curve = IntersectionCurveWithParameters::try_new(a, b, PolylineCurve(vec![start, end]), 0.01).unwrap(); + let (lo, hi) = curve.range_tuple(); + assert_eq!(curve.subs(lo), start); + assert_eq!(curve.subs(hi), end); + assert_eq!(curve.subs((lo + hi) / 2.0), Point3::new(2.0, 0.0, 0.0)); +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 1fab1618..2fbd0dcd 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -30,6 +30,8 @@ mod heal; mod helix; mod iges_in; mod iges_out; +#[cfg(test)] +mod intersection_curve_tests; mod lip; mod loopblend; mod measure; diff --git a/vendor/README.md b/vendor/README.md index 5fed3050..b468b9fe 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -7,6 +7,9 @@ are marked `SolveCraft:` in the source. ## truck-shapeops 0.4.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck) Changes: +- `transversal/intersection_curve/mod.rs`: reject empty/single-point polylines and + collapsed or non-finite segments before endpoint indexing and normalization. A hidden + constructor re-export lets the kernel workspace tests exercise the actual vendored code. - `transversal/integrate/mod.rs`: mesh both shells with `robust_triangulation` (faces made by earlier booleans have boundaries that lie on their surface only within tolerance). - `transversal/divide_face/mod.rs`: when projecting a loop onto a face's surface, fall back to diff --git a/vendor/truck-shapeops/src/lib.rs b/vendor/truck-shapeops/src/lib.rs index dabc8657..3dbb1a86 100644 --- a/vendor/truck-shapeops/src/lib.rs +++ b/vendor/truck-shapeops/src/lib.rs @@ -34,4 +34,7 @@ mod healing; pub use healing::{RobustSplitClosedEdgesAndFaces, SplitClosedEdgesAndFaces}; mod transversal; pub use transversal::{and, or, ShapeOpsCurve, ShapeOpsSurface}; +// SolveCraft: exercised by kernel regressions in the workspace CI. +#[doc(hidden)] +pub use transversal::IntersectionCurveWithParameters; mod alternative; diff --git a/vendor/truck-shapeops/src/transversal/intersection_curve/mod.rs b/vendor/truck-shapeops/src/transversal/intersection_curve/mod.rs index 3988574d..203d68de 100644 --- a/vendor/truck-shapeops/src/transversal/intersection_curve/mod.rs +++ b/vendor/truck-shapeops/src/transversal/intersection_curve/mod.rs @@ -19,12 +19,21 @@ impl From> for IntersectionCurve IntersectionCurveWithParameters where S: ParametricSurface3D + SearchNearestParameter { + /// SolveCraft: construct a projected curve, returning `None` for degenerate input. pub fn try_new( surface0: S, surface1: S, poly: PolylineCurve, tol: f64, ) -> Option { + // SolveCraft: a point contact is not a curve. Reject it before indexing the + // endpoints, and reject collapsed/non-finite segments before normalizing. + if poly.len() < 2 || poly.windows(2).any(|p| { + let length2 = (p[1] - p[0]).magnitude2(); + !length2.is_finite() || length2 <= 0.0 + }) { + return None; + } let mut polyline = PolylineCurve(Vec::new()); let mut params0 = PolylineCurve(Vec::new()); let mut params1 = PolylineCurve(Vec::new()); diff --git a/vendor/truck-shapeops/src/transversal/mod.rs b/vendor/truck-shapeops/src/transversal/mod.rs index de442179..b484ccd8 100644 --- a/vendor/truck-shapeops/src/transversal/mod.rs +++ b/vendor/truck-shapeops/src/transversal/mod.rs @@ -2,6 +2,9 @@ mod divide_face; mod faces_classification; mod integrate; mod intersection_curve; +// SolveCraft: expose the constructor for workspace regression coverage of the vendored fix. +#[doc(hidden)] +pub use intersection_curve::IntersectionCurveWithParameters; mod loops_store; mod polyline_construction; pub use integrate::{and, or, ShapeOpsCurve, ShapeOpsSurface};