solidcraft-smlib: blend faces come from the edge they replace

SMLib makes a fillet or chamfer face with no tag, so history::blends gives
each Generated face FaceOrigin::Edge for every blended edge whose two faces
both lie next to it and no longer meet directly. For the fillet shim's
history once it lands; a corner patch between blends keeps no origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brennen Herbruck
2026-10-21 00:45:05 -04:00
committed by jyviko
parent 4a4e11ef71
commit b85b51e81a
2 changed files with 64 additions and 1 deletions
+47
View File
@@ -164,6 +164,53 @@ impl Raw {
}
}
/// A blend's faces (fillet, chamfer) in `h`, the history of a blend of edges `blended` of its
/// one operand (operand 0), whose own edges `operand` lists (the faces each bounds, as in the
/// operand's identity history): SMLib makes a blend face with no tag, so each Generated face is
/// given [`FaceOrigin::Edge`] for every blended edge whose two faces both lie under faces next
/// to it and no longer meet each other directly. A corner patch between several blends matches
/// none of them and stays without origin.
pub fn blends(h: &mut History, operand: &[EdgeProv], blended: &[usize]) {
let mut next_to: Vec<BTreeSet<FaceOrigin>> = vec![BTreeSet::new(); h.faces.len()];
for e in &h.edges {
for f in &e.faces {
for g in &e.faces {
if let (Some(set), Some(p)) = (next_to.get_mut(*f), h.faces.get(*g))
&& f != g
{
set.extend(p.origins.iter().copied());
}
}
}
}
// Pairs of operand faces whose result faces still meet along an edge.
let mut meeting: BTreeSet<(FaceOrigin, FaceOrigin)> = BTreeSet::new();
for e in &h.edges {
if let [a, b] = e.faces.as_slice()
&& let (Some(a), Some(b)) = (h.faces.get(*a), h.faces.get(*b))
{
for x in &a.origins {
for y in &b.origins {
meeting.insert((*x.min(y), *x.max(y)));
}
}
}
}
for (f, around) in h.faces.iter_mut().zip(&next_to) {
if f.change != Change::Generated || !f.origins.is_empty() {
continue;
}
let edges = blended.iter().filter(|e| {
operand.get(**e).is_some_and(|p| {
let [a, b] = p.faces.as_slice() else { return false };
let (a, b) = (FaceOrigin::Face { operand: 0, face: *a }, FaceOrigin::Face { operand: 0, face: *b });
around.contains(&a) && around.contains(&b) && !meeting.contains(&(a.min(b), a.max(b)))
})
});
f.origins = edges.map(|e| FaceOrigin::Edge { operand: 0, edge: *e }).collect();
}
}
/// Reads non-negative values off the records.
struct Reader<'a>(std::slice::Iter<'a, i64>);
+17 -1
View File
@@ -1,5 +1,5 @@
use super::*;
use solidcraft_geom::brep::history::{Change, EdgeOrigin, FaceOrigin};
use solidcraft_geom::brep::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History};
fn face(operand: usize, face: usize) -> FaceOrigin {
FaceOrigin::Face { operand, face }
@@ -85,6 +85,22 @@ fn bad_orders_are_errors() {
assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 1, 3], edges: vec![] }).is_err());
}
/// A box with its edge 0 (between faces 4 and 5) filleted, as SMLib reports it: the fillet face
/// (6) has no tag; it lies between faces from 4 and 5 and two end faces.
#[test]
fn a_fillet_face_comes_from_its_edge() {
let kept = |i: usize| FaceProv { origins: vec![face(0, i)], change: Change::Modified };
let mut faces: Vec<FaceProv> = (0..6).map(kept).collect();
faces.push(FaceProv { origins: vec![], change: Change::Generated });
let edge = |a: usize, b: usize| EdgeProv { faces: vec![a, b], origins: vec![] };
let mut h = History { operands: 1, faces, edges: vec![edge(4, 6), edge(5, 6), edge(0, 6), edge(2, 6), edge(0, 4)] };
// The operand's own edges: edge 0 between faces 4 and 5, edge 1 between 0 and 4.
let operand = vec![edge(4, 5), edge(0, 4)];
blends(&mut h, &operand, &[0, 1]);
assert_eq!(h.faces[6].origins, vec![FaceOrigin::Edge { operand: 0, edge: 0 }]);
assert!(h.faces[..6].iter().all(|f| f.origins.len() == 1));
}
#[cfg(feature = "native")]
#[test]
fn a_tagged_body_is_its_own_history() {