mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 04:12:15 +00:00
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. -585334a29jyviko: kernel: planar source lineage on the record, coplanar faces joined by SMLib -5c16e8dd1jyviko: kernel: coplanar faces joined by SMLib's merge_faces, the record read from the body -c802458efjyviko: solidcraft-smlib: Brep::face_points, a point inside each face (smlib_face_points) -453a4dec7jyviko: 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:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user