diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index ef5d6bfd..4bc7e83b 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -484,3 +484,21 @@ fn horn_torus_written_as_one_spline_face() { let v = std::f64::consts::PI * 25.0 * std::f64::consts::TAU * 5.0 / 4.0; assert!(rel(measure(&imp.bodies[0].body).unwrap().volume, v) < 1e-3); } + +/// Booleans and blends build the same body every time, so an export is byte-identical from +/// run to run (truck used to emit a boolean's faces in hash order of their addresses). The +/// tee of oracle 35 with its rolling-ball fillet round the junction. +#[test] +fn tee_fillet_exports_byte_identical() { + let main = cylinder(Vec3::new(-40.0, 0.0, 0.0), Vec3::X, 15.0, 80.0).unwrap(); + let branch = cylinder(Vec3::ZERO, Vec3::Z, 10.0, 35.0).unwrap(); + let export = || { + let tee = boolean(&main, &branch, BoolOp::Union).unwrap().unwrap(); + let f = fillet(&tee, &[Vec3::new(0.0, 10.0, 125f64.sqrt())], 3.0).unwrap(); + step_export(&[&f], "tee").unwrap() + }; + let first = export(); + for _ in 0..2 { + assert!(export() == first, "export differs between runs"); + } +} diff --git a/vendor/README.md b/vendor/README.md index 4196563d..7e8cf09c 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -11,6 +11,9 @@ Changes: earlier booleans have boundaries that lie on their surface only within tolerance). - `transversal/divide_face/mod.rs`: when projecting a loop onto a face's surface, fall back to the nearest parameter when the exact search fails (intersection curves are approximations). +- `transversal/integrate/mod.rs`: the result's shells and faces keep the order of the shell + they come from (truck's connected components followed hash order of face addresses, so a + boolean's faces came out in a different order on every run). - `lib.rs`: `tr!` reports where an operation gives up when `SHAPEOPS_TRACE` is set; compiler warnings no longer fail the build. diff --git a/vendor/truck-shapeops/src/transversal/integrate/mod.rs b/vendor/truck-shapeops/src/transversal/integrate/mod.rs index 8e5d1248..405738af 100644 --- a/vendor/truck-shapeops/src/transversal/integrate/mod.rs +++ b/vendor/truck-shapeops/src/transversal/integrate/mod.rs @@ -120,7 +120,7 @@ pub fn and, S: ShapeOpsSurface>( let [res, _] = process_one_pair_of_shells(&and_shell, shell, tol)?; and_shell = res; } - let boundaries = and_shell.connected_components(); + let boundaries = components_in_order(&and_shell); Some(Solid::new(boundaries)) } @@ -143,9 +143,27 @@ pub fn or, S: ShapeOpsSurface>( let [_, res] = process_one_pair_of_shells(&or_shell, shell, tol)?; or_shell = res; } - let boundaries = or_shell.connected_components(); + let boundaries = components_in_order(&or_shell); Some(Solid::new(boundaries)) } +// SolveCraft: truck's connected components come out in hash order of face addresses, so the +// result's face order changed from run to run. Keep the shell's own order instead. +fn components_in_order(shell: &Shell) -> Vec> { + let pos: rustc_hash::FxHashMap<_, usize> = shell.face_iter().enumerate().map(|(i, f)| (f.id(), i)).collect(); + let key = |f: &Face| pos.get(&f.id()).copied().unwrap_or(usize::MAX); + let mut comps: Vec>> = shell + .connected_components() + .into_iter() + .map(|c| { + let mut faces: Vec<_> = c.into_iter().collect(); + faces.sort_by_key(|f| key(f)); + faces + }) + .collect(); + comps.sort_by_key(|c| c.first().map(|f| key(f)).unwrap_or(usize::MAX)); + comps.into_iter().map(Shell::from).collect() +} + #[cfg(test)] mod tests;