Persistent naming: shell, draft, offset-face and replace-face faces and hole placements store their faces' names (Feature.face_names) and are moved onto the named face before evaluating (a split face: the nearest piece, with a warning; a vanished face: the picked point, with a warning)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
99886594e7
commit
e7aff4fef0
6 files changed
+114
No files matched your search
@@ -885,6 +885,24 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
/// Every expression the feature uses.
|
||||
/// Points on faces the feature refers to (shell, draft, offset and replace faces, a hole's
|
||||
/// placement), for persistent face names.
|
||||
pub fn face_points_mut(&mut self) -> Vec<&mut Vec3> {
|
||||
match self {
|
||||
FeatureKind::Shell { faces, .. }
|
||||
| FeatureKind::Draft { faces, .. }
|
||||
| FeatureKind::OffsetFace { faces, .. }
|
||||
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
|
||||
FeatureKind::Hole { position, points: None, .. } => vec![position],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn face_points(&self) -> Vec<Vec3> {
|
||||
let mut k = self.clone();
|
||||
k.face_points_mut().into_iter().map(|p| *p).collect()
|
||||
}
|
||||
|
||||
pub fn expressions(&self) -> Vec<&str> {
|
||||
let mut v: Vec<&str> = Vec::new();
|
||||
match self {
|
||||
@@ -1024,6 +1042,9 @@ pub struct Feature {
|
||||
/// Persistent names of the picked edges (resolved first; see `naming`).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub edge_names: Vec<String>,
|
||||
/// Persistent names of the faces at the feature's face points (`FeatureKind::face_points`).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub face_names: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub kind: FeatureKind,
|
||||
}
|
||||
@@ -1293,6 +1314,7 @@ impl Document {
|
||||
component: 0,
|
||||
edge_refs: Vec::new(),
|
||||
edge_names: Vec::new(),
|
||||
face_names: Vec::new(),
|
||||
kind,
|
||||
};
|
||||
match self.marker {
|
||||
|
||||
@@ -517,6 +517,33 @@ impl Model {
|
||||
let t0 = now();
|
||||
let mut next = (*state).clone();
|
||||
let mut warning = None;
|
||||
// Face references by name: move each face point onto its named face.
|
||||
let renamed;
|
||||
let f = if f.face_names.iter().any(|n| !n.is_empty()) {
|
||||
let mut g = f.clone();
|
||||
let mut notes = Vec::new();
|
||||
for (p, n) in g.kind.face_points_mut().into_iter().zip(&f.face_names) {
|
||||
if n.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match crate::naming::point_on_face(&state, n, *p) {
|
||||
Some((q, split)) => {
|
||||
if split {
|
||||
notes.push(format!("face `{n}` was split; using the piece nearest the picked point"));
|
||||
}
|
||||
*p = q;
|
||||
}
|
||||
None => notes.push(format!("face `{n}` no longer exists; using the face at the picked point")),
|
||||
}
|
||||
}
|
||||
if !notes.is_empty() {
|
||||
warning = Some(notes.join("; "));
|
||||
}
|
||||
renamed = g;
|
||||
&renamed
|
||||
} else {
|
||||
f
|
||||
};
|
||||
// A parameter with an error fails the features using it, with that error.
|
||||
let bad_param = param_names(f).into_iter().find_map(|n| self.param_errors.get(&n).map(|e| format!("parameter `{n}`: {e}")));
|
||||
let r = match bad_param {
|
||||
|
||||
@@ -533,3 +533,44 @@ pub fn edge_names_at(b: &ModelBody, pts: &[Vec3]) -> Vec<String> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Names of the faces holding each point (any body; empty when none does).
|
||||
pub fn face_names_at(st: &ModelState, pts: &[Vec3]) -> Vec<String> {
|
||||
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()
|
||||
}
|
||||
|
||||
/// The point on the face with this name nearest `p` (a split face: its nearest piece, with
|
||||
/// `true` for "it was split"); `None` when no face has the name.
|
||||
pub fn point_on_face(st: &ModelState, name: &str, p: Vec3) -> Option<(Vec3, bool)> {
|
||||
let stem = strip_piece(name);
|
||||
let mut best: Option<(f64, Vec3, bool)> = None;
|
||||
for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) {
|
||||
let names = face_names(b);
|
||||
let m = b.mesh();
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let Some(n) = names.get(*f as usize) else { continue };
|
||||
let exact = n == name;
|
||||
if !exact && strip_piece(n) != stem {
|
||||
continue;
|
||||
}
|
||||
let Some([a, bb, c]) = m.tri(t) else { continue };
|
||||
let q = closest(p, a, bb, c);
|
||||
let d = q.dist(p) - if exact { 1e-9 } else { 0.0 };
|
||||
if best.is_none_or(|(bd, _, _)| d < bd) {
|
||||
best = Some((d, q, !exact));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(_, q, split)| (q, split))
|
||||
}
|
||||
@@ -193,6 +193,10 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
nf.edge_refs = solvecraft_doc::edge_refs(&before, body, a);
|
||||
nf.edge_names = solvecraft_doc::edge_names_for(&before, body, a);
|
||||
}
|
||||
// New face points: their faces' names now.
|
||||
if nf.kind.face_points() != f.kind.face_points() {
|
||||
nf.face_names = solvecraft_doc::naming::face_names_at(&s.model.state_before(id), &nf.kind.face_points());
|
||||
}
|
||||
if let Some(slot) = s.doc_mut().feature_mut(id) {
|
||||
*slot = nf;
|
||||
}
|
||||
|
||||
@@ -251,10 +251,15 @@ pub(super) fn add_feature(s: &mut Session, p: &Value, kind: FeatureKind) -> Resu
|
||||
}
|
||||
_ => (Vec::new(), Vec::new()),
|
||||
};
|
||||
let face_names = {
|
||||
let pts = kind.face_points();
|
||||
if pts.is_empty() { Vec::new() } else { solvecraft_doc::naming::face_names_at(&s.model.state(), &pts) }
|
||||
};
|
||||
let id = s.doc_mut().add_feature(kind, name)?;
|
||||
if let Some(f) = s.doc_mut().feature_mut(id) {
|
||||
f.edge_refs = refs;
|
||||
f.edge_names = names;
|
||||
f.face_names = face_names;
|
||||
}
|
||||
s.doc_mut().name_feature_inputs(id);
|
||||
let comp = s.active_component;
|
||||
|
||||
@@ -1325,4 +1325,19 @@ mod naming {
|
||||
let sk = s.model.state().sketch(s.doc.find_feature("Top").map(|f| f.id).unwrap_or(0)).map(|x| x.plane.origin.z);
|
||||
assert_eq!(sk.map(|z| (z * 1e6).round() / 1e6), Some(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_and_hole_faces_follow_by_name() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20, "name": "Base"}));
|
||||
run(&mut s, "FusionShellBodyCommand", json!({"faces": [[20, 15, 20]], "thickness": 2, "name": "Hollow"}));
|
||||
let shell = |h: f64| 40.0 * 30.0 * h - 36.0 * 26.0 * (h - 2.0);
|
||||
assert!((volume(&mut s) - shell(20.0)).abs() < 0.5, "{}", volume(&mut s));
|
||||
// Taller: the picked point is 5 mm under the open face now; its name still finds it.
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Base", "set": {"height": "25"}}));
|
||||
assert!((volume(&mut s) - shell(25.0)).abs() < 0.5, "{}", volume(&mut s));
|
||||
assert_eq!(warning(&s, "Hollow"), None);
|
||||
let h = s.doc.find_feature("Hollow").map(|f| f.face_names.clone()).unwrap_or_default();
|
||||
assert_eq!(h, vec!["F1:+z".to_string()]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user