From 3126962cd42a2d0e723643c22ab365a153611007 Mon Sep 17 00:00:00 2001 From: rlartot <94693724+rlartot@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:26:23 +0200 Subject: [PATCH] A feature that names its body finds its named face there first (#1637) (#1643) Thanks @rlartot. --- crates/doc/src/eval.rs | 90 +++++++++++++++++++++++++++++- crates/doc/src/feature_geometry.rs | 20 +++++++ crates/doc/src/name_lookup.rs | 19 +++++++ 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 12003ecd..45f8ce45 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1171,7 +1171,8 @@ impl Model { b.made = crate::lineage::Made::Before; } let mut warning = None; - // Face references by name: move each face point onto its named face. + // Face references by name: move each face point onto its named face (on the body the + // feature names, when it names one and the face is there). let renamed; // Cylindrical emboss resolves its named face in the selected body's frame // and certifies an analytic anchor; mesh projection here would undo that. @@ -1191,7 +1192,7 @@ impl Model { if n.is_empty() { continue; } - match crate::naming::locate_face(&fst, n, *p) { + match crate::name_lookup::locate_face_on_body(&fst, f.kind.face_body(), n, *p) { Some(hit) => { if let Some(note) = hit.note(n) { notes.push(note); @@ -5294,3 +5295,88 @@ pub fn sketch_line_plane(st: &ModelState, sketch: u64, line: &str) -> Result ModelState { + let mut st = ModelState::default(); + for i in 0..n { + let x = 20.0 * i as f64; + let cube = kernel::box_solid(Vec3::new(x, 0.0, 0.0), Vec3::new(x + 10.0, 10.0, 10.0)).unwrap(); + st.bodies.push(ModelBody::new(format!("Part{i}"), cube, 1)); + } + st + } + + /// The name of the face of `body` at `p`. + fn name_at(st: &ModelState, body: &str, p: Vec3) -> String { + let b = st.body(body).unwrap(); + let face = crate::appearance::face_index_at(b, p).unwrap(); + crate::naming::face_names(b)[face].clone() + } + + /// Everything a lookup found, to compare two lookups. + fn found(hit: Option) -> String { + format!("{hit:?}") + } + + /// A shell late in a long timeline spent seconds meshing every body of the model to find its + /// face by name (#287). A feature that names its body finds the face there without naming + /// the other bodies, and finds what looking over every body finds. + #[test] + fn a_face_is_found_on_the_body_the_feature_names_without_meshing_the_others() { + let st = parts(12); + let top = Vec3::new(145.0, 5.0, 10.0); + let name = name_at(&st, "Part7", top); + let hit = crate::name_lookup::locate_face_on_body(&st, Some("Part7"), &name, top); + for b in st.bodies.iter().filter(|b| b.name != "Part7") { + assert!(b.mesh.get().is_none(), "{} was meshed", b.name); + } + let h = hit.unwrap(); + assert_eq!((h.body, h.split, h.merged), (7, false, false)); + assert_eq!(found(hit), found(crate::naming::locate_face(&st, &name, top))); + // Looking over every body names (and so meshes) them all. + assert!(st.bodies.iter().all(|b| b.mesh.get().is_some())); + } + + /// A name the named body has no face of (it moved to another body: a combine took the face), + /// a body that is not there, no body, a name saved before format 14 or a name no face has: + /// looked for over every body, as without a body. + #[test] + fn a_name_not_on_the_named_body_is_looked_for_over_every_body() { + let st = parts(4); + let top = Vec3::new(45.0, 5.0, 10.0); + let name = name_at(&st, "Part2", top); + for name in [name.clone(), format!("{}{name}", crate::naming::LEGACY_MARK), "Part2:face:99".into()] { + let everywhere = found(crate::naming::locate_face(&st, &name, top)); + for body in [Some("Part1"), Some("Part2"), Some("Nope"), None] { + assert_eq!(found(crate::name_lookup::locate_face_on_body(&st, body, &name, top)), everywhere, "{name} on {body:?}"); + } + } + let moved = crate::name_lookup::locate_face_on_body(&st, Some("Part1"), &name, top).unwrap(); + assert_eq!((moved.body, moved.split), (2, false)); + } + + /// A copy of a body has its source's face names. Over every body the name finds the face on + /// both, and the one nearest the point is taken as a piece of a split face. A feature on the + /// copy takes the copy's face, whole. + #[test] + fn the_same_face_on_a_copy_is_not_a_piece_of_the_named_bodys_face() { + let mut st = parts(2); + let source = st.bodies[0].clone(); + st.bodies[1].names = crate::naming::NameCell::same(&source); + let top = Vec3::new(25.0, 5.0, 10.0); + let name = name_at(&st, "Part1", top); + assert_eq!(name, name_at(&st, "Part0", Vec3::new(5.0, 5.0, 10.0))); + let on_copy = crate::name_lookup::locate_face_on_body(&st, Some("Part1"), &name, top).unwrap(); + let everywhere = crate::naming::locate_face(&st, &name, top).unwrap(); + assert_eq!((on_copy.body, on_copy.face, on_copy.point), (everywhere.body, everywhere.face, everywhere.point)); + assert_eq!((on_copy.split, everywhere.split), (false, true)); + assert_eq!(on_copy.note(&name), None); + } +} diff --git a/crates/doc/src/feature_geometry.rs b/crates/doc/src/feature_geometry.rs index 0994beeb..09a0a488 100644 --- a/crates/doc/src/feature_geometry.rs +++ b/crates/doc/src/feature_geometry.rs @@ -53,6 +53,26 @@ impl FeatureKind { v } + /// The body a feature that works on one body's faces names (shell, draft, split, offset, + /// delete and replace face, surface offset, the placed plastic features, sheet metal + /// conversion, ruled surface): its face points pick faces of that body. + pub fn face_body(&self) -> Option<&str> { + match self { + FeatureKind::Shell { body, .. } + | FeatureKind::Draft { body, .. } + | FeatureKind::SplitFace { body, .. } + | FeatureKind::OffsetFace { body, .. } + | FeatureKind::DeleteFace { body, .. } + | FeatureKind::ReplaceFace { body, .. } + | FeatureKind::SurfaceOffset { body, .. } + | FeatureKind::Boss { body, .. } + | FeatureKind::SnapFit { body, .. } + | FeatureKind::Rest { body, .. } => body.as_deref(), + FeatureKind::SheetConvert { body, .. } | FeatureKind::SurfaceRuled { body, .. } => Some(body.as_ref()), + _ => None, + } + } + /// The feature's profile selections, each with its sketch. pub fn profile_sels_mut(&mut self) -> Vec<(u64, &mut ProfileSel)> { let mut v = Vec::new(); diff --git a/crates/doc/src/name_lookup.rs b/crates/doc/src/name_lookup.rs index b65d25da..2b246530 100644 --- a/crates/doc/src/name_lookup.rs +++ b/crates/doc/src/name_lookup.rs @@ -271,6 +271,25 @@ pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option { locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p) } +/// [`locate_face`] for a feature that works on the faces of the body called `body` (a shell, a +/// draft…): the one face of that body with the very name, found without working out the names +/// of every other body (a body named from geometry is meshed for it: seconds in a large model). +/// Else as [`locate_face`], over every body: the name moved to another body (a combine took the +/// face), or on this body it names a piece of a split face or a merged one. The same face on +/// another body (a copy of this one) is not a piece of this body's face. +pub(crate) fn locate_face_on_body(st: &ModelState, body: Option<&str>, name: &str, p: Vec3) -> Option { + if legacy_name(name).is_none() + && let Some(i) = body.and_then(|n| st.bodies.iter().position(|b| b.name == n)) + && let Some(b) = st.bodies.get(i).filter(|b| !b.body.is_mesh()) + && let Some(hit) = locate_in(std::iter::once((i, b)), name, p) + && !hit.split + && naming::names(b).faces.get(hit.face).is_some_and(|n| n == name) + { + return Some(hit); + } + locate_face(st, name, p) +} + /// [`locate_face`] among `bodies` (with their indices). fn locate_in<'a>(bodies: impl Iterator, name: &str, p: Vec3) -> Option { if let Some(old) = legacy_name(name) {