Kernel: planar_face asks solidcore-ops whether a B-spline face is flat (follow-up to #14394) (#14509)

#14394 tested whether a B-spline face is flat with its own control-net
math in crates/kernel, but SolidCraft computes no geometry
(solidcore/docs/layers.md). planar_face now asks
solidcore_ops::Body::face_plane_normal, which runs SMLib's IsPlanar at
the face's tolerance and gives the outward normal, and makes the plane
through the face's first vertex. flat_net and net_fits are deleted. The
record's solidcore-ops body is made on first use and cached with the
body, shared by clones.


Claude-Session: https://claude.ai/code/session_01Gr84VuQWHKtTwJ7cC3CP3W

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ancient_astronaut_theorist
2026-11-10 09:24:57 +00:00
committed by GitHub
parent 52c9f72249
commit 94ce45a1f2
2 changed files with 38 additions and 50 deletions
+36 -48
View File
@@ -27,6 +27,9 @@ pub struct Body {
/// The body in SMLib, built from `record` on first use (or kept from the operation that
/// made it), shared by clones because they share the record.
pub(crate) smlib: std::sync::Arc<std::sync::OnceLock<std::result::Result<solidcraft_smlib::Brep, Refusal>>>,
/// The record as solidcore-ops' body, for the kernel's answers about its faces
/// ([`Body::flat_spline_face`]): made on first use, shared by clones as `smlib` is.
pub(crate) ops: std::sync::Arc<std::sync::OnceLock<(solidcore_ops::Body, solidcore_ops::Order)>>,
/// Bounding-box diagonal of the vertices, measured when the body was made; 0 means "not
/// measured, walk the geometry". Every kernel tolerance is a fraction of it.
pub(crate) size: f64,
@@ -271,40 +274,6 @@ pub struct FaceInfo {
/// at: the mesher's own (solidcore-tess).
pub use solidcore_tess::{DISPLAY_ANGLE, MeshTolerance};
/// Whether a control net is small enough to test for flatness ([`flat_net`]): the work is one
/// pass over its points, at most 65,536 of them (a face of an imported design can carry a large
/// net, and [`Body::planar_face`] is asked of every face a pick passes).
fn net_fits<T>(net: &[Vec<T>]) -> bool {
net.iter().try_fold(0usize, |count, row| count.checked_add(row.len()).filter(|count| *count <= 1 << 16)).is_some()
}
/// A B-spline control net (`net[i][j]`, i along u) lying on one plane within `tolerance`: a point
/// of the plane and two directions on it whose cross product runs along the surface's natural
/// normal (`∂S/∂u × ∂S/∂v`), as a plane surface's axes do. `None` for a net off every plane, or
/// one too degenerate to have a normal (#14346).
fn flat_net(net: &[Vec<Vec3>], tolerance: f64) -> Option<(Vec3, Vec3, Vec3)> {
let columns = net.first()?.len();
if net.len() < 2 || columns < 2 || net.iter().any(|row| row.len() != columns) || !(tolerance.is_finite() && tolerance > 0.0) {
return None;
}
// The net's oriented area, Σ (P[i+1][j] − P[i][j]) × (P[i][j+1] − P[i][j]), runs along the
// natural normal of a flat surface that doesn't fold over itself; the u steps give its u way.
let (mut area, mut along_u) = (Vec3::ZERO, Vec3::ZERO);
for (row, next) in net.iter().zip(net.iter().skip(1)) {
for ((p, q), r) in row.iter().zip(row.iter().skip(1)).zip(next) {
area += (*r - *p).cross(*q - *p);
along_u += *r - *p;
}
}
let normal = area.normalized()?;
let origin = *net.first()?.first()?;
if net.iter().flatten().any(|p| !p.is_finite() || (*p - origin).dot(normal).abs() > tolerance) {
return None;
}
let u = (along_u - normal * along_u.dot(normal)).normalized()?;
Some((origin, u, normal.cross(u)))
}
pub(crate) fn v3(p: [f64; 3]) -> Vec3 {
Vec3::new(p[0], p[1], p[2])
}
@@ -467,6 +436,7 @@ impl Body {
Body {
record: std::sync::Arc::new(record),
smlib: std::sync::Arc::new(std::sync::OnceLock::new()),
ops: std::sync::Arc::new(std::sync::OnceLock::new()),
size,
volume: Some(std::sync::Arc::new(std::sync::Mutex::new(None))),
bounds: std::sync::Arc::new(std::sync::Mutex::new(None)),
@@ -716,9 +686,7 @@ impl Body {
/// Exact current planar surface for an explicitly fixed snapshot, without source lineage: the
/// face's plane as the face faces (out of the material in a closed shell). A plane surface,
/// or a B-spline whose control points all lie on one plane within the body's tolerance (a
/// loft's side between parallel straight edges, #14346): a B-spline surface lies in the
/// convex hull of its control points, so the whole face is on that plane.
/// or a B-spline face the kernel finds flat ([`Body::flat_spline_face`], #14346).
pub fn planar_face(&self, index: usize) -> Option<solidcore_record::Plane> {
let face = self.record.faces.get(index)?;
let (o, u, v) = match &face.surface {
@@ -727,23 +695,43 @@ impl Body {
let o = v3(*origin);
(o, v3(*u_point) - o, v3(*v_point) - o)
}
Surface::Spline { points, .. } if net_fits(points) => {
flat_net(&points.iter().map(|row| row.iter().map(|p| v3(*p)).collect()).collect::<Vec<Vec<_>>>(), self.record.tolerance)?
}
// With positive weights a rational patch stays in its points' convex hull too.
Surface::RationalSpline { homogeneous, .. } if net_fits(homogeneous) => {
let net = homogeneous
.iter()
.map(|row| row.iter().map(|&[x, y, z, w]| (w.is_finite() && w > 0.0).then(|| Vec3::new(x / w, y / w, z / w))).collect())
.collect::<Option<Vec<Vec<_>>>>()?;
flat_net(&net, self.record.tolerance)?
}
Surface::Spline { .. } | Surface::RationalSpline { .. } => return self.flat_spline_face(index),
_ => return None,
};
let v = if self.face_forward(index) { v } else { -v };
solidcore_record::Plane::new(o, u, v)
}
/// The plane of B-spline face `index` when the kernel finds it flat: SMLib's `IsPlanar` at
/// the face's own tolerance, with the normal facing out of the material as the face is used
/// ([`solidcore_ops::Body::face_plane_normal`]), through the face's first vertex. A loft's
/// side through three or more sections is skinned as a B-spline patch even when it is flat,
/// as a face of an imported design can be (#14346).
fn flat_spline_face(&self, index: usize) -> Option<solidcore_record::Plane> {
let face = self.record.faces.get(index)?;
let vertex = face.loops.iter().find_map(|l| match l {
Loop::Edges { edges } => edges.first().and_then(|u| self.record.edges.get(u.edge as usize)).and_then(|e| e.vertices.first().copied()),
Loop::Vertex { vertex } => Some(*vertex),
})?;
let point = v3(self.record.vertices.get(vertex as usize)?.point);
let (body, order) = self.ops_body()?;
let normal = body.face_plane_normal(order, index).ok()??;
solidcore_record::Plane::from_normal(point, v3(normal.value))
}
/// The record as solidcore-ops' body and the order of its entities, made on first use and
/// kept for every later face; a conversion that fails is not kept. None for a mesh body.
fn ops_body(&self) -> Option<&(solidcore_ops::Body, solidcore_ops::Order)> {
if self.is_mesh() {
return None;
}
if let Some(made) = self.ops.get() {
return Some(made);
}
let made = solidcraft_smlib::stop::unstoppable(|| solidcore_ops::Body::from_record(&self.record, true)).ok()?;
Some(self.ops.get_or_init(|| made))
}
/// Certify the two caps of a body built by [`crate::cylinder`]. Its wall stays uncertified.
pub fn with_cylinder_certificate(mut self, producer: u64) -> Result<Self> {
if producer == 0 {
+2 -2
View File
@@ -740,8 +740,8 @@ fn flat_loft_sides_are_planar_faces() {
assert!(plane.height(face.centroid).abs() < 1e-6, "face {}: its plane runs through it", face.index);
assert!((face.centroid - centre).dot(plane.normal()) > 0.0, "face {}: its plane faces out", face.index);
}
// The sides are B-spline patches, so this reaches the control-net test (were the loft to
// build planes here, another flat B-spline face would have to stand in).
// The sides are B-spline patches, so this reaches the kernel's flatness test (were the loft
// to build planes here, another flat B-spline face would have to stand in).
let splines = flat
.record()
.faces