fix(kernel): preserve intentional Split Face lines in contact joins (#782)
Intentional Split Face lines survive a join with partial planar contact, so the far face stays in two pieces. Follow-up to #704 / #674.
This commit is contained in:
1 parent
beca7eb126
commit
293d660d6c
6 files changed
+120
-1
No files matched your search
@@ -716,7 +716,9 @@ pub(crate) fn glue(a: &Body, b: &Body) -> Option<Result<Body>> {
|
||||
let from: Vec<FaceProv> = solid.face_iter().filter_map(|f| prov.get(&f.id()).cloned()).collect();
|
||||
// Each edge's origins in the solid's edge order.
|
||||
let origins = crate::body::edge_order(solid.face_iter()).0.iter().map(|e| origins.get(&e.id()).cloned()).collect();
|
||||
crate::boolean_record::heal_build(solid, size, &[], Some(from), origins, &record::counts(&[a, b]))
|
||||
let keep: Vec<_> = a.split_keep().iter().chain(b.split_keep()).cloned().collect();
|
||||
crate::boolean_record::heal_build(solid, size, &keep, Some(from), origins, &record::counts(&[a, b]))
|
||||
.map(|body| body.with_split_lines(Some(std::sync::Arc::new(keep))))
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! Partial planar contact must sew a solid without displacing or inventing material.
|
||||
use crate::*;
|
||||
use solidcraft_geom::Vec3;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[test]
|
||||
fn split_contact_arc_crossing_one_edge_twice_preserves_the_body() {
|
||||
use truck_modeling::Surface;
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(25.0, 47.5, 37.5)).unwrap();
|
||||
let b = cylinder(Vec3::new(0.0, 7.5, 0.0), -Vec3::X, 5.0, 2.5).unwrap();
|
||||
let face = a.faces(0.01).unwrap().iter().position(|f| f.plane_normal.is_some_and(|n| n.dot(-Vec3::X) > 0.99)).unwrap();
|
||||
let side = b.solid.face_iter().position(|f| !matches!(f.surface(), Surface::Plane(_))).unwrap();
|
||||
let r = split_faces(&a, &[face], &SplitTool::Face { body: &b, face: side }).unwrap();
|
||||
assert_eq!(r.face_count(), a.face_count() + 1);
|
||||
assert!(r.validity().is_empty(), "{:?}", r.validity());
|
||||
assert!(r.history().is_some());
|
||||
let (before, after) = (measure(&a).unwrap(), measure(&r).unwrap());
|
||||
assert!((before.volume - after.volume).abs() < 1e-7);
|
||||
assert!((before.area - after.area).abs() < 1e-7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_contact_does_not_join_tangent_or_separated_faces() {
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(25.0, 47.5, 37.5)).unwrap();
|
||||
for z in [-5.0, -6.0] {
|
||||
let b = cylinder(Vec3::new(0.0, 7.5, z), -Vec3::X, 5.0, 2.5).unwrap();
|
||||
assert!(crate::coplanar::imprint_union(&a, &b, &|_| true).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_contact_retains_an_authored_split_away_from_the_contact() {
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(25.0, 47.5, 37.5)).unwrap();
|
||||
let face = a.faces(0.01).unwrap().iter().position(|f| f.plane_normal.is_some_and(|n| n.dot(Vec3::X) > 0.99)).unwrap();
|
||||
let a = split_faces(&a, &[face], &SplitTool::Plane(solidcraft_geom::Plane { origin: Vec3::new(0.0, 0.0, 15.0), ..solidcraft_geom::Plane::XY }))
|
||||
.unwrap();
|
||||
let b = cylinder(Vec3::new(0.0, 7.5, 0.0), -Vec3::X, 5.0, 2.5).unwrap();
|
||||
let r = boolean(&a, &b, BoolOp::Union).unwrap().unwrap();
|
||||
assert_eq!(r.split_lines(), a.split_lines());
|
||||
assert!(r.regularization_protected);
|
||||
assert!(r.validity().is_empty());
|
||||
let remote_faces =
|
||||
r.faces(0.01).unwrap().iter().filter(|f| f.plane_normal.is_some_and(|n| n.dot(Vec3::X) > 0.99) && (f.centroid.x - 25.0).abs() < 1e-7).count();
|
||||
assert_eq!(remote_faces, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_circular_contact_preserves_material_and_history() {
|
||||
for z in [-3.0_f64, 0.0, 3.0] {
|
||||
for angle in [0.0, 0.37] {
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(25.0, 47.5, 37.5)).unwrap();
|
||||
let b = cylinder(Vec3::new(0.0, 7.5, z), -Vec3::X, 5.0, 2.5).unwrap();
|
||||
let place = |body: &Body| transform(body, Vec3::new(4.0, -8.0, 2.0), Vec3::ZERO, Vec3::new(1.0, 2.0, 3.0), angle).unwrap();
|
||||
let (a, b) = (place(&a), place(&b));
|
||||
for reversed in [false, true] {
|
||||
let (a, b) = if reversed { (&b, &a) } else { (&a, &b) };
|
||||
let r = boolean(a, b, BoolOp::Union).unwrap().unwrap();
|
||||
assert!(r.validity().is_empty(), "z={z} angle={angle}: {:?}", r.validity());
|
||||
assert_eq!(r.shell_count(), 1);
|
||||
assert!(r.history().is_some(), "missing history z={z} angle={angle}");
|
||||
let m = measure(&r).unwrap();
|
||||
let volume = 25.0 * 47.5 * 37.5 + PI * 25.0 * 2.5;
|
||||
let contact = 25.0 * (-z / 5.0).acos() + z * (25.0 - z * z).sqrt();
|
||||
let area = 2.0 * (25.0 * 47.5 + 25.0 * 37.5 + 47.5 * 37.5) + 2.0 * PI * 5.0 * (5.0 + 2.5) - 2.0 * contact;
|
||||
assert!((m.volume - volume).abs() < 0.1, "z={z} angle={angle}: volume {} expected {volume}", m.volume);
|
||||
assert!((m.area - area).abs() < 0.2, "z={z} angle={angle}: area {} expected {area}", m.area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ mod intersection_curve_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_616_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_674_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_679_tests;
|
||||
mod lip;
|
||||
mod loopblend;
|
||||
|
||||
Reference in new issue
Block a user