fix: unfold sheets shrunk by a parameter change (#349)
A sheet-metal tray built at 2000 x 2500 mm with an edge flange on all four edges (72° or 90°) and unfolded failed to rebuild when W and H went to 400 x 300, 600 x 450, or W alone to 400: "planar region arrangement failed or exceeded its bounded processing budget". The same tray drawn at 400 x 300 unfolded, and growing it worked. The sketch solved again after the change leaves its corners ~1e-13 mm off, so the flanges inherit a hair of tilt and their panels and bend zones meet the trimmed base within 1e-13 mm instead of exactly. The flat pattern is a balanced union of those pieces, and the arrangement chains its output loops within 10 tol. In a later round, ray parity classified a bend zone's edge midpoint that lay level with a corner whose two lines did not quite meet: the ray slipped through the gap without a crossing, the zone's edges counted as inside the other operand and were dropped, and the outline no longer closed. A rectangle turned by 1e-14 rad failed the same way. inside_loop now closes the gap where one line ends and the next begins with a straight connector while it counts crossings; exactly closed joints are unaffected. Joints with an arc keep the arc's own endpoint rule, which treats ends within its angular tolerance as on the ray (a connector there double counted at a circle split into two arcs). Co-authored-by: NOWN-DEV <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
3 files changed
+115
-1
No files matched your search
@@ -2250,4 +2250,37 @@ mod tests {
|
||||
s.pockets.push(pocket.clone());
|
||||
assert!(s.record_pocket(pocket.region, 0.0, 0.5, &mut work).is_err());
|
||||
}
|
||||
|
||||
/// A sketch solved again after a parameter change leaves its corners off by ~1e-13 mm, so
|
||||
/// its edges are not quite square to the axes. Such a tray (four 72° flanges, shrunk to
|
||||
/// 400 x 300 from a larger size) failed to unfold in the planar arrangement while the same
|
||||
/// tray drawn at that size unfolded.
|
||||
#[test]
|
||||
fn nearly_square_base_unfolds_like_the_exact_one() {
|
||||
let rv = RuleValues { thickness: 2.0, k_factor: 0.5, bend_radius: 1.0, ..steel() };
|
||||
let tray = |[a, b, c, d]: [Vec2; 4]| {
|
||||
let mut s = base_flange("Body1", "AL2", &rv, Plane::XY, rect(a, b, c, d));
|
||||
let picks = [Vec3::new(200.0, 0.0, 2.0), Vec3::new(400.0, 150.0, 2.0), Vec3::new(200.0, 300.0, 2.0), Vec3::new(0.0, 150.0, 2.0)];
|
||||
s.add_flanges(&picks, 38.6, 72f64.to_radians(), 1.0, "inside", false).unwrap();
|
||||
s
|
||||
};
|
||||
let flat_area = |s: &SheetBody| s.flat_regions().unwrap().iter().map(|r| r.area().abs()).sum::<f64>();
|
||||
let square = [Vec2::new(0.0, 0.0), Vec2::new(400.0, 0.0), Vec2::new(400.0, 300.0), Vec2::new(0.0, 300.0)];
|
||||
let exact = flat_area(&tray(square));
|
||||
// The corners the solver left after the parameter change, and the rectangle turned by a hair.
|
||||
let solved = [
|
||||
Vec2::new(0.0, 0.0),
|
||||
Vec2::new(400.0000000000001, 4.369543929613394e-13),
|
||||
Vec2::new(400.00000000000017, 300.0000000000008),
|
||||
Vec2::new(0.0, 300.0000000000009),
|
||||
];
|
||||
let (c, s) = (1e-14f64.cos(), 1e-14f64.sin());
|
||||
let turned = square.map(|p| Vec2::new(p.x * c - p.y * s, p.x * s + p.y * c));
|
||||
for corners in [solved, turned] {
|
||||
let mut sheet = tray(corners);
|
||||
assert!((flat_area(&sheet) - exact).abs() < 1e-6, "{} {exact}", flat_area(&sheet));
|
||||
sheet.flat = true;
|
||||
assert!((vol(&sheet.solid().unwrap()) - exact * 2.0).abs() < 1e-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -702,6 +702,45 @@ fn fold_keeps_the_larger_side_either_way() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tray shrunk by a parameter change unfolds like the same tray built at that size. The
|
||||
/// sketch solved again leaves its corners ~1e-13 mm off, and the flat pattern's planar
|
||||
/// arrangement used to drop a bend zone through a gap that small: "planar region arrangement
|
||||
/// failed", for W alone or both sizes, at 72° or 90°, while growing the tray worked.
|
||||
#[test]
|
||||
fn tray_shrunk_by_a_parameter_unfolds_like_one_built_at_that_size() {
|
||||
for angle in [72.0, 90.0] {
|
||||
let tray = |s: &mut Session, w: f64, h: f64| {
|
||||
run(s, "parameters.add", json!({"name": "W", "expression": format!("{w} mm")}));
|
||||
run(s, "parameters.add", json!({"name": "H", "expression": format!("{h} mm")}));
|
||||
run(s, "sheet.manage_rules", json!({"name": "AL2", "thickness": "2 mm", "k_factor": "0.5", "bend_radius": "1 mm", "active": true}));
|
||||
run(s, "sketch.create", json!({"plane": "XY", "name": "Tray"}));
|
||||
run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [w, h]}));
|
||||
run(s, "sketch.constraint.fix", json!({"entities": ["p1"]}));
|
||||
run(s, "sketch.dimension", json!({"entities": ["l1"], "value": "W"}));
|
||||
run(s, "sketch.dimension", json!({"entities": ["l2"], "value": "H"}));
|
||||
run(s, "sketch.finish", json!({}));
|
||||
run(s, "sheet.flange", json!({"type": "base", "sketch": "Tray"}));
|
||||
let edges = json!([[w / 2.0, 0, 2], [w, h / 2.0, 2], [w / 2.0, h, 2], [0, h / 2.0, 2]]);
|
||||
run(s, "sheet.flange", json!({"type": "edge", "edges": edges, "height": 38.6, "angle": angle}));
|
||||
run(s, "sheet.unfold", json!({}));
|
||||
};
|
||||
let mut direct = Session::default();
|
||||
tray(&mut direct, 400.0, 300.0);
|
||||
let want = flat(&mut direct)["flat_size_mm"].clone();
|
||||
let mut shrunk = Session::default();
|
||||
tray(&mut shrunk, 2000.0, 2500.0);
|
||||
for (name, expr) in [("W", "400 mm"), ("H", "300 mm")] {
|
||||
let r = run(&mut shrunk, "parameters.change", json!({"name": name, "expression": expr}));
|
||||
assert_eq!(r["errors"], json!([]), "{angle}° {name} = {expr}: {r}");
|
||||
}
|
||||
let got = flat(&mut shrunk)["flat_size_mm"].clone();
|
||||
for k in 0..3 {
|
||||
assert!((got[k].as_f64().unwrap() - want[k].as_f64().unwrap()).abs() < 1e-9, "{angle}°: {got} {want}");
|
||||
}
|
||||
assert!(rel(volume(&mut shrunk), volume(&mut direct)) < 1e-9, "{angle}°");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bend_table_exports_shop_dimensions_and_tracks_parameter_changes() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -737,9 +737,21 @@ fn crossings(s: &Seg2, t: &Seg2, tol: f64) -> Vec<(f64, f64)> {
|
||||
|
||||
/// Ray parity against analytic lines/arcs. Fragment classification must not depend on a
|
||||
/// display chord tolerance: a tiny tool can sit between a circular boundary and its chords.
|
||||
/// Loops are closed only within the chaining tolerance (an arrangement's own output joins its
|
||||
/// pieces within `10 · tol`), so a gap where one line ends and the next begins is closed by a
|
||||
/// straight connector: a ray through the gap would otherwise slip out of the loop without a
|
||||
/// crossing and flip the parity of points level with the corner. Arcs decide their own ends
|
||||
/// within an angular tolerance (below), so joints with an arc keep that rule.
|
||||
fn inside_loop(lp: &Loop2, p: Vec2) -> bool {
|
||||
let mut odd = false;
|
||||
for seg in &lp.segs {
|
||||
let nexts = lp.segs.iter().skip(1).chain(lp.segs.first());
|
||||
for (seg, next) in lp.segs.iter().zip(nexts) {
|
||||
if let (Seg2::Line { b: a, .. }, Seg2::Line { a: b, .. }) = (*seg, *next)
|
||||
&& (a.y > p.y) != (b.y > p.y)
|
||||
&& a.x + (p.y - a.y) * (b.x - a.x) / (b.y - a.y) > p.x
|
||||
{
|
||||
odd = !odd;
|
||||
}
|
||||
match *seg {
|
||||
Seg2::Line { a, b } => {
|
||||
if (a.y > p.y) != (b.y > p.y) {
|
||||
@@ -2148,6 +2160,36 @@ mod planar_tests {
|
||||
assert!(!inside_loop(&lp, Vec2::new(-5.0, -10.0)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Loops are closed only within the chaining tolerance. A ray level with one end of such a
|
||||
/// gap slipped through it without a crossing, so a point beside the loop counted as inside:
|
||||
/// a sheet shrunk by a parameter change (sketch solver residuals of 1e-13 mm) lost a bend
|
||||
/// zone that way and its unfold failed.
|
||||
#[test]
|
||||
fn ray_parity_closes_the_gaps_chaining_leaves() {
|
||||
let gap = 1e-13;
|
||||
let square = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) },
|
||||
Seg2::Line { a: Vec2::new(10.0, 0.0), b: Vec2::new(10.0, 10.0) },
|
||||
Seg2::Line { a: Vec2::new(10.0, 10.0), b: Vec2::new(0.0, 10.0) },
|
||||
Seg2::Line { a: Vec2::new(0.0, 10.0), b: Vec2::new(0.0, 5.0 + gap) },
|
||||
Seg2::Line { a: Vec2::new(0.0, 5.0), b: Vec2::new(0.0, 0.0) },
|
||||
],
|
||||
};
|
||||
for lp in [square.clone(), square.reversed()] {
|
||||
assert!(!inside_loop(&lp, Vec2::new(-5.0, 5.0)));
|
||||
assert!(inside_loop(&lp, Vec2::new(5.0, 5.0)));
|
||||
assert!(!inside_loop(&lp, Vec2::new(15.0, 5.0)));
|
||||
}
|
||||
// A neighbour with an edge level with the gap joins the square instead of being
|
||||
// dropped as if it lay inside it (which left the outline open: no result at all).
|
||||
let a = Region2 { outer: square, holes: vec![] };
|
||||
let b = rectangle(Vec2::new(-4.0, 2.0), Vec2::new(0.0, 5.0));
|
||||
let union = planar_regions_boolean(&[a], &[b], BoolOp::Union, 1e-7).unwrap();
|
||||
assert_eq!(union.len(), 1);
|
||||
assert!((union[0].area().abs() - 112.0).abs() < 1e-9, "{}", union[0].area());
|
||||
}
|
||||
#[test]
|
||||
fn tiny_tool_between_circle_and_display_chord_is_not_lost() {
|
||||
let circle = Region2 { outer: Loop2::circle(Vec2::ZERO, 10.0), holes: vec![] };
|
||||
|
||||
Reference in new issue
Block a user