feat: corner reliefs at sheet flange corners, chosen by the rule (#488)

Sheet Metal Rules get a Corner Relief Shape (notch, round or rectangular), sized by the rule's corner relief, cut where two bends meet at a corner of the base. Notch, the default, keeps existing designs as they were. File format 16.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
NOWN-DEVandClaude Opus 5.5 authored and GitHub committed 2026-10-13 00:25:50 -04:00
1 parent c2a9c71436
commit d6af554c6d
11 files changed
+830 -15

No files matched your search

+7 -2
View File
@@ -51,13 +51,17 @@
//! anchors can name a sheet metal panel side (`sheet:<body>:<panel>:<side>`). Version 14
//! designs need no transformation; the version stamp lets older readers request an update
//! instead of encountering an unknown certificate or link kind.
//! - 16: sheet metal rules can choose a corner relief shape (`corner_relief_shape`: notch,
//! round or rectangular). Version 15 designs need no transformation (an absent shape is the
//! notch they always had); the version stamp lets older readers request an update instead of
//! silently building notches where a design asks for another shape.
use serde_json::Value;
use crate::{DocError, Document, Result};
/// The format this build writes.
pub const FORMAT: u32 = 15;
pub const FORMAT: u32 = 16;
pub fn format_string(n: u32) -> String {
format!("solidcraft/{n}")
@@ -74,7 +78,7 @@ pub fn version_of(s: &str) -> Option<u32> {
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
type JsonStep = fn(&mut Value) -> Result<()>;
const JSON_STEPS: [JsonStep; 14] = [
const JSON_STEPS: [JsonStep; 15] = [
|_| Ok(()),
v2_to_v3,
v3_to_v4,
@@ -89,6 +93,7 @@ const JSON_STEPS: [JsonStep; 14] = [
|_| Ok(()),
v13_to_v14,
|_| Ok(()),
|_| Ok(()),
];
/// Stored face and edge names were worked out from geometry before format 14; they get the
+422 -4
View File
@@ -157,6 +157,11 @@ pub struct SheetRule {
pub relief_depth: String,
#[serde(default = "rule_corner_relief")]
pub corner_relief: String,
/// What is cut where two bends meet at a corner of the base: `notch` (nothing more: the
/// bends end square at the corner), `round` or `rectangular` (a relief `corner_relief`
/// across, centred on the corner the two bends share).
#[serde(default = "rule_corner_relief_shape")]
pub corner_relief_shape: String,
/// Inner radius of flat hems.
#[serde(default = "rule_hem_gap")]
pub hem_gap: String,
@@ -178,6 +183,9 @@ fn rule_relief_depth() -> String {
fn rule_corner_relief() -> String {
"Thickness * 4.0".into()
}
fn rule_corner_relief_shape() -> String {
ReliefShape::Notch.name().into()
}
fn rule_hem_gap() -> String {
"0.01 mm".into()
}
@@ -193,6 +201,7 @@ impl SheetRule {
relief_width: rule_relief_width(),
relief_depth: rule_relief_depth(),
corner_relief: rule_corner_relief(),
corner_relief_shape: rule_corner_relief_shape(),
hem_gap: rule_hem_gap(),
gap: rule_gap(),
}
@@ -214,6 +223,40 @@ impl SheetSettings {
}
}
/// The shape of the relief cut where two bends meet at a corner of the base (a rule's
/// `corner_relief_shape`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ReliefShape {
/// Nothing is cut: the bends end square at the corner.
#[default]
Notch,
/// A round about the corner of the bends, through the angle the base fills there.
Round,
/// A square with a corner on the corner of the bends (a kite where the base's corner isn't
/// square: both cuts leave their edge at a right angle).
Rectangular,
}
impl ReliefShape {
pub fn parse(s: &str) -> Option<ReliefShape> {
match s.trim().to_ascii_lowercase().as_str() {
"notch" => Some(ReliefShape::Notch),
"round" => Some(ReliefShape::Round),
"rectangular" => Some(ReliefShape::Rectangular),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
ReliefShape::Notch => "notch",
ReliefShape::Round => "round",
ReliefShape::Rectangular => "rectangular",
}
}
}
/// A rule's values (mm, unit-less K).
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct RuleValues {
@@ -223,6 +266,7 @@ pub struct RuleValues {
pub relief_width: f64,
pub relief_depth: f64,
pub corner_relief: f64,
pub corner_relief_shape: ReliefShape,
pub hem_gap: f64,
pub gap: f64,
}
@@ -255,6 +299,8 @@ impl Document {
relief_width: with_t(&r.relief_width, Kind::Length)?,
relief_depth: with_t(&r.relief_depth, Kind::Length)?,
corner_relief: with_t(&r.corner_relief, Kind::Length)?,
corner_relief_shape: ReliefShape::parse(&r.corner_relief_shape)
.ok_or_else(|| DocError::Invalid(format!("unknown corner relief shape `{}` (notch, round or rectangular)", r.corner_relief_shape)))?,
hem_gap: with_t(&r.hem_gap, Kind::Length)?,
gap: with_t(&r.gap, Kind::Length)?,
};
@@ -328,6 +374,10 @@ pub struct SheetBody {
pub flat: bool,
/// Gap between flanges that meet at a corner.
pub gap: f64,
/// The relief cut where two bends meet at a corner of the base, and how far across it is
/// (the rule's `corner_relief_shape` and `corner_relief`).
pub relief: ReliefShape,
pub relief_size: f64,
/// The base sketch's lines (id, ends in flat coordinates), naming the base panel's edges.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub base_curves: Vec<(String, Vec2, Vec2)>,
@@ -479,6 +529,9 @@ fn clip(poly: &[Vec2], o: Vec2, n: Vec2) -> Vec<Vec2> {
/// The smallest gap closed corners are given: closer than this the flanges' ends would touch.
const MIN_CORNER_GAP: f64 = 0.01;
/// The smallest corner relief (across) that is cut: a smaller one would leave hairline edges.
const MIN_CORNER_RELIEF: f64 = 0.01;
/// Two bends on the base meeting at a corner (see [`SheetBody::corners`]): flanges `a` and `b`,
/// which end of each (false = the end at `p`), and the corner point (flat coordinates).
#[derive(Clone, Copy, Debug)]
@@ -570,7 +623,8 @@ impl SheetBody {
}
/// The part of a bend along its parent's edge: where a later flange trimmed that edge back
/// (a corner), the bend stops and the flange panel runs on alone (a corner relief).
/// (a corner) or a corner relief cut into it, the bend stops and the flange panel runs on
/// alone.
pub fn zone_span(&self, f: &SheetFlange) -> (f64, f64) {
if f.parent.is_some() {
return (0.0, f.len);
@@ -1361,13 +1415,13 @@ impl SheetBody {
/// Edge flanges on several edges at once: every edge is trimmed back first, so flanges on
/// adjacent edges stop at each other's bends; the corners between them are then closed with
/// the rule's gap (`close_corners`).
/// the rule's gap and given the rule's relief (`close_corners`).
pub fn add_flanges(&mut self, picks: &[Vec3], height: f64, angle: f64, radius: f64, position: &str, flip: bool) -> Result<Vec<usize>> {
self.add_flanges_noted(picks, height, angle, radius, position, flip).map(|(made, _)| made)
}
/// [`SheetBody::add_flanges`], also returning notes on corners that could not be closed
/// (they are left open, as the bends leave them).
/// (they are left open, as the bends leave them) or relieved (they get no relief).
pub fn add_flanges_noted(
&mut self,
picks: &[Vec3],
@@ -1502,7 +1556,8 @@ impl SheetBody {
out
}
/// Close the corners the new flanges make. Returns a note for every corner left open.
/// Close the corners the new flanges make, then cut the rule's relief at each. Returns a
/// note for every corner left open or without its relief.
fn close_corners(&mut self, made: &[usize]) -> Vec<String> {
let corners = self.corners(made);
let mut notes = Vec::new();
@@ -1515,6 +1570,14 @@ impl SheetBody {
notes.push(format!("the flanges at {} are left open: {why}", at(self, k.c)));
}
}
// The reliefs come last: they move the bends' ends, which is how the corners were found.
if self.relief != ReliefShape::Notch {
for k in &corners {
if let Err(why) = self.relieve_corner(k, &corners) {
notes.push(format!("no corner relief at {}: {why}", at(self, k.c)));
}
}
}
notes
}
@@ -1610,6 +1673,84 @@ impl SheetBody {
Ok(())
}
/// Cut the rule's relief where two bends meet at a corner of the base, `relief_size / 2`
/// from the corner along both edges: a round about the corner through the angle the base
/// fills there, or the square with that side (a kite where the corner isn't square, its
/// cuts square to the two edges). The base's edges then start that far from the corner, so
/// both bends stop there, cut straight across (`zone_span`): their ends stay exact arcs,
/// the round runs on into them tangentially, and the bend table gets the shorter bends.
/// The flange panels keep the ends `close_corner` gave them. A relief too small to cut, one
/// that would leave nothing of an edge it starts on (with the relief at that edge's other
/// end, where there is one), or one reaching as far from the corner as another edge of the
/// base is, is refused with the reason.
fn relieve_corner(&mut self, k: &Corner, corners: &[Corner]) -> std::result::Result<(), String> {
let size = self.relief_size;
if !(size >= MIN_CORNER_RELIEF) {
return Err(format!("the relief size {size:.4} mm is below {MIN_CORNER_RELIEF} mm"));
}
let r = size / 2.0;
let segs = self.base.outer.ccw().segs;
let n = segs.len();
// The base's edges at the corner: `i` ends there and the next one starts there.
let at_corner =
|i: usize| segs.get(i).is_some_and(|s| s.end().dist(k.c) < 1e-6) && segs.get((i + 1) % n).is_some_and(|s| s.start().dist(k.c) < 1e-6);
let Some(i) = (0..n).find(|&i| at_corner(i)) else { return Err("the bends don't meet at a corner of the base".into()) };
let j = (i + 1) % n;
let (Some(&Seg2::Line { a: pa, b: c }), Some(&Seg2::Line { b: pb, .. })) = (segs.get(i), segs.get(j)) else {
return Err("the base's edges at the corner are not straight".into());
};
let (Some(da), Some(db)) = ((pa - c).normalized(), (pb - c).normalized()) else {
return Err("an edge of the base at the corner has no length".into());
};
// The angle the base fills at the corner.
let alpha = db.cross(da).atan2(db.dot(da));
if !(alpha > 1e-6 && alpha < std::f64::consts::PI - 1e-6) {
// An inside corner of the base: the flanges turn away from each other.
return Ok(());
}
// Both bends must keep some length. An edge whose far end is another of the corners has
// to hold a relief there too, so the reliefs at its two ends fit or are left out together.
let takes = |far: Vec2| if corners.iter().any(|q| q.c.dist(far) < 1e-6) { 2.0 * r } else { r };
if takes(pa) >= pa.dist(c) - 1e-6 || takes(pb) >= pb.dist(c) - 1e-6 {
return Err(format!("a relief {size:.3} mm across would take a whole bend away"));
}
// The relief from the edge that ends at the corner to the one that starts there, and
// how far from the corner it reaches.
let (relief, reach) = match self.relief {
ReliefShape::Notch => return Ok(()),
ReliefShape::Round => (vec![Seg2::Arc { center: c, radius: r, start: da.angle(), sweep: -alpha }], r),
ReliefShape::Rectangular => {
let x = snap(c + (da + db) * (r / (1.0 + da.dot(db))));
(vec![Seg2::Line { a: snap(c + da * r), b: x }, Seg2::Line { a: x, b: snap(c + db * r) }], x.dist(c))
}
};
let (Some(start), Some(end)) = (relief.first().map(Seg2::start), relief.last().map(Seg2::end)) else { return Ok(()) };
// The rest of the outline must stay clear of it.
let clear = segs.iter().enumerate().filter(|(e, _)| *e != i && *e != j).all(|(_, s)| {
s.polyline(1e-3).windows(2).all(|w| {
let d = w[1] - w[0];
let t = if d.len2() > 1e-30 { ((c - w[0]).dot(d) / d.len2()).clamp(0.0, 1.0) } else { 0.0 };
c.dist(w[0] + d * t) > reach + 1e-6
})
});
if !clear {
return Err(format!("a relief {size:.3} mm across reaches {reach:.3} mm from the corner, where another edge of the base is nearer"));
}
let mut outer = Vec::with_capacity(n + relief.len());
for (e, s) in segs.iter().enumerate() {
if e == i {
outer.push(Seg2::Line { a: pa, b: start });
outer.extend(relief.iter().copied());
} else if e == j {
outer.push(Seg2::Line { a: end, b: pb });
} else {
outer.push(*s);
}
}
self.base = Region2 { outer: Loop2 { segs: outer }, holes: self.base.holes.clone() };
Ok(())
}
/// A flat hem: a 180° bend with a tiny radius at the panel edge, `length` to its outer face.
pub fn add_hem(&mut self, pick: Vec3, length: f64, gap: f64, flip: bool) -> Result<usize> {
let (panel, a, b, top, dist) = self.edge_at(pick).ok_or_else(|| DocError::Invalid("no sheet edge there".into()))?;
@@ -2294,6 +2435,8 @@ pub fn base_flange(body: &str, rule: &str, rv: &RuleValues, plane: Plane, region
pockets: Vec::new(),
flat: false,
gap: rv.gap,
relief: rv.corner_relief_shape,
relief_size: rv.corner_relief,
base_curves: Vec::new(),
}
}
@@ -2346,6 +2489,8 @@ pub fn contour_flange(body: &str, rule: &str, rv: &RuleValues, plane: &Plane, pt
pockets: Vec::new(),
flat: false,
gap: rv.gap,
relief: rv.corner_relief_shape,
relief_size: rv.corner_relief,
base_curves: Vec::new(),
};
let (y0, y1) = if ysign > 0.0 { (0.0, width) } else { (-width, 0.0) };
@@ -2461,6 +2606,7 @@ mod tests {
relief_width: 2.5,
relief_depth: 1.25,
corner_relief: 10.0,
corner_relief_shape: ReliefShape::Notch,
hem_gap: 0.01,
gap: 2.5,
}
@@ -2827,6 +2973,7 @@ mod tests {
relief_width: 2.0,
relief_depth: 1.0,
corner_relief: 8.0,
corner_relief_shape: ReliefShape::Notch,
hem_gap: 0.01,
gap: 2.0,
};
@@ -2908,6 +3055,7 @@ mod tests {
relief_width: 2.0,
relief_depth: 1.0,
corner_relief: 8.0,
corner_relief_shape: ReliefShape::Notch,
hem_gap: 0.01,
gap: 0.5,
}
@@ -3087,6 +3235,248 @@ mod tests {
assert!((v - flat_area(s) * s.t).abs() < 1e-3 * v, "{what}: {v} vs {}", flat_area(s) * s.t);
}
/// The flat area a corner relief `size` across takes where the base fills the angle `alpha`
/// and both bends are `ba` wide: of the base a sector of radius size/2 (round) or the kite
/// with that side, square to both edges (rectangular), and size/2 of each bend's length.
fn relief_area(shape: ReliefShape, size: f64, alpha: f64, ba: f64) -> f64 {
let r = size / 2.0;
match shape {
ReliefShape::Notch => 0.0,
ReliefShape::Round => r * r * alpha / 2.0 + 2.0 * r * ba,
ReliefShape::Rectangular => r * r * (alpha / 2.0).tan() + 2.0 * r * ba,
}
}
#[test]
fn the_rule_names_its_corner_relief_shape() {
// A rule saved before the shape existed reads as the notch it always had.
let old: SheetRule = serde_json::from_str(r#"{"name":"Old","thickness":"2 mm","k_factor":"0.5","bend_radius":"1 mm"}"#).unwrap();
assert_eq!(old.corner_relief_shape, "notch");
assert_eq!(SheetRule::steel().corner_relief_shape, "notch");
let (doc, vals) = (Document::default(), std::collections::BTreeMap::new());
assert_eq!(doc.rule_values(&vals, &old).unwrap().corner_relief_shape, ReliefShape::Notch);
for (text, shape) in [("round", ReliefShape::Round), (" Rectangular ", ReliefShape::Rectangular), ("NOTCH", ReliefShape::Notch)] {
let rule = SheetRule { corner_relief_shape: text.into(), ..old.clone() };
assert_eq!(doc.rule_values(&vals, &rule).unwrap().corner_relief_shape, shape);
assert_eq!(ReliefShape::parse(shape.name()), Some(shape));
}
// No other name is a shape.
for text in ["square", "circle", ""] {
let err = doc.rule_values(&vals, &SheetRule { corner_relief_shape: text.into(), ..old.clone() }).unwrap_err().to_string();
assert!(err.contains(&format!("unknown corner relief shape `{text}` (notch, round or rectangular)")), "{err}");
}
}
#[test]
fn corner_reliefs_cut_the_base_corner_and_stop_the_bends() {
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4};
// The tray: the bends on x = 0 and y = 0 meet at c = (s0, s0), s0 = (R + T)·tan(θ/2) in from
// the sheet's corner. A relief 8 mm across reaches 4 mm from c along both edges of the base.
for deg in [72.0f64, 90.0] {
let th = deg.to_radians();
let (s0, ba) = (3.0 * (th / 2.0).tan(), th * 2.0);
let len = 120.0 - 2.0 * s0;
let c = Vec2::new(s0, s0);
let (notch, _) = tray(&alu(), deg, 38.6);
assert_eq!(notch.base.outer.segs.len(), 4, "{deg}°: the notch cuts nothing");
for shape in [ReliefShape::Round, ReliefShape::Rectangular] {
let what = format!("{shape:?} at {deg}°");
let (s, notes) = tray(&RuleValues { corner_relief_shape: shape, ..alu() }, deg, 38.6);
assert!(notes.is_empty(), "{what}: {notes:?}");
let segs = &s.base.outer.segs;
if shape == ReliefShape::Round {
// A quarter round of radius 4 about c, from the edge x = s0 round to the edge y = s0.
assert_eq!((segs.len(), segs.iter().filter(|g| matches!(g, Seg2::Arc { .. })).count()), (8, 4), "{what}");
let found = segs.iter().any(|g| {
matches!(g, Seg2::Arc { center, radius, .. } if center.dist(c) < 1e-8 && (radius - 4.0).abs() < 1e-12)
&& g.start().dist(c + Vec2::new(0.0, 4.0)) < 1e-9
&& g.point_at(0.5).dist(c + Vec2::from_angle(FRAC_PI_4) * 4.0) < 1e-9
&& g.end().dist(c + Vec2::new(4.0, 0.0)) < 1e-9
});
assert!(found, "{what}: {segs:?}");
} else {
// A 4 mm square: in from the edge x = s0 to (s0 + 4, s0 + 4), out to the edge y = s0.
let line =
|a: Vec2, b: Vec2| segs.iter().any(|g| matches!(g, Seg2::Line { .. }) && g.start().dist(a) < 1e-9 && g.end().dist(b) < 1e-9);
assert_eq!(segs.len(), 12, "{what}");
assert!(
line(c + Vec2::new(0.0, 4.0), c + Vec2::new(4.0, 4.0)) && line(c + Vec2::new(4.0, 4.0), c + Vec2::new(4.0, 0.0)),
"{what}: {segs:?}"
);
}
// The bend on y = 0 stops 4 mm from the corners at both its ends, and the bend table follows.
let (u0, u1) = s.zone_span(&s.flanges[0]);
assert!((u0 - 4.0).abs() < 1e-9 && (u1 - (len - 4.0)).abs() < 1e-9, "{what}: {u0} {u1}");
let bend = &s.bends()[0];
let (start, end) = (Vec2::new(s0 + 4.0, s0 - ba / 2.0), Vec2::new(120.0 - s0 - 4.0, s0 - ba / 2.0));
assert!((bend.length - (len - 8.0)).abs() < 1e-9 && bend.start.dist(start) < 1e-9 && bend.end.dist(end) < 1e-9, "{what}: {bend:?}");
// Only the base changes: the flanges are the ones the closing left.
assert_eq!(s.flanges, notch.flanges, "{what}");
// The flat pattern loses, at each of its four corners, the relief in the base and 4 mm of two bends.
let (removed, want) = (flat_area(&notch) - flat_area(&s), 4.0 * relief_area(shape, 8.0, FRAC_PI_2, ba));
assert!((removed - want).abs() < 1e-6, "{what}: {removed} vs {want}");
holds_the_flat_material(&s, &what);
let unfolded = vol(&s.flat_solid().unwrap());
assert!((unfolded - flat_area(&s) * 2.0).abs() < 1e-3 * unfolded, "{what}: {unfolded}");
}
}
}
/// The trapezoid of the closing tests with four flanges at `deg`, under rule `rv`.
fn trapezoid(rv: &RuleValues, deg: f64) -> (SheetBody, Vec<String>) {
let pts = [Vec2::new(0.0, 0.0), Vec2::new(120.0, 0.0), Vec2::new(100.0, 90.0), Vec2::new(20.0, 90.0)];
let mut s = base_flange("Body1", "AL2", rv, Plane::XY, Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] });
let picks = [Vec3::new(60.0, 0.0, 2.0), Vec3::new(110.0, 45.0, 2.0), Vec3::new(60.0, 90.0, 2.0), Vec3::new(10.0, 45.0, 2.0)];
let (_, notes) = s.add_flanges_noted(&picks, 38.6, deg.to_radians(), rv.bend_radius, "inside", false).unwrap();
(s, notes)
}
#[test]
fn corner_reliefs_follow_corners_that_are_not_square() {
use std::f64::consts::PI;
// The base fills α = atan(90/20) = 77.47° at the two lower corners of the trapezoid and 180° − α
// at the upper ones. With every edge trimmed by s0, the bends on y = 0 and on the left edge meet
// at c = (s0/tan(α/2), s0), and the left edge leaves c in the direction (cos α, sin α).
let th = 72f64.to_radians();
let (s0, ba) = (3.0 * (th / 2.0).tan(), th * 2.0);
let alpha = 90f64.atan2(20.0);
let c = Vec2::new(s0 / (alpha / 2.0).tan(), s0);
let (along, up) = (Vec2::new(1.0, 0.0), Vec2::from_angle(alpha));
let (notch, _) = trapezoid(&alu(), 72.0);
for shape in [ReliefShape::Round, ReliefShape::Rectangular] {
let what = format!("{shape:?}");
let (s, notes) = trapezoid(&RuleValues { corner_relief_shape: shape, ..alu() }, 72.0);
assert!(notes.is_empty(), "{what}: {notes:?}");
let segs = &s.base.outer.segs;
if shape == ReliefShape::Round {
// An arc of radius 4 about c through the angle α, tangent to both bends' straight ends.
let found = segs.iter().any(|g| {
matches!(g, Seg2::Arc { center, radius, sweep, .. } if center.dist(c) < 1e-8 && (radius - 4.0).abs() < 1e-12 && (sweep + alpha).abs() < 1e-9)
&& g.start().dist(c + up * 4.0) < 1e-8
&& g.end().dist(c + along * 4.0) < 1e-8
&& g.start_tangent().dot(up).abs() < 1e-9
&& g.end_tangent().dot(along).abs() < 1e-9
});
assert!(found, "{what}: {segs:?}");
} else {
// A kite: both cuts square to their edge, meeting 4/cos(α/2) from c on the bisector.
let x = c + Vec2::from_angle(alpha / 2.0) * (4.0 / (alpha / 2.0).cos());
let line =
|a: Vec2, b: Vec2| segs.iter().any(|g| matches!(g, Seg2::Line { .. }) && g.start().dist(a) < 1e-8 && g.end().dist(b) < 1e-8);
assert!(line(c + up * 4.0, x) && line(x, c + along * 4.0), "{what}: {segs:?}");
assert!((x - (c + up * 4.0)).dot(up).abs() < 1e-9 && (x - (c + along * 4.0)).dot(along).abs() < 1e-9);
}
// Two corners of α and two of 180° − α, eight bend ends.
let want = 2.0 * relief_area(shape, 8.0, alpha, ba) + 2.0 * relief_area(shape, 8.0, PI - alpha, ba);
let removed = flat_area(&notch) - flat_area(&s);
assert!((removed - want).abs() < 1e-6, "{what}: {removed} vs {want}");
assert_eq!(s.flanges, notch.flanges, "{what}");
holds_the_flat_material(&s, &what);
}
}
#[test]
fn corner_reliefs_hold_at_every_angle_and_size() {
use std::f64::consts::{FRAC_PI_2, PI};
// The round relief runs into the bends' straight ends tangentially, a contact the planar
// booleans have to keep as one point. So every 5° from 45° to 135° is built on the tray and on
// the trapezoid with both shapes, and on the tray reliefs from 0.5 to 20 mm across.
let alpha = 90f64.atan2(20.0);
for shape in [ReliefShape::Round, ReliefShape::Rectangular] {
for k in 9..=27 {
let deg = k as f64 * 5.0;
let ba = deg.to_radians() * 2.0;
let rv = RuleValues { corner_relief_shape: shape, ..alu() };
for (name, (s, notes), (notch, _), corners) in [
("tray", tray(&rv, deg, 38.6), tray(&alu(), deg, 38.6), [FRAC_PI_2; 4]),
("trapezoid", trapezoid(&rv, deg), trapezoid(&alu(), deg), [alpha, alpha, PI - alpha, PI - alpha]),
] {
let what = format!("{shape:?} on the {name} at {deg}°");
assert!(notes.is_empty(), "{what}: {notes:?}");
let want: f64 = corners.iter().map(|a| relief_area(shape, 8.0, *a, ba)).sum();
let removed = flat_area(&notch) - flat_area(&s);
assert!((removed - want).abs() < 1e-6, "{what}: {removed} vs {want}");
holds_the_flat_material(&s, &what);
}
}
for size in [0.5, 1.0, 3.0, 5.0, 12.0, 20.0] {
for deg in [72.0f64, 90.0] {
let what = format!("{shape:?} {size} mm across at {deg}°");
let (s, notes) = tray(&RuleValues { corner_relief_shape: shape, corner_relief: size, ..alu() }, deg, 38.6);
assert!(notes.is_empty(), "{what}: {notes:?}");
let want = 4.0 * relief_area(shape, size, FRAC_PI_2, deg.to_radians() * 2.0);
let removed = flat_area(&tray(&alu(), deg, 38.6).0) - flat_area(&s);
assert!((removed - want).abs() < 1e-6, "{what}: {removed} vs {want}");
holds_the_flat_material(&s, &what);
}
}
}
}
#[test]
fn a_relief_that_does_not_fit_is_left_out_with_a_note() {
let closed = tray(&alu(), 72.0, 38.6).0;
// 200 mm across reaches 100 mm along edges of which the shorter is 90 − 2·s0 = 85.6 mm long.
// 100 mm across reaches 50 mm: that fits the edge once but not from both its ends, and all four
// corners are left out together. Nothing, or less than nothing, is too small to cut.
let sizes = [(200.0, "whole bend"), (100.0, "whole bend"), (0.0, "below 0.01 mm"), (-8.0, "below 0.01 mm"), (f64::NAN, "below 0.01 mm")];
for (size, why) in sizes {
let (s, notes) = tray(&RuleValues { corner_relief_shape: ReliefShape::Round, corner_relief: size, ..alu() }, 72.0, 38.6);
assert_eq!(notes.len(), 4, "{size}: {notes:?}");
assert!(notes.iter().all(|n| n.starts_with("no corner relief at (") && n.contains(why)), "{size}: {notes:?}");
// The part is the one the notch gives, corners closed.
assert_eq!((&s.base, &s.flanges), (&closed.base, &closed.flanges), "{size}");
}
// A 120 × 12 strip with flanges on its lower and left edges, a relief 16 mm across. Its 8 mm fit
// on both edges at the corner (the left one is 12 − s0 = 9.82 long), but the far corner of the
// square is 8·√2 = 11.3 mm from the corner of the bends, and the strip's upper edge is only 9.82
// from it: no relief may reach as far from its corner as another edge is. The round one reaches
// 8 mm and fits.
let strip = |shape: ReliefShape| {
let base = rect(Vec2::new(0.0, 0.0), Vec2::new(120.0, 0.0), Vec2::new(120.0, 12.0), Vec2::new(0.0, 12.0));
let mut s = base_flange("Body1", "AL2", &RuleValues { corner_relief_shape: shape, corner_relief: 16.0, ..alu() }, Plane::XY, base);
let picks = [Vec3::new(60.0, 0.0, 2.0), Vec3::new(0.0, 6.0, 2.0)];
let (_, notes) = s.add_flanges_noted(&picks, 20.0, 72f64.to_radians(), 1.0, "inside", false).unwrap();
(s, notes)
};
let (s, notes) = strip(ReliefShape::Rectangular);
assert!(notes.len() == 1 && notes[0].contains("another edge of the base"), "{notes:?}");
assert!(s.base.outer.segs.len() == 4 && s.solid().is_ok());
let (s, notes) = strip(ReliefShape::Round);
assert!(notes.is_empty() && s.base.outer.segs.iter().filter(|g| matches!(g, Seg2::Arc { .. })).count() == 1, "{notes:?}");
holds_the_flat_material(&s, "the strip");
// A right triangle with flanges on its two legs: the base's legs are 100 − 2·s0 = 95.6 long and
// its long edge passes (100 − 2·s0)/√2 = 67.6 mm from the corner of the bends. A round relief of
// radius 75 would cut through that edge; one of radius 65 stays inside.
let triangle = |size: f64| {
let pts = [Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0), Vec2::new(0.0, 100.0)];
let rv = RuleValues { corner_relief_shape: ReliefShape::Round, corner_relief: size, ..alu() };
let mut s = base_flange("Body1", "AL2", &rv, Plane::XY, Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] });
let picks = [Vec3::new(50.0, 0.0, 2.0), Vec3::new(0.0, 50.0, 2.0)];
let (_, notes) = s.add_flanges_noted(&picks, 20.0, 72f64.to_radians(), 1.0, "inside", false).unwrap();
(s, notes)
};
let (s, notes) = triangle(150.0);
assert!(notes.len() == 1 && notes[0].contains("another edge of the base"), "{notes:?}");
assert!(s.base.outer.segs.len() == 3 && s.solid().is_ok());
let (s, notes) = triangle(130.0);
assert!(notes.is_empty() && s.base.outer.segs.len() == 4, "{notes:?}");
holds_the_flat_material(&s, "the triangle");
}
#[test]
fn a_corner_with_a_flange_of_an_earlier_feature_gets_no_relief() {
// Like the closing, the relief belongs to the corners between the flanges of one feature.
let rv = RuleValues { corner_relief_shape: ReliefShape::Round, ..alu() };
let base = rect(Vec2::new(0.0, 0.0), Vec2::new(120.0, 0.0), Vec2::new(120.0, 90.0), Vec2::new(0.0, 90.0));
let mut s = base_flange("Body1", "AL2", &rv, Plane::XY, base);
s.add_flanges(&[Vec3::new(60.0, 0.0, 2.0)], 38.6, 72f64.to_radians(), 1.0, "inside", false).unwrap();
let (_, notes) = s.add_flanges_noted(&[Vec3::new(120.0, 45.0, 2.0)], 25.0, 72f64.to_radians(), 1.0, "inside", false).unwrap();
assert!(notes.is_empty(), "{notes:?}");
assert!(s.base.outer.segs.iter().all(|g| matches!(g, Seg2::Line { .. })));
assert!(s.solid().is_ok() && s.flat_regions().unwrap().len() == 1);
}
#[test]
fn closed_corners_keep_their_flat_pattern_at_every_angle() {
// 45° to 135° in steps of 5° on the tray and on a trapezoid. Which angles lose the flat
@@ -3371,4 +3761,32 @@ mod tests {
}
assert_eq!(cases, 147);
}
#[test]
fn corner_reliefs_are_cut_between_flanges_of_unequal_angle_and_height() {
use std::f64::consts::FRAC_PI_2;
// The relief is cut in the base, so flanges with their own angle and height get the same
// one: 4 mm into the base at each of its four corners, and every bend 4 mm shorter at both
// ends, each over its own width BA = θ·(R + K·T).
let heights = [38.6, 30.0, 45.0, 25.0];
let (notch, _) = tray_each(&alu(), MIXED, heights);
for shape in [ReliefShape::Round, ReliefShape::Rectangular] {
let what = format!("{shape:?}");
let (s, notes) = tray_each(&RuleValues { corner_relief_shape: shape, ..alu() }, MIXED, heights);
assert!(notes.is_empty(), "{what}: {notes:?}");
assert_eq!(s.flanges, notch.flanges, "{what}");
// The bend on y = 0 runs between the setbacks of its neighbours at 120° and 90°:
// 120 − 3·tan 60° − 3·tan 45° long, less the two reliefs.
let (bends, before) = (s.bends(), notch.bends());
assert_eq!((bends.len(), before.len()), (4, 4), "{what}");
assert!((bends[0].length - (120.0 - 3.0 * 60f64.to_radians().tan() - 3.0 - 8.0)).abs() < 1e-9, "{what}: {}", bends[0].length);
for (b, b0) in bends.iter().zip(&before) {
assert!((b.length - (b0.length - 8.0)).abs() < 1e-9, "{what}: {} vs {}", b.length, b0.length);
}
let in_the_bends: f64 = MIXED.iter().map(|deg| 8.0 * deg.to_radians() * 2.0).sum();
let (removed, want) = (flat_area(&notch) - flat_area(&s), 4.0 * relief_area(shape, 8.0, FRAC_PI_2, 0.0) + in_the_bends);
assert!((removed - want).abs() < 1e-6, "{what}: {removed} vs {want}");
holds_the_flat_material(&s, &what);
}
}
}
+7 -1
View File
@@ -36,7 +36,9 @@ pub static COMMANDS: &[CommandSpec] = &[
.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; \
gap: between flanges that meet at a corner, whose ends are closed toward each other at any angle); active?: bool; delete?: bool",
gap: between flanges that meet at a corner, whose ends are closed toward each other at any angle); \
corner_relief_shape?: notch|round|rectangular (what is cut where two bends meet at a corner of the base, `corner_relief` across; default notch); \
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)
@@ -269,6 +271,10 @@ fn rules(s: &mut Session, p: &Value) -> Result<Value> {
*slot = e;
}
}
if let Some(shape) = p.get("corner_relief_shape").filter(|v| !v.is_null()) {
let shape = shape.as_str().and_then(solidcraft_doc::sheet::ReliefShape::parse);
rule.corner_relief_shape = shape.ok_or_else(|| bad(cmd, "`corner_relief_shape` must be notch, round or rectangular"))?.name().into();
}
let (vals, _) = s.doc.param_values();
let values = s.doc.rule_values(&vals, &rule).map_err(|e| bad(cmd, e.to_string()))?;
let doc = s.doc_mut();
+97
View File
@@ -2060,6 +2060,103 @@ fn closed_corners_reach_the_flat_pattern_and_the_dxf() {
}
}
#[test]
fn corner_reliefs_of_the_rule_reach_the_part_the_flat_pattern_and_the_dxf() {
use solidcraft_geom::Vec2;
// The tray at 72°: its bends meet at the corners of the base, s0 = 3·tan 36° in from the sheet's
// edges, and are BA = 2θ wide in the flat. A relief 8 mm across reaches r = 4 mm along both edges
// and takes, at each corner, r²·π/4 (round) or r² (rectangular) of the base and r of two bends.
let th = 72f64.to_radians();
let (s0, ba) = (3.0 * (th / 2.0).tan(), th * 2.0);
let corners = [(s0, s0), (120.0 - s0, s0), (120.0 - s0, 90.0 - s0), (s0, 90.0 - s0)];
let export = |s: &mut Session| run(s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
let mut notch = Session::default();
alu_tray(&mut notch, 72.0, json!({"corner_relief": "8 mm"}));
let v0 = volume(&mut notch);
assert_eq!(export(&mut notch).matches("0\nARC\n").count(), 0);
run(&mut notch, "sheet.unfold", json!({}));
let flat0 = volume(&mut notch);
for (shape, of_the_base) in [("round", 4.0 * PI), ("rectangular", 16.0)] {
let mut s = Session::default();
alu_tray(&mut s, 72.0, json!({"corner_relief_shape": shape, "corner_relief": "8 mm"}));
assert!(s.model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{shape}: {:?}", s.model.results);
let listed = run(&mut s, "sheet.rules", json!({}));
assert_eq!(listed["rules"][0]["rule"]["corner_relief_shape"], shape);
assert_eq!(listed["rules"][0]["values"]["corner_relief_shape"], shape);
// The folded part loses what the flat pattern loses (K = 0.5 keeps the volume of a bend).
let (v, want) = (volume(&mut s), 4.0 * (of_the_base + 8.0 * ba) * 2.0);
assert!((v0 - v - want).abs() < 0.1, "{shape}: {v0} - {v} vs {want}");
// The bend table: every bend is 8 mm shorter.
let f = flat(&mut s);
let lengths: Vec<f64> =
f["bends"].as_array().map(|b| b.iter().map(|b| b["length"].as_f64().unwrap_or(f64::NAN)).collect()).unwrap_or_default();
for (got, edge) in lengths.iter().zip([120.0, 90.0, 120.0, 90.0]) {
assert!(lengths.len() == 4 && (got - (edge - 2.0 * s0 - 8.0)).abs() < 1e-6, "{shape}: {lengths:?}");
}
assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{shape}");
// The DXF: the round reliefs are arcs of radius 4 about the four corners, the square ones lines.
let dxf = export(&mut s);
let arcs: Vec<(Vec2, f64)> = solidcraft_io::read_dxf(&dxf)
.unwrap_or_default()
.iter()
.filter_map(|g| if let solidcraft_io::Geom2::Arc { c, a, .. } = g { Some((*c, a.dist(*c))) } else { None })
.collect();
if shape == "round" {
assert_eq!(arcs.len(), 4, "{arcs:?}");
for (x, y) in corners {
assert!(arcs.iter().any(|(c, r)| c.dist(Vec2::new(x, y)) < 1e-6 && (r - 4.0).abs() < 1e-6), "({x}, {y}): {arcs:?}");
}
} else {
assert!(arcs.is_empty() && dxf.contains(&format!("{:.6}\n20\n{:.6}\n", s0 + 4.0, s0 + 4.0)), "{arcs:?}");
}
// Unfold and refold keep it, and a cut made folded still goes through: a Ø20 hole in the base.
run(&mut s, "sheet.unfold", json!({}));
assert!((flat0 - volume(&mut s) - want).abs() < 0.1, "{shape}: unfolded {flat0} - {} vs {want}", volume(&mut s));
run(&mut s, "sheet.refold", json!({}));
assert!((volume(&mut s) - v).abs() < 1e-6 * v, "{shape}");
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}));
assert!((v - volume(&mut s) - PI * 100.0 * 2.0).abs() < 0.5, "{shape}: {v} - {}", volume(&mut s));
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((volume(&mut reopened) - volume(&mut s)).abs() < 1e-6, "{shape}");
}
}
#[test]
fn the_corner_relief_shape_is_a_rule_setting_checked_and_undone() {
let mut s = Session::default();
alu_tray(&mut s, 72.0, json!({}));
let shape = |s: &mut Session| run(s, "sheet.rules", json!({}))["rules"][0]["rule"]["corner_relief_shape"].clone();
let v0 = volume(&mut s);
assert_eq!(shape(&mut s), "notch");
// Only the three names are shapes; a refused one changes nothing.
for wrong in [json!("star"), json!("square"), json!(""), json!(2)] {
let e = s.execute("sheet.manage_rules", &json!({"name": "AL2", "corner_relief_shape": wrong})).map(|_| ()).unwrap_err().to_string();
assert!(e.contains("`corner_relief_shape` must be notch, round or rectangular"), "{e}");
}
assert!(shape(&mut s) == "notch" && volume(&mut s) == v0);
// Changing the rule changes the parts made with it; other settings leave the shape alone.
let r = run(&mut s, "sheet.manage_rules", json!({"name": "AL2", "corner_relief_shape": "Round"}));
assert_eq!((&r["rule"]["corner_relief_shape"], &r["values"]["corner_relief_shape"]), (&json!("round"), &json!("round")));
let round = volume(&mut s);
assert!(round < v0 - 200.0, "{v0} -> {round}");
run(&mut s, "sheet.manage_rules", json!({"name": "AL2", "hem_gap": "0.02 mm"}));
assert!(shape(&mut s) == "round" && volume(&mut s) == round);
run(&mut s, "sheet.manage_rules", json!({"name": "AL2", "corner_relief_shape": null, "corner_relief": "6 mm"}));
assert!(shape(&mut s) == "round" && volume(&mut s) > round);
for _ in 0..3 {
run(&mut s, "edit.undo", json!({}));
}
assert!(shape(&mut s) == "notch" && volume(&mut s) == v0);
// A relief longer than the bends is left out and reported on the flange; the part is the notched one.
run(&mut s, "sheet.manage_rules", json!({"name": "AL2", "corner_relief_shape": "rectangular", "corner_relief": "200 mm"}));
let w = s.model.results.last().and_then(|r| r.warning.clone()).unwrap_or_default();
assert_eq!(w.matches("no corner relief at (").count(), 4, "{w}");
assert!(w.contains("would take a whole bend away") && volume(&mut s) == v0, "{w}");
}
#[test]
fn corners_left_open_are_reported_on_the_flange() {
let mut s = Session::default();
+17 -5
View File
@@ -32,6 +32,9 @@ const RULE_FIELDS: [(&str, &str); 8] = [
("hem_gap", "Hem Gap"),
("gap", "Flange Gap"),
];
/// The corner relief shapes the manager offers (the rule's `corner_relief_shape`) and their labels.
const RELIEF_SHAPES: [&str; 3] = ["notch", "round", "rectangular"];
const RELIEF_SHAPE_LABELS: [&str; 3] = ["Notch", "Round", "Rectangular"];
#[derive(Clone, Debug)]
pub enum Sm {
@@ -73,6 +76,8 @@ pub enum Sm {
pick: usize,
name: String,
values: Vec<String>,
/// The corner relief shape (index into `RELIEF_SHAPES`).
relief: usize,
active: bool,
loaded: Option<usize>,
},
@@ -148,7 +153,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
let active = app.session.doc.sheet.active.clone();
let rules = rule_list(app);
let pick = active.and_then(|a| rules.iter().position(|r| r.name == a)).unwrap_or(0);
(Sm::Rules { pick, name: String::new(), values: Vec::new(), active: true, loaded: None }, vec![])
(Sm::Rules { pick, name: String::new(), values: Vec::new(), relief: 0, active: true, loaded: None }, vec![])
}
"sheet.flat_pattern" => (Sm::FlatPattern { data: None, key: None, status: None }, vec![SelInput::new("Body (last if none)", BODIES, false)]),
_ => return None,
@@ -291,7 +296,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
}
Sm::Unfold { .. } => {}
Sm::Convert { rule } => rule_combo(app, ui, "sm_conv_rule", rule),
Sm::Rules { pick, name, values, active, loaded } => {
Sm::Rules { pick, name, values, relief, active, loaded } => {
let rules = rule_list(app);
let mut names: Vec<String> = rules.iter().map(|r| r.name.clone()).collect();
names.push("New rule…".into());
@@ -312,6 +317,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
base.hem_gap.clone(),
base.gap.clone(),
];
*relief = RELIEF_SHAPES.iter().position(|s| s.eq_ignore_ascii_case(base.corner_relief_shape.trim())).unwrap_or(0);
*active = app.session.doc.sheet_rule(None).name == base.name || *pick >= rules.len();
if *pick >= rules.len() {
*name = format!("Rule{}", rules.len() + 1);
@@ -323,7 +329,13 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
ui.text_edit_singleline(name);
ui.end_row();
}
for ((_, label), v) in RULE_FIELDS.iter().zip(values.iter_mut()) {
for ((key, label), v) in RULE_FIELDS.iter().zip(values.iter_mut()) {
if *key == "corner_relief" {
// The shape goes with its size, the row below.
row_label(ui, "Corner Relief Shape");
combo(ui, "sm_relief_shape", &RELIEF_SHAPE_LABELS, relief);
ui.end_row();
}
row_label(ui, label);
enter |= field(ui, v);
ui.end_row();
@@ -581,7 +593,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
}
("sheet.convert", p)
}
Sm::Rules { pick, name, values, active, .. } => {
Sm::Rules { pick, name, values, relief, active, .. } => {
let rules = rule_list(app);
let n = match rules.get(*pick) {
Some(r) => r.name.clone(),
@@ -590,7 +602,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
if n.is_empty() {
return Err("give the rule a name".into());
}
let mut p = json!({"name": n, "active": active});
let mut p = json!({"name": n, "active": active, "corner_relief_shape": RELIEF_SHAPES.get(*relief).copied().unwrap_or("notch")});
for ((k, _), v) in RULE_FIELDS.iter().zip(values) {
if !v.trim().is_empty() {
p[*k] = json!(v);
+41
View File
@@ -311,6 +311,47 @@ fn bend_table_picker_writes_the_current_body() {
assert_eq!(app.session.undo.len(), undo);
}
/// The rule manager's shape selector: picked in the dialog, the shape reaches the rule and the part
/// made with it, and the dialog shows it the next time.
#[test]
fn rules_dialog_picks_the_corner_relief_shape() {
let mut h = crate::scenario::Harness::new();
h.app
.run(
"sheet.manage_rules",
json!({"name": "AL2", "thickness": "2 mm", "k_factor": 0.5, "bend_radius": "1 mm", "gap": "0.5 mm", "active": true}),
)
.unwrap();
h.app.run("sketch.create", json!({"plane": "XY", "name": "Base"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [120, 90]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
h.app.run("sheet.flange", json!({"edges": [[60, 0, 2], [120, 45, 2], [60, 90, 2], [0, 45, 2]], "height": 20, "angle": "72 deg"})).unwrap();
// The rounds in the base of the tray, and the shape the open dialog shows.
let rounds = |app: &SolveApp| {
let state = app.session.model.state();
state.sheets.last().map(|s| s.base.outer.segs.iter().filter(|g| matches!(g, solidcraft_engine::geom::Seg2::Arc { .. })).count())
};
let shown = |app: &SolveApp| match app.dialog.as_ref().map(|d| &d.kind) {
Some(Kind::Sheet(Sm::Rules { relief, .. })) => Some(*relief),
_ => None,
};
assert_eq!(rounds(&h.app), Some(0));
h.step(&json!({"start": "sheet.manage_rules"})).unwrap();
h.frames(3);
assert_eq!(shown(&h.app), Some(0), "a rule starts with the notch");
h.step(&json!({"click": {"handle": "dialog:Corner Relief Shape"}})).unwrap();
h.step(&json!({"click": {"handle": "dialog:Corner Relief Shape:Round"}})).unwrap();
assert_eq!(shown(&h.app), Some(1));
h.step(&json!({"widget": "OK"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
assert_eq!(h.app.session.doc.sheet_rule(None).corner_relief_shape, "round");
assert_eq!(rounds(&h.app), Some(4), "one round at each corner of the tray");
h.step(&json!({"start": "sheet.manage_rules"})).unwrap();
h.frames(3);
assert_eq!(shown(&h.app), Some(1), "the dialog shows the rule's shape");
}
#[test]
fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
let mut app = plate();
@@ -0,0 +1,217 @@
[
{
"note": "Sheet Metal Rules: the Corner Relief Shape row sets the rule's corner_relief_shape, and the tray made with the rule follows. The bend on the 120 mm edge is 120 - 2*s0 long (s0 = 3*tan 36 deg) and 8 mm shorter with a relief 8 mm across at both its ends."
},
{
"call": "ui.set",
"params": {
"tab": "SHEET METAL"
}
},
{
"call": "engine.execute",
"params": {
"command": "sheet.manage_rules",
"params": {
"name": "Sheet 2 mm",
"thickness": "2 mm",
"k_factor": 0.5,
"bend_radius": "1 mm",
"gap": "0.5 mm",
"corner_relief": "8 mm",
"active": true
}
}
},
{
"call": "engine.execute",
"params": {
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Base"
}
}
},
{
"call": "engine.execute",
"params": {
"command": "sketch.rectangle.two_point",
"params": {
"p0": [
0,
0
],
"p1": [
120,
90
]
}
}
},
{
"call": "engine.execute",
"params": {
"command": "sketch.finish",
"params": {}
}
},
{
"call": "engine.execute",
"params": {
"command": "sheet.flange",
"params": {
"sketch": "Base"
}
}
},
{
"call": "engine.execute",
"params": {
"command": "sheet.flange",
"params": {
"edges": [
[
60,
0,
2
],
[
120,
45,
2
],
[
60,
90,
2
],
[
0,
45,
2
]
],
"height": 20,
"angle": "72 deg"
}
}
},
{
"call": "ui.view",
"params": {
"view": "iso"
}
},
{
"expect": {
"bodies": 1,
"errors": 0,
"query": {
"command": "sheet.flat_pattern",
"path": [
"bends",
0,
"length"
],
"value": 115.640744832
}
}
},
{
"start": "sheet.manage_rules"
},
{
"click": {
"handle": "dialog:Corner Relief Shape"
}
},
{
"click": {
"handle": "dialog:Corner Relief Shape:Round"
}
},
{
"widget": "OK"
},
{
"expect": {
"dialog": null,
"errors": 0,
"query": {
"command": "sheet.rules",
"path": [
"rules",
0,
"rule",
"corner_relief_shape"
],
"value": "round"
}
}
},
{
"expect": {
"query": {
"command": "sheet.flat_pattern",
"path": [
"bends",
0,
"length"
],
"value": 107.640744832
}
}
},
{
"shot": "s51_round_reliefs"
},
{
"start": "sheet.manage_rules"
},
{
"click": {
"handle": "dialog:Corner Relief Shape"
}
},
{
"click": {
"handle": "dialog:Corner Relief Shape:Rectangular"
}
},
{
"widget": "OK"
},
{
"expect": {
"dialog": null,
"errors": 0,
"query": {
"command": "sheet.rules",
"path": [
"rules",
0,
"rule",
"corner_relief_shape"
],
"value": "rectangular"
}
}
},
{
"expect": {
"query": {
"command": "sheet.flat_pattern",
"path": [
"bends",
0,
"length"
],
"value": 107.640744832
}
}
},
{
"shot": "s51_rectangular_reliefs"
}
]