mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
A converted sheet takes its rule's values at its own thickness (#4328)
Convert to Sheet Metal keeps a shell's (and a plate's) own thickness, but the rule's values were evaluated with `Thickness` the rule's: under the default rule (2.5 mm, bend radius `Thickness`) an L 2 mm thick with its bend modelled R 2 was rebuilt bent R 2.5, losing 8.58 mm3 (2659.911 against 2668.496), and its gap and reliefs followed 2.5 mm too. The rule is now evaluated for the sheet's thickness (Document::rule_values_at): in the conversion once the shell's thickness is known, for a converted plate, and for the flanges, hems and folds added to an existing sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+13
-16
@@ -579,10 +579,7 @@ pub(super) fn sheet_eval(
|
||||
}
|
||||
let rr = match radius {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => {
|
||||
let rule = st.sheets.get(i).map(|s| s.rule.clone()).unwrap_or_default();
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.bend_radius
|
||||
}
|
||||
None => sheet_rule_values(doc, vals, st, i)?.bend_radius,
|
||||
};
|
||||
// The corner relief these flanges ask for in place of the rule's.
|
||||
let relief = sheet::ReliefOverride {
|
||||
@@ -693,10 +690,7 @@ pub(super) fn sheet_eval(
|
||||
let l = val(vals, length, Kind::Length)?;
|
||||
let g = match gap {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => {
|
||||
let rule = st.sheets.get(i).map(|s| s.rule.clone()).unwrap_or_default();
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.hem_gap
|
||||
}
|
||||
None => sheet_rule_values(doc, vals, st, i)?.hem_gap,
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
let picks = sheet_picks(sh, f, &crate::plain(edges), warning);
|
||||
@@ -724,10 +718,7 @@ pub(super) fn sheet_eval(
|
||||
let ang = val(vals, angle, Kind::Angle)?;
|
||||
let rr = match radius {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => {
|
||||
let rule = st.sheets.get(i).map(|s| s.rule.clone()).unwrap_or_default();
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.bend_radius
|
||||
}
|
||||
None => sheet_rule_values(doc, vals, st, i)?.bend_radius,
|
||||
};
|
||||
// A fixed point that touches no material at all (e.g. far above the sheet) still picks
|
||||
// the side it lies on, so a stored fold keeps recomputing; it says so (#371).
|
||||
@@ -767,7 +758,7 @@ pub(super) fn sheet_eval(
|
||||
FeatureKind::SheetConvert { body, face, rule, bends } if !bends.is_empty() => {
|
||||
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let rv = doc.rule_values(vals, &r)?;
|
||||
let values = |t: Option<f64>| doc.rule_values_at(vals, &r, t);
|
||||
// The bend edges by name first; a name that is gone keeps its point, with a warning.
|
||||
let mut picks = Vec::with_capacity(bends.len());
|
||||
for (i, p) in bends.iter().enumerate() {
|
||||
@@ -788,7 +779,7 @@ pub(super) fn sheet_eval(
|
||||
}
|
||||
let placement = doc.component_transform(doc.model_body_component(&mb));
|
||||
let body_to_world = doc.model_body_offset(&mb).map(|offset| crate::mat_mul(&placement, offset)).unwrap_or(placement);
|
||||
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks, &body_to_world)?;
|
||||
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &values, face.0, &picks, &body_to_world)?;
|
||||
for note in notes {
|
||||
append_warning(warning, note);
|
||||
}
|
||||
@@ -839,8 +830,7 @@ pub(super) fn sheet_eval(
|
||||
));
|
||||
}
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let mut rv = doc.rule_values(vals, &r)?;
|
||||
rv.thickness = t;
|
||||
let rv = doc.rule_values_at(vals, &r, Some(t))?;
|
||||
let mut sh = sheet::base_flange(body, &r.name, &rv, plane, Region2 { outer: region.outer.clone(), holes: Vec::new() });
|
||||
sh.holes = region.holes.iter().map(|h| Region2 { outer: h.ccw(), holes: Vec::new() }).collect();
|
||||
let mut work = sheet::FlatWork::new();
|
||||
@@ -2180,6 +2170,13 @@ fn sheet_picks(sh: &crate::sheet::SheetBody, f: &Feature, edges: &[Vec3], warnin
|
||||
out
|
||||
}
|
||||
|
||||
/// The values of sheet `i`'s rule for that sheet: `Thickness` in them is the sheet's own
|
||||
/// thickness, which a converted body takes from its walls.
|
||||
fn sheet_rule_values(doc: &Document, vals: &BTreeMap<String, Value>, st: &ModelState, i: usize) -> Result<sheet::RuleValues> {
|
||||
let sh = st.sheets.get(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
doc.rule_values_at(vals, &doc.sheet_rule(Some(&sh.rule)), Some(sh.t))
|
||||
}
|
||||
|
||||
/// Tag the flanges a feature just added (`F<feature>.<k>`).
|
||||
fn tag_flanges(sh: &mut crate::sheet::SheetBody, from: usize, feature: u64) {
|
||||
for (k, fl) in sh.flanges.iter_mut().enumerate().skip(from) {
|
||||
|
||||
+10
-1
@@ -476,8 +476,17 @@ impl Document {
|
||||
|
||||
/// Evaluate a rule: `Thickness` in its expressions is the rule's thickness.
|
||||
pub fn rule_values(&self, vals: &std::collections::BTreeMap<String, Value>, r: &SheetRule) -> Result<RuleValues> {
|
||||
self.rule_values_at(vals, r, None)
|
||||
}
|
||||
|
||||
/// Evaluate a rule for a sheet `thickness` thick (a converted body's own thickness), or the
|
||||
/// rule's thickness: `Thickness` in its expressions is the sheet's.
|
||||
pub fn rule_values_at(&self, vals: &std::collections::BTreeMap<String, Value>, r: &SheetRule, thickness: Option<f64>) -> Result<RuleValues> {
|
||||
let look = |n: &str| vals.get(n).copied().ok_or_else(|| DocError::Expr(format!("unknown parameter `{n}`")));
|
||||
let t = expr::eval_with(&r.thickness, &look)?.to_kind(Kind::Length)?;
|
||||
let t = match thickness {
|
||||
Some(t) => t,
|
||||
None => expr::eval_with(&r.thickness, &look)?.to_kind(Kind::Length)?,
|
||||
};
|
||||
if !(t > 0.0 && t < 1e4) {
|
||||
return Err(DocError::Invalid("the sheet thickness must be positive".into()));
|
||||
}
|
||||
|
||||
@@ -691,15 +691,16 @@ pub fn bend_edge_names(st: &crate::ModelState, body: &str, pts: &[Vec3]) -> Vec<
|
||||
pts.iter().zip(crate::naming::edge_names_at(b, pts)).map(|(p, name)| if on(p) { name } else { String::new() }).collect()
|
||||
}
|
||||
|
||||
/// Convert body `mb` to a sheet under the rule `rule` with values `rv`: `face` is a point on the
|
||||
/// fixed face and `bends` are points on the sharp edges that become bends, in the middle of
|
||||
/// chamfers or on bend cylinders that do (the body's coordinates, mm). See the module docs.
|
||||
/// `body_to_world` places diagnostic points in world coordinates. Geometry and picks stay local.
|
||||
/// Returns the sheet and a note for every corner or end of a bend not finished as the rule says.
|
||||
/// Convert body `mb` to a sheet under the rule `rule`, whose values for a sheet of a thickness
|
||||
/// (`None`: the rule's own) are `values`: `face` is a point on the fixed face and `bends` are
|
||||
/// points on the sharp edges that become bends, in the middle of chamfers or on bend cylinders
|
||||
/// that do (the body's coordinates, mm). See the module docs. `body_to_world` places diagnostic
|
||||
/// points in world coordinates. Geometry and picks stay local. Returns the sheet and a note for
|
||||
/// every corner or end of a bend not finished as the rule says.
|
||||
pub fn convert_body(
|
||||
mb: &ModelBody,
|
||||
rule: &str,
|
||||
rv: &RuleValues,
|
||||
values: &dyn Fn(Option<f64>) -> Result<RuleValues>,
|
||||
face: Vec3,
|
||||
bends: &[Vec3],
|
||||
body_to_world: &crate::Mat,
|
||||
@@ -708,9 +709,7 @@ pub fn convert_body(
|
||||
if bends.is_empty() || bends.len() > MAX_BENDS {
|
||||
return err(format!("pick 1…{MAX_BENDS} bend edges"));
|
||||
}
|
||||
if !(rv.gap >= 0.0 && rv.bend_radius >= 0.0) {
|
||||
return err("the rule's gap and bend radius must not be negative");
|
||||
}
|
||||
let rv = values(None)?;
|
||||
let (b, mesh) = (&mb.body, mb.mesh());
|
||||
let size = b.size();
|
||||
if !(size.is_finite() && size > 0.0) {
|
||||
@@ -763,6 +762,12 @@ pub fn convert_body(
|
||||
.flatten()
|
||||
.collect();
|
||||
let t = shell.map_or(rv.thickness, |d| (d * 1e9).round() / 1e9);
|
||||
// A shell's sheet is as thick as its walls: the rule's values that follow its thickness (a
|
||||
// bend radius of `Thickness`, the gap, the reliefs) follow the walls', not the rule's own.
|
||||
let rv = if shell.is_some() { values(Some(t))? } else { rv };
|
||||
if !(rv.gap >= 0.0 && rv.bend_radius >= 0.0) {
|
||||
return err("the rule's gap and bend radius must not be negative");
|
||||
}
|
||||
let r = rv.bend_radius;
|
||||
let joined = |x: usize, y: usize| {
|
||||
mesh.edge_faces.iter().any(|faces| faces.contains(&(x as u32)) && faces.contains(&(y as u32)))
|
||||
@@ -841,7 +846,7 @@ pub fn convert_body(
|
||||
}
|
||||
panels.insert(g, wall);
|
||||
}
|
||||
let mut sh = base_flange(&mb.name, rule, &RuleValues { thickness: t, ..*rv }, frame, Region2::default());
|
||||
let mut sh = base_flange(&mb.name, rule, &RuleValues { thickness: t, ..rv }, frame, Region2::default());
|
||||
// How far behind a face of the sheet the body's face `g` starts and ends: a hole's wall
|
||||
// reaches through the sheet, a pocket's ends on its floor, an island's starts at the face.
|
||||
let mut points: BTreeMap<usize, Vec<Vec3>> = BTreeMap::new();
|
||||
|
||||
@@ -632,6 +632,23 @@ fn a_channel_with_its_bends_modelled_folds_back_into_itself_under_its_own_radius
|
||||
assert!((volume(&mut s) - solid).abs() < 1e-3 * solid);
|
||||
}
|
||||
|
||||
/// A rule's values follow the sheet's own thickness (#4328): an L shell 2 thick, its bend
|
||||
/// modelled R 2 inside, converted under the default rule (2.5 thick, bends R `Thickness`) is a
|
||||
/// sheet 2 thick bent R 2, the solid unchanged; an edge flange added to it bends R 2 as well.
|
||||
#[test]
|
||||
fn a_converted_shell_takes_its_rules_values_at_its_own_thickness() {
|
||||
let mut s = Session::default();
|
||||
let (outer, _) = bent_section(&mut s, &[(0.0, 40.0), (0.0, 0.0), (60.0, 0.0)], 2.0, 2.0, 100.0)[0];
|
||||
let solid = volume(&mut s);
|
||||
run(&mut s, "sheet.convert", json!({"body": "Section", "face": [30, 0, 50], "bends": [outer]}));
|
||||
assert!((volume(&mut s) - solid).abs() < 1e-6 * solid, "{} vs {solid}", volume(&mut s));
|
||||
let f = flat(&mut s);
|
||||
assert_eq!((f["thickness"].as_f64(), f["bends"][0]["radius"].as_f64()), (Some(2.0), Some(2.0)), "{f}");
|
||||
run(&mut s, "sheet.flange", json!({"edges": [[0, 40, 50]], "height": 10}));
|
||||
let f = flat(&mut s);
|
||||
assert_eq!(f["bends"][1]["radius"].as_f64(), Some(2.0), "{f}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_filleted_edge_of_a_solid_block_is_a_bend() {
|
||||
// A block's bottom edge rounded R 5: the faces either side meet at the old sharp edge,
|
||||
|
||||
Reference in New Issue
Block a user