feat: close sheet flange corners at any angle (#406)
Edge flanges meeting at a corner of the base were closed only at exactly 90°; at any other angle they stayed open (a V at 72°) and the rule's gap was not used. The flanges of one feature now close at any convex base corner and any bend angle: each panel end is cut square to the sheet on a line in the flat (slanted off 90°, stored as ext plus a new per-end slope) so the edges nearest each other, on the inner faces, run parallel the rule's gap apart about the plane bisecting the inner faces. The construction reads the two flanges' own angles, so it holds for unequal ones as well. At 90° it is the extension the old code gave (reference case 69-sm-tray is unchanged). A corner that can't close (a gap under 0.01 mm, flanges bending opposite ways, ends that would overlap in the flat pattern) stays open, and the flange feature reports a warning that says where and why. A flange of a later feature leaves the corner as it was (reference case 75-sm-bracket-holes: the earlier flange keeps its length). Tested at 72° and 90° against a hand derivation, on a trapezoid, and every 5° from 45° to 135° on a rectangle and a trapezoid (gap exact, one flat region without hairline edges, one valid shell holding the flat material). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0bb5b7eb09
commit
0154572580
6 files changed
+511
-34
No files matched your search
@@ -34,7 +34,8 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("convert_sheet")
|
||||
.params("body; face: [x,y,z] on its large flat face (a plate of even thickness); rule?"),
|
||||
CommandSpec::new("sheet.manage_rules", "Sheet Metal Rules", rules).at("SHEET METAL", "MODIFY").icon("sheet_rules").params(
|
||||
"name (new or existing); thickness?, k_factor?, bend_radius?, relief_width?, relief_depth?, corner_relief?, hem_gap?, gap? (expressions; `Thickness` is the rule's); active?: bool; delete?: bool",
|
||||
"name (new or existing); thickness?, k_factor?, bend_radius?, relief_width?, relief_depth?, corner_relief?, hem_gap?, gap? (expressions; `Thickness` is the rule's; \
|
||||
gap: between flanges that meet at a corner, whose ends are closed toward each other at any angle); active?: bool; delete?: bool",
|
||||
),
|
||||
CommandSpec::new("sheet.rules", "List Sheet Metal Rules", list_rules).noundo().doc_only().params("→ rules with their expressions and values, and the active one"),
|
||||
CommandSpec::new("sheet.flat_pattern", "Create Flat Pattern", flat_pattern)
|
||||
|
||||
@@ -1753,3 +1753,71 @@ fn fold_fixed_point_above_the_sheet_is_not_silently_accepted() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A 120 x 90 tray in 2 mm aluminium (R 1, K 0.5): four 38.6 mm flanges at `deg` in one feature.
|
||||
fn alu_tray(s: &mut Session, deg: f64, rule: Value) {
|
||||
let mut r = json!({"name": "AL2", "thickness": "2 mm", "k_factor": 0.5, "bend_radius": "1 mm", "gap": "0.5 mm", "active": true});
|
||||
if let (Some(r), Some(extra)) = (r.as_object_mut(), rule.as_object()) {
|
||||
r.extend(extra.clone());
|
||||
}
|
||||
run(s, "sheet.manage_rules", r);
|
||||
base(s, 120.0, 90.0);
|
||||
run(s, "sheet.flange", json!({"edges": [[60, 0, 2], [120, 45, 2], [60, 90, 2], [0, 45, 2]], "height": 38.6, "angle": format!("{deg} deg")}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_corners_reach_the_flat_pattern_and_the_dxf() {
|
||||
use std::f64::consts::SQRT_2;
|
||||
// 60° and 105° are angles whose flat pattern was lost to a rounding slip in the planar arrangement.
|
||||
for deg in [60.0f64, 72.0, 90.0, 105.0] {
|
||||
let mut s = Session::default();
|
||||
alu_tray(&mut s, deg, json!({}));
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{deg}: {:?}", s.model.results);
|
||||
// The end of the flange on y = 0 at the corner at the origin, by hand (see the doc tests):
|
||||
// x(w) = g/√2 + T·sinθ − w·cosθ at flat y = s0 − BA − (w − s0), for w = s0 and w = 38.6.
|
||||
let th = deg.to_radians();
|
||||
let (s0, ba) = (3.0 * (th / 2.0).tan(), th * 2.0);
|
||||
let x_end = |w: f64| 0.5 / SQRT_2 + 2.0 * th.sin() - w * th.cos();
|
||||
let f = flat(&mut s);
|
||||
let outline: Vec<(f64, f64)> = f["outline"][0]
|
||||
.as_array()
|
||||
.map(|l| l.iter().map(|p| (p[0].as_f64().unwrap_or(f64::NAN), p[1].as_f64().unwrap_or(f64::NAN))).collect())
|
||||
.unwrap_or_default();
|
||||
assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{deg}: {f}");
|
||||
for (x, y) in [(x_end(s0), s0 - ba), (x_end(38.6), s0 - ba - (38.6 - s0))] {
|
||||
assert!(outline.iter().any(|p| (p.0 - x).abs() < 1e-6 && (p.1 - y).abs() < 1e-6), "{deg}: ({x}, {y}) not in {outline:?}");
|
||||
}
|
||||
// The folded part holds the flat pattern's material (K = 0.5 keeps the volume).
|
||||
let n = outline.len();
|
||||
let area = (0..n).map(|i| outline[i].0 * outline[(i + 1) % n].1 - outline[(i + 1) % n].0 * outline[i].1).sum::<f64>().abs() / 2.0;
|
||||
let v = volume(&mut s);
|
||||
assert!((v - area * 2.0).abs() < 1e-3 * v, "{deg}: {v} vs {}", area * 2.0);
|
||||
// The DXF: the closed outline in straight lines, four bend lines.
|
||||
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
assert_eq!(dxf.matches("0\nLINE\n").count(), n + 4, "{deg}: {n} outline points");
|
||||
// Unfold and refold keep it.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!((volume(&mut s) - v).abs() < 1e-6 * v, "{deg}");
|
||||
// A cut made folded still goes through with the corners closed: a Ø20 hole in the base.
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Hole"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [60, 45], "diameter": 20}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Hole", "distance": 10, "operation": "cut", "through_all": true}));
|
||||
let want = PI * 100.0 * 2.0;
|
||||
assert!((v - volume(&mut s) - want).abs() < 1e-3 * want, "{deg}: {v} - {} vs {want}", volume(&mut s));
|
||||
assert_eq!(flat(&mut s)["cutouts"].as_array().map(Vec::len), Some(1), "{deg}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corners_left_open_are_reported_on_the_flange() {
|
||||
let mut s = Session::default();
|
||||
alu_tray(&mut s, 72.0, json!({"gap": "0 mm"}));
|
||||
let w = s.model.results.last().and_then(|r| r.warning.clone()).unwrap_or_default();
|
||||
assert_eq!(w.matches("left open").count(), 4, "{w}");
|
||||
// Open corners still fold, unfold and take cuts.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!(volume(&mut s) > 0.0);
|
||||
}
|
||||
Reference in new issue
Block a user