diff --git a/crates/engine/src/tests/robustness.rs b/crates/engine/src/tests/robustness.rs index 9f90fd03..1faf6af8 100644 --- a/crates/engine/src/tests/robustness.rs +++ b/crates/engine/src/tests/robustness.rs @@ -549,3 +549,13 @@ fn blend_corners_where_edges_meet_on_a_curved_wall() { let removed = base - planned_volume(&s); assert!((removed - 67.05).abs() < 0.15, "three at the corner: removed {removed}, expected 67.05"); } + +/// Seed 20 cuts a patterned cylinder's holes through a filleted plate. The lenient retry of a +/// face's parameter boundary (#970) searched the whole cylinder for a point and found it across +/// the seam, half a turn away; the face was divided wrongly and the cut failed. Such a boundary +/// is refused now, and the cut builds as before #970. +#[test] +fn a_boundary_found_across_a_seam_is_not_used() { + let r = fuzz::run(20, 8); + assert_eq!(r.outcome.kind, "ok", "seed 20: {:?}\n{}", r.outcome, r.script()); +} diff --git a/tests/robustness/goldens.json b/tests/robustness/goldens.json index 9777f92e..5c963874 100644 --- a/tests/robustness/goldens.json +++ b/tests/robustness/goldens.json @@ -82,7 +82,7 @@ "78": {"area":2510.733756,"bodies":1,"edges":36,"error":"offset: a corner whose faces don't meet the same way after t","faces":14,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","cut box","shell"],"unsupported":0,"volume":3707.416641}, "79": {"area":13585.252616,"bodies":1,"edges":27,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","join cylinder"],"unsupported":0,"volume":104810.763072}, "80": {"area":8652.533342,"bodies":1,"edges":20,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","fillet","fillet"],"unsupported":0,"volume":37387.449469}, - "81": {"area":4001.811796,"bodies":2,"edges":75,"error":null,"faces":29,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","cut sphere","circular pattern","rectangular pattern","hole","join cylinder","join box","cut sphere"],"unsupported":2,"volume":13139.562065}, + "81": {"area":4001.847983,"bodies":2,"edges":75,"error":null,"faces":29,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","cut sphere","circular pattern","rectangular pattern","hole","join cylinder","join box","cut sphere"],"unsupported":2,"volume":13139.658749}, "82": {"area":3195.487113,"bodies":1,"edges":36,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole","circular pattern","cut box"],"unsupported":0,"volume":5902.162954}, "83": {"area":5125.0,"bodies":1,"edges":28,"error":"offset: a corner whose faces don't meet the same way after t","faces":12,"outcome":"failed","rejected":0,"shells":1,"steps":["box","join box","rectangular pattern","shell"],"unsupported":0,"volume":19250.0}, "84": {"area":4906.192237,"bodies":1,"edges":67,"error":null,"faces":25,"outcome":"ok","rejected":2,"shells":1,"steps":["extruded profile","fillet","hole","hole","cut cylinder","cut box","fillet"],"unsupported":0,"volume":14219.865875}, @@ -111,6 +111,7 @@ {"reason":"a hole placed on a face without a direction drills into that face (it took the direction from any face whose plane passed through the point, the last one found, such as a side of a patterned box standing on the point's plane): the holes on these seeds now go straight in (#950)","seeds":[7,21,85]}, {"reason":"the last-resort tool nudge is tried the other way when it fails or leaves sliver faces the operands didn't have: seed 43's join builds and the design runs to the end, 31 and 78 come out with fewer faces (#827, #897)","seeds":[31,43,78]}, {"reason":"main's fillet changes since the last record (#926, #949) move seeds 33 and 91: a fillet step builds differently or is picked differently","seeds":[33,91]}, - {"reason":"hole tangent inside a stepped boss: touching sews split zero-thickness curved walls, so a pattern of holes spaced by their diameter (touching) in a stepped body cuts exactly instead of by a tool nudge: 42 builds more faces (the walls split where the holes touch), 51 picks other edges afterwards","seeds":[42,51]} + {"reason":"hole tangent inside a stepped boss: touching sews split zero-thickness curved walls, so a pattern of holes spaced by their diameter (touching) in a stepped body cuts exactly instead of by a tool nudge: 42 builds more faces (the walls split where the holes touch), 51 picks other edges afterwards","seeds":[42,51]}, + {"reason":"#952: a hole placed on a face drills into the material; seed 81's hole moves its volume by 0.1 mm\u00b3 (area by 0.04 mm\u00b2)","seeds":[81]} ] } diff --git a/vendor/truck-shapeops/src/transversal/divide_face/mod.rs b/vendor/truck-shapeops/src/transversal/divide_face/mod.rs index 2d8062a7..c2292b44 100644 --- a/vendor/truck-shapeops/src/transversal/divide_face/mod.rs +++ b/vendor/truck-shapeops/src/transversal/divide_face/mod.rs @@ -71,6 +71,18 @@ where vec.append(&mut add?); Some(vec) })?; + // SolidCraft: a lenient boundary whose points jump half the parameter range or more has been + // found across a periodic surface's seam by the whole-surface search (on a cylinder the + // points of one edge landed at both ends of its angle): it would divide the face wrongly, so + // the face is left undivided as before. + if lenient { + let (u, v) = surface.try_range_tuple(); + let half = |r: Option<(f64, f64)>| r.map_or(f64::INFINITY, |(a, b)| (b - a).abs() / 2.0); + let (hu, hv) = (half(u), half(v)); + if vec.windows(2).any(|w| (w[1].x - w[0].x).abs() >= hu || (w[1].y - w[0].y).abs() >= hv) { + return None; + } + } Some(PolylineCurve(vec)) }