Persistent naming for sheet metal: base panel edges are named after the base sketch line they lie on, flange edges after their flange (F<feature>.<k>) and side (far / side0 / side1, top or bottom face); flanges and hems store edge names and pick by name first (a name that is gone keeps the point with a warning)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
c6cb3dd1e6
commit
05b38e0f5f
5 files changed
+185
-6
No files matched your search
@@ -1838,7 +1838,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::SheetHem { .. }
|
||||
| FeatureKind::SheetFold { .. }
|
||||
| FeatureKind::SheetUnfold { .. }
|
||||
| FeatureKind::SheetConvert { .. } => more::sheet_eval(doc, vals, f, st),
|
||||
| FeatureKind::SheetConvert { .. } => more::sheet_eval(doc, vals, f, st, warning),
|
||||
FeatureKind::Boss { .. } | FeatureKind::Lip { .. } | FeatureKind::SnapFit { .. } | FeatureKind::Rest { .. } => {
|
||||
more::plastic_eval(doc, vals, f, st)
|
||||
}
|
||||
|
||||
@@ -288,7 +288,13 @@ fn mirror_y(r: &Region2) -> Region2 {
|
||||
Region2 { outer: m(&r.outer), holes: r.holes.iter().map(|h| m(h).reversed()).collect() }
|
||||
}
|
||||
|
||||
pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
pub(super) fn sheet_eval(
|
||||
doc: &Document,
|
||||
vals: &BTreeMap<String, Value>,
|
||||
f: &Feature,
|
||||
st: &mut ModelState,
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<()> {
|
||||
use crate::sheet;
|
||||
match &f.kind {
|
||||
FeatureKind::SheetBase { sketch, profiles, rule, flip } => {
|
||||
@@ -303,7 +309,22 @@ pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Fea
|
||||
(ss.plane, region)
|
||||
};
|
||||
let name = new_name(st, f, k);
|
||||
let sh = sheet::base_flange(&name, &r.name, &rv, plane, region);
|
||||
let mut sh = sheet::base_flange(&name, &r.name, &rv, plane, region);
|
||||
// The sketch's lines name the base panel's edges.
|
||||
sh.base_curves = ss
|
||||
.sketch
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| !c.construction)
|
||||
.flat_map(|(ci, c)| {
|
||||
ss.sketch.segs(ci).into_iter().filter_map(move |sg| match sg {
|
||||
Seg2::Line { a, b } => Some((c.id.clone(), a, b)),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.map(|(id, a, b)| if *flip { (id, Vec2::new(a.x, -a.y), Vec2::new(b.x, -b.y)) } else { (id, a, b) })
|
||||
.collect();
|
||||
let solid = sh.solid()?;
|
||||
st.bodies.push(ModelBody::new(name, solid, f.id));
|
||||
st.sheets.push(sh);
|
||||
@@ -349,7 +370,10 @@ pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Fea
|
||||
if sh.flat {
|
||||
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
|
||||
}
|
||||
sh.add_flanges(edges, h, a, rr, position, *flip)?;
|
||||
let picks = sheet_picks(sh, f, edges, warning);
|
||||
let n0 = sh.flanges.len();
|
||||
sh.add_flanges(&picks, h, a, rr, position, *flip)?;
|
||||
tag_flanges(sh, n0, f.id);
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetHem { edges, length, gap, flip, body } => {
|
||||
@@ -363,9 +387,12 @@ pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Fea
|
||||
}
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
for e in edges {
|
||||
let picks = sheet_picks(sh, f, edges, warning);
|
||||
let n0 = sh.flanges.len();
|
||||
for e in &picks {
|
||||
sh.add_hem(*e, l, g, *flip)?;
|
||||
}
|
||||
tag_flanges(sh, n0, f.id);
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetFold { sketch, curve, a, b, angle, radius, position, flip, fixed, body } => {
|
||||
@@ -394,7 +421,9 @@ pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Fea
|
||||
if sh.flat {
|
||||
return Err(DocError::Invalid("refold the sheet before folding it".into()));
|
||||
}
|
||||
let n0 = sh.flanges.len();
|
||||
sh.add_fold(a, b, ang, rr, position, *flip, *fixed)?;
|
||||
tag_flanges(sh, n0, f.id);
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetUnfold { body, refold } => {
|
||||
@@ -843,3 +872,31 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
_ => Err(DocError::Invalid(format!("{} is not a plastic feature", f.name))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge picks of a sheet feature, moved onto their named edges (a name that is gone keeps the
|
||||
/// point, with a warning).
|
||||
fn sheet_picks(sh: &crate::sheet::SheetBody, f: &Feature, edges: &[Vec3], warning: &mut Option<String>) -> Vec<Vec3> {
|
||||
let mut gone = Vec::new();
|
||||
let out = edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| match f.edge_names.get(i).filter(|n| !n.is_empty()) {
|
||||
Some(n) => sh.edge_by_name(n, *p).unwrap_or_else(|| {
|
||||
gone.push(n.clone());
|
||||
*p
|
||||
}),
|
||||
None => *p,
|
||||
})
|
||||
.collect();
|
||||
if !gone.is_empty() {
|
||||
*warning = Some(format!("sheet edge(s) {} no longer exist; using the edges at the picked points", gone.join(", ")));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
fl.tag = format!("F{feature}.{}", k - from);
|
||||
}
|
||||
}
|
||||
+90
-1
@@ -160,6 +160,9 @@ pub struct SheetFlange {
|
||||
/// instead of a rectangle `leg` long.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub shape: Option<Vec<Vec2>>,
|
||||
/// Which feature made it: `F<feature>.<k>` (persistent edge names).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
/// A sheet metal body (see the module docs).
|
||||
@@ -179,6 +182,9 @@ pub struct SheetBody {
|
||||
pub flat: bool,
|
||||
/// Gap between flanges that meet at a corner.
|
||||
pub gap: 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)>,
|
||||
}
|
||||
|
||||
/// A piece of the sheet: a panel (None = the base) or a flange's bend zone.
|
||||
@@ -839,6 +845,7 @@ impl SheetBody {
|
||||
radius,
|
||||
leg: leg.max(0.0),
|
||||
shape: None,
|
||||
tag: String::new(),
|
||||
ext: (0.0, 0.0),
|
||||
});
|
||||
made.push(self.flanges.len() - 1);
|
||||
@@ -905,6 +912,7 @@ impl SheetBody {
|
||||
leg,
|
||||
ext: (0.0, 0.0),
|
||||
shape: None,
|
||||
tag: String::new(),
|
||||
});
|
||||
Ok(self.flanges.len() - 1)
|
||||
}
|
||||
@@ -968,7 +976,7 @@ impl SheetBody {
|
||||
n = n * -1.0;
|
||||
}
|
||||
let l = a2.dot(n);
|
||||
let probe = SheetFlange { parent: panel, p: a2, d, len: 1.0, n, angle, radius, leg: 0.0, ext: (0.0, 0.0), shape: None };
|
||||
let probe = SheetFlange { parent: panel, p: a2, d, len: 1.0, n, angle, radius, leg: 0.0, ext: (0.0, 0.0), shape: None, tag: String::new() };
|
||||
let ba = self.allowance(&probe);
|
||||
let s0 = match position {
|
||||
"centerline" | "center" | "centre" => l - ba / 2.0,
|
||||
@@ -1018,6 +1026,7 @@ impl SheetBody {
|
||||
leg,
|
||||
ext: (0.0, 0.0),
|
||||
shape: Some(ccw(moving)),
|
||||
tag: String::new(),
|
||||
});
|
||||
let i = self.flanges.len() - 1;
|
||||
// Flanges on the moving part's edges now hang off the fold and move with it.
|
||||
@@ -1040,6 +1049,69 @@ impl SheetBody {
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
/// Name a panel edge (flat coordinates a→b on `panel`, top or bottom face): base edges by the
|
||||
/// base sketch line they lie on, flange edges by the flange (its tag) and side.
|
||||
fn edge_name_of(&self, panel: Option<usize>, a: Vec2, b: Vec2, top: bool) -> Option<String> {
|
||||
let face = if top { "top" } else { "bottom" };
|
||||
let d = (b - a).normalized()?;
|
||||
match panel {
|
||||
None => {
|
||||
let on = |p: Vec2, s: Vec2, e: Vec2| {
|
||||
let t = e - s;
|
||||
t.len() > 1e-9 && ((p - s).cross(t) / t.len()).abs() < 1e-6
|
||||
};
|
||||
self.base_curves.iter().find(|(_, s, e)| on(a, *s, *e) && on(b, *s, *e)).map(|(id, _, _)| format!("base:{id}:{face}"))
|
||||
}
|
||||
Some(i) => {
|
||||
let f = self.flanges.get(i)?;
|
||||
if f.tag.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let side = if d.cross(f.d).abs() < 1e-6 {
|
||||
// Along the bend: the far edge (or the bend side, never picked).
|
||||
if ((a - f.p).dot(f.n)) > self.allowance(f) + 1e-6 { "far" } else { "bend" }
|
||||
} else if ((a + b) * 0.5 - f.p).dot(f.d) < f.len / 2.0 {
|
||||
"side0"
|
||||
} else {
|
||||
"side1"
|
||||
};
|
||||
Some(format!("{}:{side}:{face}", f.tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The persistent name of the sheet edge nearest a world point.
|
||||
pub fn edge_name(&self, pick: Vec3) -> Option<String> {
|
||||
let (panel, a, b, top, _) = self.edge_at(pick)?;
|
||||
self.edge_name_of(panel, a, b, top)
|
||||
}
|
||||
|
||||
/// A world point on the sheet edge with this name (the one nearest `near` if several).
|
||||
pub fn edge_by_name(&self, name: &str, near: Vec3) -> Option<Vec3> {
|
||||
let mut best: Option<(f64, Vec3)> = None;
|
||||
for (pi, r) in self.panels() {
|
||||
let pts = r.outer.ccw().polyline(1e-3);
|
||||
let m = pts.len();
|
||||
let w = self.world(pi);
|
||||
for i in 0..m {
|
||||
let (Some(&a), Some(&b)) = (pts.get(i), pts.get((i + 1) % m)) else { continue };
|
||||
if a.dist(b) < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
for (z, top) in [(0.0, false), (self.t, true)] {
|
||||
if self.edge_name_of(pi, a, b, top).as_deref() == Some(name) {
|
||||
let mid = (a + b) * 0.5;
|
||||
let q = apply_point(&w, Vec3::new(mid.x, mid.y, z));
|
||||
if best.is_none_or(|(d, _)| q.dist(near) < d) {
|
||||
best = Some((q.dist(near), q));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(_, q)| q)
|
||||
}
|
||||
|
||||
/// Map a region in a world plane to flat coordinates through a panel's placement.
|
||||
pub fn region_to_flat(&self, panel: Option<usize>, plane: &Plane, r: &Region2) -> Option<Region2> {
|
||||
let inv = mat_inverse(&self.world(panel))?;
|
||||
@@ -1239,6 +1311,7 @@ pub fn base_flange(body: &str, rule: &str, rv: &RuleValues, plane: Plane, region
|
||||
holes: Vec::new(),
|
||||
flat: false,
|
||||
gap: rv.gap,
|
||||
base_curves: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1289,6 +1362,7 @@ pub fn contour_flange(body: &str, rule: &str, rv: &RuleValues, plane: &Plane, pt
|
||||
holes: Vec::new(),
|
||||
flat: false,
|
||||
gap: rv.gap,
|
||||
base_curves: Vec::new(),
|
||||
};
|
||||
let (y0, y1) = if ysign > 0.0 { (0.0, width) } else { (-width, 0.0) };
|
||||
// First segment: the base panel.
|
||||
@@ -1320,6 +1394,7 @@ pub fn contour_flange(body: &str, rule: &str, rv: &RuleValues, plane: &Plane, pt
|
||||
leg: leg.max(0.0),
|
||||
ext: (0.0, 0.0),
|
||||
shape: None,
|
||||
tag: String::new(),
|
||||
};
|
||||
let ba = sheet.allowance(&f);
|
||||
sheet.flanges.push(f);
|
||||
@@ -1421,3 +1496,17 @@ mod tests {
|
||||
assert!(s.solid().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent names of the sheet edges nearest each world point (empty when none).
|
||||
pub fn sheet_edge_names(st: &crate::ModelState, pts: &[Vec3]) -> Vec<String> {
|
||||
pts.iter()
|
||||
.map(|p| {
|
||||
st.sheets
|
||||
.iter()
|
||||
.filter_map(|sh| sh.edge_at(*p).map(|e| (e.4, sh)))
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||||
.and_then(|(_, sh)| sh.edge_name(*p))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -263,6 +263,10 @@ pub(super) fn add_feature(s: &mut Session, p: &Value, kind: FeatureKind) -> Resu
|
||||
let st = s.model.state();
|
||||
(solvecraft_doc::edge_refs(&st, body, edges), solvecraft_doc::edge_names_for(&st, body, edges))
|
||||
}
|
||||
// Sheet metal edges are named by the sheet model.
|
||||
FeatureKind::SheetFlange { edges, .. } | FeatureKind::SheetHem { edges, .. } => {
|
||||
(Vec::new(), solvecraft_doc::sheet::sheet_edge_names(&s.model.state(), edges))
|
||||
}
|
||||
_ => (Vec::new(), Vec::new()),
|
||||
};
|
||||
let face_names = {
|
||||
|
||||
@@ -298,3 +298,32 @@ fn fold_through_cut_outs() {
|
||||
let e = c.execute("SheetMetalFoldCmd", &json!({"sketch": "FoldLine", "curve": "fl"})).map(|_| ()).unwrap_err().to_string();
|
||||
assert!(e.contains("not supported yet"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flange_edges_are_named_and_follow_the_base_sketch() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Base"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [100, 60]}));
|
||||
let d = run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 100}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"sketch": "Base"}));
|
||||
run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[100, 30, 2.5]], "height": 20, "name": "Side"}));
|
||||
let side = s.doc.find_feature("Side").cloned().unwrap();
|
||||
assert_eq!(side.edge_names, vec!["base:l2:top".to_string()]);
|
||||
let v0 = volume(&mut s);
|
||||
// Narrower base: the flange stays on the right edge (now at x = 40).
|
||||
let name = d["param"].as_str().or_else(|| d["name"].as_str()).map(str::to_string).unwrap_or_else(|| "d1".into());
|
||||
run(&mut s, "ChangeParameterCommand", json!({"name": name, "expression": "40"}));
|
||||
let b = run(&mut s, "MeasureCommand", json!({}))["bodies"][0]["bbox"].clone();
|
||||
let w = b["max"][0].as_f64().unwrap_or(0.0) - b["min"][0].as_f64().unwrap_or(0.0);
|
||||
assert!((w - 40.0).abs() < 1e-3 && b["max"][2].as_f64().unwrap_or(0.0) > 19.0, "{b}");
|
||||
assert!(volume(&mut s) < v0);
|
||||
let id = side.id;
|
||||
assert_eq!(s.model.result(id).and_then(|r| r.warning.clone()), None);
|
||||
// A hem on the flange's far edge is named after the flange.
|
||||
let top = b["max"][2].as_f64().unwrap_or(0.0);
|
||||
let x = b["max"][0].as_f64().unwrap_or(0.0);
|
||||
run(&mut s, "FusionSheetMetalHemFlangeCommand", json!({"edges": [[x, 30, top]], "length": 6, "name": "H"}));
|
||||
let h = s.doc.find_feature("H").cloned().unwrap();
|
||||
assert!(h.edge_names[0].starts_with(&format!("F{id}.0:far")), "{:?}", h.edge_names);
|
||||
}
|
||||
Reference in new issue
Block a user