forked from 1b-agent/fieldcraft-upstream
geometry.face.absorb: tiny and sliver faces into a neighbour (#506)
A face merge may keep one of its faces' names (Merge::into, model FaceMerge::into): absorbing a tiny face leaves the neighbour by its own name, so sets on it still select it. Shape::absorbing_neighbour finds the face a tiny one shares the longest edges with. geometry.face.absorb absorbs one face (into a named or the best neighbour) or, from_check, every small and sliver face the last geometry.check marked; check marks now carry the small face they found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
ce02d263c7
commit
58174ba168
14 files changed
+283
-27
No files matched your search
@@ -37,6 +37,9 @@ pub struct Merge {
|
||||
pub body: String,
|
||||
/// The faces, by persistent name (a name also selects the pieces a partition cut it into).
|
||||
pub faces: Vec<String>,
|
||||
/// Absorb: the merged face keeps the name of this one of `faces` instead of `merge:<name>`
|
||||
/// (a tiny face absorbed into a neighbour leaves the neighbour, by its own name).
|
||||
pub into: Option<String>,
|
||||
}
|
||||
|
||||
/// The name of the face merge `name` makes.
|
||||
@@ -65,7 +68,13 @@ impl Design {
|
||||
self.errors.push(FeatureError { feature, message: format!("no body `{}`", m.body), bodies: vec![m.body.clone()] });
|
||||
continue;
|
||||
};
|
||||
match body.merge_faces(&merged_face_name(&m.name), &m.faces) {
|
||||
let name = m.into.clone().unwrap_or_else(|| merged_face_name(&m.name));
|
||||
if m.into.as_ref().is_some_and(|i| !m.faces.contains(i)) {
|
||||
let message = format!("the face it keeps, `{name}`, is not one it merges");
|
||||
self.errors.push(FeatureError { feature, message, bodies: vec![m.body.clone()] });
|
||||
continue;
|
||||
}
|
||||
match body.merge_faces(&name, &m.faces) {
|
||||
Ok(merged) => *body = merged,
|
||||
Err(e) => self.errors.push(FeatureError { feature, message: e.to_string(), bodies: vec![m.body.clone()] }),
|
||||
}
|
||||
@@ -105,14 +114,11 @@ impl Shape {
|
||||
}
|
||||
|
||||
/// This shape with the faces named `faces` (two or more, touching along edges) made one face
|
||||
/// named `name`, in place of the first of them.
|
||||
/// named `name` (a new name, or the name of one of them), in place of the first of them.
|
||||
pub fn merge_faces(&self, name: &str, faces: &[String]) -> Result<Shape> {
|
||||
if faces.len() > MAX_MERGE_FACES {
|
||||
return Err(Error::Invalid(format!("a merge takes at most {MAX_MERGE_FACES} faces")));
|
||||
}
|
||||
if self.faces.iter().any(|f| f.name == name) {
|
||||
return Err(Error::Invalid(format!("body `{}` already has a face `{name}`", self.name)));
|
||||
}
|
||||
let mut members = BTreeSet::new();
|
||||
for f in faces {
|
||||
let found = self.faces_named(f);
|
||||
@@ -121,6 +127,9 @@ impl Shape {
|
||||
}
|
||||
members.extend(found);
|
||||
}
|
||||
if self.faces.iter().zip(0u32..).any(|(f, i)| f.name == name && !members.contains(&i)) {
|
||||
return Err(Error::Invalid(format!("body `{}` already has a face `{name}`", self.name)));
|
||||
}
|
||||
if members.len() < 2 {
|
||||
return Err(Error::Invalid("a merge needs two or more faces".into()));
|
||||
}
|
||||
@@ -156,6 +165,28 @@ impl Shape {
|
||||
Ok(Shape { name: self.name.clone(), body: self.body.clone(), faces: kept, face_map })
|
||||
}
|
||||
|
||||
/// The neighbour face `face` shares the longest edges with (by name): where a tiny or sliver
|
||||
/// face is best absorbed. An error for a face the body lacks or one with no neighbour.
|
||||
pub fn absorbing_neighbour(&self, face: &str) -> Result<String> {
|
||||
let own = self.faces_named(face);
|
||||
let [f] = own.as_slice() else {
|
||||
return Err(Error::Invalid(format!("body `{}` has no face `{face}` (or several pieces of it: name one)", self.name)));
|
||||
};
|
||||
let mesh = self.tessellate((self.size()? * EDGE_TOLERANCE).max(1e-4))?;
|
||||
let mut shared: std::collections::BTreeMap<u32, f64> = std::collections::BTreeMap::new();
|
||||
for (points, faces) in mesh.edges.iter().zip(&mesh.edge_faces) {
|
||||
if !faces.contains(f) {
|
||||
continue;
|
||||
}
|
||||
let length: f64 = points.windows(2).map(|w| (0..3).map(|k| (w[1][k] - w[0][k]).powi(2)).sum::<f64>().sqrt()).sum();
|
||||
for g in faces.iter().filter(|g| *g != f) {
|
||||
*shared.entry(*g).or_default() += length;
|
||||
}
|
||||
}
|
||||
let best = shared.into_iter().max_by(|a, b| a.1.total_cmp(&b.1).then(b.0.cmp(&a.0))).map(|(g, _)| g);
|
||||
best.map(|g| self.face_label(g)).ok_or_else(|| Error::Invalid(format!("face `{face}` of body `{}` has no neighbour to absorb it", self.name)))
|
||||
}
|
||||
|
||||
/// An error unless `members` touch one another along edges, all in one piece.
|
||||
fn connected(&self, members: &BTreeSet<u32>) -> Result<()> {
|
||||
let mesh = self.tessellate((self.size()? * EDGE_TOLERANCE).max(1e-4))?;
|
||||
@@ -335,7 +366,7 @@ mod tests {
|
||||
assert_eq!(hole.len(), 2, "{hole:?}");
|
||||
let before = d.bodies[0].tessellate(0.01).unwrap();
|
||||
let body = d.bodies[0].name.clone();
|
||||
let d = d.merged(&[Merge { name: "Hole".into(), body, faces: hole.clone() }]).unwrap();
|
||||
let d = d.merged(&[Merge { name: "Hole".into(), body, faces: hole.clone(), into: None }]).unwrap();
|
||||
assert!(d.errors.is_empty(), "{:?}", d.errors);
|
||||
let body = &d.bodies[0];
|
||||
assert_eq!(body.faces().len(), 7);
|
||||
@@ -375,7 +406,7 @@ mod tests {
|
||||
.map(|(n, _)| n)
|
||||
.collect();
|
||||
assert_eq!(planes.len(), 2, "top and bottom");
|
||||
let m = |name: &str, body: &str, faces: Vec<String>| Merge { name: name.into(), body: body.into(), faces };
|
||||
let m = |name: &str, body: &str, faces: Vec<String>| Merge { name: name.into(), body: body.into(), faces, into: None };
|
||||
let merged = d
|
||||
.clone()
|
||||
.merged(&[
|
||||
@@ -400,13 +431,13 @@ mod tests {
|
||||
let d = plate();
|
||||
let body = d.bodies[0].name.clone();
|
||||
let hole = halves(&d);
|
||||
let d = d.merged(&[Merge { name: "Hole".into(), body: body.clone(), faces: hole }]).unwrap();
|
||||
let d = d.merged(&[Merge { name: "Hole".into(), body: body.clone(), faces: hole, into: None }]).unwrap();
|
||||
let b = &d.bodies[0];
|
||||
let f = b.face_index("merge:Hole").unwrap();
|
||||
let mesh = b.tessellate(0.01).unwrap();
|
||||
let next = mesh.edge_faces.iter().find(|fs| fs.contains(&f)).and_then(|fs| fs.iter().find(|&&g| g != f)).copied().unwrap();
|
||||
let next = b.faces()[next as usize].name.clone();
|
||||
let d = d.merged(&[Merge { name: "More".into(), body, faces: vec!["merge:Hole".into(), next] }]).unwrap();
|
||||
let d = d.merged(&[Merge { name: "More".into(), body, faces: vec!["merge:Hole".into(), next], into: None }]).unwrap();
|
||||
assert!(d.errors.is_empty(), "{:?}", d.errors);
|
||||
let b = &d.bodies[0];
|
||||
let f = b.face_index("merge:More").unwrap();
|
||||
@@ -414,4 +445,21 @@ mod tests {
|
||||
assert_eq!(b.merged_faces(), vec![(f, 3)]);
|
||||
assert_eq!(b.face_surfaces().unwrap()[f as usize], None);
|
||||
}
|
||||
|
||||
/// The neighbour a half of the hole is best absorbed into is the other half (two 10 mm seams,
|
||||
/// against a 15.7 mm rim arc with each cap); absorbing it keeps the other half's name.
|
||||
#[test]
|
||||
fn an_absorbed_face_leaves_its_neighbour_by_name() {
|
||||
let d = plate();
|
||||
let body = d.bodies[0].name.clone();
|
||||
let hole = halves(&d);
|
||||
let best = d.bodies[0].absorbing_neighbour(&hole[0]).unwrap();
|
||||
assert_eq!(best, hole[1]);
|
||||
let d = d.merged(&[Merge { name: "A".into(), body, faces: vec![hole[0].clone(), hole[1].clone()], into: Some(hole[1].clone()) }]).unwrap();
|
||||
assert!(d.errors.is_empty(), "{:?}", d.errors);
|
||||
let b = &d.bodies[0];
|
||||
assert_eq!(b.faces().len(), 7);
|
||||
assert!(b.face_index(&hole[1]).is_some() && b.face_index(&hole[0]).is_none());
|
||||
assert!(b.absorbing_neighbour("nope").is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user