fix: faces a sheet fold moves keep their names (#535) (#585)

Faces that a sheet fold or unfold moves keep their names: points on a moved panel are taken back through the panel they lie on to where the sheet held them before the feature, and named from the faces there. Un-ignores the #535 regression test. Fixes #535.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
This commit is contained in:
Jonas Wrede authored and GitHub committed 2026-10-13 04:28:27 -04:00
1 parent abff7a49bb
commit 9e2a1eb993
5 files changed
+71 -2

No files matched your search

+3
View File
@@ -253,6 +253,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
before,
depth: 1,
mats: vec![],
sheets: vec![],
})));
let captured = crate::naming::face_name_at_body(&old_body, Vec3::new(20., 6., 1.)).unwrap();
assert_eq!(crate::naming::parent_piece(&captured), Some(stem.as_str()), "actual strip capture records a piece name");
@@ -376,6 +377,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
before,
depth: 1,
mats: vec![],
sheets: vec![],
})));
let t = std::f64::consts::FRAC_1_SQRT_2;
let at = Vec3::new(34. + 6. * t, 24. + 6. * t, 1.);
@@ -515,6 +517,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
before,
depth: 1,
mats: vec![],
sheets: vec![],
})));
let at = Vec3::new(0., 15., 1.);
let former = crate::naming::face_name_at_body(&old_body, at).unwrap();
+2 -1
View File
@@ -364,7 +364,8 @@ impl ModelState {
fn stamp(&mut self, before: &Arc<ModelState>, f: &Feature, key: u64, mats: Vec<Mat>) {
let feature = f.id;
let depth = before.bodies.iter().filter_map(|b| b.names.origin.as_ref().map(|o| o.depth)).max().unwrap_or(0) + 1;
let origin = Arc::new(crate::naming::NamingOrigin { feature: f.clone(), before: before.clone(), depth, mats });
let sheets = if f.kind.is_sheet() { self.sheets.clone() } else { Vec::new() };
let origin = Arc::new(crate::naming::NamingOrigin { feature: f.clone(), before: before.clone(), depth, mats, sheets });
// Pieces a cut left are named after their faces, not the kernel's shell order (unless
// the feature was saved when they were named in that order).
if !f.shell_piece_order {
+20
View File
@@ -31,6 +31,8 @@ pub struct NamingOrigin {
pub depth: usize,
/// Instance transforms of a pattern or mirror (its copies are named after their sources).
pub mats: Vec<crate::Mat>,
/// The sheet metal bodies after a sheet feature (their folds place moved faces).
pub sheets: Vec<crate::sheet::SheetBody>,
}
/// The lazily computed names of a body, shared by its clones.
@@ -334,6 +336,24 @@ fn geometric_from(b: &ModelBody, o: &NamingOrigin, nf: usize, names_of: fn(&Mode
names = got.into_iter().map(|n| n.map(|n| format!("{n}@F{fid}.{}", k + 1))).collect();
}
}
// A sheet feature that moved panels (a fold, an unfold): faces it carried along lie on the
// faces they were, taken back through the panel they are on.
if let (Some(after), Some(before), Some(input)) = (
o.sheets.iter().find(|s| s.body == b.name),
o.before.sheets.iter().find(|s| s.body == b.name),
inputs.iter().find(|(x, _, _)| x.name == b.name),
) {
let input = std::slice::from_ref(input);
for (name, ps) in names.iter_mut().zip(&pts) {
if name.is_some() {
continue;
}
let back: Option<Vec<Vec3>> = ps.iter().map(|p| after.point_before(before, *p)).collect();
if let Some(back) = back.filter(|b| !b.is_empty()) {
*name = vote(&back, input, tol, None);
}
}
}
let empty = Vec::new();
let names: Vec<String> = names
.into_iter()
+46
View File
@@ -1327,6 +1327,39 @@ impl SheetBody {
v
}
/// Where a world point of this sheet lay in `before`, the same sheet before a feature
/// changed its folds: the point goes back to flat coordinates through the panel it lies on
/// and is folded again the way `before` folded that material. A panel a fold made was part of
/// the panel it was folded out of. `None` off every panel (on a bend).
pub(crate) fn point_before(&self, before: &SheetBody, p: Vec3) -> Option<Vec3> {
const TOL: f64 = 1e-4;
for (pi, r) in self.panels() {
let Some(inv) = mat_inverse(&self.world(pi)) else { continue };
let q = apply_point(&inv, p);
if q.z < -TOL || q.z > self.t + TOL {
continue;
}
let q2 = Vec2::new(q.x, q.y);
let pts = dedup(&r.outer.ccw().polyline(1e-3));
if !point_in(&pts, q2) && !near_outline(&pts, q2, TOL) {
continue;
}
// Panels `before` did not have yet came out of their parent's (bounded walk).
let mut owner = pi;
for _ in 0..self.flanges.len() {
match owner {
Some(i) if i >= before.flanges.len() => owner = self.flanges.get(i).and_then(|f| f.parent),
_ => break,
}
}
if owner.is_some_and(|i| i >= before.flanges.len()) {
return None;
}
return Some(apply_point(&before.world(owner), q));
}
None
}
/// The panel edge nearest a world point: (panel, edge a, edge b in flat coordinates, on the
/// top face?, distance).
pub fn edge_at(&self, p: Vec3) -> Option<(Option<usize>, Vec2, Vec2, bool, f64)> {
@@ -2409,6 +2442,19 @@ fn merge_loops(polys: &[Vec<Vec2>], work: &mut FlatWork) -> Result<Vec<Vec<Vec2>
Ok(loops)
}
/// Whether `q` lies within `tol` of the closed polygon `poly`'s outline.
fn near_outline(poly: &[Vec2], q: Vec2, tol: f64) -> bool {
let m = poly.len();
(0..m).any(|i| match (poly.get(i), poly.get((i + 1) % m)) {
(Some(&a), Some(&b)) => {
let d = b - a;
let t = if d.len2() > 1e-18 { ((q - a).dot(d) / d.len2()).clamp(0.0, 1.0) } else { 0.0 };
(a + d * t).dist(q) < tol
}
_ => false,
})
}
fn signed_area(p: &[Vec2]) -> f64 {
let m = p.len();
(0..m).filter_map(|i| Some(p.get(i)?.cross(*p.get((i + 1) % m)?))).sum::<f64>() / 2.0
-1
View File
@@ -2478,7 +2478,6 @@ fn hole_face_names(s: &mut Session) -> Vec<String> {
}
#[test]
#[ignore = "fails until #535 is fixed"]
fn a_hole_on_a_folded_panel_keeps_the_name_of_its_sketch_circle() {
// A Ø10 hole at x = 20 on a 100 x 60 plate; the fold at x = 40 stands that (smaller) side up.
let mut s = Session::default();