diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index 50c62503..1724a0d5 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -962,6 +962,42 @@ fn tray_shrunk_by_a_parameter_unfolds_like_one_built_at_that_size() { } } +/// A 55 x 30 plate whose bottom edge bends up by 1 in 100 over its last 5 mm, with a flange on +/// the straight 50 mm, unfolds at any turn. The flange keeps its start rounded to the +/// nanometre, so its bend zone meets the plate a fraction of a nanometre off the corner, and +/// the bent piece of edge, a hair from the zone's edge, crosses it a few hundredths of a +/// micrometre before the corner. Split there, the piece left a sliver shorter than the +/// tolerance, which the flat pattern's union took for a second way out of the corner and +/// closed as a loop of its own: "planar region arrangement failed" at 46 of these 90 turns. +#[test] +fn a_flange_beside_a_slightly_bent_edge_unfolds_at_any_turn() { + // Base 55 x 30 less the 5 x 0.05 triangle under the bent piece; bend π and leg 17 along 50. + let area = 55.0 * 30.0 - 5.0 * 0.05 / 2.0 + 50.0 * (PI + 17.0); + let mut failed = Vec::new(); + for k in 0..90 { + let turn = (k as f64).to_radians(); + let at = |x: f64, y: f64| [x * turn.cos() - y * turn.sin(), x * turn.sin() + y * turn.cos()]; + let mut s = Session::default(); + run(&mut s, "sheet.manage_rules", json!({"name": "R", "thickness": "2 mm", "k_factor": "0.5", "bend_radius": "1 mm", "active": true})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Outline"})); + let outline = [(95.0, 50.05), (100.0, 50.0), (150.0, 50.0), (150.0, 80.0), (95.0, 80.0)].map(|(x, y)| at(x, y)); + run(&mut s, "sketch.line", json!({"points": outline, "closed": true})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "sheet.flange", json!({"sketch": "Outline"})); + let [x, y] = at(125.0, 50.0); + run(&mut s, "sheet.flange", json!({"edges": [[x, y, 2]], "height": 20, "angle": 90, "radius": 1, "position": "outside"})); + if let Err(e) = s.execute("sheet.unfold", &json!({})) { + failed.push(format!("{k}°: {e}")); + continue; + } + let f = flat(&mut s); + assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{k}°: {f}"); + assert!((flat_area(&f) - area).abs() < 1e-6, "{k}°: {} vs {area}", flat_area(&f)); + assert!(rel(volume(&mut s), area * 2.0) < 1e-4, "{k}°: {} vs {}", volume(&mut s), area * 2.0); + } + assert!(failed.is_empty(), "{} of 90 turns: {failed:?}", failed.len()); +} + #[test] fn the_bend_table_and_the_dxf_follow_what_flat_cuts_leave_of_a_bend() { // A 200 x 100 plate with a 40 mm flange along y = 0: the bend's centre line runs at y = -pi/2 diff --git a/crates/kernel/src/prism.rs b/crates/kernel/src/prism.rs index e76f3888..e1bc2dba 100644 --- a/crates/kernel/src/prism.rs +++ b/crates/kernel/src/prism.rs @@ -1127,6 +1127,13 @@ fn boolean2d_tagged(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option| -> Vec { cuts.sort_by(f64::total_cmp); let mut out = Vec::new(); @@ -1138,7 +1145,7 @@ fn boolean2d_tagged(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option tol { + if x.length() > near && y.length() > near { out.push(x); rest = y; done = c; @@ -1210,7 +1217,6 @@ fn boolean2d_tagged(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option)> = Vec::new(); let mut used = vec![false; kept.len()]; for start in 0..kept.len() { @@ -2746,6 +2752,56 @@ mod planar_tests { } } + /// A 55 x 30 plate whose bottom edge bends up by `slope` over its last 5 mm before the + /// corner at (100, 50), and a strip 3 wide below the straight 50 mm, as a sheet's bend zone + /// lies against its base: the strip's corners on that edge rounded to the nanometre, as a + /// flange keeps them, and moved by `off`. Both turned by `theta` about the origin. + fn plate_and_strip_by_a_bent_edge(theta: f64, slope: f64, off: Vec2) -> (Region2, Region2) { + let (sn, cs) = theta.sin_cos(); + let rot = |x: f64, y: f64| Vec2::new(x * cs - y * sn, x * sn + y * cs); + let nm = |p: Vec2| Vec2::new((p.x * 1e9).round() / 1e9, (p.y * 1e9).round() / 1e9) + off; + let plate = [rot(95.0, 50.0 + 5.0 * slope), rot(100.0, 50.0), rot(150.0, 50.0), rot(150.0, 80.0), rot(95.0, 80.0)]; + let strip = [nm(rot(100.0, 50.0)), rot(100.0, 47.0), rot(150.0, 47.0), nm(rot(150.0, 50.0))]; + (Region2 { outer: Loop2::polygon(&plate), holes: vec![] }, Region2 { outer: Loop2::polygon(&strip), holes: vec![] }) + } + + /// The bent piece of the plate's edge, a hair from the strip's edge, crosses it short of + /// the corner they share: the corners a nanometre apart at most, the crossing up to a + /// hundred times that before it at a slope of 1 in 100, a thousand at 1 in 1000. A split + /// there left a sliver of edge shorter than the distance within which the loops join their + /// pieces; the chaining took it for a second way out of the corner, and a loop of that one + /// sliver is open, its area not zero away from the origin: a hole no outline held (no + /// result at all) or a region of its own. The union is the plate and the strip, whole. + #[test] + fn a_bent_edge_crossing_its_neighbour_by_their_corner_leaves_no_sliver() { + let union_is_whole = |(plate, strip): (Region2, Region2), what: &str| { + let want = plate.area() + strip.area(); + for (a, b) in [(&plate, &strip), (&strip, &plate)] { + let out = planar_regions_boolean(std::slice::from_ref(a), std::slice::from_ref(b), BoolOp::Union, 1e-7) + .unwrap_or_else(|e| panic!("{what}: {e}")); + assert_eq!(out.len(), 1, "{what}: {out:?}"); + let lp = &out[0].outer; + let shortest = lp.segs.iter().map(Seg2::length).fold(f64::MAX, f64::min); + assert!(out[0].holes.is_empty() && lp.segs.len() == 7 && shortest > 2.9, "{what}: {} edges, shortest {shortest:e}", lp.segs.len()); + assert!((out[0].area() - want).abs() < 1e-6, "{what}: {} vs {want}", out[0].area()); + } + }; + for k in 0..360 { + for slope in [0.01, 0.001] { + union_is_whole(plate_and_strip_by_a_bent_edge((k as f64 * 0.25).to_radians(), slope, Vec2::ZERO), &format!("{k} slope {slope}")); + } + } + // Corners a few nanometres apart, any way round: the sliver is then longer than the + // tolerance (1e-7 with the strip 1e-9 into the plate) but shorter than the joining + // distance (1e-6). + for k in 0..16 { + for d in [1e-9, 4e-9] { + let off = Vec2::from_angle(k as f64 * std::f64::consts::TAU / 16.0) * d; + union_is_whole(plate_and_strip_by_a_bent_edge(0.0, 0.01, off), &format!("off {off:?}")); + } + } + } + /// Touching circles meet at one point on the line of their centres, inside and outside. #[test] fn touching_lines_and_arcs_meet_at_one_point() {