feat: flanges on flanges follow their flange's end and close at corners (#601)
An edge flange on another flange's far edge (a return) runs along that edge from end to end, following the parent's closed or cut-back corner, and returns of one feature close toward each other at convex base corners, mitred the rule's gap apart. Bends stop square where the mitre allows. Fixes #593. Co-authored-by: NOWN-DEV <[email protected]>
This commit is contained in:
1 parent
9089edfb71
commit
a2f31f683d
4 files changed
+1242
-4
No files matched your search
@@ -338,6 +338,10 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
tapered or bend-crossing machining refuses atomically instead of restoring material later.
|
||||
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
|
||||
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
|
||||
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
|
||||
clear of the flange beside it, and the returns of one feature are mitred toward each other
|
||||
with the rule's gap where their flanges meet at a corner; returns of separate features, and
|
||||
flanges on returns, are not closed toward each other.
|
||||
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
|
||||
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
|
||||
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
|
||||
|
||||
+1021
-4
File diff suppressed because it is too large.
Load diff
@@ -2435,6 +2435,197 @@ fn per_edge_values_are_expressions_and_feature_inputs() {
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
}
|
||||
|
||||
/// The bends' lengths in the flat pattern, in the order the bends were made.
|
||||
fn bend_lengths(s: &mut Session) -> Vec<f64> {
|
||||
flat(s)["bends"].as_array().cloned().unwrap_or_default().iter().map(|b| b["length"].as_f64().unwrap_or(f64::NAN)).collect()
|
||||
}
|
||||
|
||||
/// A tray on a 120 wide, 90 deep base whose top edge is `top` wide (120: a rectangle), in 2 mm
|
||||
/// aluminium (R 1, K 0.5) with the flange gap `gap`: walls 20 high at `deg` in one feature and,
|
||||
/// in the next feature, a return 12 high at 180° − `deg` on the far edge of each of the walls
|
||||
/// `returns` (0…3: bottom, right, top, left; none: no feature), picked on the walls' inner faces,
|
||||
/// so that the returns turn in over the base and lie level.
|
||||
fn tray_with_returns(top: f64, deg: f64, gap: &str, returns: &[usize]) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(
|
||||
&mut s,
|
||||
"sheet.manage_rules",
|
||||
json!({"name": "AL2", "thickness": "2 mm", "k_factor": 0.5, "bend_radius": "1 mm", "gap": gap, "active": true}),
|
||||
);
|
||||
let inset = (120.0 - top) / 2.0;
|
||||
let pts = [(0.0, 0.0), (120.0, 0.0), (120.0 - inset, 90.0), (inset, 90.0)];
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
|
||||
run(&mut s, "sketch.line", json!({"points": pts.map(|p| [p.0, p.1]), "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Base"}));
|
||||
// A wall's inner face 20 up its outer face from the base's edge: that far in from the edge
|
||||
// and that high (see the doc tests).
|
||||
let th = deg.to_radians();
|
||||
let (into, up) = (-20.0 * th.cos() + 2.0 * th.sin(), 20.0 * th.sin() + 2.0 * th.cos());
|
||||
let pick = |k: usize, into: f64, z: f64| {
|
||||
let (a, b) = (pts[k], pts[(k + 1) % 4]);
|
||||
let l = (b.0 - a.0).hypot(b.1 - a.1);
|
||||
json!([(a.0 + b.0) / 2.0 - (b.1 - a.1) / l * into, (a.1 + b.1) / 2.0 + (b.0 - a.0) / l * into, z])
|
||||
};
|
||||
let edges: Vec<Value> = (0..4).map(|k| pick(k, 0.0, 2.0)).collect();
|
||||
run(&mut s, "sheet.flange", json!({"edges": edges, "height": 20, "angle": format!("{deg} deg")}));
|
||||
if !returns.is_empty() {
|
||||
let edges: Vec<Value> = returns.iter().map(|&k| pick(k, into, up)).collect();
|
||||
run(&mut s, "sheet.flange", json!({"edges": edges, "height": 12, "angle": format!("{} deg", 180.0 - deg)}));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_on_a_tray_close_at_its_corners() {
|
||||
use std::f64::consts::SQRT_2;
|
||||
// Walls at 90° with returns at 90°. The returns' feature used to fail here ("sew: nonmanifold
|
||||
// edge contact"): each return took its wall's bend length and ran through its neighbours.
|
||||
let mut s = tray_with_returns(120.0, 90.0, "0.5 mm", &[0, 1, 2, 3]);
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
// By hand (the derivation is with the doc tests; e = g/√2). The part fills 120 x 90 x 20.
|
||||
let e = 0.5 / SQRT_2;
|
||||
let b = bbox(&mut s);
|
||||
for (k, want) in [120.0, 90.0, 20.0].into_iter().enumerate() {
|
||||
let (lo, hi) = (b["min"][k].as_f64().unwrap_or(f64::NAN), b["max"][k].as_f64().unwrap_or(f64::NAN));
|
||||
assert!(lo.abs() < 1e-6 && (hi - want).abs() < 1e-6, "{b}");
|
||||
}
|
||||
// The walls' bends are the base's trimmed edges; the returns' bends stop g/√2 short of the
|
||||
// mitre x = y through (3, 3) at both ends.
|
||||
let f = flat(&mut s);
|
||||
let lengths = bend_lengths(&mut s);
|
||||
assert_eq!(lengths.len(), 8, "{f}");
|
||||
for (got, want) in lengths.iter().zip([114.0, 84.0, 114.0, 84.0, 114.0 - 2.0 * e, 84.0 - 2.0 * e, 114.0 - 2.0 * e, 84.0 - 2.0 * e]) {
|
||||
assert!((got - want).abs() < 1e-6, "{lengths:?}");
|
||||
}
|
||||
// The flat pattern is one outline through the corners of the return's panel on the wall on
|
||||
// y = 0: 9 high past two bend zones π wide and the wall's 14, mitred at 45° at both ends.
|
||||
let outline = outline_points(&f);
|
||||
assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{f}");
|
||||
let (y0, y1) = (-11.0 - 2.0 * PI, -20.0 - 2.0 * PI);
|
||||
for (x, y) in [(3.0 + e, y0), (117.0 - e, y0), (108.0 - e, y1), (12.0 + e, y1)] {
|
||||
assert!(outline.iter().any(|p| (p.0 - x).abs() < 1e-6 && (p.1 - y).abs() < 1e-6), "({x}, {y}) not in {outline:?}");
|
||||
}
|
||||
// It holds the base, the walls' bends and panels and the returns' bends and panels, nothing
|
||||
// more or less, and is 20 + 2π wider than the base on every side.
|
||||
let area = 114.0 * 84.0 + 396.0 * PI + 28.0 * (202.0 - 4.0 * e) + 2.0 * PI * (198.0 - 4.0 * e) + 18.0 * (180.0 - 4.0 * e);
|
||||
assert!(rel(polygon_area(&outline), area) < 1e-9, "{} vs {area}", polygon_area(&outline));
|
||||
for (k, want) in [160.0 + 4.0 * PI, 130.0 + 4.0 * PI, 2.0].into_iter().enumerate() {
|
||||
assert!((f["flat_size_mm"][k].as_f64().unwrap_or(f64::NAN) - want).abs() < 1e-6, "{}: {want}", f["flat_size_mm"]);
|
||||
}
|
||||
// Folded, the part holds that material (K = 0.5 keeps the volume).
|
||||
let v = volume(&mut s);
|
||||
assert!(rel(v, area * 2.0) < 1e-3, "{v} vs {}", area * 2.0);
|
||||
// The DXF: the outline in straight lines, and eight 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(), outline.len() + 8, "{} outline points", outline.len());
|
||||
// Unfold and refold keep it.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
assert!(same_points(&outline_points(&flat(&mut s)), &outline));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!(rel(volume(&mut s), v) < 1e-9);
|
||||
// Reopened, it is the same part.
|
||||
let mut again = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert!(no_errors_or_warnings(&again), "{:?}", again.model.results);
|
||||
assert!(rel(volume(&mut again), v) < 1e-9);
|
||||
assert!(same_points(&outline_points(&flat(&mut again)), &outline));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_on_leaning_walls_stay_inside_the_walls() {
|
||||
// A trapezoid (corners of 77.5° and 102.5°) with walls at 110° and level returns at 70°. Each
|
||||
// return used to run its wall's bend length from where the wall's far edge started, out
|
||||
// through the neighbouring wall: the part reached x = 120.588 where its walls end at 117.587.
|
||||
let mut s = tray_with_returns(80.0, 110.0, "0.5 mm", &[0, 1, 2, 3]);
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
// The returns lie inside the walls and no higher than their tips: the part's box is the
|
||||
// walls' own.
|
||||
let (with, without) = (bbox(&mut s), bbox(&mut tray_with_returns(80.0, 110.0, "0.5 mm", &[])));
|
||||
for end in ["min", "max"] {
|
||||
for k in 0..3 {
|
||||
let (a, b) = (with[end][k].as_f64().unwrap_or(f64::NAN), without[end][k].as_f64().unwrap_or(f64::NAN));
|
||||
assert!((a - b).abs() < 1e-6, "{with} vs {without}");
|
||||
}
|
||||
}
|
||||
// By hand (the doc tests): a return's panel starts v = 3·sin 110° + (20 − 3·tan 35°)·cos 70°
|
||||
// in from its wall's edge, and at a base corner of angle α its bend stops
|
||||
// x_b = (g/2 + v·cos(α/2)) / sin(α/2) along the edge from the corner.
|
||||
let th = 110f64.to_radians();
|
||||
let v = 3.0 * th.sin() - (20.0 - 3.0 * 35f64.to_radians().tan()) * th.cos();
|
||||
let x_b = |alpha: f64| (0.25 + v * (alpha / 2.0).cos()) / (alpha / 2.0).sin();
|
||||
let (acute, side) = ((90.0f64 / 20.0).atan(), 20f64.hypot(90.0));
|
||||
let want = [120.0 - 2.0 * x_b(acute), side - x_b(acute) - x_b(PI - acute), 80.0 - 2.0 * x_b(PI - acute), side - x_b(acute) - x_b(PI - acute)];
|
||||
let lengths = bend_lengths(&mut s);
|
||||
assert_eq!(lengths.len(), 8, "{lengths:?}");
|
||||
for (got, want) in lengths[4..].iter().zip(want) {
|
||||
assert!((got - want).abs() < 1e-6, "{lengths:?} vs {want:?}");
|
||||
}
|
||||
// One outline in the flat, and the folded part holds its material.
|
||||
let f = flat(&mut s);
|
||||
assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{f}");
|
||||
let (outline, vol) = (outline_points(&f), volume(&mut s));
|
||||
assert!(rel(vol, polygon_area(&outline) * 2.0) < 1e-3, "{vol} vs {}", polygon_area(&outline) * 2.0);
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
assert!(same_points(&outline_points(&flat(&mut s)), &outline));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!(rel(volume(&mut s), vol) < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_return_alone_on_a_leaning_wall_keeps_out_of_the_walls_beside_it() {
|
||||
use std::f64::consts::SQRT_2;
|
||||
// The tray with walls at 110° and a level return on the wall on y = 0 only. It used to take
|
||||
// that wall's bend length (120 − 2·3·tan 55° = 111.431) from where the wall's far edge starts
|
||||
// and came out through the wall on x = 120: the part reached x = 120.504 where its walls end
|
||||
// at 118.716.
|
||||
let mut s = tray_with_returns(120.0, 110.0, "0.5 mm", &[0]);
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
let (with, without) = (bbox(&mut s), bbox(&mut tray_with_returns(120.0, 110.0, "0.5 mm", &[])));
|
||||
for end in ["min", "max"] {
|
||||
for k in 0..3 {
|
||||
let (a, b) = (with[end][k].as_f64().unwrap_or(f64::NAN), without[end][k].as_f64().unwrap_or(f64::NAN));
|
||||
assert!((a - b).abs() < 1e-6, "{with} vs {without}");
|
||||
}
|
||||
}
|
||||
// By hand (the doc tests): the walls beside it lean in over it and come nearest at its top,
|
||||
// 20·sin 110° up, where their inner faces are 2/sin 110° + 20·cos 70° in from the base's edges.
|
||||
// The return stops square g/√2 short of them at both ends: 120 − 2·9.322312 = 101.355376.
|
||||
let th = 110f64.to_radians();
|
||||
let want = 120.0 - 2.0 * (0.5 / SQRT_2 + 2.0 / th.sin() - 20.0 * th.cos());
|
||||
let lengths = bend_lengths(&mut s);
|
||||
assert!(lengths.len() == 5 && (lengths[4] - want).abs() < 1e-6 && (want - 101.355376).abs() < 1e-6, "{lengths:?} vs {want}");
|
||||
// One outline in the flat, and the folded part holds its material.
|
||||
let f = flat(&mut s);
|
||||
assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{f}");
|
||||
let (outline, vol) = (outline_points(&f), volume(&mut s));
|
||||
assert!(rel(vol, polygon_area(&outline) * 2.0) < 1e-3, "{vol} vs {}", polygon_area(&outline) * 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_follow_the_rules_gap_and_stay_open_with_a_warning_when_it_is_zero() {
|
||||
use std::f64::consts::SQRT_2;
|
||||
// The mitre keeps the rule's gap: at 90° the returns' bends are 114 − 2·g/√2 and 84 − 2·g/√2.
|
||||
let mut s = tray_with_returns(120.0, 90.0, "0.5 mm", &[0, 1, 2, 3]);
|
||||
for g in [0.5, 1.0, 0.2] {
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "AL2", "gap": format!("{g} mm")}));
|
||||
assert!(no_errors_or_warnings(&s), "{g}: {:?}", s.model.results);
|
||||
let lengths = bend_lengths(&mut s);
|
||||
assert_eq!(lengths.len(), 8, "{g}: {lengths:?}");
|
||||
for (got, want) in lengths[4..].iter().zip([114.0, 84.0, 114.0, 84.0]) {
|
||||
assert!((got - (want - 2.0 * g / SQRT_2)).abs() < 1e-6, "{g}: {lengths:?}");
|
||||
}
|
||||
}
|
||||
// A gap of zero can't be kept: the walls' corners and the returns' corners are left open,
|
||||
// each feature with its warning, and each return spans its wall's far edge (here the wall's
|
||||
// bend, 120 − 2·3·tan 55°).
|
||||
let mut s = tray_with_returns(120.0, 110.0, "0 mm", &[0, 1, 2, 3]);
|
||||
let warnings: Vec<String> = s.model.results.iter().filter_map(|r| r.warning.clone()).collect();
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()) && warnings.len() == 2, "{:?}", s.model.results);
|
||||
assert!(warnings.iter().all(|w| w.matches("left open").count() == 4), "{warnings:?}");
|
||||
let lengths = bend_lengths(&mut s);
|
||||
assert!((lengths[4] - lengths[0]).abs() < 1e-9 && (lengths[0] - (120.0 - 6.0 * 55f64.to_radians().tan())).abs() < 1e-6, "{lengths:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_per_edge_input_is_refused_and_plain_edges_stay_plain() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -61,6 +61,32 @@ per edge with an angle and a height box; an empty box takes the value above.
|
||||
"height":38.6,"angle":"75 deg"}}
|
||||
```
|
||||
|
||||
A flange can also stand on the far edge of another flange: a return, such as a lip turned in
|
||||
along the top of a tray's walls. It runs along that edge from end to end, so it follows the
|
||||
closed end of the flange it stands on, slanted or not. Two returns of one feature whose flanges
|
||||
meet at a corner of the base are closed toward each other as well, with the rule's gap. They
|
||||
are mitred either side of a plane that runs along the mean of the two panels' normals, bisects
|
||||
their bend lines and passes where the lines their panels start on cross. Returns that lie in
|
||||
one plane (level returns on a tray's walls, whatever the walls' angle) then meet on the corner's
|
||||
bisector, like the sides of a picture frame. Each panel ends on a slanted line in the flat.
|
||||
Each bend stops square where all of it keeps half the gap from that plane, at the latest at
|
||||
the end of the flange it stands on, which can leave a short stretch of that flange's edge bare.
|
||||
Returns turned opposite ways pass each other. A return that meets no other at a corner ends
|
||||
with the flange it stands on; where the flange beside that one leans in over it, the return is
|
||||
cut back until it keeps as clear of that flange as the end of the flange it stands on does.
|
||||
Returns that can't be closed stay open with a warning, as on the base. Returns of separate
|
||||
features don't close toward each other either: make the returns round a corner in one feature.
|
||||
A corner relief changes none of this: it leaves the flanges' closed ends as they are, so the
|
||||
returns on them close over a relieved corner as over a whole one.
|
||||
|
||||
```json
|
||||
{"command":"sheet.flange","params":{"edges":[[60,2,20],[118,45,20],[60,88,20],[2,45,20]],
|
||||
"height":12,"angle":"90 deg"}}
|
||||
```
|
||||
|
||||
(The four returns of a 120 x 90 tray in 2 mm sheet with 90° walls 20 high: each point is the
|
||||
middle of a wall's far edge on its inner face, so the returns turn in over the base.)
|
||||
|
||||
For a shop drawing or spreadsheet, `sheet.export_bend_table` returns CSV by default; choose
|
||||
`format: "json"` for structured data. It includes the sheet rule, thickness, bend angles,
|
||||
radii, allowances and flat centre-line coordinates. Lengths use millimetres and angles use
|
||||
|
||||
Reference in new issue
Block a user