kernel: planar source lineage on the record, coplanar faces joined by SMLib

planar_sources reads each face's oriented plane from the body's record
(actual_planes: the surface's plane, turned with the face's use) instead of
truck faces; the lineage rules are unchanged. planar_regularize picks the edges
to remove as before (two faces of one shell on one certified plane, from one
source unless generated walls, off every split line) from the record's edge
uses, and SMLib joins the faces across them (Brep::merge_faces: each edge
removed where SmEdge::IsTopological finds the faces' surfaces coincide), so the
merged face is SMLib's and the history its own tags (Merged), composed through
the body's. SmBrep::Simplify is not this operation: it refits curves and
surfaces by knot removal and sampling.

The planar_sources tests read face points, edges and planes from the record;
the two-lump body is built from the two records side by side, each face given
the source of its own operand's face on that plane. Deleted:
planar_regularize's alternate_adjacency_path_cannot_remove_protected_internal_edge,
a test of the truck face builder merged_face (only listed edges are removed now,
so no other path can remove a protected one).

(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 01:02:13 -04:00
parent 60b817e67d
commit 585334a292
2 changed files with 260 additions and 317 deletions
+84 -257
View File
@@ -1,137 +1,21 @@
//! Exact topological removal of construction-induced planar subdivisions.
//! Shared edge identity and complete construction lineage select the groups.
use super::{Body, from_p3};
use crate::history::FaceOrigin;
use crate::{Result, guard};
use solidcraft_geom::Plane;
use std::collections::{BTreeMap, HashMap, HashSet};
//! 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`).
use super::Body;
use crate::{KernelError, Result, guard};
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::{self, eval::CurveEval};
use std::sync::Arc;
use truck_modeling as mt;
fn root(parent: &mut [usize], mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
/// Points along a record edge (ends and three inside).
fn edge_points(e: &brep::Edge) -> Vec<Vec3> {
let Ok(c) = CurveEval::new(&e.curve) else { return Vec::new() };
let [t0, t1] = e.interval;
(0..=4).filter_map(|k| c.point(t0 + (t1 - t0) * k as f64 / 4.0)).collect()
}
fn merged_face(faces: &[&mt::Face], members: &[usize], plane: Plane, eligible: &HashSet<mt::EdgeID>) -> Option<mt::Face> {
let mut counts = HashMap::<mt::EdgeID, (usize, bool)>::new();
for i in members {
for edge in faces.get(*i)?.boundary_iters().into_iter().flatten() {
// Whole linear supports, not a sampled plane fit.
let mt::Curve::Line(line) = edge.curve() else { return None };
let (a, b) = (from_p3(line.0), from_p3(line.1));
if !a.is_finite() || !b.is_finite() || a.len().max(b.len()) > 1e9 {
return None;
}
let front = from_p3(edge.absolute_front().point());
let back = from_p3(edge.absolute_back().point());
if !front.is_finite() || !back.is_finite() {
return None;
}
let error = 4096. * f64::EPSILON * (1. + a.len() + b.len() + plane.origin.len());
if plane.height(a).abs() > error || plane.height(b).abs() > error || a.dist(front) > error || b.dist(back) > error {
return None;
}
let count = counts.entry(edge.id()).or_insert((0, edge.orientation()));
if count.0 == 1 && (!eligible.contains(&edge.id()) || count.1 == edge.orientation()) {
return None;
}
count.0 += 1;
if count.0 > 2 {
return None;
}
}
}
let mut boundary = Vec::new();
for i in members {
for edge in faces.get(*i)?.boundary_iters().into_iter().flatten() {
if counts.get(&edge.id()).is_some_and(|(n, _)| *n == 1) {
boundary.push(edge);
}
}
}
let mut next = HashMap::new();
let mut incoming = HashMap::new();
for (i, edge) in boundary.iter().enumerate() {
if next.insert(edge.front().id(), i).is_some() || incoming.insert(edge.back().id(), i).is_some() {
return None;
}
}
if next.len() != incoming.len() || next.keys().any(|id| !incoming.contains_key(id)) {
return None;
}
let mut used = vec![false; boundary.len()];
let mut outer = None;
let mut holes = Vec::new();
for first in 0..boundary.len() {
if used[first] {
continue;
}
let start = boundary.get(first)?.front().id();
let origin = from_p3(boundary.get(first)?.front().point());
let mut index = first;
let mut edges = Vec::new();
let (mut area, mut magnitude, mut arithmetic_error) = (0., 0., 0.);
loop {
if *used.get(index)? {
return None;
}
used[index] = true;
let edge = boundary.get(index)?.clone();
let da = from_p3(edge.front().point()) - origin;
let db = from_p3(edge.back().point()) - origin;
let a = solidcraft_geom::Vec2::new(da.dot(plane.x), da.dot(plane.y));
let b = solidcraft_geom::Vec2::new(db.dot(plane.x), db.dot(plane.y));
let term = a.cross(b);
area += term;
magnitude += term.abs();
let coordinate_error =
128. * f64::EPSILON * (1. + origin.len() + from_p3(edge.front().point()).len() + from_p3(edge.back().point()).len());
arithmetic_error += coordinate_error * (da.len() + db.len() + coordinate_error) * 16.;
let back = edge.back().id();
edges.push(edge);
if back == start {
break;
}
index = *next.get(&back)?;
}
if edges.len() < 3 || !area.is_finite() || area.abs() <= arithmetic_error + 64. * f64::EPSILON * (edges.len() as f64 + 1.) * magnitude.max(1.)
{
return None;
}
let wire = mt::Wire::from(edges);
if area > 0. {
if outer.replace(wire).is_some() {
return None;
}
} else {
holes.push(wire);
}
}
let mut wires = vec![outer?];
wires.extend(holes);
mt::Face::try_new(wires, faces.get(*members.first()?)?.oriented_surface()).ok()
}
/// `body` with its history: `input`'s faces with each output face made of the input faces
/// `members` lists for it (every edge of `body` one of `input`'s, by identity), composed with
/// the history of the operation that made `input`; none where `input` has none.
fn with_joined_history(body: Body, input: &Body, members: Vec<Vec<usize>>) -> Body {
let Some(inner) = input.history() else { return body };
let faces = members.into_iter().map(|m| (m.iter().map(|&face| FaceOrigin::Face { operand: 0, face }).collect(), m.len() == 1)).collect();
let joined = crate::record::traced(
body,
&crate::record::counts(&[input]),
Some(crate::record::regions(faces)),
Some(&crate::record::edge_origins(&input.solid, 0)),
);
// Both steps are checked (`traced` against `input`, `through` composing over its checked
// history); the composed operands' counts are not known here, so it is set in place.
let history = crate::boolean_record::through(&joined, 0, Some(inner));
Body { history: history.map(Arc::new), ..joined }
fn failed(e: impl std::fmt::Display) -> KernelError {
KernelError::Failed(format!("SMLib planar regularization: {e}"))
}
impl Body {
@@ -173,29 +57,14 @@ impl Body {
let split_points = self.split_keep().iter().try_fold(0usize, |sum, line| sum.checked_add(line.len()));
let Some(split_points) = split_points.filter(|n| *n <= 65_536) else { return Ok(self) };
let result = guard("certified planar topology regularization", || {
let mut faces = Vec::new();
let mut shell_owner = Vec::new();
let mut use_budget = 65_536usize;
for (shell_index, shell) in self.solid.boundaries().iter().enumerate() {
for face in shell.face_iter() {
if faces.len() >= sources.len() {
return Ok(None);
}
for _ in face.edge_iter() {
let Some(left) = use_budget.checked_sub(1) else { return Ok(None) };
use_budget = left;
}
faces.push(face);
shell_owner.push(shell_index);
}
}
if faces.len() != sources.len() {
if self.face_count() != sources.len() {
return Ok(None);
}
let valid: Vec<_> = faces
let actual = super::planar_sources::actual_planes(&self)?;
let valid: Vec<_> = actual
.iter()
.zip(sources)
.map(|(f, s)| {
.map(|(actual, s)| {
s.as_ref()
.and_then(|source| {
if generated {
@@ -207,29 +76,36 @@ impl Body {
super::planar_sources::certified_plane(source)
}
})
.filter(|p| super::planar_sources::actual_plane(f).is_some_and(|actual| super::planar_sources::same_support(p, &actual)))
.filter(|p| actual.is_some_and(|actual| super::planar_sources::same_support(p, &actual)))
})
.collect();
let mut adjacency = HashMap::<mt::EdgeID, Vec<(usize, mt::Edge)>>::new();
for (i, face) in faces.iter().enumerate() {
for edge in face.boundary_iters().into_iter().flatten() {
adjacency.entry(edge.id()).or_default().push((i, edge));
let record = crate::native::brep_record(&self)?;
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 {
for o in faces {
if let Some(slot) = shell_of.get_mut(o.face as usize) {
*slot = k;
}
}
}
}
let mut eligible = HashSet::new();
let mut parent: Vec<_> = (0..faces.len()).collect();
// The edges between two faces of one shell on one certified plane (from one source,
// unless the faces are generated walls), off every authored split line.
let mut eligible = Vec::new();
let mut identity = 16_000_000usize;
let mut work = 2_000_000usize;
for uses in adjacency.values() {
let [(a, ea), (b, eb)] = uses.as_slice() else { continue };
if a == b || shell_owner[*a] != shell_owner[*b] || ea.orientation() == eb.orientation() {
for (k, e) in record.edges.iter().enumerate() {
let [ua, ub] = e.uses.as_slice() else { continue };
let (a, b) = (ua.face as usize, ub.face as usize);
if a == b || shell_of.get(a) != shell_of.get(b) {
continue;
}
let (Some(pa), Some(pb)) = (valid[*a], valid[*b]) else { continue };
let (Some(Some(pa)), Some(Some(pb))) = (valid.get(a), valid.get(b)) else { continue };
if pa != pb {
continue;
}
let (Some(sa), Some(sb)) = (sources[*a].as_ref(), sources[*b].as_ref()) else { continue };
let (Some(Some(sa)), Some(Some(sb))) = (sources.get(a), sources.get(b)) else { continue };
let Some(cost) = super::planar_sources::descriptor_cost(sa)
.and_then(|a| super::planar_sources::descriptor_cost(sb).and_then(|b| a.checked_add(b)))
else {
@@ -243,112 +119,63 @@ impl Body {
let Some(cost) = split_points.checked_mul(6).and_then(|n| n.checked_add(1)) else { return Ok(None) };
let Some(left) = work.checked_sub(cost) else { return Ok(None) };
work = left;
if crate::heal::on_split_line(&crate::heal::edge_points(ea), self.split_keep()) {
if crate::heal::on_split_line(&edge_points(e), self.split_keep()) {
continue;
}
eligible.insert(ea.id());
let (ra, rb) = (root(&mut parent, *a), root(&mut parent, *b));
parent[ra.max(rb)] = ra.min(rb);
eligible.push(k);
}
let mut groups = BTreeMap::<usize, Vec<usize>>::new();
for i in 0..faces.len() {
groups.entry(root(&mut parent, i)).or_default().push(i);
if eligible.is_empty() {
return Ok(None);
}
let mut replacements = HashMap::new();
let mut changed = false;
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 {
return Ok(None);
}
let check = r.check(solidcraft_smlib::CheckLevel::All).map_err(failed)?;
if !check.is_valid() {
return Err(failed(format!("SMLib's check refuses the result: {check}")));
}
let (body, or) = crate::native::body_from_smlib_ordered(&r)?;
let or = or.ok_or_else(|| failed("the result's record is numbered unlike SMLib's body"))?;
let h = r.history().map_err(failed)?.history(std::slice::from_ref(&ob), &or).map_err(failed)?;
// Each face keeps the source of the first face it is made of; a generated wall made
// of several is coalesced.
let mut metadata = 2_000_000usize;
for (leader, members) in &groups {
if members.len() < 2 {
continue;
let mut output = Vec::with_capacity(h.faces.len());
for f in &h.faces {
let members: Vec<usize> =
f.origins.iter().filter_map(|o| if let crate::history::FaceOrigin::Face { face, .. } = o { Some(*face) } else { None }).collect();
let mut source = members.iter().min().and_then(|m| sources.get(*m)).cloned().flatten();
if let Some(s) = &source {
let Some(cost) = super::planar_sources::descriptor_cost(s) else { return Ok(None) };
let Some(left) = metadata.checked_sub(cost) else { return Ok(None) };
metadata = left;
}
let Some(face) = valid[*leader].and_then(|p| merged_face(&faces, members, p, &eligible)) else { continue };
replacements.insert(*leader, face);
changed = true;
}
if !changed {
return Ok(None);
}
let mut shells = Vec::new();
let mut output_sources = Vec::new();
let mut output_ids = Vec::new();
// The input faces each output face is made of, for its history.
let mut output_members: Vec<Vec<usize>> = Vec::new();
let mut offset = 0;
for shell in self.solid.boundaries() {
let mut output = Vec::new();
for face in shell.face_iter() {
let leader = root(&mut parent, offset);
if let Some(merged) = replacements.get(&leader) {
if offset == leader {
output.push(merged.clone());
let Some(members) = groups.remove(&leader) else { return Ok(None) };
output_members.push(members);
} else {
offset += 1;
continue;
}
} else {
output.push(face.clone());
output_members.push(vec![offset]);
}
output_ids.push(output.last().ok_or_else(|| crate::KernelError::Failed("missing regularized face".into()))?.id());
let source = sources[offset].as_ref();
if let Some(source) = source {
let Some(cost) = super::planar_sources::descriptor_cost(source) else { return Ok(None) };
let Some(left) = metadata.checked_sub(cost) else { return Ok(None) };
metadata = left;
}
let mut source = source.cloned();
if generated
&& replacements.contains_key(&leader)
&& let Some(super::FaceSource::ExtrudeSide { coalesced, .. }) = &mut source
{
*coalesced = true;
}
output_sources.push(source);
offset += 1;
if generated
&& members.len() > 1
&& let Some(super::FaceSource::ExtrudeSide { coalesced, .. }) = &mut source
{
*coalesced = true;
}
shells.push(mt::Shell::from(output));
output.push(source);
}
let solid = mt::Solid::try_new(shells).map_err(|e| crate::KernelError::Failed(format!("planar regularization does not close: {e}")))?;
let body = Body::new(solid)?.with_color(self.color()).with_paint(self.paint().cloned()).with_split_lines(self.split_lines().cloned());
if !body.solid.face_iter().map(|f| f.id()).eq(output_ids) {
return Ok(None);
}
Ok(Some(with_joined_history(body.with_face_sources(Some(Arc::new(output_sources))), &self, output_members)))
let body = body
.with_color(self.color())
.with_paint(self.paint().cloned())
.with_split_lines(self.split_lines().cloned())
.with_face_sources(Some(Arc::new(output)))
.with_checked_history(Some(h), &crate::record::counts(&[&self]));
// Over the operands of the operation that made `self`, as `self`'s history is.
let history = crate::boolean_record::through(&body, 0, self.history());
Ok(Some(Body { history: history.map(Arc::new), ..body }))
})?;
Ok(result.unwrap_or(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alternate_adjacency_path_cannot_remove_protected_internal_edge() {
let vertices: Vec<_> =
(0..9).map(|i| mt::builder::vertex(super::super::p3(solidcraft_geom::Vec3::new((i % 3) as f64, (i / 3) as f64, 0.)))).collect();
let mut edges = HashMap::<(usize, usize), mt::Edge>::new();
let mut faces = Vec::new();
for (x, y) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let ids = [y * 3 + x, y * 3 + x + 1, (y + 1) * 3 + x + 1, (y + 1) * 3 + x];
let wire: mt::Wire = (0..4)
.map(|i| {
let (a, b) = (ids[i], ids[(i + 1) % 4]);
let key = (a.min(b), a.max(b));
let edge = edges.entry(key).or_insert_with(|| mt::builder::line(&vertices[key.0], &vertices[key.1]));
if a == key.0 { edge.clone() } else { edge.inverse() }
})
.collect();
faces.push(mt::builder::try_attach_plane(&[wire]).unwrap());
}
let refs: Vec<_> = faces.iter().collect();
let mut eligible: HashSet<_> = edges.values().map(|e| e.id()).collect();
assert!(merged_face(&refs, &[0, 1, 2, 3], Plane::XY, &eligible).is_some());
eligible.remove(&edges[&(1, 4)].id());
assert!(
merged_face(&refs, &[0, 1, 2, 3], Plane::XY, &eligible).is_none(),
"remaining three internal joins cannot bypass authored split protection"
);
}
}
+176 -60
View File
@@ -1,21 +1,43 @@
//! Exact oriented planar support lineage through a trim and an inside shell.
//! No tessellated face names, centroids, or nearest-plane choices are used.
use super::{Body, FaceSource, from_p3};
use super::{Body, FaceSource};
use crate::{KernelError, Result, guard};
use solidcraft_geom::brep::{self, Surface};
use solidcraft_geom::{Plane, Vec3};
use std::sync::Arc;
use truck_modeling as mt;
pub(super) fn actual_plane(face: &mt::Face) -> Option<Plane> {
let mt::Surface::Plane(p) = face.oriented_surface() else { return None };
let u = p.u_axis();
let v = p.v_axis();
let u = Vec3::new(u.x, u.y, u.z);
let v = Vec3::new(v.x, v.y, v.z);
if !u.is_finite() || !v.is_finite() || u.cross(v).len() <= u.len() * v.len() * 1e-8 {
return None;
/// 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 mut forward = vec![true; record.faces.len()];
for shell in &record.shells {
if let brep::Shell::Faces { faces, .. } = shell {
for o in faces {
if let Some(f) = forward.get_mut(o.face as usize) {
*f = o.forward;
}
}
}
}
Plane::new(from_p3(p.origin()), u, v)
let v = |p: [f64; 3]| Vec3::new(p[0], p[1], p[2]);
Ok(record
.faces
.iter()
.zip(forward)
.map(|(face, forward)| {
let (o, u, w) = match face.surface {
Surface::Plane { origin, u_axis, v_axis } => (v(origin), v(u_axis), v(v_axis)),
Surface::PlaneThrough { origin, u_point, v_point } => (v(origin), v(u_point) - v(origin), v(v_point) - v(origin)),
_ => return None,
};
if !o.is_finite() || !u.is_finite() || !w.is_finite() || u.cross(w).len() <= u.len() * w.len() * 1e-8 {
return None;
}
// A reversed face faces against its surface's normal.
if forward { Plane::new(o, u, w) } else { Plane::new(o, w, u) }
})
.collect())
}
/// How far apart the normals' x components of two planes `same_support` accepts can be, with
@@ -157,13 +179,13 @@ impl Body {
return Ok(self);
}
if inputs.len() > 2
|| inputs.iter().any(|input| input.solid.face_iter().count() > 16_384)
|| self.solid.face_iter().count() > 16_384
|| blocker.is_some_and(|b| b.solid.face_iter().count() > 16_384)
|| inputs.iter().any(|input| input.face_count() > 16_384)
|| self.face_count() > 16_384
|| blocker.is_some_and(|b| b.face_count() > 16_384)
{
return Err(KernelError::Invalid("planar source face budget exceeded".into()));
}
if inputs.iter().any(|input| input.face_sources().is_some_and(|sources| sources.len() != input.solid.face_iter().count())) {
if inputs.iter().any(|input| input.face_sources().is_some_and(|sources| sources.len() != input.face_count())) {
return Err(KernelError::Invalid("planar source alignment mismatch".into()));
}
if shell.is_some_and(|t| !t.is_finite() || t <= 0.) {
@@ -175,7 +197,7 @@ impl Body {
let opening_tolerance = if shell.is_some() { inputs.iter().map(|input| input.size()).fold(0., f64::max) * 1e-4 + 1e-6 } else { 0. };
let mapped = guard("exact planar support lineage", || {
let mut competing = Vec::new();
for plane in blocker.into_iter().flat_map(|b| b.solid.face_iter()).filter_map(actual_plane) {
for plane in blocker.map(actual_planes).transpose()?.into_iter().flatten().flatten() {
// A tool face blocks a face on its plane facing either way.
let opposing = Plane::new(plane.origin, plane.x, -plane.y).ok_or_else(|| KernelError::Invalid("invalid opposing plane".into()))?;
competing.extend([plane, opposing]);
@@ -183,12 +205,14 @@ impl Body {
competing.sort_by(|a, b| a.normal().x.total_cmp(&b.normal().x));
let mut candidates = Vec::new();
let mut unknown_supports = Vec::new();
let faces = inputs.iter().flat_map(|input| {
let mut faces = Vec::new();
for input in inputs {
let sources = input.face_sources().unwrap_or_default();
input.solid.face_iter().enumerate().map(move |(index, face)| (face, sources.get(index).and_then(Option::as_ref)))
});
for (face, source) in faces {
let Some(actual) = actual_plane(face) else { continue };
faces
.extend(actual_planes(input)?.into_iter().enumerate().map(|(index, plane)| (plane, sources.get(index).and_then(Option::as_ref))));
}
for (actual, source) in faces {
let Some(actual) = actual else { continue };
let Some(source) = source else {
unknown_supports.push(actual);
continue;
@@ -224,9 +248,9 @@ impl Body {
let mut metadata = 2_000_000usize;
let mut identity_bytes = 16_000_000usize;
let mut result = Vec::new();
for (index, face) in self.solid.face_iter().enumerate() {
for (index, actual) in actual_planes(&self)?.into_iter().enumerate() {
let mut found = None;
if let Some(actual) = actual_plane(face) {
if let Some(actual) = actual {
let mut blocked = false;
let nx = actual.normal().x;
for plane in normal_window(&unknown_supports, |p| p, nx) {
@@ -336,6 +360,105 @@ mod tests {
use super::*;
use crate::{box_solid, fillet, shell, transform_matrix};
/// 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 mut out = Vec::new();
for l in &record.faces[i].loops {
match l {
brep::Loop::Edges { edges } => {
for e in edges {
for v in record.edges[e.edge as usize].vertices {
let p = record.vertices[v as usize].point;
out.push(Vec3::new(p[0], p[1], p[2]));
}
}
}
brep::Loop::Vertex { vertex } => {
let p = record.vertices[*vertex as usize].point;
out.push(Vec3::new(p[0], p[1], p[2]));
}
}
}
out
}
/// 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 at = |v: u32| {
let p = record.vertices[v as usize].point;
Vec3::new(p[0], p[1], p[2])
};
record.faces[i]
.loops
.iter()
.flat_map(|l| match l {
brep::Loop::Edges { edges } => edges.iter().map(|e| record.edges[e.edge as usize].vertices).map(|[a, b]| (at(a), at(b))).collect(),
brep::Loop::Vertex { .. } => Vec::new(),
})
.collect()
}
/// One body holding `a` and `b` side by side (their records' shells together, `b` wholly
/// 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 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() {
for l in &mut face.loops {
match l {
brep::Loop::Edges { edges } => edges.iter_mut().for_each(|e| e.edge += ne),
brep::Loop::Vertex { vertex } => *vertex += nv,
}
}
r.faces.push(face);
}
for mut e in rb.edges.clone() {
e.vertices = e.vertices.map(|v| v + nv);
e.uses.iter_mut().for_each(|u| u.face += nf);
r.edges.push(e);
}
r.vertices.extend(rb.vertices.iter().cloned());
for mut sh in rb.shells.clone() {
if let brep::Shell::Faces { faces, wires } = &mut sh {
faces.iter_mut().for_each(|o| o.face += nf);
wires.iter_mut().for_each(|w| *w += ne);
}
r.shells.push(sh);
}
for mut region in rb.regions.clone().into_iter().filter(|g| !g.void) {
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 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;
let sources: Vec<Option<FaceSource>> = pc
.iter()
.enumerate()
.map(|(i, p)| {
let p = (*p)?;
let find = |of: &Body, ps: &[Option<Plane>]| {
ps.iter().position(|q| q.is_some_and(|q| same_support(&q, &p))).and_then(|i| of.face_sources()?[i].clone())
};
let in_a = face_points(&combined, i).iter().all(|q| q.x <= a_right + 1e-9);
if in_a {
find(a, &pa)
} else if b_sources {
find(b, &pb)
} else {
None
}
})
.collect();
combined.face_sources = Some(Arc::new(sources));
combined
}
#[test]
fn rounded_shell_planar_roles_are_exact_distinct_and_transformable() {
for (width, thickness) in [(80., 2.), (100., 3.)] {
@@ -381,18 +504,16 @@ mod tests {
assert!(moved.face_plane(right).unwrap().normal().dot(Vec3::Y) > 0.999999);
let raw = cup.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
let display = cup.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
let cylindrical: Vec<_> = cup
.solid
.face_iter()
.enumerate()
.filter_map(|(i, f)| {
let cylindrical: Vec<_> = (0..cup.face_count())
.filter_map(|i| {
let crate::FaceSurface::Cylinder { radius, .. } = crate::face_surface(&cup, i)? else { return None };
let lo = f.vertex_iter().map(|v| v.point().z).fold(f64::INFINITY, f64::min);
let hi = f.vertex_iter().map(|v| v.point().z).fold(f64::NEG_INFINITY, f64::max);
let z: Vec<f64> = face_points(&cup, i).iter().map(|p| p.z).collect();
let lo = z.iter().copied().fold(f64::INFINITY, f64::min);
let hi = z.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Some((i, radius, lo, hi))
})
.collect();
eprintln!("roundedShell width{width} thickness{thickness} raw{} cylindrical roles{cylindrical:?}", cup.solid.face_iter().count());
eprintln!("roundedShell width{width} thickness{thickness} raw{} cylindrical roles{cylindrical:?}", cup.face_count());
let mut corner_joins = 0;
for (edge, adjacent) in raw.edge_faces.iter().enumerate() {
let [a, b] = adjacent.as_slice() else { continue };
@@ -415,8 +536,7 @@ mod tests {
for high in [false, true] {
let matching: Vec<_> = sources.iter().enumerate().filter_map(|(i,s)| matches!(s,Some(FaceSource::ShellPlanar {inner:false,source,..}) if matches!(source.as_ref(),FaceSource::Box {axis:a,high:h,..} if *a==axis && *h==high)).then_some(i)).collect();
assert_eq!(matching.len(), 1, "each straight exterior wall is one actual face");
let face = cup.solid.face_iter().nth(matching[0]).unwrap();
let z: Vec<_> = face.vertex_iter().map(|v| v.point().z).collect();
let z: Vec<_> = face_points(&cup, matching[0]).iter().map(|p| p.z).collect();
assert!(z.iter().any(|z| z.abs() < 1e-9));
assert!(z.iter().any(|z| (*z - 30.).abs() < 1e-9));
}
@@ -437,13 +557,15 @@ mod tests {
// STEP may additionally coalesce cylindrical corner patches. The
// certified planar wall topology itself must survive unchanged.
for (axis, coordinate) in [(Vec3::X, width / 2.), (-Vec3::X, width / 2.), (Vec3::Y, 30.), (-Vec3::Y, 30.)] {
let walls: Vec<_> = copy
.solid
.face_iter()
.filter(|f| actual_plane(f).is_some_and(|p| p.normal().dot(axis) > 0.999 && (p.origin.dot(axis) - coordinate).abs() < 1e-8))
let walls: Vec<usize> = actual_planes(copy)
.unwrap()
.iter()
.enumerate()
.filter(|(_, p)| p.is_some_and(|p| p.normal().dot(axis) > 0.999 && (p.origin.dot(axis) - coordinate).abs() < 1e-8))
.map(|(i, _)| i)
.collect();
assert_eq!(walls.len(), 1);
let z: Vec<_> = walls[0].vertex_iter().map(|v| v.point().z).collect();
let z: Vec<_> = face_points(copy, walls[0]).iter().map(|p| p.z).collect();
assert!(z.iter().any(|z| z.abs() < 1e-8));
assert!(z.iter().any(|z| (*z - 30.).abs() < 1e-8));
}
@@ -458,7 +580,7 @@ mod tests {
let source = box_solid(Vec3::ZERO, Vec3::new(40., 30., 20.)).unwrap().with_box_certificate(1).unwrap();
let pending = shell(&source, &[Vec3::new(20., 15., 20.)], 2.).unwrap();
let cup = pending.clone().with_shell_certificate(2).unwrap();
assert_eq!(cup.solid.face_iter().count(), 11);
assert_eq!(cup.face_count(), 11);
let mesh = cup.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
assert!((mesh.measure().volume - (40. * 30. * 20. - 36. * 26. * 18.)).abs() < 1e-6);
assert!(mesh.contains(Vec3::new(20., 1., 1.)));
@@ -467,7 +589,7 @@ mod tests {
let imported = crate::step_import(&crate::step_export(&[&cup], "regularized shell").unwrap()).unwrap();
assert!(imported.warnings.is_empty());
let copy = &imported.bodies[0].body;
assert_eq!(copy.solid.face_iter().count(), 11);
assert_eq!(copy.face_count(), 11);
assert!((copy.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().volume - mesh.measure().volume).abs() < 1e-6);
}
#[test]
@@ -479,8 +601,8 @@ mod tests {
let joined = boolean(&a, &b, BoolOp::Union).unwrap().unwrap();
let sources = joined.face_sources().unwrap();
let mut seen = Vec::new();
for (i, face) in joined.solid.face_iter().enumerate() {
let plane = actual_plane(face).unwrap();
for (i, plane) in actual_planes(&joined).unwrap().into_iter().enumerate() {
let plane = plane.unwrap();
let source = sources[i].as_ref();
let height = plane.origin.z;
if plane.normal().z > 0.999 && (height - 10.).abs() < 1e-9 {
@@ -502,30 +624,25 @@ mod tests {
fn coincident_competing_source_planes_and_tool_planes_stay_unknown() {
let a = box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().with_box_certificate(1).unwrap();
let b = box_solid(Vec3::new(20., 0., 0.), Vec3::new(30., 10., 10.)).unwrap().with_box_certificate(2).unwrap();
let mut shells = a.solid.boundaries().to_vec();
shells.extend(b.solid.boundaries().iter().cloned());
let mut combined = Body::new(mt::Solid::try_new(shells).unwrap()).unwrap();
combined.face_sources = Some(Arc::new(a.face_sources().unwrap().iter().chain(b.face_sources().unwrap()).cloned().collect()));
let combined = side_by_side(&a, &b, true);
let result = a.clone().with_preserved_planar_sources(&combined).unwrap();
for (i, face) in result.solid.face_iter().enumerate() {
if actual_plane(face).unwrap().normal().dot(Vec3::Z).abs() > 0.999 {
for (i, p) in actual_planes(&result).unwrap().into_iter().enumerate() {
if p.unwrap().normal().dot(Vec3::Z).abs() > 0.999 {
assert!(result.face_sources().unwrap()[i].is_none());
}
}
let mut mixed = combined.clone();
mixed.face_sources =
Some(Arc::new(a.face_sources().unwrap().iter().cloned().chain(std::iter::repeat_n(None, b.solid.face_iter().count())).collect()));
let mixed = side_by_side(&a, &b, false);
let guarded = a.clone().with_preserved_planar_sources(&mixed).unwrap();
for (i, face) in guarded.solid.face_iter().enumerate() {
if actual_plane(face).unwrap().normal().dot(Vec3::Z).abs() > 0.999 {
for (i, p) in actual_planes(&guarded).unwrap().into_iter().enumerate() {
if p.unwrap().normal().dot(Vec3::Z).abs() > 0.999 {
assert!(guarded.face_sources().unwrap()[i].is_none(), "uncertified competing support must not gain identity");
}
}
let cup = shell(&a, &[Vec3::new(5., 5., 10.)], 1.).unwrap().with_shell_certificate(3).unwrap();
let tool = box_solid(Vec3::new(2., 2., 1.), Vec3::new(4., 4., 3.)).unwrap();
let preserved = cup.clone().with_preserved_boolean_planar_sources(&cup, &tool, false).unwrap();
for (i, face) in preserved.solid.face_iter().enumerate() {
let Some(p) = actual_plane(face) else { continue };
for (i, p) in actual_planes(&preserved).unwrap().into_iter().enumerate() {
let Some(p) = p else { continue };
if p.normal().dot(Vec3::Z) > 0.999 && (p.origin.z - 1.).abs() < 1e-9 {
assert!(preserved.face_sources().unwrap()[i].is_none());
}
@@ -554,16 +671,15 @@ mod tests {
let raw = pocket.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
let display = pocket.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
let sources = pocket.face_sources().unwrap();
assert_eq!(pocket.solid.face_iter().count(), 11, "four outer walls, four inner walls, rim, floor, bottom");
assert_eq!(pocket.face_count(), 11, "four outer walls, four inner walls, rim, floor, bottom");
let sides: Vec<_> =
sources.iter().enumerate().filter_map(|(i, s)| matches!(s, Some(FaceSource::CertifiedExtrudeSide { .. })).then_some(i)).collect();
assert_eq!(sides.len(), 4);
for i in sides {
let face = pocket.solid.face_iter().nth(i).unwrap();
let z: Vec<_> = face.vertex_iter().map(|v| v.point().z).collect();
let z: Vec<_> = face_points(&pocket, i).iter().map(|p| p.z).collect();
assert!(z.iter().any(|z| z.abs() < 1e-9));
assert!(z.iter().any(|z| (*z - 10.).abs() < 1e-9));
assert!(!face.edge_iter().any(|e| (e.front().point().z - 1.).abs() < 1e-9 && (e.back().point().z - 1.).abs() < 1e-9));
assert!(!face_edges(&pocket, i).iter().any(|(a, b)| (a.z - 1.).abs() < 1e-9 && (b.z - 1.).abs() < 1e-9));
}
assert!(display.contains(Vec3::new(0., 0., 0.5)));
assert!(!display.contains(Vec3::new(0., 0., 5.)));
@@ -574,7 +690,7 @@ mod tests {
let imported = crate::step_import(&crate::step_export(&[&pocket], "regularized pocket").unwrap()).unwrap();
assert!(imported.warnings.is_empty());
let copy = &imported.bodies[0].body;
assert_eq!(copy.solid.face_iter().count(), 11);
assert_eq!(copy.face_count(), 11);
let copy_mesh = copy.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
assert!((copy_mesh.measure().volume - raw.measure().volume).abs() < 1e-6);
assert!(copy_mesh.contains(Vec3::new(0., 0., 0.5)));