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 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
294597cc96
commit
fba397ff65
2 files changed
+84
-1
No files matched your search
@@ -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" }
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user