diff --git a/crates/doc/src/eval_more.rs b/crates/doc/src/eval_more.rs index 90b106ff0b..7368c05da9 100644 --- a/crates/doc/src/eval_more.rs +++ b/crates/doc/src/eval_more.rs @@ -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| 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, st: &ModelState, i: usize) -> Result { + 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.`). fn tag_flanges(sh: &mut crate::sheet::SheetBody, from: usize, feature: u64) { for (k, fl) in sh.flanges.iter_mut().enumerate().skip(from) { diff --git a/crates/doc/src/sheet.rs b/crates/doc/src/sheet.rs index 652beba1d7..40a4025c31 100644 --- a/crates/doc/src/sheet.rs +++ b/crates/doc/src/sheet.rs @@ -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, r: &SheetRule) -> Result { + 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, r: &SheetRule, thickness: Option) -> Result { 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())); } diff --git a/crates/doc/src/sheet_convert.rs b/crates/doc/src/sheet_convert.rs index ef9fd60ecd..90687e8bd8 100644 --- a/crates/doc/src/sheet_convert.rs +++ b/crates/doc/src/sheet_convert.rs @@ -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) -> Result, 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> = BTreeMap::new(); diff --git a/crates/engine/src/cmd/sheet_convert_tests.rs b/crates/engine/src/cmd/sheet_convert_tests.rs index 1618b1a24e..7bf488583d 100644 --- a/crates/engine/src/cmd/sheet_convert_tests.rs +++ b/crates/engine/src/cmd/sheet_convert_tests.rs @@ -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,