Persistent naming: sketches on faces store the face's name and find it by name first (an upstream feature that adds a nearer parallel face no longer moves the sketch); a vanished face falls back to the old geometric search with a warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 11:37:33 +00:00
1 parent 452ee5f9f9
commit 99886594e7
5 files changed
+73 -6

No files matched your search

+1 -1
View File
@@ -132,7 +132,7 @@ fn map_plane(m: &Mat, p: &Plane) -> Plane {
fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
match p {
PlaneRef::Custom { plane } => *plane = map_plane(m, plane),
PlaneRef::Face { plane, at } => {
PlaneRef::Face { plane, at, .. } => {
*plane = map_plane(m, plane);
*at = apply_point(m, *at);
}
+8 -2
View File
@@ -45,8 +45,14 @@ pub enum PlaneRef {
Construction { name: String },
/// A planar body face: found again on each evaluation (the face with this plane's normal
/// nearest `at`), so sketches on it follow when earlier features change; `plane` is the
/// plane as picked (its frame, and the fallback when the face is gone).
Face { plane: Plane, at: Vec3 },
/// plane as picked (its frame, and the fallback when the face is gone). `name` is the face's
/// persistent name (found first; see `naming`).
Face {
plane: Plane,
at: Vec3,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
}
/// Which closed profiles of a sketch a feature uses.
+32 -2
View File
@@ -1729,8 +1729,38 @@ mod more;
/// The plane of the body face a sketch sits on, found again: among planar faces with the
/// picked plane's normal, the one nearest `at`. The picked frame moves along its normal onto it.
fn face_plane(st: &ModelState, picked: &Plane, at: Vec3) -> (Plane, Option<String>) {
fn face_plane(st: &ModelState, picked: &Plane, at: Vec3, name: Option<&str>) -> (Plane, Option<String>) {
let n = picked.normal();
// By name first: the face (or a piece of it) with that name, facing the same way.
if let Some(name) = name {
let stem = crate::naming::strip_piece(name);
let mut found: Option<(f64, f64)> = None;
for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) {
let names = crate::naming::face_names(b);
let tol = (b.body.size() * 1e-3).max(1e-3);
let Ok(faces) = b.body.faces(tol) else { continue };
for f in faces {
let hit = names.get(f.index).is_some_and(|x| crate::naming::strip_piece(x) == stem);
if let (true, Some(fnrm)) = (hit, f.plane_normal)
&& fnrm.dot(n) > 1.0 - 1e-6
{
let d = f.centroid.dist(at);
if found.is_none_or(|x| d < x.0) {
found = Some((d, f.centroid.dot(n)));
}
}
}
}
if let Some((_, off)) = found {
return (picked.offset(off - picked.origin.dot(n)), None);
}
let (pl, w) = face_plane(st, picked, at, None);
let note = format!(
"the face `{name}` the sketch was on no longer exists; {}",
if w.is_some() { "the sketch stays where it was" } else { "using the face at the picked point" }
);
return (pl, Some(note));
}
let mut best: Option<(f64, f64)> = None; // (distance from `at`, plane offset along n)
for b in &st.bodies {
let tol = (b.body.size() * 1e-3).max(1e-3);
@@ -1781,7 +1811,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
| FeatureKind::Remove { .. } => more::eval(doc, vals, f, st),
FeatureKind::Sketch { plane, sketch } => {
let (plane, face_warning) = match plane {
PlaneRef::Face { plane: picked, at } => face_plane(st, picked, *at),
PlaneRef::Face { plane: picked, at, name } => face_plane(st, picked, *at, name.as_deref()),
other => (doc.resolve_plane(vals, other, 0)?, None),
};
let mut sk = sketch.clone();