diff --git a/src/boolean45/mod.rs b/src/boolean45/mod.rs index 77c28a96..df46b7a6 100644 --- a/src/boolean45/mod.rs +++ b/src/boolean45/mod.rs @@ -2,7 +2,7 @@ //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. use std::mem; -use std::collections::HashMap; +use std::collections::BTreeMap; use crate::{Manifold, Real, Vec3, Half, Tref, face_of}; use crate::boolean03::Boolean03; use crate::bounds::BBox; @@ -115,8 +115,8 @@ fn add_new_edge_verts( i12: &[i32], v12_r: &[i32], hs_p: &[Half], - pt_old: &mut HashMap>, - pt_new: &mut HashMap<(usize, usize), Vec>, + pt_old: &mut BTreeMap>, + pt_new: &mut BTreeMap<(usize, usize), Vec>, fwd: bool, oft: usize, ) { @@ -184,7 +184,7 @@ fn append_partial_edges( fwd: bool, // hs_r: &mut [Half], // halfedge data of mfd_r, empty yet rs_r: &mut [Tref], // map from halfedge in mfd_r to triangle info - pt_p: &mut HashMap>, // + pt_p: &mut BTreeMap>, // face_ptr_r: &mut [i32], // whole_flag: &mut [bool], // a flag to find out a halfedge from mfd_p is entirely usable in mfd_r ) { @@ -238,7 +238,7 @@ fn append_new_edges( fid_pq2r: &[i32], // nf_p: usize, // num of faces in mfd_p face_ptr_r: &mut[i32], // - pt_new: &mut HashMap<(usize, usize), Vec>, // + pt_new: &mut BTreeMap<(usize, usize), Vec>, // hs_r: &mut [Half], // the halfedge data of mfd_r, empty yet rs_r: &mut [Tref], // ) { @@ -362,9 +362,9 @@ pub fn boolean45( for i in 0..b03.v12.len() { duplicate_verts(&i12, &vid_12r, &b03.v12, &mut ps_r, i); } for i in 0..b03.v21.len() { duplicate_verts(&i21, &vid_21r, &b03.v21, &mut ps_r, i); } - let mut pt_p = HashMap::new(); - let mut pt_q = HashMap::new(); - let mut pt_new = HashMap::new(); + let mut pt_p = BTreeMap::new(); + let mut pt_q = BTreeMap::new(); + let mut pt_new = BTreeMap::new(); add_new_edge_verts(&b03.p1q2, &i12, &vid_12r, &mp.hs, &mut pt_p, &mut pt_new, true, 0); add_new_edge_verts(&b03.p2q1, &i21, &vid_21r, &mq.hs, &mut pt_q, &mut pt_new, false, b03.p1q2.len()); diff --git a/src/tests.rs b/src/tests.rs index 7ec812a1..2c580da2 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -763,3 +763,42 @@ mod test_values { } } + +#[cfg(test)] +mod test_determinism { + use crate::{Manifold, OpType, compute_boolean}; + + fn cube(lo: [f64; 3], hi: [f64; 3]) -> Manifold { + let mut ps = vec![]; + for i in 0..8 { + ps.push(if i & 1 == 0 { lo[0] } else { hi[0] }); + ps.push(if i & 2 == 0 { lo[1] } else { hi[1] }); + ps.push(if i & 4 == 0 { lo[2] } else { hi[2] }); + } + let fs = [0, 2, 1, 1, 2, 3, 4, 5, 6, 5, 7, 6, 0, 1, 4, 1, 5, 4, 2, 6, 3, 3, 6, 7, 0, 4, 2, 2, 4, 6, 1, 3, 5, 3, 7, 5]; + Manifold::new(&ps, &fs).unwrap() + } + + fn fingerprint(m: &Manifold) -> (Vec<[u64; 3]>, Vec<[usize; 3]>) { + let ps = m.ps.iter().map(|p| [p.x as f64, p.y as f64, p.z as f64].map(f64::to_bits)).collect(); + let hs = m.hs.iter().map(|h| [h.tail, h.head, h.pair]).collect(); + (ps, hs) + } + + // Ties in the ear queue and the order of the edge maps once followed addresses and hash seeds. + #[test] + fn test_same_input_same_output() { + let block = cube([0.; 3], [10.; 3]); + let cases: [(Manifold, fn() -> OpType); 3] = [ + (cube([2., 2., 5.], [8., 8., 10.]), || OpType::Subtract), + (cube([10., 2., 2.], [15., 8., 8.]), || OpType::Add), + (cube([4., 4., -1.], [6., 6., 11.]), || OpType::Subtract), + ]; + for (other, op) in cases { + let first = fingerprint(&compute_boolean(&block, &other, op()).unwrap()); + for _ in 0..30 { + assert_eq!(fingerprint(&compute_boolean(&block, &other, op()).unwrap()), first); + } + } + } +} diff --git a/src/triangulation/ear_clip.rs b/src/triangulation/ear_clip.rs index d643cd1c..86b5ac8b 100644 --- a/src/triangulation/ear_clip.rs +++ b/src/triangulation/ear_clip.rs @@ -18,6 +18,7 @@ pub struct Ecvt { pub vl: Option>>, pub vr: Option>>, pub cost: Real, + pub seq: usize, // creation order, breaks ties without depending on addresses } impl Ecvt { @@ -33,7 +34,7 @@ impl Ecvt { pub fn ptr_r_of_l(&self) -> EvPtr { self.ptr_l().borrow().ptr_r() } pub fn new(idx: usize, pos: Vec2) -> Self { - Self { idx, pos, dir: Vec2::new(0., 0.), ear: None, vl: None, vr: None, cost: 0. } + Self { idx, pos, dir: Vec2::new(0., 0.), ear: None, vl: None, vr: None, cost: 0., seq: 0 } } // Shorter than half of epsilon, to be conservative so that it doesn't @@ -234,11 +235,7 @@ impl Ord for EvPtrMinCost { self.0.borrow().cost .partial_cmp(&other.0.borrow().cost) .unwrap_or(Ordering::Equal) - .then_with(|| { - let ptr1 = Rc::as_ptr(&self.0) as usize; - let ptr2 = Rc::as_ptr(&other.0) as usize; - ptr1.cmp(&ptr2) - }) + .then_with(|| self.0.borrow().seq.cmp(&other.0.borrow().seq)) } } @@ -256,11 +253,7 @@ impl Ord for EvPtrMaxPosX { other.0.borrow().pos.x .partial_cmp(&self.0.borrow().pos.x) .unwrap_or(Ordering::Equal) - .then_with(|| { - let ptr1 = Rc::as_ptr(&self.0) as usize; - let ptr2 = Rc::as_ptr(&other.0) as usize; - ptr1.cmp(&ptr2) - }) + .then_with(|| self.0.borrow().seq.cmp(&other.0.borrow().seq)) } } @@ -344,6 +337,13 @@ impl EarClip { std::mem::take(&mut self.tris) } + fn add(&mut self, mut v: Ecvt) -> EvPtr { + v.seq = self.polygon.len(); + let v = Rc::new(RefCell::new(v)); + self.polygon.push(Rc::clone(&v)); + v + } + // This function and JoinPolygons are the only functions that affect // the circular list data structure. This helps ensure it remains circular. fn link(vl: &EvPtr, vr: &EvPtr) { @@ -381,17 +381,14 @@ impl EarClip { let mut bgns = vec![]; for poly in polys.iter() { let v = poly.first().unwrap(); - self.polygon.push(Rc::new(RefCell::new(Ecvt::new(v.idx, v.pos)))); - - let first = Rc::clone(self.polygon.last().unwrap()); + let first = self.add(Ecvt::new(v.idx, v.pos)); let mut last = Rc::clone(&first); self.bbox.union(first.borrow().pos); bgns.push(Rc::clone(&first)); for v in poly.iter().skip(1) { self.bbox.union(v.pos); - self.polygon.push(Rc::new(RefCell::new(Ecvt::new(v.idx, v.pos)))); - let next = Rc::clone(self.polygon.last().unwrap()); + let next = self.add(Ecvt::new(v.idx, v.pos)); Self::link(&last, &next); last = Rc::clone(&next); } @@ -533,10 +530,8 @@ impl EarClip { // To do this, both verts are duplicated and reattached. This process may create degenerate ears, // so these are clipped if necessary to keep from confusing sub_sequent key-holing operations. fn join_polygons(&mut self, sta: &EvPtr, con: &EvPtr) { - let sta1 = Rc::new(RefCell::new(sta.borrow().clone())); - let con1 = Rc::new(RefCell::new(con.borrow().clone())); - self.polygon.push(Rc::clone(&sta1)); - self.polygon.push(Rc::clone(&con1)); + let sta1 = self.add(sta.borrow().clone()); + let con1 = self.add(con.borrow().clone()); sta.borrow().ptr_r().borrow_mut().vl = Some(Rc::downgrade(&sta1)); con.borrow().ptr_l().borrow_mut().vr = Some(Rc::downgrade(&con1)); Self::link(sta, con);