Persistent naming: joint origins on faces and circular ends, and face appearances, store the face's name and find it by name first (a joint whose face is gone reports it instead of attaching elsewhere)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 e7aff4fef0
commit e7a85b30f7
4 files changed
+62 -9

No files matched your search

+18 -1
View File
@@ -42,6 +42,9 @@ pub struct FaceLook {
pub body: String,
pub point: Vec3,
pub look: Look,
/// The face's persistent name (found first).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
/// The design's appearances.
@@ -123,7 +126,21 @@ impl Document {
/// assignment to the same face wins.
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
let mut out: Vec<(usize, Look)> = Vec::new();
for (p, look) in self.face_looks(&b.name) {
let names = crate::naming::face_names(b);
for f in self.appearances.faces.iter().filter(|f| f.body == b.name) {
let (p, look) = (f.point, f.look.clone());
// By name (every piece of a split face), else by the point.
if let Some(n) = &f.name {
let stem = crate::naming::strip_piece(n);
let hits: Vec<usize> = (0..names.len()).filter(|i| names.get(*i).is_some_and(|x| crate::naming::strip_piece(x) == stem)).collect();
if !hits.is_empty() {
for i in hits {
out.retain(|(j, _)| *j != i);
out.push((i, look.clone()));
}
continue;
}
}
if let Some(i) = face_index_at(b, p) {
out.retain(|(j, _)| *j != i);
out.push((i, look));
+27 -5
View File
@@ -30,11 +30,20 @@ pub enum Snap {
#[serde(default, skip_serializing_if = "Option::is_none")]
x: Option<Vec3>,
},
/// The centre of the planar face nearest `pick`, z along its outward normal.
FaceCenter { pick: Vec3 },
/// The centre of the planar face nearest `pick`, z along its outward normal. `face` is the
/// face's persistent name (found first).
FaceCenter {
pick: Vec3,
#[serde(default, skip_serializing_if = "Option::is_none")]
face: Option<String>,
},
/// The centre of the circular end nearest `pick` of the cylindrical face there, z along its
/// axis (pointing out of the face's end).
CircleCenter { pick: Vec3 },
CircleCenter {
pick: Vec3,
#[serde(default, skip_serializing_if = "Option::is_none")]
face: Option<String>,
},
/// A point, z along `z` (default +Z).
Point {
point: Vec3,
@@ -225,12 +234,24 @@ fn component_of(doc: &Document, occ: u64) -> Result<u64> {
}
/// Resolve a joint origin to a frame in its component's coordinates.
/// A snap's pick point moved onto its named face (an error when the face is gone: a joint must
/// not quietly attach elsewhere).
fn named_pick(st: &ModelState, pick: Vec3, face: Option<&str>) -> Result<Vec3> {
match face {
Some(n) => crate::naming::point_on_face(st, n, pick)
.map(|(q, _)| q)
.ok_or_else(|| DocError::Invalid(format!("joint origin: the face `{n}` no longer exists (edit the joint to pick again)"))),
None => Ok(pick),
}
}
pub fn resolve_origin(doc: &Document, st: &ModelState, o: &JointOrigin) -> Result<Mat> {
let bad = || DocError::Invalid("joint origin: degenerate frame".into());
match &o.snap {
Snap::Frame { origin, z, x } => frame(*origin, *z, *x).ok_or_else(bad),
Snap::Point { point, z } => frame(*point, z.unwrap_or(Vec3::Z), None).ok_or_else(bad),
Snap::FaceCenter { pick } => {
Snap::FaceCenter { pick, face } => {
let pick = &named_pick(st, *pick, face.as_deref())?;
let comp = component_of(doc, o.occurrence)?;
let mut best: Option<(f64, Vec3, Vec3)> = None;
for b in component_bodies(doc, st, comp) {
@@ -246,7 +267,8 @@ pub fn resolve_origin(doc: &Document, st: &ModelState, o: &JointOrigin) -> Resul
let (_, c, n) = best.ok_or_else(|| DocError::Invalid("joint origin: no planar face there".into()))?;
frame(c, n, None).ok_or_else(bad)
}
Snap::CircleCenter { pick } => {
Snap::CircleCenter { pick, face } => {
let pick = &named_pick(st, *pick, face.as_deref())?;
let comp = component_of(doc, o.occurrence)?;
let cyl = component_bodies(doc, st, comp)
.into_iter()
+12 -1
View File
@@ -354,6 +354,16 @@ fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
if bodies.is_empty() && faces.is_empty() && comps.is_empty() {
return Err(bad(cmd, "give `bodies`, `faces` or `components`"));
}
// Persistent names of the picked faces.
let st_names: Vec<(String, solvecraft_geom::Vec3, Option<String>)> = faces
.iter()
.map(|(b, pt)| {
let n = st.body(b).and_then(|mb| {
solvecraft_doc::appearance::face_index_at(mb, *pt).and_then(|i| solvecraft_doc::naming::face_names(mb).get(i).cloned())
});
(b.clone(), *pt, n)
})
.collect();
let a = &mut s.doc_mut().appearances;
for b in &bodies {
match &look {
@@ -368,7 +378,8 @@ fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
if a.faces.len() >= 100_000 {
return Err(bad(cmd, "too many face appearances"));
}
a.faces.push(FaceLook { body: body.clone(), point: *pt, look: l.clone() });
let name = st_names.iter().find(|(b, p, _)| b == body && p.dist(*pt) < 1e-12).and_then(|(_, _, n)| n.clone());
a.faces.push(FaceLook { body: body.clone(), point: *pt, look: l.clone(), name });
}
}
for c in &comps {
+5 -2
View File
@@ -131,8 +131,11 @@ fn origin_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<JointOrigin
let local = |p: Vec3| solvecraft_doc::apply_point(&inv, p);
let local_dir = |d: Vec3| solvecraft_doc::apply_vector(&inv, d);
let snap = match what {
"face" => Snap::FaceCenter { pick: local(world) },
"circle" => Snap::CircleCenter { pick: local(world) },
"face" | "circle" => {
let pick = local(world);
let face = solvecraft_doc::naming::face_names_at(&s.model.state(), &[pick]).into_iter().next().filter(|n| !n.is_empty());
if what == "face" { Snap::FaceCenter { pick, face } } else { Snap::CircleCenter { pick, face } }
}
"point" => Snap::Point { point: local(world), z: v.get("z").and_then(vec3).map(local_dir) },
_ => {
let f = v.get("frame").cloned().unwrap_or_default();