Booleans are deterministic: result faces keep their shell's order
truck's connected components came out in hash order of face addresses, so a
boolean's faces, and everything built on them (fillet bands, STEP entity
order), changed from run to run. That made the oracle 35 export differ run to
run. Its old step_export flakes came from the orientation check's drifting
search, removed in 2db9e66. The result's shells and faces now keep the
order of the shell they come from (vendored truck-shapeops). Oracle results
are unchanged (73 PASS before and after). Test: the tee of oracle 35 with its
junction fillet exports byte-identical three times in one process (it failed
before).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
a3d9eec238
commit
81c85b553c
3 files changed
+41
-2
No files matched your search
@@ -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;
|
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);
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+3
@@ -11,6 +11,9 @@ Changes:
|
|||||||
earlier booleans have boundaries that lie on their surface only within tolerance).
|
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
|
- `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).
|
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
|
- `lib.rs`: `tr!` reports where an operation gives up when `SHAPEOPS_TRACE` is set; compiler
|
||||||
warnings no longer fail the build.
|
warnings no longer fail the build.
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -120,7 +120,7 @@ pub fn and<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
|
|||||||
let [res, _] = process_one_pair_of_shells(&and_shell, shell, tol)?;
|
let [res, _] = process_one_pair_of_shells(&and_shell, shell, tol)?;
|
||||||
and_shell = res;
|
and_shell = res;
|
||||||
}
|
}
|
||||||
let boundaries = and_shell.connected_components();
|
let boundaries = components_in_order(&and_shell);
|
||||||
Some(Solid::new(boundaries))
|
Some(Solid::new(boundaries))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +143,27 @@ pub fn or<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
|
|||||||
let [_, res] = process_one_pair_of_shells(&or_shell, shell, tol)?;
|
let [_, res] = process_one_pair_of_shells(&or_shell, shell, tol)?;
|
||||||
or_shell = res;
|
or_shell = res;
|
||||||
}
|
}
|
||||||
let boundaries = or_shell.connected_components();
|
let boundaries = components_in_order(&or_shell);
|
||||||
Some(Solid::new(boundaries))
|
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<C, S>(shell: &Shell<Point3, C, S>) -> Vec<Shell<Point3, C, S>> {
|
||||||
|
let pos: rustc_hash::FxHashMap<_, usize> = shell.face_iter().enumerate().map(|(i, f)| (f.id(), i)).collect();
|
||||||
|
let key = |f: &Face<Point3, C, S>| pos.get(&f.id()).copied().unwrap_or(usize::MAX);
|
||||||
|
let mut comps: Vec<Vec<Face<Point3, C, S>>> = 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)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
Reference in new issue
Block a user