A feature that names its body finds its named face there first (#1637) (#1643)

Thanks @rlartot.
This commit is contained in:
rlartot authored and GitHub committed 2026-10-16 06:26:23 -04:00
1 parent 64b0b674d2
commit 3126962cd4
3 files changed
+127 -2

No files matched your search

+88 -2
View File
@@ -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<Pla
let x = (wb - wa).normalized().ok_or_else(|| DocError::Invalid(format!("`{line}` has no length")))?;
Ok(Plane { origin: wa, x, y: ss.plane.normal() })
}
#[cfg(test)]
mod named_face_lookup_tests {
use super::*;
use crate::naming::FaceHit;
/// A row of `n` 10 mm cubes `Part0`, `Part1`… 20 mm apart along x, without history: their
/// faces are named from geometry, which meshes them.
fn parts(n: usize) -> 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<FaceHit>) -> 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);
}
}
+20
View File
@@ -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();
+19
View File
@@ -271,6 +271,25 @@ pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
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<FaceHit> {
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<Item = (usize, &'a ModelBody)>, name: &str, p: Vec3) -> Option<FaceHit> {
if let Some(old) = legacy_name(name) {