mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-10 18:55:14 +00:00
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:
committed by
GitHub
parent
52c9f72249
commit
94ce45a1f2
+36
-48
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user