Pattern Features also picked on the canvas: a face stands for the feature that made it (traced through the timeline states, cached per revision)

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 14:14:29 +00:00
1 parent 294597cc96
commit fba397ff65
2 files changed
+84 -1

No files matched your search

+70 -1
View File
@@ -591,7 +591,15 @@ impl Dialog {
/// The selection a hit would add to the active input.
pub fn candidate(&self, s: &Session, h: &Hit) -> Option<Sel> {
self.active_input()?.accepts(h, |body, face| planar_face(s, body, face).is_some())
let inp = self.active_input()?;
// Features are also picked on the canvas: a face stands for the feature that made it.
if inp.accept == selection::FEATURES {
return match h {
Hit::Face { body, index, point } => feature_of_face(s, body, *index, *point).map(|id| Sel::Feature { id }),
_ => None,
};
}
inp.accepts(h, |body, face| planar_face(s, body, face).is_some())
}
/// A pick in the viewport (already accepted by the active input).
@@ -793,6 +801,67 @@ pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) {
});
}
thread_local! {
/// Faces already traced to their feature, by (revision, body, face).
static FACE_FEATURE: std::cell::RefCell<(u64, std::collections::HashMap<(String, usize), Option<u64>>)> = Default::default();
}
/// The feature that made a face: the latest feature before which the face's point lay on no
/// face of the model.
fn feature_of_face(s: &Session, body: &str, index: usize, p: Vec3) -> Option<u64> {
let key = (body.to_string(), index);
if let Some(hit) = FACE_FEATURE.with(|c| {
let c = c.borrow();
if c.0 == s.revision { c.1.get(&key).copied() } else { None }
}) {
return hit;
}
let on_model = |st: &solvecraft_engine::doc::ModelState| {
st.bodies.iter().any(|b| {
let m = b.mesh();
let bb = m.bounds();
let tol = 1e-4 * (1.0 + bb.diagonal());
p.x >= bb.min.x - tol
&& p.y >= bb.min.y - tol
&& p.z >= bb.min.z - tol
&& p.x <= bb.max.x + tol
&& p.y <= bb.max.y + tol
&& p.z <= bb.max.z + tol
&& m.triangles.iter().filter_map(|t| m.tri(t)).any(|t| point_tri_dist(p, t) < tol)
})
};
let found = s
.doc
.features
.iter()
.rev()
.filter(|f| !matches!(f.kind, FeatureKind::Sketch { .. }) && s.model.result(f.id).is_some_and(|r| r.error.is_none()))
.find(|f| !on_model(&s.model.state_before(f.id)))
.map(|f| f.id);
FACE_FEATURE.with(|c| {
let mut c = c.borrow_mut();
if c.0 != s.revision {
*c = (s.revision, std::collections::HashMap::new());
}
c.1.insert(key, found);
});
found
}
/// Distance from a point to a triangle.
pub(crate) fn point_tri_dist(p: Vec3, [a, b, c]: [Vec3; 3]) -> f64 {
let n = (b - a).cross(c - a);
let nn = n.dot(n);
if nn > 1e-24 {
let q = p - n * ((p - a).dot(n) / nn);
let inside = [(a, b), (b, c), (c, a)].iter().all(|(u, v)| (*v - *u).cross(q - *u).dot(n) >= 0.0);
if inside {
return (p - q).len();
}
}
p.dist_to_segment(a, b).min(p.dist_to_segment(b, c)).min(p.dist_to_segment(c, a))
}
/// The pattern objects' label: features are picked in the timeline.
fn objects_label(accept: Accept) -> &'static str {
if accept == selection::FEATURES { "Features" } else { "Objects" }
+14
View File
@@ -156,6 +156,20 @@ fn a_hole_is_patterned_as_a_feature() {
}
let after = app.session.model.state().bodies[0].mesh().measure().volume;
assert!(after < before - 500.0, "three more holes: {before} -> {after}");
// On the canvas, a hole's wall stands for the hole; the top face for the box.
let st = app.session.model.state();
let m = st.bodies[0].mesh();
let face_at = |p: Vec3| {
let t = m.triangles.iter().position(|t| super::dialogs::point_tri_dist(p, m.tri(t).unwrap()) < 0.05).unwrap();
let [a, b, c] = m.tri(&m.triangles[t]).unwrap();
crate::viewport::Hit::Face { body: "Body1".into(), index: m.tri_face[t] as usize, point: (a + b + c) * (1.0 / 3.0) }
};
let mut d = start(&mut app, vec![], "PatternCircular");
d.inputs[0].accept = crate::selection::FEATURES;
let box_id = app.session.doc.features[0].id;
assert_eq!(d.candidate(&app.session, &face_at(Vec3::new(12.5, 10.0, 5.0))), Some(Sel::Feature { id: hole }));
assert_eq!(d.candidate(&app.session, &face_at(Vec3::new(30.0, 30.0, 10.0))), Some(Sel::Feature { id: box_id }));
app.dialog = None;
// Edit Feature shows the hole as a feature again.
let pat = app.session.doc.features.last().unwrap().id;
let e = crate::dialogs::for_feature(&app, pat, None).unwrap();