//! Persistent names of faces and edges. //! //! A fillet stores the edges it rounds, a sketch the face it sits on: by name, so that an //! upstream edit that adds, removes, splits or reorders faces does not move them to another //! face. Every face of an evaluated body is named after how it was made: a face that came from a //! face of the feature's input keeps that face's name; a face new to the feature is named by its //! role (`F:side:`, `F:start`/`top`/`bottom`/`end`, //! `F:blend:`, `F:inner:`, …). Pieces of a split face are told //! apart by the faces around them (`F2:top#[F2:side:l4,F4:side:l4]`). Edges are named by the //! faces they separate (`|`, `~k` for several edges between the same two). //! //! Where the feature that made a body reports exact history (`crate::lineage`), the names follow //! it (`crate::history_names`). Bodies without history (made by operations that do not report //! it yet, imported meshes, legacy chains) are named from geometry: sample points of each face //! are looked up on the input's faces. Those names are marked unverified, and references that //! resolve through them say so. Names are worked out lazily, on first request. use std::sync::{Arc, OnceLock}; use solidcraft_geom::{Mesh, Seg2, Vec2, Vec3}; use crate::history_names::Names; use crate::lineage::Lineage; use crate::{Feature, FeatureKind, ModelBody, ModelState}; /// How a body came to be: the feature and the model just before it. #[derive(Debug)] pub struct NamingOrigin { pub feature: Feature, pub before: Arc, pub depth: usize, /// Instance transforms of a pattern or mirror (its copies are named after their sources). pub mats: Vec, /// The sheet metal bodies after a sheet feature (their folds place moved faces). pub sheets: Vec, } /// The lazily computed names of a body, shared by its clones. #[derive(Clone, Debug, Default)] pub struct NameCell { pub origin: Option>, /// What the body's faces came from, when the feature that made it reports it. lineage: Option>, names: Arc>>, /// The names designs saved before format 14 gave its faces (see [`LEGACY_MARK`]). legacy: Arc>>>, /// The body this one is a moved copy of (its legacy names are that body's). copy_of: Option>, } impl NameCell { pub fn new(origin: Option>) -> Self { Self::traced(origin, None) } /// Names of a body made by `origin`'s feature, following `lineage` where it is known. pub(crate) fn traced(origin: Option>, lineage: Option>) -> Self { NameCell { origin, lineage, ..NameCell::default() } } /// Names already known, taken as they are. pub fn known(names: Arc>) -> Self { let legacy = OnceLock::new(); let _ = legacy.set(names.clone()); NameCell { legacy: Arc::new(legacy), ..Self::shared(Arc::new(Names::given(names))) } } /// The names of `body` (a moved copy of a body keeps its faces' names, and how sure they are). /// /// Worked out on first request, from the source: moving a body (a transfer into a /// component, a placed instance) does not name every face of every moved body up front. pub fn same(body: &ModelBody) -> Self { NameCell { copy_of: Some(Arc::new(body.clone())), ..NameCell::default() } } fn shared(names: Arc) -> Self { let cell = OnceLock::new(); let _ = cell.set(names); NameCell { names: Arc::new(cell), ..NameCell::default() } } } pub use crate::name_lookup::{EdgeHit, FaceHit, LEGACY_MARK, face_has_name, faces_with_stem, legacy_name, locate_edge, locate_face, locate_one}; pub use crate::piece_names::parent_piece; /// Deeper chains than this are named by position only (bounded work and stack). const MAX_DEPTH: usize = 400; /// Up to this many sample points per face. const SAMPLES: usize = 9; /// Unit normals of each face's triangles. fn normals(m: &Mesh, nf: usize) -> Vec> { let mut per: Vec> = vec![Vec::new(); nf]; for (t, f) in m.triangles.iter().zip(&m.tri_face) { if let (Some(v), Some([a, b, c])) = (per.get_mut(*f as usize), m.tri(t)) && let Some(n) = (b - a).cross(c - a).normalized() { v.push(n); } } per } /// Triangle centroids spread over each face: face → points. fn samples(m: &Mesh, nf: usize) -> Vec> { let mut per: Vec> = vec![Vec::new(); nf]; for (t, f) in m.triangles.iter().zip(&m.tri_face) { if let (Some(v), Some([a, b, c])) = (per.get_mut(*f as usize), m.tri(t)) { v.push((a + b + c) * (1.0 / 3.0)); } } per.into_iter() .map(|v| { if v.len() <= SAMPLES { return v; } let step = v.len() as f64 / SAMPLES as f64; (0..SAMPLES).filter_map(|i| v.get((i as f64 * step) as usize).copied()).collect() }) .collect() } /// The point of triangle `abc` nearest `p`. A zero-length edge (a cone's tip repeats its apex /// vertex) has no interior to project onto, so its branch is skipped rather than dividing 0 by 0. pub(crate) fn closest(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 { let (ab, ac, ap) = (b - a, c - a, p - a); let (d1, d2) = (ab.dot(ap), ac.dot(ap)); if d1 <= 0.0 && d2 <= 0.0 { return a; } let bp = p - b; let (d3, d4) = (ab.dot(bp), ac.dot(bp)); if d3 >= 0.0 && d4 <= d3 { return b; } let vc = d1 * d4 - d3 * d2; if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 && d1 - d3 > 0.0 { return a + ab * (d1 / (d1 - d3)); } let cp = p - c; let (d5, d6) = (ab.dot(cp), ac.dot(cp)); if d6 >= 0.0 && d5 <= d6 { return c; } let vb = d5 * d2 - d1 * d6; if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 && d2 - d6 > 0.0 { return a + ac * (d2 / (d2 - d6)); } let va = d3 * d6 - d5 * d4; if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 && (d4 - d3) + (d5 - d6) > 0.0 { return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6))); } let den = va + vb + vc; if den.abs() < 1e-300 { return a; } a + ab * (vb / den) + ac * (vc / den) } /// A body's faces as triangle lists with bounding boxes, for "does this point lie on face f". struct FaceIndex { faces: Vec<(Vec3, Vec3, Vec<[Vec3; 3]>)>, } impl FaceIndex { fn new(m: &Mesh, nf: usize) -> FaceIndex { let mut faces: Vec<(Vec3, Vec3, Vec<[Vec3; 3]>)> = vec![(Vec3::new(f64::MAX, f64::MAX, f64::MAX), Vec3::new(f64::MIN, f64::MIN, f64::MIN), Vec::new()); nf]; for (t, f) in m.triangles.iter().zip(&m.tri_face) { if let (Some(slot), Some(tri)) = (faces.get_mut(*f as usize), m.tri(t)) { for p in tri { slot.0 = Vec3::new(slot.0.x.min(p.x), slot.0.y.min(p.y), slot.0.z.min(p.z)); slot.1 = Vec3::new(slot.1.x.max(p.x), slot.1.y.max(p.y), slot.1.z.max(p.z)); } slot.2.push(tri); } } FaceIndex { faces } } /// The face a point lies on (within `tol`). fn face_at(&self, p: Vec3, tol: f64) -> Option { let mut best: Option<(f64, usize)> = None; for (i, (lo, hi, tris)) in self.faces.iter().enumerate() { if p.x < lo.x - tol || p.y < lo.y - tol || p.z < lo.z - tol || p.x > hi.x + tol || p.y > hi.y + tol || p.z > hi.z + tol { continue; } let d = tris.iter().map(|[a, b, c]| closest(p, *a, *b, *c).dist(p)).fold(f64::MAX, f64::min); if d <= tol && best.is_none_or(|(bd, _)| d < bd) { best = Some((d, i)); } } best.map(|(_, i)| i) } } fn face_count(b: &ModelBody) -> usize { b.mesh().tri_face.iter().map(|f| *f as usize + 1).max().unwrap_or(0) } /// Names of a body's faces (by B-rep face index). pub fn face_names(b: &ModelBody) -> Arc> { names(b).faces.clone() } /// Whether face `index`'s name follows exact history (`false`: worked out from geometry, which an /// upstream edit can fool). pub fn face_name_exact(b: &ModelBody, index: usize) -> bool { names(b).verified(index) } /// A body's names, with what finding them again needs: from the history of the feature that /// made it where it reports one, else from geometry (unverified). pub(crate) fn names(b: &ModelBody) -> Arc { // A moved copy has its source's names (the source's cell keeps them for both). if let Some(source) = &b.names.copy_of { return b.names.names.get_or_init(|| names(source)).clone(); } b.names .names .get_or_init(|| { // Exact history needs no mesh; geometry does (its face count is the mesh's). let names = match b.names.origin.as_ref() { Some(o) if o.depth < MAX_DEPTH => b .names .lineage .as_ref() .and_then(|l| crate::history_names::derive(b, o, l)) .unwrap_or_else(|| Names::unverified(geometric_names(b, o, face_count(b)), &mesh_adjacency(b))), _ => Names::unverified((0..face_count(b)).map(|k| format!("{}:face:{k}", b.name)).collect(), &[]), }; Arc::new(names) }) .clone() } /// The names builds before format 14 gave a body's faces: from geometry, over the inputs' names /// as those builds gave them, pieces of a split face numbered along the longest axis of their /// spread (`#1`, `#2`, …). pub(crate) fn legacy_face_names(b: &ModelBody) -> Arc> { if let Some(source) = &b.names.copy_of { return legacy_face_names(source); } b.names .legacy .get_or_init(|| { let nf = face_count(b); let names = match b.names.origin.as_ref() { Some(o) if o.depth < MAX_DEPTH => { let mut names = geometric_from(b, o, nf, legacy_face_names); number_pieces(&mut names, &samples(&b.mesh(), nf), '#'); names } _ => (0..nf).map(|k| format!("{}:face:{k}", b.name)).collect(), }; Arc::new(names) }) .clone() } /// The names builds before format 14 gave a body's edges. pub(crate) fn legacy_edge_names(b: &ModelBody) -> Vec { edge_names_from(b, &legacy_face_names(b)) } /// Pairs of faces of a body that share an edge (from its mesh's edges). pub(crate) fn mesh_adjacency(b: &ModelBody) -> Vec<(usize, usize)> { crate::history_names::face_pairs(b.mesh().edge_faces.iter().map(|fs| fs.iter().map(|f| *f as usize))) } /// Work out a body's face names from the feature that made it and the model before it, by /// geometry: where each face's sample points lie on the input's faces. Faces sharing a name are /// pieces of one, told apart afterwards. pub(crate) fn geometric_names(b: &ModelBody, o: &NamingOrigin, nf: usize) -> Vec { geometric_from(b, o, nf, face_names) } /// [`geometric_names`], the input bodies' faces named by `names_of`. fn geometric_from(b: &ModelBody, o: &NamingOrigin, nf: usize, names_of: fn(&ModelBody) -> Arc>) -> Vec { // A rigid Move changes coordinates, not topology or face ownership. Geometric // overlap with the unmoved input cannot recover the names at the new position. if matches!(o.feature.kind, FeatureKind::Move { .. }) && let Some(input) = o.before.body(&b.name) { return names_of(input).as_ref().clone(); } let m = b.mesh(); let size = b.body.size().max(1e-6); let tol = (size * 1e-4).max(1e-6); let pts = samples(&m, nf); let bb = m.bounds(); // Inputs that overlap this body. let overlaps = |x: &ModelBody| { let xb = x.mesh().bounds(); !(xb.max.x < bb.min.x - tol || xb.min.x > bb.max.x + tol || xb.max.y < bb.min.y - tol || xb.min.y > bb.max.y + tol || xb.max.z < bb.min.z - tol || xb.min.z > bb.max.z + tol) }; // Independently created solids own their faces even when another body // occupies the same coordinates. Boolean operations still inherit inputs. let independent_solid = matches!( o.feature.kind, FeatureKind::Box { operation: crate::Operation::NewBody, .. } | FeatureKind::Cylinder { operation: crate::Operation::NewBody, .. } | FeatureKind::Sphere { operation: crate::Operation::NewBody, .. } | FeatureKind::Torus { operation: crate::Operation::NewBody, .. } | FeatureKind::Extrude { operation: crate::Operation::NewBody, .. } | FeatureKind::Revolve { operation: crate::Operation::NewBody, .. } ); let inputs: Vec<(&ModelBody, FaceIndex, Arc>)> = o .before .bodies .iter() .filter(|x| !independent_solid && !x.body.is_mesh() && overlaps(x)) .take(64) .map(|x| { let n = face_count(x); (x, FaceIndex::new(&x.mesh(), n), names_of(x)) }) .collect(); let fid = o.feature.id; let ns = normals(&m, nf); let mut names: Vec> = pts.iter().map(|ps| vote(ps, &inputs, tol, None)).collect(); // A pattern or mirror copy: its faces lie on its source's faces moved by one instance. let found = names.iter().filter(|n| n.is_some()).count(); if !o.mats.is_empty() && found * 2 < nf.max(1) { let all: Vec<(&ModelBody, FaceIndex, Arc>)> = o .before .bodies .iter() .filter(|x| !x.body.is_mesh()) .take(64) .map(|x| (x, FaceIndex::new(&x.mesh(), face_count(x)), names_of(x))) .collect(); let mut best: Option<(usize, usize, Vec>)> = None; for (k, mat) in o.mats.iter().enumerate() { let Some(inv) = crate::mat_inverse(mat) else { continue }; let got: Vec> = pts.iter().map(|ps| vote(ps, &all, tol, Some(&inv))).collect(); let n = got.iter().filter(|x| x.is_some()).count(); if n > found && best.as_ref().is_none_or(|(bn, _, _)| n > *bn) { best = Some((n, k, got)); } } if let Some((_, k, got)) = best { 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> = 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 = names .into_iter() .enumerate() .map(|(fi, n)| { n.unwrap_or_else(|| { let (ps, nn) = (pts.get(fi).map(Vec::as_slice).unwrap_or(&[]), ns.get(fi).unwrap_or(&empty)); // A shell's inner face: the face its wall runs back to. if matches!(o.feature.kind, FeatureKind::Shell { .. }) && let Some(n) = behind(ps, nn, &inputs) { return format!("F{fid}:inner:{}", strip_piece(&n)); } role_name(&o.feature, &o.before, ps, nn, fi, fid) }) }) .collect(); names } /// The input face a face looks back at through the material (along minus its normal): the /// face a shell wall's inner side offsets. fn behind(ps: &[Vec3], ns: &[Vec3], inputs: &[(&ModelBody, FaceIndex, Arc>)]) -> Option { let n = ns.first().copied()?; let d = n * -1.0; let mut votes: Vec<(String, usize)> = Vec::new(); for p in ps { let mut best: Option<(f64, String)> = None; for (_, idx, nm) in inputs { for (fi, (_, _, tris)) in idx.faces.iter().enumerate() { for [a, b, c] in tris { if let Some(t) = ray_hit(*p, d, *a, *b, *c) && t > 1e-9 && best.as_ref().is_none_or(|(bt, _)| t < *bt) && let Some(name) = nm.get(fi) { best = Some((t, name.clone())); } } } } if let Some((_, n)) = best { match votes.iter_mut().find(|(v, _)| *v == n) { Some(v) => v.1 += 1, None => votes.push((n, 1)), } } } votes.sort_by_key(|v| std::cmp::Reverse(v.1)); votes.first().filter(|(_, k)| *k * 2 >= ps.len().max(1)).map(|(n, _)| n.clone()) } pub(crate) fn ray_hit(o: Vec3, d: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Option { let (e1, e2) = (b - a, c - a); let h = d.cross(e2); let det = e1.dot(h); if det.abs() < 1e-14 { return None; } let f = 1.0 / det; let s = o - a; let u = f * s.dot(h); if !(0.0..=1.0).contains(&u) { return None; } let q = s.cross(e1); let v = f * d.dot(q); if v < 0.0 || u + v > 1.0 { return None; } Some(f * e2.dot(q)) } /// The input face most of a face's sample points lie on (mapped by `map` first), if it holds /// at least half of them. fn vote(ps: &[Vec3], inputs: &[(&ModelBody, FaceIndex, Arc>)], tol: f64, map: Option<&crate::Mat>) -> Option { let mut votes: Vec<(String, usize)> = Vec::new(); for p in ps { let p = match map { Some(m) => crate::apply_point(m, *p), None => *p, }; for (_, idx, nm) in inputs { if let Some(f) = idx.face_at(p, tol) && let Some(n) = nm.get(f) { // A later operation may merge several pieces of the same input face. // Count their shared ancestry before testing the majority; a piece // number identifies topology subdivision, not a different source. let n = strip_piece(n); match votes.iter_mut().find(|(v, _)| v == n) { Some(v) => v.1 += 1, None => votes.push((n.to_string(), 1)), } break; } } } votes.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); votes.first().filter(|(_, n)| *n * 2 >= ps.len().max(1) && votes.get(1).is_none_or(|(_, other)| n > other)).map(|(n, _)| n.clone()) } /// A face name without its piece qualifiers: the face it is a piece of (`F2:top` for /// `F2:top#[F4:side:l1]`, for pieces of pieces, and for the numbered pieces of older designs). pub fn strip_piece(n: &str) -> &str { let mut stem = n; while let Some(parent) = parent_piece(stem) { stem = parent; } stem } /// An edge name without piece numbers: its own (`~k`) and its faces' (`#[…]`, and the numbered /// pieces of older designs), so pieces of a split edge, or of an edge between split faces, match. pub fn strip_edge_piece(n: &str) -> String { crate::piece_names::strip_edge_piece(n) } /// Edges sharing a name: number them along the longest axis of their spread. fn number_pieces(names: &mut [String], pts: &[Vec], mark: char) { // The pieces of each name, gathered in one pass (a faceted body has thousands of names). let mut groups: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for (i, n) in names.iter().enumerate() { groups.entry(n.clone()).or_default().push(i); } for (n, same) in groups { if same.len() < 2 { continue; } let centre = |j: usize| { let ps = pts.get(j).cloned().unwrap_or_default(); ps.iter().fold(Vec3::ZERO, |a, p| a + *p) * (1.0 / ps.len().max(1) as f64) }; let cs: Vec<(usize, Vec3)> = same.iter().map(|j| (*j, centre(*j))).collect(); let (lo, hi) = cs.iter().fold((Vec3::new(f64::MAX, f64::MAX, f64::MAX), Vec3::new(f64::MIN, f64::MIN, f64::MIN)), |(l, h), (_, c)| { (Vec3::new(l.x.min(c.x), l.y.min(c.y), l.z.min(c.z)), Vec3::new(h.x.max(c.x), h.y.max(c.y), h.z.max(c.z))) }); let ext = hi - lo; let key = |c: Vec3| { if ext.x >= ext.y && ext.x >= ext.z { (c.x, c.y, c.z) } else if ext.y >= ext.z { (c.y, c.z, c.x) } else { (c.z, c.x, c.y) } }; let mut order = cs.clone(); // Pieces lying almost level along the sort axis differ only in the last bits of their centre: // the centre is the mean of a tessellated boundary, and that depends on where the loop's // polyline starts, which can differ between processes. Comparing the keys with a tolerance // makes those pieces equal, so the stable sort keeps the order they arrived in and a name // keeps its number across runs (solidcraft#569). total_cmp keeps a NaN deterministic. let tol = ext.x.max(ext.y).max(ext.z).max(1.0) * 1e-9; let bucket = |c: Vec3| { let k = key(c); // Rounding onto the grid, rather than comparing with a tolerance, is what keeps this a // total order: `a` and `b` in one bucket and `b` and `c` in the next must not leave `a` // and `c` in different ones, or the sort has no order to work with. Since Rust 1.81 // `sort_by` may panic on a comparator that is not one. Two pieces can still flip when // they straddle a bucket edge; that is the price of a key that ignores their low bits. ((k.0 / tol).round() as i64, (k.1 / tol).round() as i64, (k.2 / tol).round() as i64) }; order.sort_by_key(|a| bucket(a.1)); for (k, (j, _)) in order.iter().enumerate() { if let Some(slot) = names.get_mut(*j) { *slot = format!("{n}{mark}{}", k + 1); } } } } /// The name of a face new to `f`. fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usize, fid: u64) -> String { let c = ps.iter().fold(Vec3::ZERO, |a, p| a + *p) * (1.0 / ps.len().max(1) as f64); match &f.kind { FeatureKind::Extrude { sketch, .. } => { let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") }; let n = ss.plane.normal(); // A face across the extrusion is a cap; others sweep a sketch curve. let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(n).len() < 1e-6); if flat { // On the sketch plane, above it or below it. return format!("F{fid}:{}", crate::lineage::extrude_cap_role((c - ss.plane.origin).dot(n))); } let q = ss.plane.to_local(c); nearest_curve(ss, Vec2::new(q.x, q.y)).map(|id| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}")) } FeatureKind::Revolve { sketch, axis, .. } => { let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") }; let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") }; let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z)); // A flat face holding the axis is an angle cap: on the sketch plane, or the far one. let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6) && ns.iter().all(|m| m.dot(d).abs() < 1e-6); if flat { let on_plane = ps.iter().all(|p| (*p - ss.plane.origin).dot(ss.plane.normal()).abs() < 1e-6 * (1.0 + p.len())); return format!("F{fid}:{}", if on_plane { "start" } else { "end" }); } // A swept face: turn its centre back into the sketch plane and find the curve. let q = c - o; let t = q.dot(d); let r = (q - d * t).len(); let u3 = ss.plane.normal().cross(d).normalized().unwrap_or(Vec3::X); let best = [u3, u3 * -1.0] .iter() .filter_map(|u| { let w = ss.plane.to_local(o + d * t + *u * r); let q2 = Vec2::new(w.x, w.y); nearest_curve_dist(ss, q2) }) .min_by(|a, b| a.0.total_cmp(&b.0)); best.map(|(_, id)| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}")) } FeatureKind::Sweep { .. } | FeatureKind::Loft { .. } => { let sk = match &f.kind { FeatureKind::Sweep { sketch, .. } => Some(*sketch), FeatureKind::Loft { sections, .. } => sections.first().map(|x| x.sketch), _ => None, }; let Some(ss) = sk.and_then(|k| before.sketch(k)) else { return format!("F{fid}:face:{fi}") }; let n = ss.plane.normal(); let h = |p: &Vec3| (*p - ss.plane.origin).dot(n); let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6); if flat && ps.iter().all(|p| h(p).abs() < 1e-6 * (1.0 + p.len())) { return format!("F{fid}:start"); } if flat && ns.iter().all(|m| m.cross(n).len() < 1e-6) { return format!("F{fid}:end"); } // A side: the profile curve under its sample nearest the start. let Some(p0) = ps.iter().min_by(|a, b| h(a).abs().total_cmp(&h(b).abs())) else { return format!("F{fid}:face:{fi}") }; let q = ss.plane.to_local(*p0); nearest_curve(ss, Vec2::new(q.x, q.y)).map(|id| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}")) } FeatureKind::Box { .. } => { let n = ns.first().copied().unwrap_or(Vec3::Z); let axis = |v: f64, p: &str, m: &str| { if v > 0.5 { Some(p.to_string()) } else if v < -0.5 { Some(m.to_string()) } else { None } }; let role = axis(n.x, "+x", "-x").or_else(|| axis(n.y, "+y", "-y")).or_else(|| axis(n.z, "+z", "-z")).unwrap_or_else(|| format!("face:{fi}")); format!("F{fid}:{role}") } FeatureKind::Cylinder { axis, .. } => { let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(*axis).len() < 1e-6); let role = if !flat { "side" } else if ns.first().is_some_and(|m| m.dot(*axis) > 0.0) { "top" } else { "bottom" }; format!("F{fid}:{role}") } FeatureKind::Fillet { edges, .. } | FeatureKind::Chamfer { edges, .. } => { let k = edges.iter().enumerate().min_by(|a, b| a.1.dist(c).total_cmp(&b.1.dist(c))).map(|(k, _)| k).unwrap_or(0); match f.edge_names.get(k) { Some(e) => format!("F{fid}:blend:{e}"), None => format!("F{fid}:blend:{k}"), } } _ => format!("F{fid}:face:{fi}"), } } /// The sketch curve (id) nearest a point in the sketch plane, and how near. fn nearest_curve_dist(ss: &crate::SolvedSketch, q: Vec2) -> Option<(f64, String)> { let mut best: Option<(f64, String)> = None; for (ci, c) in ss.sketch.curves.iter().enumerate() { if c.construction { continue; } for s in ss.sketch.segs(ci) { let d = seg_dist(&s, q); if best.as_ref().is_none_or(|(bd, _)| d < *bd) { best = Some((d, c.id.clone())); } } } best } /// The sketch curve (id) nearest a point in the sketch plane. fn nearest_curve(ss: &crate::SolvedSketch, q: Vec2) -> Option { let mut best: Option<(f64, String)> = None; for (ci, c) in ss.sketch.curves.iter().enumerate() { if c.construction { continue; } for s in ss.sketch.segs(ci) { let d = seg_dist(&s, q); if best.as_ref().is_none_or(|(bd, _)| d < *bd) { best = Some((d, c.id.clone())); } } } best.map(|(_, id)| id) } fn seg_dist(s: &Seg2, q: Vec2) -> f64 { let pts = s.polyline(1e-3); pts.windows(2) .map(|w| { let (a, b) = (w[0], w[1]); let d = b - a; let t = if d.len2() > 0.0 { ((q - a).dot(d) / d.len2()).clamp(0.0, 1.0) } else { 0.0 }; (a + d * t).dist(q) }) .fold(f64::MAX, f64::min) } /// Names of a body's edges (by mesh edge index): `|`, numbered when repeated. pub fn edge_names(b: &ModelBody) -> Vec { edge_names_from(b, &face_names(b)) } /// Names of a body's edges from its faces' names `faces`. fn edge_names_from(b: &ModelBody, faces: &[String]) -> Vec { let m = b.mesh(); let mut names: Vec = m .edge_faces .iter() .map(|fs| { let mut n: Vec<&str> = fs.iter().filter_map(|f| faces.get(*f as usize).map(String::as_str)).collect(); n.sort(); n.join("|") }) .collect(); let pts: Vec> = m.edges.clone(); number_pieces(&mut names, &pts, '~'); names } /// The edge of a body with this name: exact, else the pieces of the same edge nearest `near` /// (`Err` explains when there is none). pub fn find_edge(b: &ModelBody, name: &str, near: Vec3) -> Result<(Vec3, bool), String> { locate_edge(b, name, near).map(|hit| (hit.point, hit.split)) } /// Names of the edges nearest each point on a body (for storing references). pub fn edge_names_at(b: &ModelBody, pts: &[Vec3]) -> Vec { let m = b.mesh(); let names = edge_names(b); pts.iter() .map(|p| { m.edges .iter() .enumerate() .map(|(i, e)| (e.windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min), i)) .min_by(|a, b| a.0.total_cmp(&b.0)) .and_then(|(_, i)| names.get(i).cloned()) .unwrap_or_default() }) .collect() } /// Names of the faces holding each point (any body; empty when none does). pub fn face_names_at(st: &ModelState, pts: &[Vec3]) -> Vec { pts.iter() .map(|p| { for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) { let tol = (b.body.size() * 1e-3).max(1e-3); let idx = FaceIndex::new(&b.mesh(), face_count(b)); if let Some(f) = idx.face_at(*p, tol) { return face_names(b).get(f).cloned().unwrap_or_default(); } } String::new() }) .collect() } /// Capture a face name from the recorded projection body only. pub fn face_name_at_body(body: &ModelBody, point: Vec3) -> Option { if body.body.is_mesh() || !point.is_finite() { return None; } let mesh = body.mesh(); let count = face_count(body); if mesh.triangles.len() > 2_000_000 || count > 50_000 { return None; } let tolerance = (body.body.size() * 1e-3).max(1e-3); let index = FaceIndex::new(&mesh, count).face_at(point, tolerance)?; face_names(body).get(index).filter(|name| !name.is_empty()).cloned() } /// Where a face is, for a script: a point that lies on it and picks it, and the plane, cylinder, /// cone, sphere or torus it is part of (when it is one). #[derive(Clone, Copy, Debug)] pub struct FaceSpot { pub point: Vec3, pub surface: Option, } /// The incentre and inradius of the roomiest triangle (greatest inradius) of each face of a mesh, /// of face `only` alone if given. fn roomiest(m: &Mesh, only: Option) -> Vec> { let mut best = vec![None; m.tri_face.iter().map(|f| *f as usize + 1).max().unwrap_or(0)]; for (t, f) in m.triangles.iter().zip(&m.tri_face) { let f = *f as usize; if only.is_some_and(|i| i != f) { continue; } let Some([a, b, c]) = m.tri(t) else { continue }; // The incentre weighs each corner by the side opposite it; the inradius is area / half the perimeter. let (la, lb, lc) = (b.dist(c), c.dist(a), a.dist(b)); let perimeter = la + lb + lc; let radius = (b - a).cross(c - a).len() / perimeter; let centre = (a * la + b * lb + c * lc) / perimeter; if let Some(slot) = best.get_mut(f) && radius.is_finite() && centre.is_finite() && slot.is_none_or(|(old, _)| radius > old) { *slot = Some((radius, centre)); } } best } /// The spot of face `index` of `b` from its roomiest triangle: the triangle's incentre, so a point /// that lies on the face however it is shaped (the centroid of a plate with a hole is in the hole, /// that of a half cylinder inside the cylinder) and well away from its edges, so a pick there finds /// this face. On a face of an analytic surface the point is moved onto the surface itself (a facet /// of a curved face lies inside it by its sag), as far as that stays well inside the triangle. fn spot(b: &ModelBody, index: usize, roomiest: Option<(f64, Vec3)>) -> Option { let (radius, centre) = roomiest?; let surface = solidcraft_kernel::face_surface(&b.body, index); let point = surface.and_then(|s| s.nearest(centre)).filter(|q| q.dist(centre) <= radius * 0.5).unwrap_or(centre); Some(FaceSpot { point, surface }) } /// [`face_spot`] of every face of a body (`None`: the face has no triangles), in one pass. pub fn face_spots(b: &ModelBody) -> Vec> { roomiest(&b.mesh(), None).into_iter().enumerate().map(|(i, best)| spot(b, i, best)).collect() } /// Where face `index` of `b` is (`None`: it has no triangles). pub fn face_spot(b: &ModelBody, index: usize) -> Option { spot(b, index, roomiest(&b.mesh(), Some(index)).get(index).copied().flatten()) } /// A point on the face with this name, the name `model.faces` lists for it, on the body called /// `body` or, without one, on any body (see [`locate_one`]): what a command that takes a face as /// a point is given instead when the face is named. pub fn named_face_point(st: &ModelState, body: Option<&str>, name: &str) -> Result { let hit = locate_one(st, body, name)?; let b = st.bodies.get(hit.body).ok_or_else(|| format!("no face is named `{name}`"))?; face_spot(b, hit.face).map(|spot| spot.point).ok_or_else(|| format!("face `{name}` has no surface to pick")) } /// The point on the face with this name nearest `p` (a split face: its nearest piece, with /// `true` for "not exactly that face": split, or merged into another); `None` when no face has /// the name. pub fn point_on_face(st: &ModelState, name: &str, p: Vec3) -> Option<(Vec3, bool)> { locate_face(st, name, p).map(|hit| (hit.point, hit.split || hit.merged)) } /// Resolve a legacy named face only on its recorded source body. Return the /// selected face index as well as the closest point, which may lie on a shared /// edge and therefore must not be used to pick the face a second time. pub fn face_on_body(body: &ModelBody, name: &str, p: Vec3) -> Option<(usize, Vec3, bool)> { if body.body.is_mesh() { return None; } crate::name_lookup::locate_on(body, name, p).map(|hit| (hit.face, hit.point, hit.split || hit.merged)) } #[cfg(test)] mod tests { /// Pieces lying almost level along the sort axis must keep the order they arrived in, whatever /// the low bits of their centres do. The centre is the mean of a tessellated boundary, so those /// bits depend on where the loop's polyline starts and can differ between processes — which is /// how a name's number used to swap between runs (solidcraft#569). #[test] fn level_pieces_keep_their_input_order() { let piece = |dx: f64| vec![Vec3::new(10.0 + dx, 0.0, 0.0), Vec3::new(10.0 + dx, 0.0, 5.0)]; for (first, second) in [(0.0, 1e-12), (1e-12, 0.0)] { let mut names = vec!["f".to_string(), "f".to_string()]; let pts = vec![piece(first), piece(second)]; number_pieces(&mut names, &pts, '#'); assert_eq!(names, vec!["f#1".to_string(), "f#2".to_string()], "input order not kept for {first} then {second}"); } } /// Three pieces spaced just under the tolerance apart: `a` and `b` in one bucket and `b` and `c` /// in the next, so a tolerance comparison would call `a≈b` and `b≈c` while `a>(); number_pieces(&mut names, &pts, '#'); assert_eq!(names, want, "wrong order for offsets {a} {b} {c}"); } } use super::*; /// A cone's tip repeats its apex vertex, so a triangle can have a zero-length edge. The nearest /// point on it must still be a real point (it used to be NaN, which broke picking the tip). #[test] fn closest_point_on_a_degenerate_triangle_is_finite() { let (a, c) = (Vec3::new(0., 0., 0.), Vec3::new(2., 0., 0.)); for (b, c) in [(a, c), (c, c), (a, a)] { for p in [Vec3::new(1., 1., 0.), Vec3::new(-1., 0.5, 0.), Vec3::new(3., -2., 1.), a] { let q = closest(p, a, b, c); assert!(q.x.is_finite() && q.y.is_finite() && q.z.is_finite(), "{a:?} {b:?} {c:?} {p:?} -> {q:?}"); } } assert_eq!(closest(Vec3::new(1., 1., 0.), a, a, c), Vec3::new(1., 0., 0.)); } #[test] fn merged_face_votes_collectively_for_pieces_of_one_source() { let body = ModelBody::new("Input".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 1., 1.)).unwrap(), 1); let mut mesh = Mesh::default(); let mut points = Vec::new(); for i in 0..3u32 { let x = f64::from(i); mesh.positions.extend([Vec3::new(x, 0., 0.), Vec3::new(x + 0.8, 0., 0.), Vec3::new(x, 0.8, 0.)]); mesh.triangles.push([i * 3, i * 3 + 1, i * 3 + 2]); mesh.tri_face.push(i); points.push(Vec3::new(x + 0.2, 0.2, 0.)); } let pieces = Arc::new(vec!["F1:side:wall#1".into(), "F1:side:wall#2".into(), "F1:side:wall#3".into()]); assert_eq!(vote(&points, &[(&body, FaceIndex::new(&mesh, 3), pieces)], 1e-8, None).as_deref(), Some("F1:side:wall")); let distinct = Arc::new(vec!["F1:side:wall#1".into(), "F2:side:wall#2".into(), "F3:side:wall#3".into()]); let inputs = [(&body, FaceIndex::new(&mesh, 3), distinct)]; assert_eq!(vote(&points, &inputs, 1e-8, None), None); assert_eq!(vote(&points[..2], &inputs, 1e-8, None), None, "equal different-source votes must not pick a lexicographic winner"); } }