diff --git a/crates/kernel/src/boolean_history_tests.rs b/crates/kernel/src/boolean_history_tests.rs index 4f0d26f8bf..ab4bdd6215 100644 --- a/crates/kernel/src/boolean_history_tests.rs +++ b/crates/kernel/src/boolean_history_tests.rs @@ -585,6 +585,35 @@ mod smlib { assert_eq!(meets(h).len(), 4, "{}", show(h)); } + /// Cubes sharing an edge, and cubes sharing a vertex: SMLib's union touches itself there; + /// it comes back as two closed shells, the shared edge and vertices split into one copy per + /// cube, each copy with its edge's history (verified against the geometry). + #[test] + fn cubes_touching_along_an_edge_or_at_a_vertex_unite_into_two_solids() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + for (name, b) in [("edge", boxed(10.0, 10.0, 0.0, 20.0, 20.0, 10.0)), ("vertex", boxed(10.0, 10.0, 10.0, 20.0, 20.0, 20.0))] { + let r = through_smlib(&a, &b, BoolOp::Union); + assert_eq!(r.solid.boundaries().len(), 2, "{name}"); + assert_eq!((r.face_count(), r.edge_count()), (12, 24), "{name}"); + let v = crate::measure(&r).unwrap().volume; + assert!((v - 2000.0).abs() < 1e-6 * 2000.0, "{name}: {v}"); + let h = verified(&r, &[&a, &b], 0.02); + assert!(h.faces.iter().all(|f| f.origins.len() == 1), "{name}: {}", show(h)); + } + } + + /// Pieces of a cut meeting along a line (two opposite quarters of a slab cut away) come + /// back as separate solids rather than a refusal. + #[test] + fn a_cut_leaving_pieces_along_a_line_gives_two_solids() { + let a = boxed(0.0, 0.0, 0.0, 20.0, 20.0, 10.0); + let once = through_smlib(&a, &boxed(10.0, -1.0, -1.0, 21.0, 10.0, 11.0), BoolOp::Cut); + let r = through_smlib(&once, &boxed(-1.0, 10.0, -1.0, 10.0, 21.0, 11.0), BoolOp::Cut); + assert_eq!(r.solid.boundaries().len(), 2); + let v = crate::measure(&r).unwrap().volume; + assert!((v - 2000.0).abs() < 1e-6 * 2000.0, "{v}"); + } + #[test] fn disjoint_intersection_is_empty() { let a = boxed(0.0, 0.0, 0.0, 1.0, 1.0, 1.0); diff --git a/crates/kernel/src/smlib_boolean.rs b/crates/kernel/src/smlib_boolean.rs index 789dfdc45c..8f2af04819 100644 --- a/crates/kernel/src/smlib_boolean.rs +++ b/crates/kernel/src/smlib_boolean.rs @@ -35,7 +35,18 @@ mod smlib { BoolOp::Intersect => BooleanOp::Intersect, }; let Some(r) = ta.boolean(&tb, kind).map_err(|e| failed("operation", e))? else { return Ok(None) }; - let check = r.check(CheckLevel::All).map_err(|e| failed("check", e))?; + // Solids touching each other along an edge or at a vertex come back as one non-manifold + // body, which the export splits into separate solids: SMLib's check is of those, imported + // as given (its check of the touching arrangement itself refuses the radial order of + // faces round an edge they share). + let manifold = r.is_manifold_solid().map_err(|e| failed("check", e))?; + let check = if manifold { + r.check(CheckLevel::All).map_err(|e| failed("check", e))? + } else { + let record = r.to_native().map_err(|e| failed("export", e))?; + let split = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| failed("the separate solids", e))?; + split.check(CheckLevel::All).map_err(|e| failed("check", e))? + }; if !check.is_valid() { // Whether an operand failed it already says whose the fault is. let operands: Vec<&str> = [("the first operand", &sa), ("the second operand", &sb)] diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV index a4b5678c70..1e7dca84fe 100644 --- a/crates/smlib/SMLIB_REV +++ b/crates/smlib/SMLIB_REV @@ -1 +1 @@ -ffcf56d195d1d0c98c5190dd993d61732f099642 +e87e9b3bdc348b00de5238963f387bfa4ba337d6 diff --git a/crates/smlib/src/export.rs b/crates/smlib/src/export.rs index 35c81ddafb..41b14cf615 100644 --- a/crates/smlib/src/export.rs +++ b/crates/smlib/src/export.rs @@ -34,7 +34,7 @@ use solidcraft_geom::Vec3; use solidcraft_geom::brep as b; use crate::flat::{self, *}; -use crate::history::Order; +use crate::history::{Order, Piece}; use crate::import::Arrays; use crate::{Brep, Result, error, ffi, init}; @@ -576,6 +576,7 @@ enum Loop { } /// One edge before renumbering, along its record's parameter. +#[derive(Clone)] struct Edge { curve: b::Curve, interval: [f64; 2], @@ -727,66 +728,59 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { face_uses.get_mut(f).ok_or_else(|| invalid("face"))?.push((s, same)); } } - let mut shell_of: Vec> = vec![None; n_shells]; - let mut shells: Vec> = Vec::new(); - for s in 0..n_shells { - if shell_of.get(s).copied().flatten().is_some() { - continue; - } - let uses = shell_faceuses.get(s).ok_or_else(|| invalid("shell"))?; - let (f0, _) = faceuse(uses.start).map_err(|_| invalid("empty shell"))?; - let mate = match face_uses.get(f0).map(Vec::as_slice) { - Some([(a, _), (b, _)]) => { - if *a == s { - *b - } else { - *a - } - } - _ => return Err(invalid("a face without its two uses")), - }; - let side = if mate == s { - s - } else { - let (rs, rm) = (region_of.get(s).copied().unwrap_or(0), region_of.get(mate).copied().unwrap_or(0)); - match (material(rs), material(rm)) { - (false, true) => s, - (true, false) => mate, - _ => return Err(unsupported("a shell between two material regions or two empty ones")), - } - }; - let mut oriented = Vec::new(); - for fu in shell_faceuses.get(side).ok_or_else(|| invalid("shell"))?.clone() { + // Each record shell is one closed boundary between an empty region and a material one: the + // faces with the same runtime shell on the empty side and on the material side, as the empty + // side uses them. Solids touching along an edge or at a vertex share their empty side's + // runtime shell but not their material side's, so each gets its own record shell (the edges + // and vertices they share are split below). A sheet's one shell holds both uses of each + // face: the `same` one is kept. + let mut groups: Vec<((usize, usize), Vec<(u32, bool)>)> = Vec::new(); + for (s, uses) in shell_faceuses.iter().enumerate() { + for fu in uses.clone() { let (f, same) = faceuse(fu)?; - // A sheet lists both uses of each face in its one shell: keep the `same` one. - if mate == s && !same { - continue; - } - if !face_uses.get(f).is_some_and(|u| u.iter().any(|(o, _)| *o == mate)) { - return Err(unsupported("a shell whose faces' other sides are in different shells")); - } + let key = match face_uses.get(f).map(Vec::as_slice) { + Some([(a, _), (b, _)]) if a == b => { + if !same { + continue; + } + (*a, *a) + } + Some([(a, _), (b, _)]) => { + let other = if *a == s { *b } else { *a }; + let (here, there) = (region_of.get(s).copied().unwrap_or(0), region_of.get(other).copied().unwrap_or(0)); + match (material(here), material(there)) { + (false, true) => (s, other), + (true, false) => continue, + _ => return Err(unsupported("a face between two material regions or two empty ones")), + } + } + _ => return Err(invalid("a face without its two uses")), + }; let agrees = faces.get(f).ok_or_else(|| invalid("face"))?.agrees; - oriented.push((index(f)?, same == agrees)); - } - let id = index(shells.len())?; - shells.push(oriented); - for k in [s, mate] { - if let Some(slot) = shell_of.get_mut(k) { - *slot = Some(id); - } + let at = match groups.iter().position(|(k, _)| *k == key) { + Some(at) => at, + None => { + groups.push((key, Vec::new())); + groups.len() - 1 + } + }; + groups.get_mut(at).ok_or_else(|| invalid("shell"))?.1.push((index(f)?, same == agrees)); } } let mut regions = Vec::with_capacity(region_shells.len()); for (r, block) in region_shells.iter().enumerate() { let mut listed = Vec::new(); for s in block.clone() { - let id = shell_of.get(s).copied().flatten().ok_or_else(|| invalid("shell"))?; - if !listed.contains(&id) { - listed.push(id); + for (g, ((empty, solid), _)) in groups.iter().enumerate() { + let id = index(g)?; + if (*empty == s || *solid == s) && !listed.contains(&id) { + listed.push(id); + } } } regions.push(b::Region { shells: listed, void: r > 0 && !material(r) }); } + let shells: Vec> = groups.into_iter().map(|(_, faces)| faces).collect(); // Shells renumbered in first appearance walking the regions. let mut order: Vec = Vec::new(); for r in ®ions { @@ -818,7 +812,10 @@ struct Parts { /// the edges and then the vertex loops, with each face's pcurves; validated. With it, the /// canonical index of each array face and edge. fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res<(b::Brep, Order)> { - let Parts { faces, edges, shells, regions } = parts; + let Parts { mut faces, mut edges, shells, regions } = parts; + let n_edges = edges.len(); + let (edge_source, vertex_source) = split_shared(&mut faces, &mut edges, &shells, n_vertices)?; + let n_vertices = vertex_source.len(); let mut face_new: Vec> = vec![None; faces.len()]; let (mut face_order, mut direction) = (Vec::new(), Vec::new()); for shell in &shells { @@ -865,7 +862,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res return Err(unsupported("wire edges from SMLib")); } if edge_uses.iter().any(|u| u.len() > 2) { - return Err(unsupported("the radial order of an edge with more than two uses")); + return Err(unsupported("a solid touching itself along an edge")); } let mut vertex_new: Vec> = vec![None; n_vertices]; let mut vertex_order = Vec::new(); @@ -934,7 +931,13 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res face_records.push(b::Face { surface: face.surface.clone(), domain: face.domain, loops }); } let positions = flat.f(VERTEX_POSITION); - let vertices = vertex_order.iter().map(|&v| Ok(b::Vertex { point: v3(positions, v as usize, "vertex")?, tolerance })).collect::>>()?; + let vertices = vertex_order + .iter() + .map(|&v| { + let at = vertex_source.get(v as usize).copied().ok_or_else(|| invalid("vertex"))?; + Ok(b::Vertex { point: v3(positions, at, "vertex")?, tolerance }) + }) + .collect::>>()?; let shells = shells .into_iter() .map(|s| { @@ -952,10 +955,111 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res let placed = |new: &[Option]| { new.iter().map(|i| i.map(|i| i as usize).ok_or_else(|| invalid("an entity outside the canonical walk"))).collect::>>() }; - let order = Order { faces: placed(&face_new)?, edges: placed(&edge_new)? }; + let edge_new = placed(&edge_new)?; + // SMLib's edges split where solids touch: every piece, with the faces it bounds. + let mut pieces = Vec::new(); + for (e, &canonical) in edge_new.iter().enumerate() { + let edge = edge_source.get(e).copied().ok_or_else(|| invalid("edge"))?; + if edge_source.iter().filter(|s| **s == edge).count() > 1 { + let record = brep.edges.get(canonical).ok_or_else(|| invalid("edge"))?; + let faces: std::collections::BTreeSet = record.uses.iter().map(|u| u.face as usize).collect(); + pieces.push(Piece { edge, canonical, faces: faces.into_iter().collect() }); + } + } + pieces.sort_by_key(|p| (p.edge, p.canonical)); + let mut edges_out = Vec::with_capacity(n_edges); + for e in 0..n_edges { + edges_out + .push(pieces.iter().find(|p| p.edge == e).map_or_else(|| edge_new.get(e).copied().ok_or_else(|| invalid("edge")), |p| Ok(p.canonical))?); + } + let order = Order { faces: placed(&face_new)?, edges: edges_out, pieces }; Ok((brep, order)) } +/// Edges and vertices that solids touching each other share (faces of more than one shell use +/// them), split into one copy per shell, so each shell is a closed manifold boundary of its own. +/// The geometry is unchanged. Returns, for every edge and vertex after the split, the array edge +/// and vertex it is (a copy's original). +fn split_shared(faces: &mut [Face], edges: &mut Vec, shells: &[Vec<(u32, bool)>], n_vertices: usize) -> Res<(Vec, Vec)> { + let mut shell_of = vec![None; faces.len()]; + for (k, shell) in shells.iter().enumerate() { + for &(f, _) in shell { + *shell_of.get_mut(f as usize).ok_or_else(|| invalid("face"))? = Some(k); + } + } + let mut edge_source: Vec = (0..edges.len()).collect(); + // Edges: the first shell to use one keeps it, every other gets a copy. + let mut edge_shell: Vec> = vec![None; edges.len()]; + let mut copies: HashMap<(u32, usize), u32> = HashMap::new(); + for (f, face) in faces.iter_mut().enumerate() { + let Some(k) = shell_of.get(f).copied().flatten() else { continue }; + for l in &mut face.loops { + let Loop::Edges(uses) = l else { continue }; + for (e, _) in uses.iter_mut() { + let slot = edge_shell.get_mut(*e as usize).ok_or_else(|| invalid("edge"))?; + match slot { + None => *slot = Some(k), + Some(owner) if *owner == k => {} + Some(_) => { + let copy = match copies.get(&(*e, k)) { + Some(c) => *c, + None => { + let c = index(edges.len())?; + edges.push(edges.get(*e as usize).ok_or_else(|| invalid("edge"))?.clone()); + edge_source.push(*e as usize); + edge_shell.push(Some(k)); + copies.insert((*e, k), c); + c + } + }; + *e = copy; + } + } + } + } + } + // Vertices: likewise, through the edges (each now in one shell) and the vertex loops. + let mut vertex_source: Vec = (0..n_vertices).collect(); + let mut vertex_shell: Vec> = vec![None; n_vertices]; + let mut vertex_copies: HashMap<(u32, usize), u32> = HashMap::new(); + let mut place = |v: &mut u32, k: usize| -> Res<()> { + let slot = vertex_shell.get_mut(*v as usize).ok_or_else(|| invalid("vertex"))?; + match slot { + None => *slot = Some(k), + Some(owner) if *owner == k => {} + Some(_) => { + let copy = match vertex_copies.get(&(*v, k)) { + Some(c) => *c, + None => { + let c = index(vertex_source.len())?; + vertex_source.push(*v as usize); + vertex_shell.push(Some(k)); + vertex_copies.insert((*v, k), c); + c + } + }; + *v = copy; + } + } + Ok(()) + }; + for (e, edge) in edges.iter_mut().enumerate() { + let Some(k) = edge_shell.get(e).copied().flatten() else { continue }; + for v in &mut edge.vertices { + place(v, k)?; + } + } + for (f, face) in faces.iter_mut().enumerate() { + let Some(k) = shell_of.get(f).copied().flatten() else { continue }; + for l in &mut face.loops { + if let Loop::Vertex(v) = l { + place(v, k)?; + } + } + } + Ok((edge_source, vertex_source)) +} + /// A face's pcurves, edge loop by edge loop, from the plain-record routine on the face's /// records, each loop moved by whole periods into the face's domain. fn pcurves(surface: &b::Surface, domain: [[f64; 2]; 2], loops: &[Loop], edges: &[b::Edge], tolerance: f64) -> Res>> { diff --git a/crates/smlib/src/ffi.rs b/crates/smlib/src/ffi.rs index 368d86ca1b..18d2e92b90 100644 --- a/crates/smlib/src/ffi.rs +++ b/crates/smlib/src/ffi.rs @@ -7,7 +7,7 @@ use std::ffi::{c_char, c_void}; /// `SMLIB_ABI_VERSION` this crate was written against. -pub const ABI_VERSION: i32 = 2; +pub const ABI_VERSION: i32 = 3; pub const OK: i32 = 0; pub const ERR_EXCEPTION: i32 = -1000; diff --git a/crates/smlib/src/history.rs b/crates/smlib/src/history.rs index a81690ed03..5ad04c3a9b 100644 --- a/crates/smlib/src/history.rs +++ b/crates/smlib/src/history.rs @@ -41,10 +41,26 @@ pub struct Raw { /// Where one body's faces and edges go in canonical order: `faces[i]` is the canonical index of /// SMLib's face i, likewise for edges. Empty means SMLib's order is the canonical one. +/// +/// An SMLib edge solids touch along (non-manifold) becomes one record edge per solid: +/// `edges[i]` is the first of them, and `pieces` lists every piece of such an edge, the first +/// too, with the canonical faces it bounds. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Order { pub faces: Vec, pub edges: Vec, + pub pieces: Vec, +} + +/// One record edge of an SMLib edge split where solids touch along it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Piece { + /// The SMLib edge. + pub edge: usize, + /// The record edge. + pub canonical: usize, + /// The canonical faces it bounds, ascending. + pub faces: Vec, } impl Brep { @@ -122,7 +138,23 @@ impl Raw { } Ok(at) }; - let (fat, eat) = (place(&result.faces, self.faces.len())?, place(&result.edges, self.edges.len())?); + let fat = place(&result.faces, self.faces.len())?; + // Each raw edge's record edges with the faces each bounds where it is split (`None`: as + // SMLib has them); together a permutation of the result's edges. + let n_edges = self.edges.len() + result.pieces.len() - result.pieces.iter().map(|p| p.edge).collect::>().len(); + let mut targets: Vec<(usize, usize, Option<&[usize]>)> = Vec::with_capacity(n_edges); + for i in 0..self.edges.len() { + let mut split = result.pieces.iter().filter(|p| p.edge == i).peekable(); + if split.peek().is_none() { + targets.push((i, map(&result.edges, i)?, None)); + } else { + targets.extend(split.map(|p| (i, p.canonical, Some(p.faces.as_slice())))); + } + } + let distinct: BTreeSet = targets.iter().map(|t| t.1).collect(); + if targets.len() != n_edges || distinct.len() != n_edges || targets.iter().any(|t| t.1 >= n_edges) { + return Err(bad("the result's order is no permutation")); + } let mut faces: Vec> = vec![None; self.faces.len()]; for (f, at) in self.faces.iter().zip(&fat) { @@ -131,14 +163,20 @@ impl Raw { } let faces: Vec = faces.into_iter().collect::>().ok_or(bad("the result's order is no permutation"))?; - let mut edges: Vec> = vec![None; self.edges.len()]; - for (e, at) in self.edges.iter().zip(&eat) { - let bounds: BTreeSet = e - .faces - .iter() - .map(|f| fat.get(*f).copied().ok_or(bad("an edge lies on a face outside the result"))) - .collect::>()?; - let bounds: Vec = bounds.into_iter().collect(); + let mut edges: Vec> = vec![None; n_edges]; + for &(i, at, split) in &targets { + let e = self.edges.get(i).ok_or(bad("an edge lies outside the result"))?; + let bounds: Vec = match split { + Some(faces) => faces.to_vec(), + None => { + let bounds: BTreeSet = e + .faces + .iter() + .map(|f| fat.get(*f).copied().ok_or(bad("an edge lies on a face outside the result"))) + .collect::>()?; + bounds.into_iter().collect() + } + }; let origins = if e.origins.is_empty() { // New: where its two faces meet, each side what that face came from. let side = |f: usize| faces.get(f).and_then(|p| Side::new(p.origins.iter().copied())); @@ -157,7 +195,7 @@ impl Raw { .collect::>()?; set.into_iter().collect() }; - edges[*at] = Some(EdgeProv { faces: bounds, origins }); + *edges.get_mut(at).ok_or(bad("the result's order is no permutation"))? = Some(EdgeProv { faces: bounds, origins }); } let edges: Vec = edges.into_iter().collect::>().ok_or(bad("the result's order is no permutation"))?; Ok(History { operands: operands.len(), faces, edges }) diff --git a/crates/smlib/src/history/tests.rs b/crates/smlib/src/history/tests.rs index bf457034d2..06ecf734bf 100644 --- a/crates/smlib/src/history/tests.rs +++ b/crates/smlib/src/history/tests.rs @@ -60,9 +60,9 @@ fn renumbers_into_canonical_order() { let raw = Raw::decode(RECORDS).unwrap(); // Operand 0's SMLib face 2 is its canonical face 5, edge 7 its edge 0; the result's faces // and edges are listed backwards. - let a = Order { faces: vec![1, 2, 5, 0, 3, 4], edges: vec![1, 2, 3, 4, 5, 6, 7, 0] }; - let b = Order { faces: vec![0, 1, 2, 3, 4], edges: vec![0, 1, 2, 3] }; - let result = Order { faces: vec![2, 1, 0], edges: vec![2, 1, 0] }; + let a = Order { faces: vec![1, 2, 5, 0, 3, 4], edges: vec![1, 2, 3, 4, 5, 6, 7, 0], pieces: vec![] }; + let b = Order { faces: vec![0, 1, 2, 3, 4], edges: vec![0, 1, 2, 3], pieces: vec![] }; + let result = Order { faces: vec![2, 1, 0], edges: vec![2, 1, 0], pieces: vec![] }; let h = raw.history(&[a, b], &result).unwrap(); assert_eq!(h.faces[2].origins, vec![face(0, 5)]); assert_eq!(h.faces[0].origins, vec![face(0, 1), face(1, 0)]); @@ -79,10 +79,10 @@ fn bad_orders_are_errors() { // An origin on an operand that isn't there. assert!(raw.history(std::slice::from_ref(&ok), &ok).is_err()); // An operand order too short for an origin. - assert!(raw.history(&[Order { faces: vec![0], edges: vec![] }, ok.clone()], &ok).is_err()); + assert!(raw.history(&[Order { faces: vec![0], edges: vec![], pieces: vec![] }, ok.clone()], &ok).is_err()); // A result order that isn't a permutation. - assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 0, 1], edges: vec![] }).is_err()); - assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 1, 3], edges: vec![] }).is_err()); + assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 0, 1], edges: vec![], pieces: vec![] }).is_err()); + assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 1, 3], edges: vec![], pieces: vec![] }).is_err()); } /// A box with its edge 0 (between faces 4 and 5) filleted, as SMLib reports it: the fillet face