diff --git a/crates/doc/src/appearance.rs b/crates/doc/src/appearance.rs index 766d4dd3..f9a10238 100644 --- a/crates/doc/src/appearance.rs +++ b/crates/doc/src/appearance.rs @@ -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, } /// 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 = (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)); diff --git a/crates/doc/src/joints.rs b/crates/doc/src/joints.rs index 178c7738..5c4a6eee 100644 --- a/crates/doc/src/joints.rs +++ b/crates/doc/src/joints.rs @@ -30,11 +30,20 @@ pub enum Snap { #[serde(default, skip_serializing_if = "Option::is_none")] x: Option, }, - /// 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, + }, /// 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, + }, /// A point, z along `z` (default +Z). Point { point: Vec3, @@ -225,12 +234,24 @@ fn component_of(doc: &Document, occ: u64) -> Result { } /// 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 { + 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 { 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() diff --git a/crates/engine/src/cmd/inspect.rs b/crates/engine/src/cmd/inspect.rs index 38bbb006..6d1b0d3b 100644 --- a/crates/engine/src/cmd/inspect.rs +++ b/crates/engine/src/cmd/inspect.rs @@ -354,6 +354,16 @@ fn appearance(s: &mut Session, p: &Value) -> Result { 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)> = 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 { 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 { diff --git a/crates/engine/src/cmd/joints.rs b/crates/engine/src/cmd/joints.rs index 5284f613..292091fa 100644 --- a/crates/engine/src/cmd/joints.rs +++ b/crates/engine/src/cmd/joints.rs @@ -131,8 +131,11 @@ fn origin_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result 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();