kernel: coplanar faces joined by SMLib's merge_faces, the record read from the body

planar_regularize now calls the fork's smlib_merge_faces as it is
(SmBrep::RemoveTopologicalEdgesAndVertices with the edges under the kept points
staying): every edge the lineage does not choose is kept by a point at its
middle, so SMLib joins the faces across the chosen ones only. A body with more
kept edges than one call takes stays as it is, as the other budgets leave it.

splitface, planar_sources and planar_regularize read the body's record in place
(Body::record) and a face's use from Body::face_forward; the two-lump test body
is Body::from_record of the two records side by side.

(not built: base in progress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:17:17 -04:00
parent 585334a292
commit 5c16e8dd10
3 changed files with 34 additions and 26 deletions
+24 -9
View File
@@ -1,6 +1,7 @@
//! Exact topological removal of construction-induced planar subdivisions.
//! Shared edge identity and complete construction lineage select the edges; SMLib joins the
//! faces either side of them where they are topological (`solidcraft_smlib::Brep::merge_faces`).
//! faces either side of them where they are topological (`solidcraft_smlib::Brep::merge_faces`,
//! every other edge kept by a point at its middle).
use super::Body;
use crate::{KernelError, Result, guard};
use solidcraft_geom::Vec3;
@@ -14,6 +15,12 @@ fn edge_points(e: &brep::Edge) -> Vec<Vec3> {
(0..=4).filter_map(|k| c.point(t0 + (t1 - t0) * k as f64 / 4.0)).collect()
}
/// The point halfway along a record edge's interval.
fn edge_middle(e: &brep::Edge) -> Option<Vec3> {
let [t0, t1] = e.interval;
CurveEval::new(&e.curve).ok()?.point(0.5 * (t0 + t1))
}
fn failed(e: impl std::fmt::Display) -> KernelError {
KernelError::Failed(format!("SMLib planar regularization: {e}"))
}
@@ -79,7 +86,7 @@ impl Body {
.filter(|p| actual.is_some_and(|actual| super::planar_sources::same_support(p, &actual)))
})
.collect();
let record = crate::native::brep_record(&self)?;
let record = self.record();
let mut shell_of = vec![usize::MAX; record.faces.len()];
for (k, shell) in record.shells.iter().enumerate() {
if let brep::Shell::Faces { faces, .. } = shell {
@@ -127,15 +134,23 @@ impl Body {
if eligible.is_empty() {
return Ok(None);
}
// SMLib removes the topological edges and vertices but those under the points kept:
// every edge not chosen (at its middle) stays.
let keep: Vec<[f64; 3]> = record
.edges
.iter()
.enumerate()
.filter(|(k, _)| eligible.binary_search(k).is_err())
.map(|(_, e)| edge_middle(e).map(|p| [p.x, p.y, p.z]))
.collect::<Option<_>>()
.ok_or_else(|| failed("an edge of the body has no middle point"))?;
if keep.len() > solidcraft_smlib::MAX_POINTS {
return Ok(None);
}
let (sb, ob) = crate::native::smlib_body_ordered(&self)?;
let ob = ob.ok_or_else(|| failed("SMLib numbers the body's edges unlike its record"))?;
let at: Vec<usize> = eligible
.iter()
.map(|e| if ob.edges.is_empty() { Some(*e) } else { ob.edges.iter().position(|c| c == e) })
.collect::<Option<_>>()
.ok_or_else(|| failed("an edge is not in SMLib's body"))?;
let (r, removed) = sb.tagged(0).map_err(failed)?.merge_faces(&at).map_err(failed)?;
if removed == 0 {
let r = sb.tagged(0).map_err(failed)?.merge_faces(&keep, (self.size() * 1e-6).max(1e-9)).map_err(failed)?;
if r.counts().map_err(failed)?.first() == Some(&self.face_count()) {
return Ok(None);
}
let check = r.check(solidcraft_smlib::CheckLevel::All).map_err(failed)?;
+5 -5
View File
@@ -9,7 +9,7 @@ use std::sync::Arc;
/// The oriented plane each face of `body` lies on (in face order), from its record: the
/// surface's plane, its normal turned with the face; none for a face on any other surface.
pub(super) fn actual_planes(body: &Body) -> Result<Vec<Option<Plane>>> {
let record = crate::native::brep_record(body)?;
let record = body.record();
let mut forward = vec![true; record.faces.len()];
for shell in &record.shells {
if let brep::Shell::Faces { faces, .. } = shell {
@@ -362,7 +362,7 @@ mod tests {
/// The corner points of face `i` of `b`, from its record.
fn face_points(b: &Body, i: usize) -> Vec<Vec3> {
let record = crate::native::brep_record(b).unwrap();
let record = b.record();
let mut out = Vec::new();
for l in &record.faces[i].loops {
match l {
@@ -385,7 +385,7 @@ mod tests {
/// The edges of face `i` of `b` as their end points, from its record.
fn face_edges(b: &Body, i: usize) -> Vec<(Vec3, Vec3)> {
let record = crate::native::brep_record(b).unwrap();
let record = b.record();
let at = |v: u32| {
let p = record.vertices[v as usize].point;
Vec3::new(p[0], p[1], p[2])
@@ -404,7 +404,7 @@ mod tests {
/// beyond `a` along x), each face's source that of the face of its own operand on the same
/// plane (none from `b` when `b_sources` is false).
fn side_by_side(a: &Body, b: &Body, b_sources: bool) -> Body {
let (ra, rb) = (crate::native::brep_record(a).unwrap(), crate::native::brep_record(b).unwrap());
let (ra, rb) = (a.record(), b.record());
let mut r = ra.clone();
let (nf, ne, nv, ns) = (r.faces.len() as u32, r.edges.len() as u32, r.vertices.len() as u32, r.shells.len() as u32);
for mut face in rb.faces.clone() {
@@ -433,7 +433,7 @@ mod tests {
region.shells.iter_mut().for_each(|s| *s += ns);
r.regions.push(region);
}
let mut combined = crate::native::body_from_smlib(&solidcraft_smlib::Brep::from_record(&r, true).unwrap()).unwrap();
let mut combined = Body::from_record(r).unwrap();
let planes = |x: &Body| actual_planes(x).unwrap();
let (pa, pb, pc) = (planes(a), planes(b), planes(&combined));
let a_right = a.outline_bounds().max.x;
+5 -12
View File
@@ -151,16 +151,9 @@ fn cutters(tool: &SplitTool, reach: &Aabb3) -> Result<Vec<Vec<Patch>>> {
/// where the surface is), facing out of the body.
fn face_patch(body: &Body, face: usize) -> Result<Patch> {
body.require_brep("a split tool")?;
let record = crate::native::brep_record(body)?;
let record = body.record();
let f = record.faces.get(face).ok_or_else(|| invalid(format!("the tool body has no face {face}")))?;
let forward = record
.shells
.iter()
.find_map(|s| match s {
brep::Shell::Faces { faces, .. } => faces.iter().find(|o| o.face as usize == face).map(|o| o.forward),
_ => None,
})
.ok_or_else(|| invalid(format!("face {face} of the tool body lies in no shell")))?;
let forward = body.face_forward(face);
// The surface's own parameter box, unbounded where it is.
let dom = SurfaceEval::new(&f.surface).map_err(|e| invalid(format!("the split tool's surface: {e}")))?.domain();
let range = |k: usize| dom.range[k].map_or(ALL, |(a, b)| [a, b]);
@@ -440,7 +433,7 @@ pub(crate) fn face_sides(body: &Body, tool: &SplitTool) -> Result<Vec<f64>> {
/// and how many there are. Faces are in one region when they meet at an edge the tool does not
/// run along; an edge along the tool (a split line) parts them.
pub(crate) fn regions(split: &Body, tool: &SplitTool) -> Result<(Vec<usize>, usize)> {
let record = crate::native::brep_record(split)?;
let record = split.record();
let fields: Vec<ToolSide> = cutters(tool, &reach_of(split))?
.iter()
.map(|g| g.iter().map(Measured::new).collect::<Result<Vec<_>>>().map(ToolSide))
@@ -590,7 +583,7 @@ fn split(body: &Body, faces: &[usize], tool: &SplitTool) -> Result<(Body, Vec<us
/// The edges of `split` that no edge of the body was before (its split lines), as polylines
/// through their exact points.
fn new_lines(split: &Body, h: &History) -> Result<Vec<Vec<Vec3>>> {
let record = crate::native::brep_record(split)?;
let record = split.record();
let mut out = Vec::new();
for (e, prov) in record.edges.iter().zip(&h.edges) {
if prov.origins.iter().any(|o| matches!(o, EdgeOrigin::Edge { .. })) {
@@ -814,7 +807,7 @@ mod tests {
let quarter = std::f64::consts::PI * 36.0 / 4.0;
// (Face areas come from meshes.)
assert!(rel(tops[0], quarter) < 1e-3 && rel(tops[1], 200.0 - quarter) < 1e-3, "{tops:?}");
let record = crate::native::brep_record(&s).unwrap();
let record = s.record();
assert!(record.edges.iter().any(|e| matches!(e.curve, brep::Curve::Circle { .. } | brep::Curve::RationalSpline { .. })));
// A listed face the tool misses is named: the plane z = 10 crosses the sides, not the top.
let mid = Plane { origin: Vec3::new(0.0, 0.0, 10.0), ..Plane::XY };