Why: every reference a user makes to a face or edge (fillet and chamfer edges, sketches on faces, shell/draft/offset faces, joints, appearances) is stored as a name and found again after any upstream edit. Those names were re-derived by sampling points of each face and voting, and pieces of a split face were numbered by where their centres lay. After an upstream edit that splits, merges, mirrors or shrinks a face, a fillet or sketch could silently re-attach to the wrong face, and nothing told the user. The kernel now reports exact history for its operations, so names follow it and fail loudly instead of guessing. - crates/doc/src/lineage.rs: a feature's kernel histories composed into one lineage over nameable operands (bodies before the feature, moved copies, shell cavities, tools named by role: sketch curve, cap, box side, imported face). Instrumented: extrude/revolve/sweep/loft tools, join/cut/intersect, lumps after a cut, fillet/chamfer, shell, draft, split face/body, pattern/mirror (bodies and features), combine, boundary fill, move/scale, offset/delete face, thread, import. - crates/doc/src/history_names.rs: names from lineage; split pieces told apart by the faces only they touch (F2:top#[F2:side:l4,F4:side:l4]), merged faces answer to every name (aliases), lump bodies ordered by their faces' names instead of shell order. - crates/doc/src/name_lookup.rs, piece_names.rs: references resolve by exact name, alias or bounding faces, else nearest piece with a warning; names worked out from geometry (operations without history) are reported when only geometry decided. - Format 14: names stored by older formats get a legacy mark and are found as the build that saved them found them (some geometric names name a face history names differently: a tube's outer wall was named after its inner circle). New corpus file saved by 9a979cf. - kernel: Body::lumps carries each piece's history over the whole. - Adversarial tests: split, merge, mirrored twins, shrink to a sliver, moving cut with body pieces, history-less bodies, certified sketch. - docs/book/src/dev/naming.md describes the real mechanism and why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
334 lines
14 KiB
Rust
334 lines
14 KiB
Rust
//! Finding a face or edge again by the name a feature stored for it.
|
|
//!
|
|
//! The face with that name is the face; else the one face answering to it (a face merged into
|
|
//! it, or the named piece told apart by the faces around it, which still bound it); else, when
|
|
//! several faces are pieces of the named one, the piece nearest the reference's point, and the
|
|
//! feature says the face was split. Names saved before format 14 ([`LEGACY_MARK`]) are found
|
|
//! among the names geometry gives, as the builds that saved them found them. A face whose name
|
|
//! was worked out from geometry, found where the reference's point no longer lies, is reported:
|
|
//! only geometry decided.
|
|
|
|
use solvecraft_geom::{Mesh, Vec3};
|
|
|
|
use crate::history_names::{Match, Names};
|
|
use crate::naming::{self, closest, edge_names, face_names, legacy_edge_names, legacy_face_names, strip_piece};
|
|
use crate::piece_names::{strip_edge_number, strip_edge_piece};
|
|
use crate::{ModelBody, ModelState};
|
|
|
|
/// The mark on a name saved before format 14 (`?F2:top#2`). Those names were worked out from
|
|
/// geometry, and a few of them name a face that exact history names differently (a tube's outer
|
|
/// wall was named after the nearest sketch curve to its centre, its inner circle): such a name is
|
|
/// found again among the names that geometry gives today, as the build that saved it did, so an
|
|
/// older design evaluates as it was saved. Picking the face again stores its exact name.
|
|
pub const LEGACY_MARK: char = '?';
|
|
|
|
/// A name saved before format 14, without its mark (`None`: an exact name).
|
|
pub fn legacy_name(name: &str) -> Option<&str> {
|
|
name.strip_prefix(LEGACY_MARK)
|
|
}
|
|
|
|
/// Where a stored edge name was found.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct EdgeHit {
|
|
/// A point on the edge: the reference's own point when it lies on it, else the edge's middle.
|
|
pub point: Vec3,
|
|
/// The named edge is not one edge any more: the piece nearest the point was taken.
|
|
pub split: bool,
|
|
/// Whether the names of the faces it separates follow exact history (and the edge is not
|
|
/// one of several between them, numbered by where they lie).
|
|
pub verified: bool,
|
|
/// Whether the reference's point still lies on the edge.
|
|
pub on_point: bool,
|
|
/// The name was saved before format 14 and found as that build did ([`LEGACY_MARK`]).
|
|
pub legacy: bool,
|
|
}
|
|
|
|
/// The edge of a body a stored edge name finds: the edge with that name; else the one edge
|
|
/// between the faces it names (followed through splits and merges of those faces); else the
|
|
/// piece of it nearest `near` (`split`). `Err` explains when there is none.
|
|
pub fn locate_edge(b: &ModelBody, name: &str, near: Vec3) -> Result<EdgeHit, String> {
|
|
if let Some(old) = legacy_name(name) {
|
|
return legacy_locate_edge(b, old, near).map_err(|_| format!("edge `{name}` no longer exists"));
|
|
}
|
|
let m = b.mesh();
|
|
let names = edge_names(b);
|
|
let faces = naming::names(b);
|
|
let mid = |i: usize| edge_mid(&m, i);
|
|
// Several edges between the same two faces are numbered by where they lie: not exact.
|
|
let verified = |i: usize| {
|
|
m.edge_faces.get(i).is_some_and(|fs| fs.iter().all(|f| faces.verified(*f as usize)))
|
|
&& names.get(i).is_some_and(|n| strip_edge_number(n).len() == n.len())
|
|
};
|
|
let exact = |i: usize| -> Result<EdgeHit, String> {
|
|
let on = edge_near(b, &m, i, near);
|
|
mid(i)
|
|
.map(|p| EdgeHit { point: if on { near } else { p }, split: false, verified: verified(i), on_point: on, legacy: false })
|
|
.ok_or_else(|| "edge".into())
|
|
};
|
|
if let Some(i) = names.iter().position(|n| n == name) {
|
|
return exact(i);
|
|
}
|
|
// By the faces it separates: the best way any edge matches.
|
|
let wanted = strip_edge_number(name);
|
|
let mut best: Option<(Match, Vec<usize>)> = None;
|
|
for (i, fs) in m.edge_faces.iter().enumerate() {
|
|
let [fa, fb] = fs.as_slice() else { continue };
|
|
let Some(level) = edge_match(&faces, *fa as usize, *fb as usize, wanted) else { continue };
|
|
keep_best(&mut best, level, i);
|
|
}
|
|
let (level, cands) = best.ok_or_else(|| format!("edge `{name}` no longer exists"))?;
|
|
if let ([i], true) = (cands.as_slice(), level >= Match::Piece) {
|
|
return exact(*i);
|
|
}
|
|
cands
|
|
.iter()
|
|
.filter_map(|i| mid(*i).map(|p| (p, *i)))
|
|
.min_by(|a, b| a.0.dist(near).total_cmp(&b.0.dist(near)))
|
|
.map(|(p, i)| EdgeHit { point: p, split: true, verified: verified(i), on_point: false, legacy: false })
|
|
.ok_or_else(|| format!("edge `{name}` no longer exists"))
|
|
}
|
|
|
|
/// Whether the picked point `near` lies on edge `i` of body `b` (whose mesh is `m`): then a
|
|
/// reference keeps it.
|
|
fn edge_near(b: &ModelBody, m: &Mesh, i: usize, near: Vec3) -> bool {
|
|
m.edges.get(i).is_some_and(|e| e.windows(2).any(|w| near.dist_to_segment(w[0], w[1]) < (b.body.size() * 2e-3).max(1e-3)))
|
|
}
|
|
|
|
/// The point of face `face` of a mesh nearest `p`, and how far it is.
|
|
fn nearest_on_face(m: &Mesh, face: usize, p: Vec3) -> Option<(f64, Vec3)> {
|
|
let mut best: Option<(f64, Vec3)> = None;
|
|
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
|
if *f as usize != face {
|
|
continue;
|
|
}
|
|
let Some([a, b, c]) = m.tri(t) else { continue };
|
|
let q = closest(p, a, b, c);
|
|
let d = q.dist(p);
|
|
if best.is_none_or(|x| d < x.0) {
|
|
best = Some((d, q));
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
/// The middle of edge `i` of a mesh (along its polyline).
|
|
fn edge_mid(m: &Mesh, i: usize) -> Option<Vec3> {
|
|
let e = m.edges.get(i)?;
|
|
let total: f64 = e.windows(2).map(|w| w[0].dist(w[1])).sum();
|
|
let mut acc = 0.0;
|
|
for w in e.windows(2) {
|
|
let l = w[0].dist(w[1]);
|
|
if acc + l >= total / 2.0 {
|
|
let t = if l > 0.0 { (total / 2.0 - acc) / l } else { 0.0 };
|
|
return Some(w[0] + (w[1] - w[0]) * t);
|
|
}
|
|
acc += l;
|
|
}
|
|
e.first().copied()
|
|
}
|
|
|
|
/// [`locate_edge`] for a name saved before format 14 (without its mark), as those builds found
|
|
/// it: the edge with that name among the names geometry gives, else the piece of it nearest
|
|
/// `near`.
|
|
fn legacy_locate_edge(b: &ModelBody, name: &str, near: Vec3) -> Result<EdgeHit, ()> {
|
|
let m = b.mesh();
|
|
let names = legacy_edge_names(b);
|
|
if let Some(i) = names.iter().position(|n| n == name) {
|
|
let on = edge_near(b, &m, i, near);
|
|
return edge_mid(&m, i)
|
|
.map(|p| EdgeHit { point: if on { near } else { p }, split: false, verified: false, on_point: on, legacy: true })
|
|
.ok_or(());
|
|
}
|
|
let stem = strip_edge_piece(name);
|
|
(0..names.len())
|
|
.filter(|i| names.get(*i).is_some_and(|n| strip_edge_piece(n) == stem))
|
|
.filter_map(|i| edge_mid(&m, i))
|
|
.min_by(|a, b| a.dist(near).total_cmp(&b.dist(near)))
|
|
.map(|p| EdgeHit { point: p, split: true, verified: false, on_point: false, legacy: true })
|
|
.ok_or(())
|
|
}
|
|
|
|
/// How well the stored edge name `wanted` (without its own number) names the edge between faces
|
|
/// `a` and `b`: the weaker of its two faces' matches, read every way the name splits into two
|
|
/// face names (a face name may hold `|` itself: a fillet face is named after its edge).
|
|
fn edge_match(faces: &Names, a: usize, b: usize, wanted: &str) -> Option<Match> {
|
|
let mut best = None;
|
|
for (i, _) in wanted.match_indices('|') {
|
|
let (Some(x), Some(y)) = (wanted.get(..i), wanted.get(i + 1..)) else { continue };
|
|
for (p, q) in [(a, b), (b, a)] {
|
|
if let (Some(m1), Some(m2)) = (crate::history_names::matches(faces, p, x), crate::history_names::matches(faces, q, y)) {
|
|
best = best.max(Some(m1.min(m2)));
|
|
}
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
/// Keep `item` among the best matches so far.
|
|
fn keep_best<T>(best: &mut Option<(Match, Vec<T>)>, level: Match, item: T) {
|
|
match best {
|
|
Some((b, items)) if *b == level => items.push(item),
|
|
Some((b, _)) if *b > level => {}
|
|
_ => *best = Some((level, vec![item])),
|
|
}
|
|
}
|
|
|
|
/// Where a stored face name was found.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct FaceHit {
|
|
/// The body (its index in the model's bodies).
|
|
pub body: usize,
|
|
/// The face (its index in the body).
|
|
pub face: usize,
|
|
/// The point on the face nearest the reference's point.
|
|
pub point: Vec3,
|
|
/// The named face is not one face any more (split, or found on several bodies): the piece
|
|
/// nearest the point was taken.
|
|
pub split: bool,
|
|
/// The named face was merged into this one.
|
|
pub merged: bool,
|
|
/// Whether the face's name follows exact history.
|
|
pub verified: bool,
|
|
/// Whether the reference's point still lies on the face (so a name worked out from geometry
|
|
/// and the pick agree).
|
|
pub on_point: bool,
|
|
/// The name was saved before format 14 and found as that build did ([`LEGACY_MARK`]).
|
|
pub legacy: bool,
|
|
}
|
|
|
|
impl FaceHit {
|
|
/// What a feature says about how it found a face by its stored name `name` (`None`: exactly).
|
|
pub fn note(&self, name: &str) -> Option<String> {
|
|
if self.split {
|
|
Some(format!("face `{name}` was split; using the piece nearest the picked point"))
|
|
} else if self.merged {
|
|
Some(format!("face `{name}` was merged into another face; using that face"))
|
|
} else if !self.verified && !self.on_point && !self.legacy {
|
|
Some(format!("face `{name}` was found by geometry, not exact history; check it"))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EdgeHit {
|
|
/// What a feature says about how it found an edge by its stored name `name` (`None`: exactly).
|
|
pub fn note(&self, name: &str) -> Option<String> {
|
|
if self.split {
|
|
Some(format!("edge `{name}` was split; using the piece nearest the picked point"))
|
|
} else if !self.verified && !self.on_point && !self.legacy {
|
|
Some(format!("edge `{name}` was found by geometry, not exact history; check it"))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The face a stored face name finds, on any body: the face with that name, or the one face
|
|
/// answering to it (merged into it, or the named piece told apart by the faces around it); else
|
|
/// the piece of it nearest `p` (`split`).
|
|
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`] 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) {
|
|
return legacy_locate(bodies, old, p);
|
|
}
|
|
let mut best: Option<(Match, Vec<(usize, &ModelBody, usize)>)> = None;
|
|
for (bi, b) in bodies {
|
|
let names = naming::names(b);
|
|
for face in 0..names.faces.len() {
|
|
if let Some(level) = crate::history_names::matches(&names, face, name) {
|
|
keep_best(&mut best, level, (bi, b, face));
|
|
}
|
|
}
|
|
}
|
|
let (level, faces) = best?;
|
|
let unique = faces.len() == 1 && level >= Match::Piece;
|
|
let mut nearest: Option<(f64, usize, &ModelBody, usize, Vec3)> = None;
|
|
for (bi, b, face) in faces {
|
|
if let Some((d, q)) = nearest_on_face(&b.mesh(), face, p)
|
|
&& nearest.as_ref().is_none_or(|n| d < n.0)
|
|
{
|
|
nearest = Some((d, bi, b, face, q));
|
|
}
|
|
}
|
|
let (distance, body, b, face, point) = nearest?;
|
|
let on_point = distance <= (b.body.size() * 1e-3).max(1e-3);
|
|
Some(FaceHit {
|
|
body,
|
|
face,
|
|
point,
|
|
split: !unique,
|
|
merged: level == Match::Alias,
|
|
verified: naming::names(b).verified(face),
|
|
on_point,
|
|
legacy: false,
|
|
})
|
|
}
|
|
|
|
/// [`locate_face`] for a name saved before format 14 (without its mark), as those builds found
|
|
/// it: among the names geometry gives, the face with that name (or a piece of the same face, if
|
|
/// nearer `p`), the nearest point on it.
|
|
fn legacy_locate<'a>(bodies: impl Iterator<Item = (usize, &'a ModelBody)>, name: &str, p: Vec3) -> Option<FaceHit> {
|
|
let stem = strip_piece(name);
|
|
let mut best: Option<(f64, usize, usize, Vec3, bool, f64)> = None;
|
|
for (bi, b) in bodies {
|
|
let names = legacy_face_names(b);
|
|
let tol = (b.body.size() * 1e-3).max(1e-3);
|
|
let m = b.mesh();
|
|
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
|
let Some(n) = names.get(*f as usize) else { continue };
|
|
let exact = n == name;
|
|
if !exact && strip_piece(n) != stem {
|
|
continue;
|
|
}
|
|
let Some([a, bb, c]) = m.tri(t) else { continue };
|
|
let q = closest(p, a, bb, c);
|
|
let d = q.dist(p) - if exact { 1e-9 } else { 0.0 };
|
|
if best.is_none_or(|x| d < x.0) {
|
|
best = Some((d, bi, *f as usize, q, !exact, tol));
|
|
}
|
|
}
|
|
}
|
|
best.map(|(d, body, face, point, split, tol)| FaceHit {
|
|
body,
|
|
face,
|
|
point,
|
|
split,
|
|
merged: false,
|
|
verified: false,
|
|
on_point: d <= tol,
|
|
legacy: true,
|
|
})
|
|
}
|
|
|
|
/// Whether face `index` of a body is the face a stored name names: that name (or one merged into
|
|
/// it); for a name saved before format 14, that name among the names those builds gave.
|
|
pub fn face_has_name(b: &ModelBody, index: usize, name: &str) -> bool {
|
|
match legacy_name(name) {
|
|
Some(old) => legacy_face_names(b).get(index).is_some_and(|n| n == old),
|
|
None => crate::history_names::matches(&naming::names(b), index, name).is_some_and(|m| m >= Match::Alias),
|
|
}
|
|
}
|
|
|
|
/// The faces of a body that are the stored name's face or pieces of it (by stem), as the name's
|
|
/// own era names them.
|
|
pub fn faces_with_stem(b: &ModelBody, name: &str) -> Vec<usize> {
|
|
let (names, wanted) = match legacy_name(name) {
|
|
Some(old) => (legacy_face_names(b), old),
|
|
None => (face_names(b), name),
|
|
};
|
|
let stem = strip_piece(wanted);
|
|
let aliases =
|
|
|i: usize| legacy_name(name).is_none() && naming::names(b).info.get(i).is_some_and(|x| x.aliases.iter().any(|a| strip_piece(a) == stem));
|
|
(0..names.len()).filter(|i| names.get(*i).is_some_and(|n| strip_piece(n) == stem) || aliases(*i)).collect()
|
|
}
|
|
|
|
/// [`locate_face`] on one body.
|
|
pub(crate) fn locate_on(body: &ModelBody, name: &str, p: Vec3) -> Option<FaceHit> {
|
|
locate_in(std::iter::once((0, body)), name, p)
|
|
}
|