//! 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 crate::{KernelError, Result, guard}; use solidcraft_geom::{Plane, Vec3}; use std::sync::Arc; use truck_modeling as mt; pub(super) fn actual_plane(face: &mt::Face) -> Option { 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; } Plane::new(from_p3(p.origin()), u, v) } /// How far apart the normals' x components of two planes `same_support` accepts can be, with /// room to spare: their unit normals are within about 1e-12 of each other. const NORMAL_WINDOW: f64 = 1e-9; /// The entries of `sorted` (ordered by their plane's normal x) whose normal x is within /// [`NORMAL_WINDOW`] of `nx`: the only ones `same_support` can accept for a plane with that normal. fn normal_window(sorted: &[T], plane: impl Fn(&T) -> &Plane, nx: f64) -> &[T] { let first = sorted.partition_point(|t| plane(t).normal().x < nx - NORMAL_WINDOW); let last = sorted.partition_point(|t| plane(t).normal().x <= nx + NORMAL_WINDOW); sorted.get(first..last.max(first)).unwrap_or_default() } pub(super) fn same_support(a: &Plane, b: &Plane) -> bool { let error = 4096. * f64::EPSILON * (1. + a.origin.len() + b.origin.len()); a.normal().cross(b.normal()).len() <= 4096. * f64::EPSILON && a.normal().dot(b.normal()) > 0. && (a.origin - b.origin).dot(a.normal()).abs() <= error } fn base_plane(source: &FaceSource, actual: &Plane) -> Option { match source { FaceSource::CertifiedExtrudeSide { plane: Some(p), .. } | FaceSource::CertifiedExtrudeCap { plane: p, .. } | FaceSource::Box { producer: Some(_), plane: p, .. } | FaceSource::CylinderCap { plane: p, .. } | FaceSource::ShellPlanar { producer: Some(_), plane: p, .. } => Some(*p), FaceSource::Imported { producer: Some(_), payload_sha256: Some(_), .. } => Some(*actual), _ => None, } } pub(super) fn certified_plane(source: &FaceSource) -> Option { match source { FaceSource::CertifiedExtrudeSide { plane: Some(p), .. } | FaceSource::CertifiedExtrudeCap { plane: p, .. } | FaceSource::Box { producer: Some(_), plane: p, .. } | FaceSource::CylinderCap { plane: p, .. } | FaceSource::ShellPlanar { producer: Some(_), plane: p, .. } => Some(*p), _ => None, } } fn boolean_source(source: &FaceSource, constructed: bool) -> bool { matches!(source, FaceSource::ShellPlanar { producer: Some(_), .. }) || (constructed && certified_plane(source).is_some()) } pub(super) fn descriptor_cost(source: &FaceSource) -> Option { let mut cost = 256usize; match source { FaceSource::CertifiedExtrudeCap { outer, holes, .. } => { if holes.len() > 4096 { return None; } cost = cost.checked_add(holes.len().checked_mul(64)?)?; let mut entries = 4096usize; for id in outer.iter().chain(holes.iter().flatten()) { entries = entries.checked_sub(1)?; if id.len() > 128 { return None; } cost = cost.checked_add(id.len().checked_add(64)?)?; } } FaceSource::CertifiedExtrudeSide { curve, .. } => { if curve.len() > 128 { return None; } cost = cost.checked_add(curve.len())?; } FaceSource::Imported { payload_sha256: Some(digest), .. } => { if digest.len() > 64 { return None; } cost = cost.checked_add(digest.len())?; } FaceSource::ShellPlanar { source, .. } if !matches!(source.as_ref(), FaceSource::ShellPlanar { .. }) => { cost = cost.checked_add(descriptor_cost(source)?)?; } FaceSource::ExtrudeCap { region, .. } => { if region.holes.len() > 8192 { return None; } cost = cost.checked_add(region.holes.len().checked_mul(64)?)?; let count = region.holes.iter().try_fold(region.outer.segs.len(), |sum, h| sum.checked_add(h.segs.len()))?; if count > 8192 { return None; } cost = cost.checked_add(count.checked_mul(128)?)?; } FaceSource::ExtrudeSide { .. } | FaceSource::Imported { .. } | FaceSource::Box { .. } | FaceSource::CylinderCap { .. } => {} _ => return None, } Some(cost) } impl Body { /// Preserve uniquely certified unchanged planar construction supports. pub fn with_preserved_planar_sources(self, input: &Body) -> Result { self.map_planar_sources(&[input], None, None, &[], false) } /// Generate distinct outer/inner roles from the exact original supports of /// an inside shell. Generated rims and curved walls are intentionally absent. pub(crate) fn with_shell_planar_sources(self, input: &Body, thickness: f64, open: &[Vec3]) -> Result { self.map_planar_sources(&[input], Some(thickness), None, open, false) } /// Retain exact shell roles, and construction roles for Cut, only on unchanged /// supports without a competing coplanar tool. No imported or fitted role is inferred. pub(crate) fn with_preserved_boolean_planar_sources(self, input: &Body, tool: &Body, constructed: bool) -> Result { let self_ = self.inherit_regularization_protection(input).inherit_regularization_protection(tool); if !input.face_sources().is_some_and(|sources| sources.iter().flatten().any(|s| boolean_source(s, constructed))) { return Ok(self_); } self_.map_planar_sources(&[input], None, Some(tool), &[], constructed) } /// Retain exact construction and shell roles through a Union from both operands, only on /// unchanged supports. Coplanar supports of the two operands carry distinct identities, so /// a face merged from both stays without a role. pub(crate) fn with_preserved_union_planar_sources(self, a: &Body, b: &Body) -> Result { let self_ = self.inherit_regularization_protection(a).inherit_regularization_protection(b); if ![a, b].iter().any(|input| input.face_sources().is_some_and(|sources| sources.iter().flatten().any(|s| boolean_source(s, true)))) { return Ok(self_); } self_.map_planar_sources(&[a, b], None, None, &[], true) } fn map_planar_sources(mut self, inputs: &[&Body], shell: Option, blocker: Option<&Body>, open: &[Vec3], constructed: bool) -> Result { // Every boolean (a Cut's tool, or a Union's second operand) leaves the result with // faces whose support no input role describes. let boolean = blocker.is_some() || inputs.len() > 1; for input in inputs { self.regularization_protected |= input.regularization_protected; } if inputs.iter().all(|input| input.face_sources().is_none()) { 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) { 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())) { return Err(KernelError::Invalid("planar source alignment mismatch".into())); } if shell.is_some_and(|t| !t.is_finite() || t <= 0.) { return Err(KernelError::Invalid("invalid shell source thickness".into())); } if open.len() > 256 { return Ok(self); } 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) { // 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]); } 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 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 }; let Some(source) = source else { unknown_supports.push(actual); continue; }; if (boolean && !boolean_source(source, constructed)) || (shell.is_some() && matches!(source, FaceSource::ShellPlanar { .. })) { unknown_supports.push(actual); continue; } let Some(plane) = base_plane(source, &actual) else { unknown_supports.push(actual); continue; }; if !same_support(&plane, &actual) || (shell.is_some() && open.iter().any(|p| actual.height(*p).abs() <= opening_tolerance)) { unknown_supports.push(actual); continue; } let Some(cost) = descriptor_cost(source) else { unknown_supports.push(actual); continue; }; candidates.push((plane, source, false, cost)); if let Some(t) = shell { let inner = Plane::new(plane.origin - plane.normal() * t, plane.x, -plane.y) .ok_or_else(|| KernelError::Invalid("invalid shell inner source plane".into()))?; candidates.push((inner, source, true, cost)); } } // Planes that match must have (nearly) the same unit normal (`same_support`), so the // lists are sorted by the normal's x and each face looks only at its window of them. unknown_supports.sort_by(|a, b| a.normal().x.total_cmp(&b.normal().x)); candidates.sort_by(|a, b| a.0.normal().x.total_cmp(&b.0.normal().x)); let mut work = 2_000_000usize; 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() { let mut found = None; if let Some(actual) = actual_plane(face) { let mut blocked = false; let nx = actual.normal().x; for plane in normal_window(&unknown_supports, |p| p, nx) { work = work.checked_sub(1).ok_or_else(|| KernelError::Invalid("unknown planar source comparison budget exceeded".into()))?; if same_support(plane, &actual) { blocked = true; break; } } for plane in normal_window(&competing, |p| p, nx) { work = work.checked_sub(1).ok_or_else(|| KernelError::Invalid("planar tool comparison budget exceeded".into()))?; if same_support(plane, &actual) { blocked = true; break; } } if blocked { let existing = self .face_sources() .and_then(|sources| sources.get(index)) .and_then(|s| s.as_ref()) .filter(|s| (boolean || self.face_was_coalesced(index)) && !boolean_source(s, constructed)); let keep = existing.and_then(|s| { descriptor_cost(s).and_then(|cost| { metadata.checked_sub(cost).map(|remaining| { metadata = remaining; s.clone() }) }) }); result.push(keep); continue; } let mut chosen: Option<(Plane, &FaceSource, bool, usize)> = None; for (plane, source, inner, cost) in normal_window(&candidates, |c| &c.0, nx) { work = work.checked_sub(1).ok_or_else(|| KernelError::Invalid("planar source comparison budget exceeded".into()))?; if same_support(plane, &actual) { if let Some((previous_plane, previous_source, previous_inner, _)) = chosen { let Some(remaining) = identity_bytes.checked_sub(*cost) else { chosen = None; break; }; identity_bytes = remaining; // Several current pieces of one exact source are one // logical face; distinct source identities remain ambiguous. if previous_plane != *plane || previous_source != *source || previous_inner != *inner { chosen = None; break; } } else { chosen = Some((*plane, *source, *inner, *cost)); } } } if let Some((plane, source, inner, cost)) = chosen && let Some(remaining) = metadata.checked_sub(cost) { metadata = remaining; found = Some(if shell.is_some() { FaceSource::ShellPlanar { producer: None, source: Box::new(source.clone()), inner, plane } } else { source.clone() }); } } if found.is_none() && (boolean || self.face_was_coalesced(index)) { found = self .face_sources() .and_then(|sources| sources.get(index)) .and_then(|s| s.as_ref()) .filter(|s| !boolean_source(s, constructed)) .and_then(|s| { descriptor_cost(s).and_then(|cost| { metadata.checked_sub(cost).map(|remaining| { metadata = remaining; s.clone() }) }) }); } result.push(found); } Ok(result) })?; self.face_sources = Some(Arc::new(mapped)); self.regularize_certified_planar_faces() } pub fn with_shell_certificate(mut self, producer: u64) -> Result { if producer == 0 { return Err(KernelError::Invalid("invalid shell source producer".into())); } if let Some(sources) = &self.face_sources { let mut sources = sources.as_ref().clone(); for source in sources.iter_mut().flatten() { if let FaceSource::ShellPlanar { producer: id, .. } = source { *id = Some(producer); } } self.face_sources = Some(Arc::new(sources)); } self.regularize_certified_planar_faces() } } #[cfg(test)] mod tests { use super::*; use crate::{box_solid, fillet, shell, transform_matrix}; #[test] fn rounded_shell_planar_roles_are_exact_distinct_and_transformable() { for (width, thickness) in [(80., 2.), (100., 3.)] { let box_body = box_solid(Vec3::new(-width / 2., -30., 0.), Vec3::new(width / 2., 30., 30.)).unwrap().with_box_certificate(7).unwrap(); let rounded = fillet( &box_body, &[ Vec3::new(width / 2., 30., 15.), Vec3::new(-width / 2., 30., 15.), Vec3::new(-width / 2., -30., 15.), Vec3::new(width / 2., -30., 15.), ], 6., ) .unwrap() .with_preserved_planar_sources(&box_body) .unwrap(); assert_eq!(rounded.face_sources().unwrap().iter().flatten().count(), 6); let cup = shell(&rounded, &[Vec3::new(0., 0., 30.)], thickness).unwrap().with_shell_certificate(9).unwrap(); let sources = cup.face_sources().unwrap(); assert!( !sources .iter() .flatten() .any(|s| matches!(s,FaceSource::ShellPlanar {source, ..} if matches!(source.as_ref(),FaceSource::Box {axis:2,high:true,..}))) ); let right = sources.iter().position(|s| matches!(s, Some(FaceSource::ShellPlanar { inner:false, source, .. }) if matches!(source.as_ref(),FaceSource::Box {axis:0,high:true,..}))).unwrap(); let floor = sources.iter().position(|s| matches!(s, Some(FaceSource::ShellPlanar { inner:true, source, .. }) if matches!(source.as_ref(),FaceSource::Box {axis:2,high:false,..}))).unwrap(); assert!((cup.face_plane(right).unwrap().origin.x - width / 2.).abs() < 1e-9); let floor_plane = cup.face_plane(floor).unwrap(); assert!((floor_plane.origin.z - thickness).abs() < 1e-9); assert!(floor_plane.normal().dot(Vec3::Z) > 0.999999); for (i, s) in sources.iter().enumerate() { if matches!(s, Some(FaceSource::ExtrudeSide { coalesced: true, plane: None, .. })) { assert!(cup.face_plane(i).is_none(), "loss witness is not source authority"); } else if s.is_some() { assert!(same_support(&cup.face_plane(i).unwrap(), &cup.planar_face(i).unwrap())); } } let moved = transform_matrix(&cup, [[0., 1., 0., 0.], [-1., 0., 0., 0.], [0., 0., 1., 0.], [100., 200., 300., 1.]]).unwrap(); assert!((moved.face_plane(floor).unwrap().origin.z - (300. + thickness)).abs() < 1e-9); assert!(matches!(moved.face_sources().unwrap().get(right), Some(Some(FaceSource::ShellPlanar { producer: Some(9), inner: false, .. })))); assert!(moved.face_plane(right).unwrap().normal().dot(Vec3::Y) > 0.999999); let raw = cup.tessellate(0.01).unwrap(); let display = cup.display_mesh(0.01).unwrap(); let cylindrical: Vec<_> = cup .solid .face_iter() .enumerate() .filter_map(|(i, f)| { 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); Some((i, radius, lo, hi)) }) .collect(); eprintln!("roundedShell width{width} thickness{thickness} raw{} cylindrical roles{cylindrical:?}", cup.solid.face_iter().count()); let mut corner_joins = 0; for (edge, adjacent) in raw.edge_faces.iter().enumerate() { let [a, b] = adjacent.as_slice() else { continue }; if raw.edges[edge].iter().all(|p| (p.z - thickness).abs() < 1e-8) && cylindrical.iter().any(|(i, r, _, _)| *i == *a as usize && (*r - 6.).abs() < 1e-6) && cylindrical.iter().any(|(i, r, _, _)| *i == *b as usize && (*r - 6.).abs() < 1e-6) { corner_joins += 1; assert!(display.seams[edge], "existing cylindrical corner joins must not draw a floor-height arc"); } } assert_eq!(corner_joins, 0, "no physical lower corner strip remains"); assert_eq!(cup.face_count(), 19); let outer: Vec<_> = cylindrical.iter().filter(|(_, r, _, _)| (*r - 6.).abs() < 1e-6).collect(); assert_eq!(outer.len(), 4); assert!(outer.iter().all(|(i, _, _, _)| cup.face_was_coalesced(*i) && moved.face_was_coalesced(*i))); assert!(outer.iter().all(|(_, _, lo, hi)| lo.abs() < 1e-9 && (*hi - 30.).abs() < 1e-9)); for axis in 0..2 { 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(); assert!(z.iter().any(|z| z.abs() < 1e-9)); assert!(z.iter().any(|z| (*z - 30.).abs() < 1e-9)); } } assert!(display.contains(Vec3::new(width / 2. - thickness / 2., 0., thickness / 2.))); assert!(display.contains(Vec3::new(width / 2. - thickness / 2., 0., 15.))); assert!(!display.contains(Vec3::new(0., 0., 15.))); // Different meshers (the display plan and the reference), both within 0.01 mm. let (rv, dv) = (raw.measure().volume, display.measure().volume); assert!((rv - dv).abs() <= raw.measure().area * 0.01, "display volume {dv} vs {rv}"); let expected = (width * 60. - (4. - std::f64::consts::PI) * 36.) * 30. - ((width - 2. * thickness) * (60. - 2. * thickness) - (4. - std::f64::consts::PI) * (6. - thickness).powi(2)) * (30. - thickness); assert!((cup.tessellate(1e-4).unwrap().measure().volume - expected).abs() < 0.2); let imported = crate::step_import(&crate::step_export(&[&cup], "regularized rounded shell").unwrap()).unwrap(); assert!(imported.warnings.is_empty()); let copy = &imported.bodies[0].body; assert_eq!(copy.face_count(), 19); // 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)) .collect(); assert_eq!(walls.len(), 1); let z: Vec<_> = walls[0].vertex_iter().map(|v| v.point().z).collect(); assert!(z.iter().any(|z| z.abs() < 1e-8)); assert!(z.iter().any(|z| (*z - 30.).abs() < 1e-8)); } let copy_mesh = copy.tessellate(1e-4).unwrap(); assert!((copy_mesh.measure().volume - expected).abs() < 0.2); assert!(copy_mesh.contains(Vec3::new(width / 2. - thickness / 2., 0., 15.))); assert!(!copy_mesh.contains(Vec3::new(0., 0., 15.))); } } #[test] fn simple_shell_has_one_full_height_face_per_outer_wall_and_exports_that_topology() { 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); let mesh = cup.tessellate(0.01).unwrap(); assert!((mesh.measure().volume - (40. * 30. * 20. - 36. * 26. * 18.)).abs() < 1e-6); assert!(mesh.contains(Vec3::new(20., 1., 1.))); assert!(mesh.contains(Vec3::new(20., 1., 10.))); assert!(!mesh.contains(Vec3::new(20., 15., 10.))); 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!((copy.tessellate(0.01).unwrap().measure().volume - mesh.measure().volume).abs() < 1e-6); } #[test] fn union_keeps_unchanged_supports_of_both_operands_and_drops_shared_ones() { use crate::{BoolOp, boolean}; let a = box_solid(Vec3::ZERO, Vec3::new(20., 20., 10.)).unwrap().with_box_certificate(1).unwrap(); // A taller box overlapping one side: the bottoms share z = 0, the tops do not. let b = box_solid(Vec3::new(10., 5., 0.), Vec3::new(30., 15., 15.)).unwrap().with_box_certificate(2).unwrap(); 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(); let source = sources[i].as_ref(); let height = plane.origin.z; if plane.normal().z > 0.999 && (height - 10.).abs() < 1e-9 { assert!(matches!(source, Some(FaceSource::Box { producer: Some(1), axis: 2, high: true, .. })), "{source:?}"); seen.push("a top"); } else if plane.normal().z > 0.999 && (height - 15.).abs() < 1e-9 { assert!(matches!(source, Some(FaceSource::Box { producer: Some(2), axis: 2, high: true, .. })), "{source:?}"); seen.push("b top"); } else if plane.normal().z < -0.999 { assert!(source.is_none(), "a bottom merged from both operands has no single identity: {source:?}"); seen.push("bottom"); } } for want in ["a top", "b top", "bottom"] { assert!(seen.contains(&want), "{want} missing from {seen:?}"); } } #[test] 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 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 { 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 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 { 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 }; if p.normal().dot(Vec3::Z) > 0.999 && (p.origin.z - 1.).abs() < 1e-9 { assert!(preserved.face_sources().unwrap()[i].is_none()); } } } #[test] fn pocket_cut_removes_internal_wall_faces_in_actual_topology() { use solidcraft_geom::{Loop2, Region2, Vec2}; let region = Region2 { outer: Loop2::polygon(&[Vec2::new(-30., -27.), Vec2::new(30., -27.), Vec2::new(30., 27.), Vec2::new(-30., 27.)]), holes: vec![], }; let original = crate::extrude(&Plane::XY, std::slice::from_ref(®ion), 0., 10.).unwrap().pop().unwrap(); let curves: Vec<_> = original .face_sources() .unwrap() .iter() .enumerate() .map(|(i, source)| matches!(source, Some(FaceSource::ExtrudeSide { .. })).then(|| format!("l{i}"))) .collect(); let original = original.with_extrude_certificates(2, 1, &curves).unwrap(); let (x, y) = (24.73317426966596, 21.733174269665962); let cutter = box_solid(Vec3::new(-x, -y, 1.), Vec3::new(x, y, 10.)).unwrap(); let pocket = crate::boolean(&original, &cutter, crate::BoolOp::Cut).unwrap().unwrap(); let raw = pocket.tessellate(0.01).unwrap(); let display = pocket.display_mesh(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"); 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(); 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!(display.contains(Vec3::new(0., 0., 0.5))); assert!(!display.contains(Vec3::new(0., 0., 5.))); assert!(display.contains(Vec3::new(28., 0., 5.))); assert!((display.measure().volume - (60. * 54. * 10. - 4. * x * y * 9.)).abs() < 1e-7); // Different triangulations of the same polyhedron: equal up to rounding. assert!((raw.measure().volume - display.measure().volume).abs() < 1e-9 * raw.measure().volume); 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); let copy_mesh = copy.tessellate(0.01).unwrap(); assert!((copy_mesh.measure().volume - raw.measure().volume).abs() < 1e-6); assert!(copy_mesh.contains(Vec3::new(0., 0., 0.5))); assert!(!copy_mesh.contains(Vec3::new(0., 0., 5.))); let competing = box_solid(Vec3::new(30., -1., 0.), Vec3::new(31., 1., 2.)).unwrap(); let guarded = pocket.clone().with_preserved_boolean_planar_sources(&pocket, &competing, true).unwrap(); for (index, source) in guarded.face_sources().unwrap().iter().enumerate() { if pocket.face_plane(index).is_some_and(|p| p.normal().dot(Vec3::X) > 0.999 && (p.origin.x - 30.).abs() < 1e-9) { assert!(source.is_none(), "competing tool plane cannot retain source identity"); } } } }