Files
OneCAD/crates/kernel/src/planar_faces.rs
T
Giovanni 57d738eac4 Join walls swept along splines in 2D (#908)
Walls swept along a spline have flat B-spline tops and bottoms, so booleans between them went to the general intersection, which fails on coincident spline faces. Such faces are now put on true planes, and two prisms of the same height and direction are decided in 2D with their spline outlines fitted by arcs within a micron. The result is kept only when its volume is plausible; otherwise the general boolean takes over. The free-form flag is cleared by a drop guard so an unwinding boolean can't leave it set.

Fixes #844.
2026-10-13 20:48:27 -04:00

107 lines
4.7 KiB
Rust

//! Flat faces that sit on B-spline surfaces become planes.
//!
//! A wall swept along a curve has a top and a bottom that are flat but are made of the same
//! B-spline surfaces as everything else the sweep makes. The coplanar handling of a boolean
//! (coincident faces pushed apart, faces glued back to back, the exact planar fallback) looks
//! for faces on true planes, so it never sees them: two such walls that share a top plane were
//! left to the general intersection, which fails on faces that coincide.
use solidcraft_geom::Vec3;
use truck_modeling as mt;
use crate::body::{Body, Solid, from_p3, p3};
use crate::{KernelError, guard};
/// Most control points looked at in one surface.
const MAX_CONTROL_POINTS: usize = 1_000_000;
/// The control points of a B-spline or NURBS surface, as points.
fn control_points(s: &mt::Surface) -> Option<Vec<Vec3>> {
match s {
mt::Surface::BSplineSurface(b) => {
let pts: Vec<Vec3> = b.control_points().iter().flatten().take(MAX_CONTROL_POINTS + 1).map(|p| from_p3(*p)).collect();
(pts.len() <= MAX_CONTROL_POINTS).then_some(pts)
}
mt::Surface::NurbsSurface(n) => {
let mut pts = Vec::new();
for v in n.control_points().iter().flatten() {
if !(v.w > 0.0) || pts.len() >= MAX_CONTROL_POINTS {
return None;
}
pts.push(Vec3::new(v.x / v.w, v.y / v.w, v.z / v.w));
}
Some(pts)
}
_ => None,
}
}
/// A point and the unit normal of the plane every point lies within `tol` of, if there is one.
fn common_plane(pts: &[Vec3], tol: f64) -> Option<(Vec3, Vec3)> {
let p0 = *pts.first()?;
let far = |from: Vec3, pick: &dyn Fn(Vec3) -> f64| pts.iter().copied().max_by(|a, b| pick(*a - from).total_cmp(&pick(*b - from)));
let p1 = far(p0, &|d| d.len())?;
let dir = (p1 - p0).normalized()?;
let p2 = far(p0, &|d| (d - dir * d.dot(dir)).len())?;
let n = (p1 - p0).cross(p2 - p0).normalized()?;
pts.iter().all(|p| n.dot(*p - p0).abs() <= tol).then_some((p0, n))
}
/// `b` with every face that is flat on a B-spline surface put on a plane; `None` when there is
/// no such face. The faces keep their order, their edges and their orientation.
pub(crate) fn planarized(b: &Body) -> Option<Body> {
if b.is_mesh() {
return None;
}
let tol = (b.size() * 1e-9).max(1e-9);
guard("planar faces", || {
use mt::{ParametricSurface, ParametricSurface3D};
let mut changed = 0usize;
let mut shells = Vec::new();
for sh in b.solid.boundaries() {
let mut faces: Vec<mt::Face> = Vec::new();
for f in sh.face_iter() {
let surface = f.surface();
let plane = control_points(&surface).and_then(|pts| common_plane(&pts, tol));
let Some((origin, mut n)) = plane else {
faces.push(f.clone());
continue;
};
// The plane faces the way the surface does; a surface with no normal at any of the
// places looked at is left as it is.
let (ur, vr) = surface.try_range_tuple();
let ((u0, u1), (v0, v1)) = (ur.unwrap_or((0.0, 1.0)), vr.unwrap_or((0.0, 1.0)));
let old = [(0.5, 0.5), (0.25, 0.25), (0.75, 0.75), (0.25, 0.75), (0.75, 0.25)]
.iter()
.map(|(s, t)| surface.normal(u0 + s * (u1 - u0), v0 + t * (v1 - v0)))
.find(|v| v.x.is_finite() && v.y.is_finite() && v.z.is_finite() && v.x * v.x + v.y * v.y + v.z * v.z > 1e-18);
let Some(old) = old else {
faces.push(f.clone());
continue;
};
if n.dot(Vec3::new(old.x, old.y, old.z)) < 0.0 {
n = -n;
}
let u = n.any_perp().normalized().ok_or_else(|| KernelError::Failed("planar faces: no axis".into()))?;
let w = n.cross(u);
let plane = mt::Plane::new(p3(origin), p3(origin + u), p3(origin + w));
let mut nf =
mt::Face::try_new(f.absolute_boundaries().to_vec(), mt::Surface::Plane(plane)).map_err(|e| KernelError::Failed(e.to_string()))?;
if !f.orientation() {
nf.invert();
}
faces.push(nf);
changed += 1;
}
shells.push(faces.into());
}
if changed == 0 {
return Ok(None);
}
let solid = Solid::try_new(shells).map_err(|e| KernelError::Failed(e.to_string()))?;
Ok(Some(Body::new(solid)?))
})
.ok()
.flatten()
}