Wave 161/509: kernel: planar source lineage on the record, coplanar faces joined by SMLib (and 3 more)

4 commits, squashed; the originals stay reachable from tag archive/merge-main-develop.

- 585334a29 jyviko: kernel: planar source lineage on the record, coplanar faces joined by SMLib
- 5c16e8dd1 jyviko: kernel: coplanar faces joined by SMLib's merge_faces, the record read from the body
- c802458ef jyviko: solidcraft-smlib: Brep::face_points, a point inside each face (smlib_face_points)
- 453a4dec7 jyviko: kernel: a trimmed piece's side is read at a point inside it, exactly

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
jyviko
2026-10-22 07:17:16 -04:00
parent 5fded8864a
commit 739c9d8f5b
6 changed files with 320 additions and 346 deletions
+97 -255
View File
@@ -1,137 +1,28 @@
//! 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`,
//! every other edge kept by a point at its middle).
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()
/// 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))
}
/// `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 +64,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 +83,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 = 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 {
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 +126,71 @@ 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;
// 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 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)?;
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 = body.record();
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 = b.record();
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 = b.record();
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) = (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() {
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 = 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;
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)));
+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 };
+18 -17
View File
@@ -57,8 +57,8 @@ pub fn trim(body: &Body, tool: &SplitTool, keep: Vec3) -> Result<Body> {
/// A surface body trimmed by a tool, keeping what `keep` says. Each kept face is the face it is
/// a piece of (its history is over `[body]`).
///
/// The body is split where the tool meets it ([`crate::splitface::split_any`]) and the pieces
/// not kept are taken away ([`crate::remove_faces`]); both carry their history, so the kept
/// The body is split where the tool meets it ([`crate::splitface::split_any`]), each piece's side
/// read at a point inside it ([`face_sides`]), and the pieces not kept are taken away ([`crate::remove_faces`]); both carry their history, so the kept
/// faces are the body's own, cut.
pub fn trim_keep(body: &Body, tool: &SplitTool, keep: TrimKeep) -> Result<Body> {
body.require_brep("trim")?;
@@ -95,23 +95,24 @@ pub fn trim_keep(body: &Body, tool: &SplitTool, keep: TrimKeep) -> Result<Body>
Ok(crate::record::after(trimmed, &split, &crate::record::counts(&[body])))
}
/// The side of the tool each face of `body` (in its face order) lies on: the tool's field at
/// the centre of the face's mesh triangle where it is largest (a face lies on one side once
/// the tool has cut it; on the tool's own surface the field is the rounding of zero).
/// The side of the tool each face of `body` (in its face order) lies on: the tool's field at a
/// point inside the face, on its surface and inside its trims (SMLib's
/// `CalculateAnInternalPoint`). Once the tool has cut the body each piece lies on one side, so
/// one such point says which.
fn face_sides(body: &Body, side: &ToolSide) -> Result<Vec<f64>> {
let m = body.mesh(&crate::MeshTolerance::chord(body.size() * 2e-3))?;
let mut best = vec![0.0f64; body.face_count()];
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
let Some([a, b, c]) = m.tri(t) else { continue };
let x = side.at((a + b + c) / 3.0);
if let Some(slot) = best.get_mut(*f as usize)
&& x.is_finite()
&& x.abs() > slot.abs()
{
*slot = x;
}
let (sm, order) = crate::native::smlib_body_ordered(body)?;
let order = order.ok_or_else(|| KernelError::Failed("trim: SMLib numbers the pieces' faces differently from their record".into()))?;
let points = sm.face_points().map_err(|e| KernelError::Failed(format!("SMLib trim: a point inside each piece: {e}")))?;
let mut sides = vec![f64::NAN; points.len()];
for (i, p) in points.iter().enumerate() {
let face = if order.faces.is_empty() { i } else { order.faces.get(i).copied().unwrap_or(usize::MAX) };
let slot = sides.get_mut(face).ok_or_else(|| KernelError::Failed("trim: a piece's face lies outside the body".into()))?;
*slot = side.at(Vec3::new(p[0], p[1], p[2]));
}
Ok(best)
if sides.iter().any(|x| !x.is_finite()) {
return Err(KernelError::Failed("trim: the side of a piece can't be told".into()));
}
Ok(sides)
}
/// For each point, the face of `body` nearest it (over one tessellation).
+16 -2
View File
@@ -1,6 +1,6 @@
//! The untrim and extend blocks of `capi/include/smlib.h` (`smlib_untrim_face`,
//! `smlib_extend_sheet`; the fork's `capi/src/smlib_untrim.cpp` and `smlib_extend.cpp`), declared
//! by hand. Part of the FFI boundary, like its parent: called only through [`crate::trim`].
//! `smlib_extend_sheet`; the fork's `capi/src/smlib_untrim.cpp` and `smlib_extend.cpp`) and
//! `smlib_face_points` (`smlib_imprint.cpp`, the pieces a trim chooses from), declared by hand. Part of the FFI boundary, like its parent: called only through [`crate::trim`].
#![allow(unsafe_code)]
@@ -29,6 +29,7 @@ unsafe extern "C" {
in_place: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_face_points(body: *const RawBrep, out: *mut f64, n_faces: i64) -> i32;
fn smlib_extend_sheet(sheet: *const RawBrep, point: *const f64, tol: f64, distance: f64, mode: i32, out: *mut *mut RawBrep) -> i32;
}
@@ -56,3 +57,16 @@ pub fn untrim_face(body: &Handle, point: &[f64; 3], tol: f64, holes: Option<&[[f
pub fn untrim_face(_: &Handle, _: &[f64; 3], _: f64, _: Option<&[[f64; 3]]>, _: bool, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(feature = "native")]
pub fn face_points(body: &Handle, n_faces: usize) -> Result<Vec<[f64; 3]>, i32> {
let n = i64::try_from(n_faces).map_err(|_| super::ERR_ARG)?;
let mut out = vec![[0.0f64; 3]; n_faces];
let s = unsafe { smlib_face_points(body.raw(), out.as_mut_ptr().cast::<f64>(), n) };
if s == super::OK { Ok(out) } else { Err(s) }
}
#[cfg(not(feature = "native"))]
pub fn face_points(_: &Handle, _: usize) -> Result<Vec<[f64; 3]>, i32> {
Err(NOT_BUILT)
}
+8
View File
@@ -56,4 +56,12 @@ impl Brep {
s => error(s),
})
}
/// A point inside each face, in SMLib's face order: on the face's surface, inside its trims
/// (`SmFace::CalculateAnInternalPoint`).
pub fn face_points(&self) -> Result<Vec<[f64; 3]>> {
init()?;
let [faces, _, _] = self.counts()?;
ffi::trim::face_points(&self.0, faces).map_err(error)
}
}