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 <[email protected]> Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
"base: sketch + profiles? (rule?, flip?) | contour: sketch + curves (open chain of lines) + distance (rule?, flip?, reverse?) | \
|
||||
edge: edges: [[x,y,z] on top/bottom edges, or {point: [x,y,z], angle?, height?} for an edge with its own angle and height] + \
|
||||
height (to the outer face), angle? (default 90 deg), radius? (default: rule), \
|
||||
position?: inside|outside|middle, flip?, body?; type?: base|edge|contour (default from the inputs); name?, body_name? (the new body). \
|
||||
Thickness, K-factor and bend radius come from the rule (sheet.manage_rules); passing them here is refused",
|
||||
position?: inside|outside|middle, flip?, body?, corner_relief_shape?: notch|round|rectangular|tear and corner_relief? (size across): \
|
||||
the relief at the corners these flanges make (default: the rule's); \
|
||||
type?: base|edge|contour (default from the inputs); name?, body_name? (the new body). \
|
||||
Thickness, K-factor and bend radius come from the rule (sheet.manage_rules); passing them here is refused, \
|
||||
and so is the corner relief on a base or contour flange",
|
||||
),
|
||||
CommandSpec::new("sheet.perforate", "Perforate", perforate).at("SHEET METAL", "CREATE").icon("hem").params(
|
||||
"diameter, pitch: expr; margin?: expr (clear of edges, cut-outs and bends; default: the diameter); pattern?: staggered (60°, default) | square; body? - a perforation field over the whole flat sheet: holes in the flat pattern (perforations), the DXF (layer PERFORATION) and the measured volume, not cut into the B-rep (#732)",
|
||||
@@ -133,10 +136,20 @@ fn rule_param(s: &Session, p: &Value, cmd: &str) -> Result<Option<String>> {
|
||||
const RULE_ONLY: [&str; 8] =
|
||||
["thickness", "k_factor", "bend_radius", "relief_width", "relief_depth", "corner_relief", "corner_relief_shape", "hem_gap"];
|
||||
|
||||
/// The two rule values an edge flange may set for itself: the corner relief.
|
||||
const EDGE_RELIEF: [&str; 2] = ["corner_relief", "corner_relief_shape"];
|
||||
|
||||
fn flange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sheet.flange";
|
||||
let nested = p.get("base").filter(|b| b.is_object());
|
||||
if let Some(key) = RULE_ONLY.iter().find(|k| p.get(**k).is_some() || nested.is_some_and(|b| b.get(**k).is_some())) {
|
||||
let ty = match str_(p, "type") {
|
||||
Some(t) => t.to_ascii_lowercase(),
|
||||
None if p.get("edges").is_some() => "edge".into(),
|
||||
None if p.get("curves").is_some() => "contour".into(),
|
||||
None => "base".into(),
|
||||
};
|
||||
let refused = |k: &&&str| !(ty == "edge" && EDGE_RELIEF.contains(*k));
|
||||
if let Some(key) = RULE_ONLY.iter().filter(refused).find(|k| p.get(**k).is_some() || nested.is_some_and(|b| b.get(**k).is_some())) {
|
||||
return Err(bad(
|
||||
cmd,
|
||||
format!(
|
||||
@@ -145,12 +158,6 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
),
|
||||
));
|
||||
}
|
||||
let ty = match str_(p, "type") {
|
||||
Some(t) => t.to_ascii_lowercase(),
|
||||
None if p.get("edges").is_some() => "edge".into(),
|
||||
None if p.get("curves").is_some() => "contour".into(),
|
||||
None => "base".into(),
|
||||
};
|
||||
match ty.as_str() {
|
||||
"base" => {
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
@@ -194,7 +201,35 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, "`position` must be inside, outside or middle"));
|
||||
}
|
||||
let body = str_(p, "body").map(str::to_string);
|
||||
add_feature(s, p, FeatureKind::SheetFlange { edges, height, angle, radius, position, flip: bool_(p, "flip").unwrap_or(false), body })
|
||||
// The corner relief these flanges make, in place of the rule's; null is not given.
|
||||
let corner_relief_shape = match p.get("corner_relief_shape").filter(|v| !v.is_null()) {
|
||||
Some(v) => Some(
|
||||
v.as_str()
|
||||
.and_then(solidcraft_doc::sheet::ReliefShape::parse)
|
||||
.map(|shape| shape.name().to_string())
|
||||
.ok_or_else(|| bad(cmd, "`corner_relief_shape` must be notch, round, rectangular or tear"))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let corner_relief = expr(p, "corner_relief");
|
||||
if let Some(c) = &corner_relief {
|
||||
check_expr(s, c, Kind::Length, cmd, "corner_relief")?;
|
||||
}
|
||||
add_feature(
|
||||
s,
|
||||
p,
|
||||
FeatureKind::SheetFlange {
|
||||
edges,
|
||||
height,
|
||||
angle,
|
||||
radius,
|
||||
position,
|
||||
flip: bool_(p, "flip").unwrap_or(false),
|
||||
body,
|
||||
corner_relief_shape,
|
||||
corner_relief,
|
||||
},
|
||||
)
|
||||
}
|
||||
other => Err(bad(cmd, format!("unknown flange type `{other}` (base, edge or contour)"))),
|
||||
}
|
||||
|
||||
@@ -3082,6 +3082,226 @@ fn the_corner_relief_shape_is_a_rule_setting_checked_and_undone() {
|
||||
assert!(w.contains("would take a whole bend away") && volume(&mut s) == v0, "{w}");
|
||||
}
|
||||
|
||||
/// A 120 x 90 base in 2 mm aluminium (R 1, K 0.5, gap 0.5; `rule` adds to its settings) with two edge
|
||||
/// flange features, 20 high at 90°: `near` on the bottom and left edges, which meet at the corner at
|
||||
/// the origin, and `far` on the top and right edges, which meet at the corner at (120, 90). The two
|
||||
/// objects hold what each feature is given besides its edges.
|
||||
fn two_flange_groups(rule: Value, near: Value, far: Value) -> Session {
|
||||
let mut s = Session::default();
|
||||
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(&mut s, "sheet.manage_rules", r);
|
||||
base(&mut s, 120.0, 90.0);
|
||||
for (edges, own) in [(json!([[60, 0, 2], [0, 45, 2]]), near), (json!([[60, 90, 2], [120, 45, 2]]), far)] {
|
||||
let mut p = json!({"edges": edges, "height": 20, "angle": "90 deg"});
|
||||
if let (Some(p), Some(own)) = (p.as_object_mut(), own.as_object()) {
|
||||
p.extend(own.clone());
|
||||
}
|
||||
run(&mut s, "sheet.flange", p);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// What the DXF of the flat pattern draws of the corner reliefs: the arcs (centre, radius) and the
|
||||
/// lines' end points.
|
||||
fn relief_drawing(s: &mut Session) -> (Vec<(solidcraft_geom::Vec2, f64)>, Vec<solidcraft_geom::Vec2>) {
|
||||
let dxf = run(s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
let read = solidcraft_io::read_dxf(&dxf).unwrap_or_default();
|
||||
let arcs = read.iter().filter_map(|g| if let solidcraft_io::Geom2::Arc { c, a, .. } = g { Some((*c, a.dist(*c))) } else { None }).collect();
|
||||
let ends = read.iter().flat_map(|g| if let solidcraft_io::Geom2::Line(a, b) = g { vec![*a, *b] } else { Vec::new() }).collect();
|
||||
(arcs, ends)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_edge_flange_feature_chooses_the_corner_relief_of_its_own_corners() {
|
||||
use solidcraft_geom::Vec2;
|
||||
// At 90° the bends of a corner meet at (s0, s0) in from the sheet's edges, s0 = (R + T)·tan 45° = 3,
|
||||
// and are BA = π/2·(R + K·T) = π wide. A relief d across reaches r = d/2 along both bends and takes,
|
||||
// at its corner, r²·π/4 (round) or r² (rectangular) of the base and r of each bend, 2 mm thick.
|
||||
let (s0, ba) = (3.0, PI);
|
||||
let at = |ends: &[Vec2], x: f64, y: f64| ends.iter().any(|p| p.dist(Vec2::new(x, y)) < 1e-6);
|
||||
// The rule says notch, as it does by default: the corners are not relieved.
|
||||
let mut plain = two_flange_groups(json!({}), json!({}), json!({}));
|
||||
assert!(no_errors_or_warnings(&plain), "{:?}", plain.model.results);
|
||||
let v0 = volume(&mut plain);
|
||||
assert!(relief_drawing(&mut plain).0.is_empty());
|
||||
// The flanges at the origin ask for a round 6 mm across, those at (120, 90) for a rectangular 3 mm.
|
||||
let near = json!({"corner_relief_shape": "round", "corner_relief": "6 mm"});
|
||||
let far = json!({"corner_relief_shape": "Rectangular", "corner_relief": "3 mm"});
|
||||
let mut s = two_flange_groups(json!({}), near, far);
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
let listed = run(&mut s, "sheet.rules", json!({}));
|
||||
let rule = &listed["rules"][0];
|
||||
assert_eq!((&rule["rule"]["corner_relief_shape"], &rule["values"]["corner_relief_shape"]), (&json!("notch"), &json!("notch")));
|
||||
// The folded part loses what the two reliefs take.
|
||||
let (v, want) = (volume(&mut s), ((PI * 9.0 / 4.0 + 6.0 * ba) + (1.5 * 1.5 + 3.0 * ba)) * 2.0);
|
||||
assert!((v0 - v - want).abs() < 0.1, "{v0} - {v} vs {want}");
|
||||
// The DXF: one arc of radius 3 about the corner at the origin and nothing round at the other corner;
|
||||
// there the relief is a kite ending 1.5 mm in from the corner of the bends, and none at the origin.
|
||||
let (arcs, ends) = relief_drawing(&mut s);
|
||||
assert!(arcs.len() == 1 && arcs[0].0.dist(Vec2::new(s0, s0)) < 1e-6 && (arcs[0].1 - 3.0).abs() < 1e-6, "{arcs:?}");
|
||||
assert!(at(&ends, 120.0 - s0 - 1.5, 90.0 - s0 - 1.5), "the corner at (120, 90) is relieved with a rectangular relief");
|
||||
assert!(!at(&ends, s0 + 1.5, s0 + 1.5), "the corner at the origin is not");
|
||||
// Unfolding, refolding and reopening the design keep both.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!((volume(&mut s) - v).abs() < 1e-6 * v);
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert!((volume(&mut reopened) - v).abs() < 1e-6 * v);
|
||||
assert_eq!(relief_drawing(&mut reopened).0.len(), 1);
|
||||
// The size is an input of the feature like its height: another size moves the relief.
|
||||
let near_feature = s.doc.features.iter().find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::SheetFlange { .. }));
|
||||
let names = near_feature.map(|f| f.param_names.clone()).unwrap_or_default();
|
||||
assert_eq!(names.len(), 3, "height, angle and the relief: {names:?}");
|
||||
run(&mut s, "parameters.change", json!({"name": names[2], "expression": "4 mm"}));
|
||||
let arcs = relief_drawing(&mut s).0;
|
||||
assert!(arcs.len() == 1 && (arcs[0].1 - 2.0).abs() < 1e-6, "{arcs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edge_flange_feature_follows_the_rule_for_the_relief_it_leaves_out() {
|
||||
use solidcraft_geom::Vec2;
|
||||
let s0 = 3.0;
|
||||
let at = |ends: &[Vec2], x: f64, y: f64| ends.iter().any(|p| p.dist(Vec2::new(x, y)) < 1e-6);
|
||||
let round8 = json!({"corner_relief_shape": "round", "corner_relief": "8 mm"});
|
||||
// The rule says a round 8 mm across. A feature that asks for nothing gets it, one that asks for a
|
||||
// size alone keeps the rule's shape.
|
||||
let mut s = two_flange_groups(round8.clone(), json!({"corner_relief": "6 mm"}), json!({}));
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
let mut arcs = relief_drawing(&mut s).0;
|
||||
arcs.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
assert_eq!(arcs.len(), 2, "{arcs:?}");
|
||||
assert!(arcs[0].0.dist(Vec2::new(s0, s0)) < 1e-6 && (arcs[0].1 - 3.0).abs() < 1e-6, "{arcs:?}");
|
||||
assert!(arcs[1].0.dist(Vec2::new(120.0 - s0, 90.0 - s0)) < 1e-6 && (arcs[1].1 - 4.0).abs() < 1e-6, "{arcs:?}");
|
||||
// A shape alone keeps the rule's size.
|
||||
let mut s = two_flange_groups(round8, json!({"corner_relief_shape": "rectangular"}), json!({}));
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
let (arcs, ends) = relief_drawing(&mut s);
|
||||
assert!(arcs.len() == 1 && arcs[0].0.dist(Vec2::new(120.0 - s0, 90.0 - s0)) < 1e-6 && (arcs[0].1 - 4.0).abs() < 1e-6, "{arcs:?}");
|
||||
assert!(at(&ends, s0 + 4.0, s0 + 4.0), "a kite 8 mm across at the origin");
|
||||
// A change of the rule still reaches the feature that asks for nothing.
|
||||
run(&mut s, "sheet.manage_rules", json!({"name": "AL2", "corner_relief": "6 mm"}));
|
||||
let arcs = relief_drawing(&mut s).0;
|
||||
assert!(arcs.len() == 1 && (arcs[0].1 - 3.0).abs() < 1e-6, "{arcs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edge_flange_feature_can_tear_its_corners_under_a_notch_rule() {
|
||||
use std::f64::consts::SQRT_2;
|
||||
// Torn, the two bends of a corner run to the corner of the bends and each is cut straight to its
|
||||
// flange's end: at 90° it is grow = sin 90° − gap/√2 past the corner, a triangle BA·grow/2 at each of
|
||||
// the two bend ends, and a corner less at each (see a_tear_of_the_rule_reaches_the_part_the_flat_pattern_and_the_dxf).
|
||||
let (ba, grow) = (PI, 1.0 - 0.5 / SQRT_2);
|
||||
let mut notch = two_flange_groups(json!({}), json!({}), json!({}));
|
||||
let (v0, stepped) = (volume(&mut notch), outline_points(&flat(&mut notch)));
|
||||
let mut s = two_flange_groups(json!({}), json!({"corner_relief_shape": "tear"}), json!({}));
|
||||
assert!(no_errors_or_warnings(&s), "{:?}", s.model.results);
|
||||
let torn = s.model.state().sheets.last().map(|b| b.flanges.iter().map(|f| f.tear).collect::<Vec<_>>()).unwrap_or_default();
|
||||
assert_eq!(torn.iter().filter(|t| **t != (false, false)).count(), 2, "{torn:?}");
|
||||
let want = 2.0 * ba * grow / 2.0 * 2.0;
|
||||
assert!((volume(&mut s) - v0 - want).abs() < 0.05, "{} - {v0} vs {want}", volume(&mut s));
|
||||
assert_eq!(outline_points(&flat(&mut s)).len() + 2, stepped.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_edge_flange_takes_a_corner_relief_of_its_own() {
|
||||
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", "active": true}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [120, 90]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let features = s.doc.features.len();
|
||||
// A base flange and a contour flange take their relief from the rule, as before: refused, and
|
||||
// the message names the key.
|
||||
for (params, key) in [
|
||||
(json!({"sketch": "Base", "corner_relief_shape": "round"}), "corner_relief_shape"),
|
||||
(json!({"sketch": "Base", "corner_relief": "6 mm"}), "corner_relief"),
|
||||
(json!({"type": "base", "sketch": "Base", "corner_relief_shape": null}), "corner_relief_shape"),
|
||||
(json!({"sketch": "Base", "curves": ["l1"], "distance": 10, "corner_relief": "6 mm"}), "corner_relief"),
|
||||
(json!({"type": "contour", "sketch": "Base", "curves": ["l1"], "distance": 10, "corner_relief_shape": "tear"}), "corner_relief_shape"),
|
||||
(json!({"base": {"sketch": "Base", "corner_relief": "6 mm"}}), "corner_relief"),
|
||||
] {
|
||||
let e = s.execute("sheet.flange", ¶ms).map(|_| ()).unwrap_err().to_string();
|
||||
let today = format!(
|
||||
"`{key}` is not a sheet.flange parameter: the sheet metal rule sets it. Make a rule with sheet.manage_rules {{name, {key}, …}} and pass its name as `rule`"
|
||||
);
|
||||
assert!(e.contains(&today), "{params}: {e}");
|
||||
}
|
||||
assert_eq!(s.doc.features.len(), features, "a refused flange changes nothing");
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Base"}));
|
||||
// The rest of the rule stays with the rule for an edge flange as well, and a relief that is no shape
|
||||
// or no length is refused before anything changes.
|
||||
let features = s.doc.features.len();
|
||||
let edge = |extra: Value| {
|
||||
let mut p = json!({"edges": [[60, 0, 2], [0, 45, 2]], "height": 20});
|
||||
if let (Some(p), Some(extra)) = (p.as_object_mut(), extra.as_object()) {
|
||||
p.extend(extra.clone());
|
||||
}
|
||||
p
|
||||
};
|
||||
for (extra, want) in [
|
||||
(json!({"thickness": "3 mm"}), "`thickness` is not a sheet.flange parameter"),
|
||||
(json!({"relief_width": "1 mm"}), "`relief_width` is not a sheet.flange parameter"),
|
||||
(json!({"corner_relief_shape": "star"}), "`corner_relief_shape` must be notch, round, rectangular or tear"),
|
||||
(json!({"corner_relief_shape": 2}), "`corner_relief_shape` must be notch, round, rectangular or tear"),
|
||||
(json!({"corner_relief": "wide"}), "corner_relief"),
|
||||
(json!({"corner_relief": "30 deg"}), "corner_relief"),
|
||||
] {
|
||||
let e = s.execute("sheet.flange", &edge(extra.clone())).map(|_| ()).unwrap_err().to_string();
|
||||
assert!(e.contains(want), "{extra}: {e}");
|
||||
}
|
||||
assert_eq!(s.doc.features.len(), features);
|
||||
// Null is not given: the corners follow the rule. A shape is stored by its name.
|
||||
run(&mut s, "sheet.flange", edge(json!({"corner_relief_shape": null})));
|
||||
let mut torn = edge(json!({"corner_relief_shape": " Tear ", "corner_relief": 5}));
|
||||
torn["edges"] = json!([[60, 90, 2], [120, 45, 2]]);
|
||||
run(&mut s, "sheet.flange", torn);
|
||||
let saved: Value = serde_json::from_str(&s.doc.to_json()).unwrap_or_default();
|
||||
let own: Vec<(Value, Value)> = saved["features"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|f| f["type"] == "sheet_flange")
|
||||
.map(|f| (f["corner_relief_shape"].clone(), f["corner_relief"].clone()))
|
||||
.collect();
|
||||
assert_eq!(own, [(Value::Null, Value::Null), (json!("tear"), json!("5"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_corner_relief_of_an_edge_flange_is_saved_with_it_and_designs_without_one_save_as_before() {
|
||||
let flange_features = |s: &Session| -> Vec<Value> {
|
||||
let saved: Value = serde_json::from_str(&s.doc.to_json()).unwrap_or_default();
|
||||
saved["features"].as_array().cloned().unwrap_or_default().into_iter().filter(|f| f["type"] == "sheet_flange").collect()
|
||||
};
|
||||
// Without a relief of its own the feature saves what it always did: neither key.
|
||||
let plain = two_flange_groups(json!({}), json!({}), json!({}));
|
||||
let features = flange_features(&plain);
|
||||
assert_eq!(features.len(), 2);
|
||||
assert!(features.iter().all(|f| f.get("corner_relief_shape").is_none() && f.get("corner_relief").is_none()), "{features:?}");
|
||||
// With one, the file has it and a design read from it builds the same part.
|
||||
let near = json!({"corner_relief_shape": "round", "corner_relief": "6 mm"});
|
||||
let mut s = two_flange_groups(json!({}), near, json!({"corner_relief_shape": "tear"}));
|
||||
let features = flange_features(&s);
|
||||
assert_eq!((&features[0]["corner_relief_shape"], &features[0]["corner_relief"]), (&json!("round"), &json!("6 mm")));
|
||||
assert_eq!(features[1]["corner_relief_shape"], "tear");
|
||||
assert!(features[1].get("corner_relief").is_none(), "{}", features[1]);
|
||||
let saved = s.doc.to_json();
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
|
||||
assert_eq!(reopened.doc.features, s.doc.features);
|
||||
assert!((volume(&mut reopened) - volume(&mut s)).abs() < 1e-6);
|
||||
assert_eq!(relief_drawing(&mut reopened), relief_drawing(&mut s));
|
||||
// timeline.edit changes the relief of a saved feature, and null takes it back to the rule's.
|
||||
let id = s.doc.features.iter().rev().nth(1).map_or(0, |f| f.id);
|
||||
run(&mut s, "timeline.edit", json!({"feature": id, "set": {"corner_relief_shape": "rectangular"}}));
|
||||
assert!(relief_drawing(&mut s).0.is_empty());
|
||||
run(&mut s, "timeline.edit", json!({"feature": id, "set": {"corner_relief_shape": null, "corner_relief": null}}));
|
||||
assert!(flange_features(&s)[0].get("corner_relief_shape").is_none());
|
||||
assert!(flange_features(&s)[0].get("corner_relief").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corners_left_open_are_reported_on_the_flange() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -35,6 +35,9 @@ const RULE_FIELDS: [(&str, &str); 8] = [
|
||||
/// The corner relief shapes the manager offers (the rule's `corner_relief_shape`) and their labels.
|
||||
const RELIEF_SHAPES: [&str; 4] = ["notch", "round", "rectangular", "tear"];
|
||||
const RELIEF_SHAPE_LABELS: [&str; 4] = ["Notch", "Round", "Rectangular", "Tear"];
|
||||
/// The corner relief shapes an edge flange offers: the rule's (index 0, nothing of its own), then
|
||||
/// the shapes of `RELIEF_SHAPES` in order.
|
||||
const FLANGE_RELIEF_LABELS: [&str; 5] = ["Rule", "Notch", "Round", "Rectangular", "Tear"];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Sm {
|
||||
@@ -52,6 +55,10 @@ pub enum Sm {
|
||||
/// Edge flanges: the edges with an angle and/or a height of their own (the picked point,
|
||||
/// angle, height; an empty text means the dialog's), from the rows per edge.
|
||||
own: Vec<(Vec3, String, String)>,
|
||||
/// Edge flanges: the corner relief shape of their own (index into `FLANGE_RELIEF_LABELS`;
|
||||
/// 0 is the rule's) and its size (empty: the rule's).
|
||||
relief: usize,
|
||||
relief_size: String,
|
||||
},
|
||||
Hem {
|
||||
length: String,
|
||||
@@ -134,6 +141,8 @@ fn new_flange(ty: usize) -> Sm {
|
||||
reverse: false,
|
||||
rule: 0,
|
||||
own: Vec::new(),
|
||||
relief: 0,
|
||||
relief_size: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +208,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
|
||||
let t = Tokens::get();
|
||||
let mut enter = false;
|
||||
match k {
|
||||
Sm::Flange { ty, height, angle, radius, position, flip, distance, reverse, rule, own } => {
|
||||
Sm::Flange { ty, height, angle, radius, position, flip, distance, reverse, rule, own, relief, relief_size } => {
|
||||
*ty = flange_type(inputs);
|
||||
row_label(ui, "Type");
|
||||
ui.label(FLANGE_TYPES.get(*ty).copied().unwrap_or(""));
|
||||
@@ -220,6 +229,15 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
|
||||
crate::params_dialog::complete(ui, &r, radius);
|
||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
ui.end_row();
|
||||
// The relief at the corners these flanges make: the rule's unless set here.
|
||||
row_label(ui, "Corner Relief Shape");
|
||||
combo(ui, "sm_flange_relief", &FLANGE_RELIEF_LABELS, relief);
|
||||
ui.end_row();
|
||||
row_label(ui, "Corner Relief");
|
||||
let r = ui.add(egui::TextEdit::singleline(relief_size).hint_text(crate::i18n::tr_label("from the rule")));
|
||||
crate::params_dialog::complete(ui, &r, relief_size);
|
||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
ui.end_row();
|
||||
// With several edges, a row for each: an angle and a height of its own, left
|
||||
// empty for the values above (shown as the hints).
|
||||
let points = edge_point_list(inputs);
|
||||
@@ -514,7 +532,7 @@ fn flange_edges(inputs: &[SelInput], own: &[(Vec3, String, String)]) -> Vec<Valu
|
||||
pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
|
||||
let s = &app.session;
|
||||
let (cmd, mut p): (&str, Value) = match k {
|
||||
Sm::Flange { height, angle, radius, position, flip, distance, reverse, rule, own, .. } => {
|
||||
Sm::Flange { height, angle, radius, position, flip, distance, reverse, rule, own, relief, relief_size, .. } => {
|
||||
let mut p = match flange_type(inputs) {
|
||||
1 => {
|
||||
let mut p = json!({"type": "edge", "edges": flange_edges(inputs, own), "height": height, "angle": angle,
|
||||
@@ -522,6 +540,12 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
|
||||
if !radius.trim().is_empty() {
|
||||
p["radius"] = json!(radius);
|
||||
}
|
||||
if let Some(shape) = relief.checked_sub(1).and_then(|i| RELIEF_SHAPES.get(i)) {
|
||||
p["corner_relief_shape"] = json!(shape);
|
||||
}
|
||||
if !relief_size.trim().is_empty() {
|
||||
p["corner_relief"] = json!(relief_size);
|
||||
}
|
||||
p
|
||||
}
|
||||
2 => {
|
||||
@@ -667,7 +691,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
}
|
||||
(f, curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect())
|
||||
}
|
||||
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body } => {
|
||||
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
|
||||
if let Some(b) = body {
|
||||
extra.insert("body".into(), json!(b));
|
||||
}
|
||||
@@ -692,6 +716,11 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
reverse: false,
|
||||
rule: 0,
|
||||
own,
|
||||
relief: corner_relief_shape
|
||||
.as_deref()
|
||||
.and_then(|name| RELIEF_SHAPES.iter().position(|s| s.eq_ignore_ascii_case(name.trim())))
|
||||
.map_or(0, |i| i + 1),
|
||||
relief_size: corner_relief.clone().unwrap_or_default(),
|
||||
};
|
||||
(f, picked)
|
||||
}
|
||||
|
||||
@@ -365,6 +365,78 @@ fn rules_dialog_picks_the_corner_relief_shape() {
|
||||
assert_eq!((rounds(&h.app), torn), (Some(0), Some(4)));
|
||||
}
|
||||
|
||||
/// The Flange dialog's relief rows: Rule (the default) and an empty size leave the relief to the rule,
|
||||
/// a shape and a size reach the feature, Edit Feature shows them again, and Rule takes them back.
|
||||
#[test]
|
||||
fn flange_dialog_picks_a_corner_relief_of_its_own() {
|
||||
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();
|
||||
let edges: Vec<Sel> = [[60.0, 0.0, 2.0], [0.0, 45.0, 2.0]].iter().filter_map(|p| edge_sel(&h.app.session, Vec3::new(p[0], p[1], p[2]))).collect();
|
||||
assert_eq!(edges.len(), 2);
|
||||
h.app.session.selection = edges;
|
||||
h.app.start("sheet.flange");
|
||||
h.frames(3);
|
||||
let shown = |app: &SolveApp| match app.dialog.as_ref().map(|d| &d.kind) {
|
||||
Some(Kind::Sheet(Sm::Flange { relief, relief_size, .. })) => Some((*relief, relief_size.clone())),
|
||||
_ => None,
|
||||
};
|
||||
let relief = |app: &SolveApp| match &app.session.doc.features.last().map(|f| &f.kind) {
|
||||
Some(FeatureKind::SheetFlange { corner_relief_shape, corner_relief, .. }) => Some((corner_relief_shape.clone(), corner_relief.clone())),
|
||||
_ => None,
|
||||
};
|
||||
// Untouched, the dialog asks for nothing: the rule's relief.
|
||||
assert_eq!(shown(&h.app), Some((0, String::new())));
|
||||
let d = h.app.dialog.clone().unwrap();
|
||||
let c = apply_commands(&h.app, &d).unwrap();
|
||||
assert!(c[0].1.get("corner_relief_shape").is_none() && c[0].1.get("corner_relief").is_none(), "{}", c[0].1);
|
||||
// Round from the selector, 6 mm in the size row.
|
||||
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((2, String::new())));
|
||||
if let Some(Kind::Sheet(Sm::Flange { relief_size, .. })) = h.app.dialog.as_mut().map(|d| &mut d.kind) {
|
||||
*relief_size = "6 mm".into();
|
||||
}
|
||||
let d = h.app.dialog.clone().unwrap();
|
||||
let c = apply_commands(&h.app, &d).unwrap();
|
||||
assert_eq!((&c[0].1["corner_relief_shape"], &c[0].1["corner_relief"]), (&json!("round"), &json!("6 mm")));
|
||||
h.step(&json!({"widget": "OK"})).unwrap();
|
||||
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
|
||||
assert_eq!(relief(&h.app), Some((Some("round".into()), Some("6 mm".into()))));
|
||||
assert!(h.app.session.model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{:?}", h.app.session.model.results);
|
||||
let arcs = |app: &SolveApp| {
|
||||
app.session
|
||||
.model
|
||||
.state()
|
||||
.sheets
|
||||
.last()
|
||||
.map(|s| s.base.outer.segs.iter().filter(|g| matches!(g, solidcraft_engine::geom::Seg2::Arc { .. })).count())
|
||||
};
|
||||
assert_eq!(arcs(&h.app), Some(1), "the round is cut at the one corner the two flanges make");
|
||||
// Edit Feature shows both again; Rule and an empty size take the feature back to the rule's relief.
|
||||
let id = h.app.session.doc.features.last().map(|f| f.id).unwrap();
|
||||
let mut e = edit_dialog(&h.app, id, None).unwrap();
|
||||
assert!(matches!(&e.kind, Kind::Sheet(Sm::Flange { relief: 2, relief_size, .. }) if relief_size == "6 mm"));
|
||||
let c = apply_commands(&h.app, &e).unwrap();
|
||||
assert_eq!((&c[0].1["params"]["corner_relief_shape"], &c[0].1["params"]["corner_relief"]), (&json!("round"), &json!("6 mm")));
|
||||
if let Kind::Sheet(Sm::Flange { relief, relief_size, .. }) = &mut e.kind {
|
||||
*relief = 0;
|
||||
relief_size.clear();
|
||||
}
|
||||
let commands = apply_commands(&h.app, &e).unwrap();
|
||||
run_all(&mut h.app, commands);
|
||||
assert_eq!(relief(&h.app), Some((None, None)));
|
||||
assert_eq!(arcs(&h.app), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
|
||||
let mut app = plate();
|
||||
|
||||
Reference in new issue
Block a user