Sheet metal: an Edge Flange can choose its own corner relief (#1481)
An Edge Flange feature can set the shape and size of the relief at the corners its flanges make, in place of the sheet metal rule's. Design format 24. Co-authored-by: NOWN-DEV <NOWN-DEV@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
22 files changed
+543
-43
No files matched your search
@@ -1013,6 +1013,15 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(body)]
|
||||
body: Option<String>,
|
||||
/// The relief cut where two of this feature's bends meet at a corner of the base, in place
|
||||
/// of the rule's: `corner_relief_shape` (notch | round | rectangular | tear) and
|
||||
/// `corner_relief`, the size across. Either one left out follows the rule.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
corner_relief_shape: Option<String>,
|
||||
/// Walked last, after the edges' values, so the inputs before it keep their names.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(length = "Corner relief")]
|
||||
corner_relief: Option<String>,
|
||||
},
|
||||
/// Flat hems on sheet edges.
|
||||
SheetHem {
|
||||
|
||||
@@ -449,7 +449,7 @@ pub(super) fn sheet_eval(
|
||||
st.sheets.push(sh);
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body } => {
|
||||
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
|
||||
let i = sheet_for(st, body, edges.first().map(|e| e.point))?;
|
||||
let (h, a) = (val(vals, height, Kind::Length)?, val(vals, angle, Kind::Angle)?);
|
||||
// Each edge's own height and angle, else the feature's.
|
||||
@@ -472,6 +472,20 @@ pub(super) fn sheet_eval(
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.bend_radius
|
||||
}
|
||||
};
|
||||
// The corner relief these flanges ask for in place of the rule's.
|
||||
let relief = sheet::ReliefOverride {
|
||||
shape: match corner_relief_shape {
|
||||
Some(name) => Some(
|
||||
sheet::ReliefShape::parse(name)
|
||||
.ok_or_else(|| DocError::Invalid(format!("unknown corner relief shape `{name}` (notch, round, rectangular or tear)")))?,
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
size: match corner_relief {
|
||||
Some(e) => Some(val(vals, e, Kind::Length)?),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
if sh.flat {
|
||||
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
|
||||
@@ -483,7 +497,7 @@ pub(super) fn sheet_eval(
|
||||
.map(|(pick, (height, angle))| sheet::EdgeFlange { pick, height, angle })
|
||||
.collect();
|
||||
let n0 = sh.flanges.len();
|
||||
let (_, notes) = sh.add_flanges_each(&each, rr, position, *flip)?;
|
||||
let (_, notes) = sh.add_flanges_each(&each, rr, position, *flip, relief)?;
|
||||
if !notes.is_empty() {
|
||||
let notes = notes.join("; ");
|
||||
*warning = Some(match warning.take() {
|
||||
|
||||
@@ -114,7 +114,7 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
|
||||
("sheet_contour", json!({"type": "sheet_contour", "sketch": 1, "curves": ["l1"], "distance": "contour.distance"})),
|
||||
(
|
||||
"sheet_flange",
|
||||
json!({"type": "sheet_flange", "edges": [{"point": o, "angle": "flange.e1_angle", "height": "flange.e1_height"}, {"point": o, "angle": null, "height": "flange.e2_height"}], "height": "flange.height", "angle": "flange.angle", "radius": "flange.radius"}),
|
||||
json!({"type": "sheet_flange", "edges": [{"point": o, "angle": "flange.e1_angle", "height": "flange.e1_height"}, {"point": o, "angle": null, "height": "flange.e2_height"}], "height": "flange.height", "angle": "flange.angle", "radius": "flange.radius", "corner_relief_shape": "round", "corner_relief": "flange.relief"}),
|
||||
),
|
||||
("sheet_hem", json!({"type": "sheet_hem", "edges": [o], "length": "hem.length", "gap": "hem.gap"})),
|
||||
(
|
||||
|
||||
@@ -243,7 +243,8 @@ sheet_flange
|
||||
Edge angle = flange.e1_angle (Angle)
|
||||
Edge height = flange.e1_height (Length)
|
||||
Edge height = flange.e2_height (Length)
|
||||
expressions: flange.height, flange.angle, flange.radius, flange.e1_angle, flange.e1_height, flange.e2_height
|
||||
Corner relief = flange.relief (Length)
|
||||
expressions: flange.height, flange.angle, flange.radius, flange.e1_angle, flange.e1_height, flange.e2_height, flange.relief
|
||||
sheet_hem
|
||||
inputs:
|
||||
Length = hem.length (Length)
|
||||
|
||||
@@ -233,13 +233,18 @@
|
||||
//! read through cadmpeg); its `step` is still the STEP text the file was restated as, so the
|
||||
//! feature's fields are unchanged and version 22 designs need no transformation; 0.13.x asks
|
||||
//! for an update before it meets these imports.
|
||||
//! - 24: an Edge Flange feature can choose its own corner relief (`corner_relief_shape`: notch,
|
||||
//! round, rectangular or tear, and `corner_relief`, the size across), for the corners its
|
||||
//! flanges make. Version 23 designs need no transformation: a feature without them follows the
|
||||
//! sheet metal rule, as it always did, and saves as before; 0.13.0 (which reads up to 22) asks
|
||||
//! for an update instead of silently building the rule's relief where a design asks for another.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
/// The format this build writes.
|
||||
pub const FORMAT: u32 = 23;
|
||||
pub const FORMAT: u32 = 24;
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
format!("solidcraft/{n}")
|
||||
@@ -256,7 +261,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; 22] = [
|
||||
const JSON_STEPS: [JsonStep; 23] = [
|
||||
|_| Ok(()),
|
||||
v2_to_v3,
|
||||
v3_to_v4,
|
||||
@@ -279,6 +284,7 @@ const JSON_STEPS: [JsonStep; 22] = [
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
];
|
||||
|
||||
/// Stored face and edge names were worked out from geometry before format 14; they get the
|
||||
@@ -1011,6 +1017,45 @@ fn normalise(d: &mut Document) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 24 added the corner relief of an edge flange: a no-op step carries a 23 design up, a flange
|
||||
/// without a relief of its own saves as before, and one with it is saved under 24 and kept.
|
||||
#[test]
|
||||
fn version_twenty_three_designs_open_and_an_edge_flange_relief_is_saved_in_twenty_four() {
|
||||
assert_eq!(JSON_STEPS.len(), FORMAT as usize - 1, "one upgrade step for each format");
|
||||
let flange = |shape: Option<&str>, size: Option<&str>| crate::FeatureKind::SheetFlange {
|
||||
edges: vec![crate::FlangeEdge::at(solidcraft_geom::Vec3::ZERO)],
|
||||
height: "10 mm".into(),
|
||||
angle: "90 deg".into(),
|
||||
radius: None,
|
||||
position: "inside".into(),
|
||||
flip: false,
|
||||
body: None,
|
||||
corner_relief_shape: shape.map(Into::into),
|
||||
corner_relief: size.map(Into::into),
|
||||
};
|
||||
let saved = |document: &Document| -> Value { serde_json::from_str(&document.to_json()).unwrap() };
|
||||
// A flange that follows the rule saves neither key, and a design of format 23 opens as it was.
|
||||
let mut plain = Document::new("A");
|
||||
plain.add_feature(flange(None, None), None).unwrap();
|
||||
let mut file = saved(&plain);
|
||||
let feature = file["features"].as_array().and_then(|f| f.last()).cloned().unwrap();
|
||||
assert_eq!(feature["type"], "sheet_flange");
|
||||
assert!(feature.get("corner_relief_shape").is_none() && feature.get("corner_relief").is_none(), "{feature}");
|
||||
file["format"] = Value::from("solidcraft/23");
|
||||
let opened = read(&file.to_string()).unwrap();
|
||||
assert_eq!(opened.format, format_string(FORMAT));
|
||||
assert_eq!(opened.features, plain.features);
|
||||
// One that chooses its relief says so in the file, which is stamped 24 and keeps it.
|
||||
let mut own = Document::new("A");
|
||||
own.add_feature(flange(Some("round"), Some("6 mm")), None).unwrap();
|
||||
let file = saved(&own);
|
||||
let feature = file["features"].as_array().and_then(|f| f.last()).cloned().unwrap();
|
||||
assert_eq!((&feature["corner_relief_shape"], &feature["corner_relief"]), (&Value::from("round"), &Value::from("6 mm")));
|
||||
// A build that reads up to 23 asks for an update on that stamp instead of dropping the relief.
|
||||
assert_eq!(version_of(file["format"].as_str().unwrap_or_default()), Some(FORMAT));
|
||||
assert_eq!(read(&file.to_string()).unwrap(), own);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_thirteen_names_keep_their_meaning_under_the_legacy_mark() {
|
||||
let file = serde_json::json!({
|
||||
|
||||
+39
-20
@@ -361,6 +361,15 @@ impl ReliefShape {
|
||||
}
|
||||
}
|
||||
|
||||
/// The corner relief an edge flange feature asks for in place of the rule's. A value left out
|
||||
/// follows the rule.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct ReliefOverride {
|
||||
pub shape: Option<ReliefShape>,
|
||||
/// How far across the relief is (mm).
|
||||
pub size: Option<f64>,
|
||||
}
|
||||
|
||||
/// A rule's values (mm, unit-less K).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
pub struct RuleValues {
|
||||
@@ -2151,7 +2160,7 @@ impl SheetBody {
|
||||
flip: bool,
|
||||
) -> Result<(Vec<usize>, Vec<String>)> {
|
||||
let each: Vec<EdgeFlange> = picks.iter().map(|&pick| EdgeFlange { pick, height, angle }).collect();
|
||||
self.add_flanges_each(&each, radius, position, flip)
|
||||
self.add_flanges_each(&each, radius, position, flip, ReliefOverride::default())
|
||||
}
|
||||
|
||||
/// Edge flanges that each have their own height and angle, in one go: every edge is trimmed
|
||||
@@ -2164,8 +2173,16 @@ impl SheetBody {
|
||||
/// the base that ends beside more of the base gets the rule's bend relief there
|
||||
/// (`relieve_bend_ends`). Where cut-outs have taken part of a picked edge away, the flange
|
||||
/// runs along the stretch they leave nearest the pick and takes its setback off that stretch
|
||||
/// only; an edge they took whole is refused.
|
||||
pub fn add_flanges_each(&mut self, edges: &[EdgeFlange], radius: f64, position: &str, flip: bool) -> Result<(Vec<usize>, Vec<String>)> {
|
||||
/// only; an edge they took whole is refused. The corners between these flanges are relieved
|
||||
/// as the rule says, except where `relief` names another shape or size.
|
||||
pub fn add_flanges_each(
|
||||
&mut self,
|
||||
edges: &[EdgeFlange],
|
||||
radius: f64,
|
||||
position: &str,
|
||||
flip: bool,
|
||||
relief: ReliefOverride,
|
||||
) -> Result<(Vec<usize>, Vec<String>)> {
|
||||
if edges.is_empty() || edges.len() > 1000 {
|
||||
return Err(DocError::Invalid("pick 1…1000 edges".into()));
|
||||
}
|
||||
@@ -2293,7 +2310,7 @@ impl SheetBody {
|
||||
for (&i, &free) in made.iter().zip(&freed) {
|
||||
notes.extend(self.relieve_bend_ends(i, free));
|
||||
}
|
||||
notes.extend(self.close_corners(&made));
|
||||
notes.extend(self.close_corners(&made, relief));
|
||||
notes.extend(self.close_return_corners(&made));
|
||||
Ok((made, notes))
|
||||
}
|
||||
@@ -2711,9 +2728,10 @@ impl SheetBody {
|
||||
out
|
||||
}
|
||||
|
||||
/// Close the corners the new flanges make, then cut the rule's relief at each (or tear it).
|
||||
/// Returns a note for every corner left open, without its relief or not torn.
|
||||
fn close_corners(&mut self, made: &[usize]) -> Vec<String> {
|
||||
/// Close the corners the new flanges make, then cut the relief at each (or tear it): the
|
||||
/// rule's, but for the shape or size `relief` names. Returns a note for every corner left
|
||||
/// open, without its relief or not torn.
|
||||
fn close_corners(&mut self, made: &[usize], relief: ReliefOverride) -> Vec<String> {
|
||||
let corners = self.corners(made);
|
||||
let mut notes = Vec::new();
|
||||
let mut closed = Vec::with_capacity(corners.len());
|
||||
@@ -2723,7 +2741,7 @@ impl SheetBody {
|
||||
Err(why) => notes.push(format!("the flanges at {} are left open: {why}", self.shown(k.c))),
|
||||
}
|
||||
}
|
||||
notes.extend(self.relieve_corners(&corners, &closed));
|
||||
notes.extend(self.relieve_corners(&corners, &closed, relief));
|
||||
notes
|
||||
}
|
||||
|
||||
@@ -2733,16 +2751,18 @@ impl SheetBody {
|
||||
format!("({:.3}, {:.3}, {:.3})", w.x, w.y, w.z)
|
||||
}
|
||||
|
||||
/// Cut the rule's relief at each of `corners`, or tear those of them that are `closed`.
|
||||
/// Returns a note for every corner without its relief or not torn.
|
||||
fn relieve_corners(&mut self, corners: &[Corner], closed: &[Corner]) -> Vec<String> {
|
||||
/// Cut the relief at each of `corners`, or tear those of them that are `closed`: the rule's,
|
||||
/// but for the shape or size `relief` names. Returns a note for every corner without its
|
||||
/// relief or not torn.
|
||||
fn relieve_corners(&mut self, corners: &[Corner], closed: &[Corner], relief: ReliefOverride) -> Vec<String> {
|
||||
let mut notes = Vec::new();
|
||||
match self.relief {
|
||||
let (shape, size) = (relief.shape.unwrap_or(self.relief), relief.size.unwrap_or(self.relief_size));
|
||||
match shape {
|
||||
ReliefShape::Notch => {}
|
||||
// The reliefs come last: they move the bends' ends, which is how the corners were found.
|
||||
ReliefShape::Round | ReliefShape::Rectangular => {
|
||||
for k in corners {
|
||||
if let Err(why) = self.relieve_corner(k, corners) {
|
||||
if let Err(why) = self.relieve_corner(k, corners, shape, size) {
|
||||
notes.push(format!("no corner relief at {}: {why}", self.shown(k.c)));
|
||||
}
|
||||
}
|
||||
@@ -2918,7 +2938,7 @@ impl SheetBody {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cut the rule's relief where two bends meet at a corner of the base, `relief_size / 2`
|
||||
/// Cut a relief `size` across where two bends meet at a corner of the base, `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
|
||||
@@ -2928,8 +2948,7 @@ impl SheetBody {
|
||||
/// 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;
|
||||
fn relieve_corner(&mut self, k: &Corner, corners: &[Corner], shape: ReliefShape, size: f64) -> std::result::Result<(), String> {
|
||||
if !(size >= MIN_CORNER_RELIEF) {
|
||||
return Err(format!("the relief size {size:.4} mm is below {MIN_CORNER_RELIEF} mm"));
|
||||
}
|
||||
@@ -2961,7 +2980,7 @@ impl SheetBody {
|
||||
}
|
||||
// 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 {
|
||||
let (relief, reach) = match shape {
|
||||
ReliefShape::Notch | ReliefShape::Tear => return Ok(()),
|
||||
ReliefShape::Round => (vec![Seg2::Arc { center: c, radius: r, start: da.angle(), sweep: -alpha }], r),
|
||||
ReliefShape::Rectangular => {
|
||||
@@ -6075,7 +6094,7 @@ mod tests {
|
||||
/// sheet and the notes on corners left open.
|
||||
fn flanges_each(base: Region2, rv: &RuleValues, each: &[EdgeFlange]) -> (SheetBody, Vec<String>) {
|
||||
let mut s = base_flange("Body1", "AL2", rv, Plane::XY, base);
|
||||
let notes = s.add_flanges_each(each, rv.bend_radius, "inside", false).unwrap().1;
|
||||
let notes = s.add_flanges_each(each, rv.bend_radius, "inside", false, ReliefOverride::default()).unwrap().1;
|
||||
(s, notes)
|
||||
}
|
||||
|
||||
@@ -6630,7 +6649,7 @@ mod tests {
|
||||
let (mut s, _) = tray(&rv, 90.0, 20.0);
|
||||
let each: Vec<EdgeFlange> =
|
||||
(0..4).map(|i| EdgeFlange { pick: far_edge_pick(&s, i, true), height: 12.0, angle: [90f64, 70.0][i % 2].to_radians() }).collect();
|
||||
let (made, notes) = s.add_flanges_each(&each, 1.0, "inside", false).unwrap();
|
||||
let (made, notes) = s.add_flanges_each(&each, 1.0, "inside", false, ReliefOverride::default()).unwrap();
|
||||
assert!(notes.is_empty() && made == [4, 5, 6, 7], "{made:?} {notes:?}");
|
||||
for (a, a_end, b, b_end) in TRAY_RETURNS {
|
||||
let (fa, fb) = (&s.flanges[a], &s.flanges[b]);
|
||||
@@ -6803,7 +6822,7 @@ mod tests {
|
||||
EdgeFlange { pick: far_edge_pick(&s, i, true), height: 12.0, angle: deg.to_radians() }
|
||||
})
|
||||
.collect();
|
||||
let (_, notes) = s.add_flanges_each(&each, 1.0, "inside", false).unwrap();
|
||||
let (_, notes) = s.add_flanges_each(&each, 1.0, "inside", false, ReliefOverride::default()).unwrap();
|
||||
assert!(notes.is_empty(), "{what}: {notes:?}");
|
||||
returns_keep_out_of_the_walls(&s, &what);
|
||||
holds_the_flat_material(&s, &what);
|
||||
|
||||
@@ -53,7 +53,7 @@ use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use solidcraft_geom::{Loop2, Mesh, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
use solidcraft_kernel::{Body, FaceSurface, face_surface};
|
||||
|
||||
use super::{FlatWork, RuleValues, SheetBody, SheetFlange, base_flange, point_in, polys_cross, signed_area, snap};
|
||||
use super::{FlatWork, ReliefOverride, RuleValues, SheetBody, SheetFlange, base_flange, point_in, polys_cross, signed_area, snap};
|
||||
use crate::{DocError, ModelBody, Result, apply_point, mat_inverse};
|
||||
|
||||
const MAX_BENDS: usize = 1000;
|
||||
@@ -1002,7 +1002,7 @@ pub fn convert_body(mb: &ModelBody, rule: &str, rv: &RuleValues, face: Vec3, ben
|
||||
Err(why) => notes.push(format!("the corner at {} stays cut open: {why}", sh.shown(k.c))),
|
||||
}
|
||||
}
|
||||
notes.extend(sh.relieve_corners(&corners, &closed));
|
||||
notes.extend(sh.relieve_corners(&corners, &closed, ReliefOverride::default()));
|
||||
Ok((sh, notes))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user