Kernel: pole-free six-patch spheres; vendored truck-shapeops with robust meshing and nearest-parameter loop projection (sphere and box booleans work); least-squares sphere classification

Oracle: 25/29 (14 and 15 now match Fusion).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 21:30:18 +00:00
1 parent 7ef3426cb3
commit 043a542e29
34 files changed
+6228 -43

No files matched your search

@@ -0,0 +1,174 @@
use truck_base::cgmath64::*;
use truck_geometry::prelude::*;
use truck_meshalgo::prelude::*;
/// polyline base ntersection curve with parameter
#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
pub struct IntersectionCurveWithParameters<S> {
#[deref]
#[deref_mut]
ic: IntersectionCurve<PolylineCurve<Point3>, S>,
params0: PolylineCurve<Point2>,
params1: PolylineCurve<Point2>,
}
impl<S> From<IntersectionCurveWithParameters<S>> for IntersectionCurve<PolylineCurve<Point3>, S> {
fn from(a: IntersectionCurveWithParameters<S>) -> Self { a.ic }
}
impl<S> IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
pub fn try_new(
surface0: S,
surface1: S,
poly: PolylineCurve<Point3>,
tol: f64,
) -> Option<Self> {
let mut polyline = PolylineCurve(Vec::new());
let mut params0 = PolylineCurve(Vec::new());
let mut params1 = PolylineCurve(Vec::new());
for p in poly.windows(2) {
let n = (p[1] - p[0]).normalize();
let (q, p0, p1) = double_projection(&surface0, None, &surface1, None, p[0], n, 100)?;
polyline.push(q);
params0.push(p0);
params1.push(p1);
}
let (q, p0, p1) = if poly[0].near(&poly[poly.len() - 1]) {
(polyline[0], params0[0], params1[0])
} else {
let n = (poly[poly.len() - 1] - poly[poly.len() - 2]).normalize();
double_projection(
&surface0,
None,
&surface1,
None,
poly[poly.len() - 1],
n,
100,
)?
};
polyline.push(q);
params0.push(p0);
params1.push(p1);
Some(Self {
ic: IntersectionCurve::new_unchecked(
Box::new(surface0),
Box::new(surface1),
polyline,
tol,
),
params0,
params1,
})
}
}
impl<S> ParametricCurve for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
type Vector = Vector3;
#[inline(always)]
fn subs(&self, t: f64) -> Point3 { self.ic.subs(t) }
#[inline(always)]
fn der(&self, t: f64) -> Vector3 { self.ic.der(t) }
#[inline(always)]
fn der2(&self, t: f64) -> Vector3 { self.ic.der2(t) }
#[inline(always)]
fn parameter_range(&self) -> ParameterRange { self.ic.parameter_range() }
}
impl<S> BoundedCurve for IntersectionCurveWithParameters<S> where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3> {}
impl<S> ParameterDivision1D for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
#[inline(always)]
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<Point3>) {
self.ic.parameter_division(range, tol)
}
}
impl<S> Cut for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
#[inline(always)]
fn cut(&mut self, t: f64) -> Self {
Self {
ic: self.ic.cut(t),
params0: self.params0.cut(t),
params1: self.params1.cut(t),
}
}
}
impl<S: Clone> Invertible for IntersectionCurveWithParameters<S> {
fn invert(&mut self) {
self.ic.invert();
self.params0.invert();
self.params1.invert();
}
}
impl<S> SearchParameter<D1> for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<f64> {
self.ic.search_parameter(point, hint, trials)
}
}
impl<S> SearchNearestParameter<D1> for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<f64> {
self.ic.search_nearest_parameter(point, hint, trials)
}
}
pub fn intersection_curves<S>(
surface0: S,
polygon0: &PolygonMesh,
surface1: S,
polygon1: &PolygonMesh,
tol: f64,
) -> Vec<(
PolylineCurve<Point3>,
Option<IntersectionCurveWithParameters<S>>,
)>
where
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
let interferences = polygon0.extract_interference(polygon1);
let polylines = super::polyline_construction::construct_polylines(&interferences);
polylines
.into_iter()
.map(|polyline| {
let curve = IntersectionCurveWithParameters::try_new(
surface0.clone(),
surface1.clone(),
polyline.clone(),
tol,
);
(polyline, curve)
})
.collect()
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,173 @@
use super::*;
use std::f64::consts::PI;
mod double_projection_tests {
use super::*;
fn create_axis(n: Vector3) -> (Vector3, Vector3) {
let idx = if n[0].abs() < n[1].abs() { 0 } else { 1 };
let idx = if n[idx].abs() < n[2].abs() { idx } else { 2 };
let mut e = Vector3::zero();
e[idx] = 1.0;
let x = n.cross(e).normalize();
(x, n.cross(x))
}
fn exec_plane_case() {
let c0 = Point3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
);
let n0 = Vector3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
)
.normalize();
let (x, y) = create_axis(n0);
let plane0 = Plane::new(c0, c0 + x, c0 + y);
let c1 = Point3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
);
let n1 = Vector3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
)
.normalize();
let (x, y) = create_axis(n1);
let plane1 = Plane::new(c1, c1 + x, c1 + y);
let n = n0.cross(n1).normalize();
let mut o = None;
for i in 0..10 {
let t = i as f64;
let p = Point3::origin() + t * n;
let (q, p0, p1) = double_projection(&plane0, None, &plane1, None, p, n, 100)
.unwrap_or_else(|| panic!("plane0: {plane0:?}\nplane1: {plane1:?}\n p: {p:?}"));
assert_near!(q, plane0.subs(p0.x, p0.y));
assert_near!(q, plane1.subs(p1.x, p1.y));
if let Some(o) = o {
assert_near!(q.distance2(o), t * t);
} else {
o = Some(q);
}
}
}
#[test]
fn plane_case() { (0..100).for_each(|_| exec_plane_case()); }
#[test]
fn sphere_case() {
let sphere0 = Sphere::new(Point3::new(0.0, 0.0, 1.0), f64::sqrt(2.0));
let sphere1 = Sphere::new(Point3::new(0.0, 0.0, -1.0), f64::sqrt(2.0));
for _ in 0..100 {
let t = 2.0 * PI * rand::random::<f64>();
let r = 2.0 * rand::random::<f64>();
let p = Point3::new(r * f64::cos(t), r * f64::sin(t), 0.0);
let n = Vector3::new(-f64::sin(t), f64::cos(t), 0.0);
let (q, p0, p1) = double_projection(&sphere0, None, &sphere1, None, p, n, 100)
.unwrap_or_else(|| panic!("p: {p:?}"));
assert_near!(q, sphere0.subs(p0.x, p0.y));
assert_near!(q, sphere1.subs(p1.x, p1.y));
assert_near!(q, Point3::new(f64::cos(t), f64::sin(t), 0.0));
}
}
}
#[test]
fn intersection_curve_sphere_case() {
let sphere0 = Sphere::new(Point3::new(0.0, 0.0, 1.0), f64::sqrt(2.0));
let sphere1 = Sphere::new(Point3::new(0.0, 0.0, -1.0), f64::sqrt(2.0));
const M: usize = 5;
let polyline = (0..=M)
.map(|i| {
let t = 2.0 * PI * i as f64 / M as f64;
Point3::new(0.8 * f64::cos(t), 0.8 * f64::sin(t), 0.0)
})
.collect::<PolylineCurve<_>>();
let curve: IntersectionCurve<_, _> =
IntersectionCurveWithParameters::try_new(sphere0, sphere1, polyline, 0.5)
.unwrap()
.into();
const N: usize = 100;
let mut sum = 0.0;
let (t0, t1) = curve.range_tuple();
for i in 0..N {
let t = t0 + (t1 - t0) * i as f64 / N as f64;
let pt = curve.subs(t);
assert_near!(pt.distance(Point3::origin()), 1.0);
let vec = curve.der(t);
assert!(pt.dot(vec).so_small(), "{i} {t} {vec:?}");
assert!(vec[2].so_small());
sum += vec.magnitude() * (t1 - t0) / N as f64;
}
assert!(f64::abs(sum - 2.0 * PI) < 0.1, "{}", sum);
let theta = 2.0 * PI * rand::random::<f64>();
let pt = Point3::new(f64::cos(theta), f64::sin(theta), 0.0);
let t = curve.search_parameter(pt, None, 1).unwrap();
assert!(curve.subs(t).near(&pt));
let pt = Point3::new(1.1 * f64::cos(theta), 1.1 * f64::sin(theta), 0.0);
assert!(curve.search_parameter(pt, None, 1).is_none());
let mut curve0 = curve.clone();
let curve1 = curve0.cut(2.5);
assert_near!(curve0.front(), curve.front());
assert_near!(curve0.back(), curve.subs(2.5));
assert_near!(curve1.front(), curve.subs(2.5));
assert_near!(curve1.back(), curve.back());
let mut curve0 = curve.clone();
let curve1 = curve0.cut(2.0);
assert_near!(curve0.front(), curve.front());
assert_near!(curve0.back(), curve.subs(2.0));
assert_near!(curve1.front(), curve.subs(2.0));
assert_near!(curve1.back(), curve.back());
}
#[test]
fn collide_parabola() {
const TOL: f64 = 0.05;
// define surfaces
#[rustfmt::skip]
let ctrl0 = vec![
vec![Point3::new(-1.0, -1.0, 3.0), Point3::new(-1.0, 0.0, -1.0), Point3::new(-1.0, 1.0, 3.0)],
vec![Point3::new(0.0, -1.0, -1.0), Point3::new(0.0, 0.0, -5.0), Point3::new(0.0, 1.0, -1.0)],
vec![Point3::new(1.0, -1.0, 3.0), Point3::new(1.0, 0.0, -1.0), Point3::new(1.0, 1.0, 3.0)],
];
#[rustfmt::skip]
let ctrl1 = vec![
vec![Point3::new(-1.0, -1.0, -3.0), Point3::new(-1.0, 0.0, 1.0), Point3::new(-1.0, 1.0, -3.0)],
vec![Point3::new(0.0, -1.0, 1.0), Point3::new(0.0, 0.0, 5.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, -1.0, -3.0), Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, -3.0)],
];
let surface0 = BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl0);
let surface1 = BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl1);
// meshing surface
let instant = std::time::Instant::now();
let polygon0 = StructuredMesh::from_surface(&surface0, surface0.range_tuple(), TOL).destruct();
let polygon1 = StructuredMesh::from_surface(&surface1, surface1.range_tuple(), TOL).destruct();
println!("Meshing Surfaces: {}s", instant.elapsed().as_secs_f64());
// extract intersection curves
let instant = std::time::Instant::now();
let curves = intersection_curves(surface0, &polygon0, surface1, &polygon1, TOL);
println!(
"Extracting Intersection: {}s",
instant.elapsed().as_secs_f64()
);
assert_eq!(curves.len(), 1);
let curve = curves[0].1.clone().unwrap();
const N: usize = 100;
for i in 0..N {
let t1 = curve.range_tuple().1;
let t = t1 * i as f64 / N as f64;
let pt = curve.subs(t);
assert_near!(pt.distance(Point3::origin()) * 0.5, f64::sqrt(0.5) * 0.5);
}
}