-
Notifications
You must be signed in to change notification settings - Fork 1
fix(algo): correct sequential multi-tool cuts on thin-walled solids #779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| //! Regression: sequential disjoint hex-prism cuts through one wall of a | ||
| //! hollow thin-walled box must each remove exactly the analytic prism | ||
| //! volume. Historically cut 2 onward was erratic (wrong deltas, gouges) | ||
| //! and from cut ~13 every cut silently no-opped while returning Ok. | ||
|
|
||
| #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] | ||
|
|
||
| use brepkit_math::mat::Mat4; | ||
| use brepkit_math::vec::Vec3; | ||
| use brepkit_operations::boolean::{BooleanOp, boolean}; | ||
| use brepkit_operations::extrude::extrude; | ||
| use brepkit_operations::primitives; | ||
| use brepkit_operations::transform::transform_solid; | ||
| use brepkit_topology::Topology; | ||
| use brepkit_topology::builder::{make_planar_face_from_wire, make_regular_polygon_wire}; | ||
|
|
||
| const HEX_R: f64 = 1.8; | ||
| const WEB: f64 = 0.8; | ||
| const WALL_T: f64 = 1.2; | ||
|
|
||
| fn make_hex_prism(topo: &mut Topology, r: f64, depth: f64) -> brepkit_topology::solid::SolidId { | ||
| let wire = make_regular_polygon_wire(topo, r, 6, 1e-7).expect("hex wire"); | ||
| let face = make_planar_face_from_wire(topo, wire).expect("hex face"); | ||
| extrude(topo, face, Vec3::new(0.0, 0.0, 1.0), depth).expect("hex prism") | ||
| } | ||
|
|
||
| fn make_hollow_bin(topo: &mut Topology) -> brepkit_topology::solid::SolidId { | ||
| let outer = primitives::make_box(topo, 84.0, 84.0, 30.0).expect("outer"); | ||
| let cavity = | ||
| primitives::make_box(topo, 84.0 - 2.0 * WALL_T, 84.0 - 2.0 * WALL_T, 30.0).expect("cavity"); | ||
| transform_solid(topo, cavity, &Mat4::translation(WALL_T, WALL_T, WALL_T)).expect("mv cavity"); | ||
| boolean(topo, BooleanOp::Cut, outer, cavity).expect("hollow shell") | ||
| } | ||
|
Comment on lines
+27
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/operations/tests/regress_hexwall_cuts.rs
Line: 27-33
Comment:
**`make_hollow_bin` produces an open-top bin, not a six-sided hollow box.** The cavity is `30.0` mm tall (matching the outer box), translated by `WALL_T = 1.2` mm in z, so it runs from `z = 1.2` to `z = 31.2`. Since the outer box ceiling is at `z = 30.0`, the top face is fully removed by the boolean cut — the bin has no lid. The PR description says "hollow 84×84×30 box (1.2mm walls)", which implies closed walls on all sides. For the specific regression being tested (front-wall hex cuts) this doesn't affect correctness, but a comment noting the intentionally open top would prevent confusion, and a future test that relies on top-wall geometry would silently use the wrong shape.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| /// Honeycomb centers on the front wall (y=0), prism axis along +y, | ||
| /// piercing the full 1.2mm wall thickness. | ||
| fn hex_centers(n: usize) -> Vec<(f64, f64)> { | ||
| let col_sp = 3.0_f64.sqrt() * HEX_R + WEB; | ||
| let row_sp = 1.5 * HEX_R + WEB; | ||
| let mut centers = Vec::with_capacity(n.min(256)); | ||
| let mut row = 0usize; | ||
| 'outer: loop { | ||
| let z = 5.0 + row as f64 * row_sp; | ||
| if z > 27.0 { | ||
| break; | ||
| } | ||
| let x_off = if row % 2 == 1 { col_sp / 2.0 } else { 0.0 }; | ||
| let mut col = 0usize; | ||
| loop { | ||
| let x = 5.0 + x_off + col as f64 * col_sp; | ||
| if x > 79.0 { | ||
| break; | ||
| } | ||
| if centers.len() >= n { | ||
| break 'outer; | ||
| } | ||
| centers.push((x, z)); | ||
| col += 1; | ||
| } | ||
| row += 1; | ||
| } | ||
| centers | ||
| } | ||
|
|
||
| fn run_sequential_hex_cuts(n: usize) { | ||
| let depth = WALL_T * 4.0; | ||
| let hex_area = 1.5 * 3.0_f64.sqrt() * HEX_R * HEX_R; | ||
| let expected_delta = hex_area * WALL_T; | ||
|
|
||
| let mut topo = Topology::new(); | ||
| let mut result = make_hollow_bin(&mut topo); | ||
| let mut prev_vol = | ||
| brepkit_operations::measure::solid_volume(&topo, result, 0.1).expect("shell volume"); | ||
|
|
||
| let mut failures = Vec::new(); | ||
| for (i, (x, z)) in hex_centers(n).iter().enumerate() { | ||
| let prism = make_hex_prism(&mut topo, HEX_R, depth); | ||
| let rot = Mat4::rotation_x(-std::f64::consts::FRAC_PI_2); | ||
| let mat = Mat4::translation(*x, WALL_T / 2.0 - depth / 2.0, *z) * rot; | ||
| transform_solid(&mut topo, prism, &mat).expect("mv prism"); | ||
| result = boolean(&mut topo, BooleanOp::Cut, result, prism) | ||
| .unwrap_or_else(|e| panic!("cut {} failed: {e}", i + 1)); | ||
| let vol = | ||
| brepkit_operations::measure::solid_volume(&topo, result, 0.1).expect("cut volume"); | ||
| let delta = prev_vol - vol; | ||
| if (delta - expected_delta).abs() > 0.05 { | ||
| failures.push(format!( | ||
| "cut {}: delta {delta:.3} expected {expected_delta:.3}", | ||
| i + 1 | ||
| )); | ||
| } | ||
| prev_vol = vol; | ||
| } | ||
| assert!( | ||
| failures.is_empty(), | ||
| "{} of {n} cuts removed the wrong volume:\n{}", | ||
| failures.len(), | ||
| failures.join("\n") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn hexwall_sequential_cuts_20() { | ||
| run_sequential_hex_cuts(20); | ||
| } | ||
|
|
||
| /// Full honeycomb wall: every center the spacing pattern yields (133 with | ||
| /// these parameters). Also guards against the historical multi-minute | ||
| /// mesh-fallback churn — this must complete in seconds. | ||
| #[test] | ||
| fn hexwall_sequential_cuts_full() { | ||
| run_sequential_hex_cuts(usize::MAX); | ||
| } | ||
|
Comment on lines
+107
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/operations/tests/regress_hexwall_cuts.rs
Line: 107-113
Comment:
**No timeout guard for the "must complete in seconds" invariant.** The comment explicitly calls out the historical multi-minute mesh-fallback churn and says the test must complete in seconds, but there is no enforcement. If a future change silently re-introduces the fallback, the test would still pass — it would just be very slow. Consider wrapping the call with a deadline: e.g. spawn the heavy work on a thread and `join` with a `Duration`, or use a test harness timeout attribute. Without this, the performance invariant is documentation-only.
How can I resolve this? If you propose a fix, please make it concise. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
axis_tolvalue of1e-9is a magic constant sitting betweenf64::EPSILON(~2.2e-16) andtol.linear(1e-7). The intent — reject any normal that isn't essentially exactly axis-aligned — is clear from the comment, but the choice of1e-9vs. something likef64::EPSILON * 4096.0(a few thousand ULPs) is unexplained and easy to accidentally bump. A named constant with a short rationale comment would make future adjustments deliberate rather than arbitrary.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!