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

+140
View File
@@ -0,0 +1,140 @@
#![allow(clippy::many_single_char_names)]
use super::faces_classification::FacesClassification;
use super::loops_store::*;
use rustc_hash::FxHashMap as HashMap;
use std::ops::Deref;
use truck_meshalgo::prelude::*;
use truck_topology::*;
fn create_parameter_boundary<P, C, S>(
face: &Face<P, C, S>,
wire: &Wire<P, C>,
polys: &mut HashMap<EdgeID<C>, PolylineCurve<P>>,
tol: f64,
) -> Option<PolylineCurve<Point2>>
where
P: Copy,
C: BoundedCurve<Point = P> + ParameterDivision1D<Point = P>,
S: Clone + SearchParameter<D2, Point = P> + SearchNearestParameter<D2, Point = P>,
{
let surface = face.surface();
let pt = wire.front_vertex().unwrap().point();
// SolveCraft: intersection-curve points lie on the surface only within the tolerance; fall
// back to the nearest parameter when the exact search fails.
let p: Point2 = surface
.search_parameter(pt, None, 100)
.or_else(|| surface.search_nearest_parameter(pt, None, 100))?
.into();
let vec = wire.edge_iter().try_fold(vec![p], |mut vec, edge| {
let poly = polys.entry(edge.id()).or_insert_with(|| {
let curve = edge.curve();
let div = curve.parameter_division(curve.range_tuple(), tol).1;
PolylineCurve(div)
});
let mut p = *vec.last().unwrap();
let closure = |q: &P| -> Option<Point2> {
p = surface
.search_parameter(*q, Some(p.into()), 100)
.or_else(|| surface.search_nearest_parameter(*q, Some(p.into()), 100))?
.into();
Some(p)
};
let add: Option<Vec<Point2>> = match edge.orientation() {
true => poly.iter().skip(1).map(closure).collect(),
false => poly.iter().rev().skip(1).map(closure).collect(),
};
vec.append(&mut add?);
Some(vec)
})?;
Some(PolylineCurve(vec))
}
#[derive(Clone, Debug)]
struct WireChunk<'a, C> {
poly: PolylineCurve<Point2>,
wire: &'a BoundaryWire<Point3, C>,
}
type FaceWithShapesOpStatus<C, S> = (Face<Point3, C, S>, ShapesOpStatus);
fn divide_one_face<C, S>(
face: &Face<Point3, C, S>,
loops: &Loops<Point3, C>,
tol: f64,
) -> Option<Vec<FaceWithShapesOpStatus<C, S>>>
where
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
S: Clone + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
{
let (mut pre_faces, mut negative_wires) = (Vec::new(), Vec::new());
let mut map = HashMap::default();
loops.iter().try_for_each(|wire| {
let poly = crate::tr!(create_parameter_boundary(face, wire, &mut map, tol), "parameter boundary");
match poly.area() > 0.0 {
true => pre_faces.push(vec![WireChunk { poly, wire }]),
false => negative_wires.push(WireChunk { poly, wire }),
}
Some(())
})?;
negative_wires.into_iter().try_for_each(|chunk| {
let pt = chunk.poly.front();
let op = crate::tr!(pre_faces.iter_mut().find(|face| face[0].poly.include(pt)), "loop not inside any face piece");
op.push(chunk);
Some(())
})?;
let vec: Vec<_> = pre_faces
.into_iter()
.map(|pre_face| {
let surface = face.surface();
let op = pre_face
.iter()
.find(|chunk| chunk.wire.status() != ShapesOpStatus::Unknown);
let status = match op {
Some(chunk) => chunk.wire.status(),
None => ShapesOpStatus::Unknown,
};
let wires: Vec<Wire<Point3, C>> = pre_face
.into_iter()
.map(|chunk| chunk.wire.deref().clone())
.collect();
let mut new_face = Face::debug_new(wires, surface);
if !face.orientation() {
new_face.invert();
}
(new_face, status)
})
.collect();
Some(vec)
}
pub fn divide_faces<C, S>(
shell: &Shell<Point3, C, S>,
loops_store: &LoopsStore<Point3, C>,
tol: f64,
) -> Option<FacesClassification<Point3, C, S>>
where
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
S: Clone + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
{
let mut res = FacesClassification::<Point3, C, S>::default();
shell
.iter()
.zip(loops_store)
.try_for_each(|(face, loops)| {
if loops
.iter()
.all(|wire| wire.status() == ShapesOpStatus::Unknown)
{
res.push(face.clone(), ShapesOpStatus::Unknown);
} else {
let vec = crate::tr!(divide_one_face(face, loops, tol), "divide one face");
vec.into_iter()
.for_each(|(face, status)| res.push(face, status));
}
Some(())
})?;
Some(res)
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,226 @@
use super::*;
use shell::ShellCondition;
use truck_geometry::prelude::*;
use truck_topology::Vertex;
const TOL: f64 = 0.05;
fn line(v0: &Vertex<Point3>, v1: &Vertex<Point3>) -> Edge<Point3, BSplineCurve<Point3>> {
let curve = BSplineCurve::new(KnotVec::bezier_knot(1), vec![v0.point(), v1.point()]);
Edge::new(v0, v1, curve)
}
fn parabola(
v0: &Vertex<Point3>,
v1: &Vertex<Point3>,
pt: Point3,
) -> Edge<Point3, BSplineCurve<Point3>> {
let curve = BSplineCurve::new(KnotVec::bezier_knot(2), vec![v0.point(), pt, v1.point()]);
Edge::new(v0, v1, curve)
}
#[test]
fn divide_plane_test() {
let v = Vertex::news([
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 4.0, 0.0),
Point3::new(-1.0, 1.0, 0.0),
Point3::new(-1.0, 3.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 3.0, 0.0),
]);
let edge = vec![
parabola(&v[0], &v[1], Point3::new(-4.0, 2.0, 0.0)),
parabola(&v[0], &v[1], Point3::new(4.0, 2.0, 0.0)),
line(&v[0], &v[1]),
parabola(&v[2], &v[3], Point3::new(-3.0, 2.0, 0.0)),
parabola(&v[2], &v[3], Point3::new(-1.0, 2.0, 0.0)),
parabola(&v[4], &v[5], Point3::new(1.0, 2.0, 0.0)),
parabola(&v[4], &v[5], Point3::new(3.0, 2.0, 0.0)),
];
let wire: Vec<Wire<_, _>> = vec![
vec![edge[1].clone(), edge[0].inverse()].into(),
vec![edge[2].clone(), edge[0].inverse()].into(),
vec![edge[1].clone(), edge[2].inverse()].into(),
vec![edge[3].clone(), edge[4].inverse()].into(),
vec![edge[5].clone(), edge[6].inverse()].into(),
];
let face = Face::new(
vec![wire[0].clone(), wire[3].clone(), wire[4].clone()],
Plane::new(
Point3::origin(),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
),
);
let loops: Loops<_, _> = vec![
BoundaryWire::new(wire[1].clone(), ShapesOpStatus::Or),
BoundaryWire::new(wire[2].clone(), ShapesOpStatus::And),
BoundaryWire::new(wire[3].clone(), ShapesOpStatus::Unknown),
BoundaryWire::new(wire[4].clone(), ShapesOpStatus::Unknown),
]
.into_iter()
.collect();
let res = divide_one_face(&face, &loops, 0.01).unwrap();
assert_eq!(res.len(), 2);
let (mut or, mut and) = (true, true);
for (face, status) in res {
let bdd = face.absolute_boundaries();
match status {
ShapesOpStatus::Or => {
assert_eq!(bdd.len(), 2);
assert!(bdd[0] == wire[1] || bdd[0] == wire[3]);
assert!(bdd[1] == wire[1] || bdd[1] == wire[3]);
assert_ne!(bdd[0], bdd[1]);
assert!(or);
or = false;
}
ShapesOpStatus::And => {
assert_eq!(bdd.len(), 2);
assert!(bdd[0] == wire[2] || bdd[0] == wire[4]);
assert!(bdd[1] == wire[2] || bdd[1] == wire[4]);
assert_ne!(bdd[0], bdd[1]);
assert!(and);
and = false;
}
_ => panic!("There must be no unknown!"),
}
}
}
type AlternativeIntersection = crate::alternative::Alternative<
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve<Point3>, AlternativeSurface>,
>;
type AlternativeSurface = crate::alternative::Alternative<BSplineSurface<Point3>, Plane>;
crate::impl_from!(
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve<Point3>, AlternativeSurface>
);
crate::impl_from!(BSplineSurface<Point3>, Plane);
fn parabola_surfaces() -> (AlternativeSurface, AlternativeSurface) {
// 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)],
];
(
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl0).into(),
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl1).into(),
)
}
#[test]
fn independent_intersection() {
// prepare geoetries
let arc00: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc01: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc10: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, -1.0, 1.0),
],
))
.into();
let arc11: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, -1.0, 1.0),
],
))
.into();
let (surface0, surface1) = parabola_surfaces();
let plane0: AlternativeSurface = Plane::new(
Point3::new(0.0, 0.0, 1.0),
Point3::new(1.0, 0.0, 1.0),
Point3::new(0.0, 1.0, 1.0),
)
.into();
let plane1: AlternativeSurface = Plane::new(
Point3::new(0.0, 0.0, -1.0),
Point3::new(1.0, 0.0, -1.0),
Point3::new(0.0, 1.0, -1.0),
)
.into();
// prepare topologies
let v00 = Vertex::new(Point3::new(1.0, 0.0, 1.0));
let v01 = Vertex::new(Point3::new(-1.0, 0.0, 1.0));
let v10 = Vertex::new(Point3::new(1.0, 0.0, -1.0));
let v11 = Vertex::new(Point3::new(-1.0, 0.0, -1.0));
let wire0: Wire<_, _> = vec![Edge::new(&v00, &v01, arc00), Edge::new(&v01, &v00, arc01)].into();
let wire1: Wire<_, _> = vec![Edge::new(&v10, &v11, arc10), Edge::new(&v11, &v10, arc11)].into();
let shell0: Shell<_, _, _> = vec![
Face::new(vec![wire0.clone()], plane0),
Face::new(vec![wire0], surface0).inverse(),
]
.into();
assert_eq!(shell0.shell_condition(), ShellCondition::Closed);
let shell1: Shell<_, _, _> = vec![
Face::new(vec![wire1.clone()], plane1).inverse(),
Face::new(vec![wire1], surface1),
]
.into();
assert_eq!(shell1.shell_condition(), ShellCondition::Closed);
let poly_shell0 = shell0.triangulation(TOL);
let poly_shell1 = shell1.triangulation(TOL);
let LoopsStoreQuadruple {
geom_loops_store0: loops_store0,
geom_loops_store1: loops_store1,
..
} = create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL).unwrap();
let [and0, or0, unknown0] = divide_faces(&shell0, &loops_store0, TOL)
.unwrap()
.and_or_unknown();
let [and1, or1, unknown1] = divide_faces(&shell1, &loops_store1, TOL)
.unwrap()
.and_or_unknown();
assert_eq!(and0.len(), 1);
assert_eq!(or0.len(), 1);
assert_eq!(unknown0.len(), 1);
assert_eq!(and1.len(), 1);
assert_eq!(or1.len(), 1);
assert_eq!(unknown1.len(), 1);
match unknown0[0].surface() {
AlternativeSurface::FirstType(_) => panic!("This face must plane!"),
AlternativeSurface::SecondType(_) => {}
}
match unknown1[0].surface() {
AlternativeSurface::FirstType(_) => panic!("This face must plane!"),
AlternativeSurface::SecondType(_) => {}
}
let and_shell: Shell<_, _, _> = vec![and0[0].clone(), and1[0].clone()].into();
assert_eq!(and_shell.shell_condition(), ShellCondition::Closed);
}