Persistent naming, step 1 (plan/adr/0002): faces are named from their history (a face lying on an input face keeps its name; new faces by role: F<feature>:side:<sketch curve>, :start/:top/:bottom caps, :blend:<edge>, …; split pieces #k along their spread), edges by the faces they separate (pieces ~k); names worked out lazily per body and shared by its clones; fillets and chamfers store edge names and resolve by name first (a split edge takes the piece nearest the picked point with a warning, a vanished one falls back to the point with a warning); model.faces and model.edges report names; tests for resize, suppress, reorder, an upstream split and a removed sketch segment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
3efe347809
commit
72297047d5
8 files changed
+628
-24
No files matched your search
@@ -1015,6 +1015,9 @@ pub struct Feature {
|
||||
/// is found again when earlier edits move or resize the body.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub edge_refs: Vec<crate::EdgeRef>,
|
||||
/// Persistent names of the picked edges (resolved first; see `naming`).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub edge_names: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub kind: FeatureKind,
|
||||
}
|
||||
@@ -1275,7 +1278,17 @@ impl Document {
|
||||
Some(n) if !n.trim().is_empty() => n.trim().to_string(),
|
||||
_ => self.unique_name(kind.base_name()),
|
||||
};
|
||||
let f = Feature { id, name, suppressed: false, body_names: Vec::new(), param_names: Vec::new(), component: 0, edge_refs: Vec::new(), kind };
|
||||
let f = Feature {
|
||||
id,
|
||||
name,
|
||||
suppressed: false,
|
||||
body_names: Vec::new(),
|
||||
param_names: Vec::new(),
|
||||
component: 0,
|
||||
edge_refs: Vec::new(),
|
||||
edge_names: Vec::new(),
|
||||
kind,
|
||||
};
|
||||
match self.marker {
|
||||
Some(m) if m < self.features.len() => {
|
||||
self.features.insert(m, f);
|
||||
|
||||
+67
-10
@@ -23,11 +23,13 @@ pub struct ModelBody {
|
||||
/// Identity of this body's content for the evaluation cache: the key of the evaluation
|
||||
/// that produced it (kept while later features pass it through unchanged).
|
||||
key: u64,
|
||||
/// Persistent face names (worked out on first use).
|
||||
pub(crate) names: crate::naming::NameCell,
|
||||
}
|
||||
|
||||
impl ModelBody {
|
||||
pub fn new(name: String, body: Body, feature: u64) -> Self {
|
||||
ModelBody { name, body, feature, mesh: OnceLock::new(), key: 0 }
|
||||
ModelBody { name, body, feature, mesh: OnceLock::new(), key: 0, names: Default::default() }
|
||||
}
|
||||
/// Display mesh (chord tolerance 1/1000 of the body size), computed once.
|
||||
pub fn mesh(&self) -> Arc<Mesh> {
|
||||
@@ -216,14 +218,24 @@ impl ModelState {
|
||||
|
||||
/// After a feature evaluated to this state from `before` with cache key `key`: bodies it
|
||||
/// made or changed, and sketches it made, take that key; the rest keep theirs.
|
||||
fn stamp(&mut self, before: &ModelState, feature: u64, key: u64) {
|
||||
fn stamp(&mut self, before: &Arc<ModelState>, f: &Feature, key: u64) {
|
||||
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 });
|
||||
for b in &mut self.bodies {
|
||||
let kept = before.bodies.iter().find(|x| x.name == b.name && x.body.same(&b.body)).map(|x| x.key);
|
||||
b.key = kept.unwrap_or_else(|| {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(key, &b.name).hash(&mut h);
|
||||
h.finish()
|
||||
});
|
||||
let kept = before.bodies.iter().find(|x| x.name == b.name && x.body.same(&b.body));
|
||||
match kept {
|
||||
Some(x) => {
|
||||
b.key = x.key;
|
||||
b.names = x.names.clone();
|
||||
}
|
||||
None => {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(key, &b.name).hash(&mut h);
|
||||
b.key = h.finish();
|
||||
b.names = crate::naming::NameCell::new(Some(origin.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.sketches.iter().any(|s| s.feature == feature) {
|
||||
self.sketch_keys.insert(feature, key);
|
||||
@@ -516,7 +528,7 @@ impl Model {
|
||||
let ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let error = match r {
|
||||
Ok(()) => {
|
||||
next.stamp(&state, f.id, key);
|
||||
next.stamp(&state, f, key);
|
||||
state = Arc::new(next);
|
||||
None
|
||||
}
|
||||
@@ -902,6 +914,14 @@ pub fn edge_refs(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<Ed
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Persistent names of the edges picked at `pts` on the body a blend would go on.
|
||||
pub fn edge_names_for(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<String> {
|
||||
match blend_target(st, body, pts).ok().and_then(|i| st.bodies.get(i)) {
|
||||
Some(b) => crate::naming::edge_names_at(b, pts),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge reference points re-found on the body: points on an edge stay; a point that moved off
|
||||
/// (an upstream edit) is carried with the body (its place in the body's bounding box then and
|
||||
/// now) to an edge running the same way; failing that it goes to the nearest edge with a
|
||||
@@ -1919,7 +1939,44 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let r = val(vals, radius, Kind::Length)?;
|
||||
let ti = blend_target(st, body, edges)?;
|
||||
let Some(mb) = st.bodies.get(ti) else { return Err(DocError::Invalid("body".into())) };
|
||||
let edges = resolve_edges(&mb.body, edges, &f.edge_refs, warning)?;
|
||||
let edges = if f.edge_names.iter().any(|n| !n.is_empty()) {
|
||||
// By name first; a name that is gone falls back to the point, with a warning.
|
||||
let (mut out, mut notes) = (Vec::new(), Vec::new());
|
||||
for (i, p) in edges.iter().enumerate() {
|
||||
match f.edge_names.get(i).filter(|n| !n.is_empty()).map(|n| (n, crate::naming::find_edge(mb, n, *p))) {
|
||||
Some((_, Ok((q, false)))) => out.push(q),
|
||||
Some((n, Ok((q, true)))) => {
|
||||
notes.push(format!("edge `{n}` was split; using the piece nearest the picked point"));
|
||||
out.push(q);
|
||||
}
|
||||
Some((_, Err(_))) | None => {
|
||||
let mut w = None;
|
||||
let refs: Vec<EdgeRef> = f.edge_refs.get(i).cloned().into_iter().collect();
|
||||
if let Ok(q) = resolve_edges(&mb.body, &[*p], &refs, &mut w) {
|
||||
out.extend(q);
|
||||
notes.push(match f.edge_names.get(i) {
|
||||
Some(n) if !n.is_empty() => format!("edge `{n}` no longer exists; using the edge at the picked point"),
|
||||
_ => "an edge reference without a name was found by its point".into(),
|
||||
});
|
||||
} else {
|
||||
notes.push(format!(
|
||||
"edge `{}` no longer exists and was skipped",
|
||||
f.edge_names.get(i).map(String::as_str).unwrap_or("?")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(DocError::Invalid("none of the selected edges exist any more".into()));
|
||||
}
|
||||
if !notes.is_empty() {
|
||||
*warning = Some(notes.join("; "));
|
||||
}
|
||||
out
|
||||
} else {
|
||||
resolve_edges(&mb.body, edges, &f.edge_refs, warning)?
|
||||
};
|
||||
let nb = if matches!(f.kind, FeatureKind::Fillet { .. }) {
|
||||
kernel::fillet(&mb.body, &edges, r)?
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod expr;
|
||||
pub mod format;
|
||||
pub mod joints;
|
||||
pub mod motion;
|
||||
pub mod naming;
|
||||
mod params;
|
||||
pub mod plastic;
|
||||
pub mod project;
|
||||
@@ -26,7 +27,8 @@ pub mod sheet;
|
||||
pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, mat_inverse, mat_mul, rigid};
|
||||
pub use document::*;
|
||||
pub use eval::{
|
||||
EdgeRef, FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, edge_refs, parse_metric_thread, pattern_matrices, world_state,
|
||||
EdgeRef, FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, edge_names_for, edge_refs, parse_metric_thread, pattern_matrices,
|
||||
world_state,
|
||||
};
|
||||
pub use params::{ParamRow, ParamUser, cycles};
|
||||
pub use solvecraft_kernel as kernel;
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
//! Persistent names of faces and edges (plan/adr/0002-persistent-naming.md).
|
||||
//!
|
||||
//! Every face of an evaluated body is named after how it was made: a face that lies on a face
|
||||
//! of the feature's input keeps that face's name; a face new to the feature is named by its role
|
||||
//! (`F<feature>:side:<sketch curve>`, `F<feature>:start`/`end`, `F<feature>:blend:<k>`, …).
|
||||
//! Pieces of a split face get `#1`, `#2`, … along the longest axis. Edges are named by the faces
|
||||
//! they separate. Names are worked out lazily, on first request, from the body, the feature
|
||||
//! that made it and the model before that feature.
|
||||
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use solvecraft_geom::{Mesh, Seg2, Vec2, Vec3};
|
||||
|
||||
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<ModelState>,
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
/// The lazily computed names of a body, shared by its clones.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NameCell {
|
||||
pub origin: Option<Arc<NamingOrigin>>,
|
||||
names: Arc<OnceLock<Arc<Vec<String>>>>,
|
||||
}
|
||||
|
||||
impl NameCell {
|
||||
pub fn new(origin: Option<Arc<NamingOrigin>>) -> Self {
|
||||
NameCell { origin, names: Arc::new(OnceLock::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Vec<Vec3>> {
|
||||
let mut per: Vec<Vec<Vec3>> = 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<Vec<Vec3>> {
|
||||
let mut per: Vec<Vec<Vec3>> = 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()
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return a + ac * (d2 / (d2 - d6));
|
||||
}
|
||||
let va = d3 * d6 - d5 * d4;
|
||||
if va <= 0.0 && (d4 - d3) >= 0.0 && (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<usize> {
|
||||
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<Vec<String>> {
|
||||
b.names
|
||||
.names
|
||||
.get_or_init(|| {
|
||||
let nf = face_count(b);
|
||||
let names = match b.names.origin.as_ref() {
|
||||
Some(o) if o.depth < MAX_DEPTH => derive(b, o, nf),
|
||||
_ => (0..nf).map(|k| format!("{}:face:{k}", b.name)).collect(),
|
||||
};
|
||||
Arc::new(names)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Work out a body's face names from the feature that made it and the model before it.
|
||||
fn derive(b: &ModelBody, o: &NamingOrigin, nf: usize) -> Vec<String> {
|
||||
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)
|
||||
};
|
||||
let inputs: Vec<(&ModelBody, FaceIndex, Arc<Vec<String>>)> = o
|
||||
.before
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|x| !x.body.is_mesh() && overlaps(x))
|
||||
.take(64)
|
||||
.map(|x| {
|
||||
let n = face_count(x);
|
||||
(x, FaceIndex::new(&x.mesh(), n), face_names(x))
|
||||
})
|
||||
.collect();
|
||||
let fid = o.feature.id;
|
||||
let ns = normals(&m, nf);
|
||||
let mut names: Vec<String> = Vec::with_capacity(nf);
|
||||
for (fi, ps) in pts.iter().enumerate() {
|
||||
// Vote: the input face most samples lie on, if it holds at least half of them.
|
||||
let mut votes: Vec<(String, usize)> = Vec::new();
|
||||
for p in ps {
|
||||
for (_, idx, nm) in &inputs {
|
||||
if let Some(f) = idx.face_at(*p, tol)
|
||||
&& let Some(n) = nm.get(f)
|
||||
{
|
||||
match votes.iter_mut().find(|(v, _)| v == n) {
|
||||
Some(v) => v.1 += 1,
|
||||
None => votes.push((n.clone(), 1)),
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
votes.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
let inherited = votes.first().filter(|(_, n)| *n * 2 >= ps.len().max(1)).map(|(n, _)| strip_piece(n).to_string());
|
||||
let empty = Vec::new();
|
||||
names.push(inherited.unwrap_or_else(|| role_name(&o.feature, &o.before, ps, ns.get(fi).unwrap_or(&empty), fi, fid)));
|
||||
}
|
||||
number_pieces(&mut names, &pts, '#');
|
||||
names
|
||||
}
|
||||
|
||||
/// A face name without its piece number (`#k`).
|
||||
pub fn strip_piece(n: &str) -> &str {
|
||||
strip_mark(n, '#')
|
||||
}
|
||||
|
||||
/// An edge name without piece numbers: its own (`~k`) and its faces' (`#k`), so pieces of a
|
||||
/// split edge, or of an edge between split faces, match.
|
||||
pub fn strip_edge_piece(n: &str) -> String {
|
||||
let n = strip_mark(n, '~');
|
||||
let mut out = String::with_capacity(n.len());
|
||||
let mut it = n.chars().peekable();
|
||||
while let Some(c) = it.next() {
|
||||
if c == '#' && it.peek().is_some_and(|d| d.is_ascii_digit()) {
|
||||
while it.peek().is_some_and(|d| d.is_ascii_digit()) {
|
||||
it.next();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn strip_mark(n: &str, mark: char) -> &str {
|
||||
match n.rfind(mark) {
|
||||
Some(i) if n[i + 1..].chars().all(|c| c.is_ascii_digit()) && i + 1 < n.len() => &n[..i],
|
||||
_ => n,
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces sharing a name are pieces of one: number them along the longest axis of their spread.
|
||||
fn number_pieces(names: &mut [String], pts: &[Vec<Vec3>], mark: char) {
|
||||
let mut seen: Vec<String> = Vec::new();
|
||||
for i in 0..names.len() {
|
||||
let n = names[i].clone();
|
||||
if seen.contains(&n) {
|
||||
continue;
|
||||
}
|
||||
seen.push(n.clone());
|
||||
let same: Vec<usize> = (0..names.len()).filter(|j| names[*j] == n).collect();
|
||||
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();
|
||||
order.sort_by(|a, b| key(a.1).partial_cmp(&key(b.1)).unwrap_or(std::cmp::Ordering::Equal));
|
||||
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.
|
||||
let h = (c - ss.plane.origin).dot(n);
|
||||
let tol = 1e-6 * (1.0 + h.abs());
|
||||
return format!(
|
||||
"F{fid}:{}",
|
||||
if h.abs() <= tol {
|
||||
"start"
|
||||
} else if h > 0.0 {
|
||||
"top"
|
||||
} else {
|
||||
"bottom"
|
||||
}
|
||||
);
|
||||
}
|
||||
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::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.
|
||||
fn nearest_curve(ss: &crate::SolvedSketch, q: Vec2) -> Option<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.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): `<face A>|<face B>`, numbered when repeated.
|
||||
pub fn edge_names(b: &ModelBody) -> Vec<String> {
|
||||
let m = b.mesh();
|
||||
let faces = face_names(b);
|
||||
let mut names: Vec<String> = 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<Vec<Vec3>> = 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> {
|
||||
let m = b.mesh();
|
||||
let names = edge_names(b);
|
||||
let mid = |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()
|
||||
};
|
||||
if let Some(i) = names.iter().position(|n| n == name) {
|
||||
// Keep the picked point when it is on that edge.
|
||||
let on = 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)));
|
||||
return mid(i).map(|p| (if on { near } else { p }, false)).ok_or_else(|| "edge".into());
|
||||
}
|
||||
let stem = strip_edge_piece(name);
|
||||
let cands: Vec<usize> = (0..names.len()).filter(|i| names.get(*i).is_some_and(|n| strip_edge_piece(n) == stem)).collect();
|
||||
cands
|
||||
.iter()
|
||||
.filter_map(|i| mid(*i))
|
||||
.min_by(|a, b| a.dist(near).total_cmp(&b.dist(near)))
|
||||
.map(|p| (p, true))
|
||||
.ok_or_else(|| format!("edge `{name}` no longer exists"))
|
||||
}
|
||||
|
||||
/// Names of the edges nearest each point on a body (for storing references).
|
||||
pub fn edge_names_at(b: &ModelBody, pts: &[Vec3]) -> Vec<String> {
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user