Compare commits

...

1 Commits

Author SHA1 Message Date
bherbruck 133569e695 A modelled thread hovers, lights and selects as one surface
A modelled thread is thousands of small faces (26,518 for an M8 x 100), and the
pointer over it found the vertices and edges between them, lit one sliver at a
time, and a click selected one sliver. Now the thread is one surface to point at:

- Which faces belong to a thread comes from what made them: a face made by a
  Thread feature is named after it (F<id>:...), a piece of a face the thread
  cut names it among the faces around it. The pieces of one face of an
  imported file group the same way. The names of a big body take seconds, so
  the groups are worked out on a worker, once per body
  (doc::face_groups::face_groups_soon), and nothing waits for them.
- Hover: over a thread the pointer lands on the surface (the edges and
  vertices between the thread's own faces are no boundary to pick), and the
  whole thread lights up, outlined where it meets the rest of the body. The
  highlight is kept while the pointer moves over the thread. A face small on
  screen (a finely faceted surface) wins over vertices and edges further than
  two points away.
- Click: the thread is one item in the selection; shown, and given to a
  command that takes faces, it stands for every face of the thread.
- Thousands of selected faces are drawn in one pass per body and hashed by
  index, not written out, every frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:15:51 -04:00
4 changed files with 376 additions and 29 deletions
+126
View File
@@ -0,0 +1,126 @@
//! Faces that are picked as one (#3745 follow-up): a modelled thread is thousands of small faces
//! (its flanks, and the pieces of the surface it was cut into), but it is one surface to point at,
//! light and select; so are the pieces of one face an imported file had (a face wrapping all the
//! way round, split in two for the kernel).
//!
//! The groups come from what made the faces, never from their shape: a face made by a Thread
//! feature is named after it (`F<id>:…`), and a piece of a face the thread cut names it among the
//! faces around it (`…[F<id>:…]`). Working the names out for a large body takes a while, so it is
//! done on a worker thread, once per body, and asked for without waiting.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::{Document, FeatureKind, ModelBody};
/// Each face's group (an index into `groups`), and the groups: faces picked together, in order.
#[derive(Debug, Default)]
pub struct FaceGroups {
of_face: Vec<Option<u32>>,
groups: Vec<Arc<Vec<usize>>>,
}
impl FaceGroups {
/// The group of `face` (`None`: it is picked alone).
pub fn group_of(&self, face: usize) -> Option<u32> {
self.of_face.get(face).copied().flatten()
}
/// The faces picked together with `face` (`None`: it is picked alone).
pub fn of(&self, face: usize) -> Option<Arc<Vec<usize>>> {
let g = self.of_face.get(face).copied().flatten()?;
self.groups.get(g as usize).cloned()
}
/// Work out the groups of `b` (the names of its faces are worked out here: slow for a big body).
pub fn of_body(threads: &[u64], b: &ModelBody) -> FaceGroups {
let mut keyed: HashMap<(u8, u64), Vec<usize>> = HashMap::new();
if !threads.is_empty() {
let names = crate::naming::face_names(b);
let marks: Vec<(u64, String, String)> = threads.iter().map(|id| (*id, format!("F{id}:"), format!("[F{id}:"))).collect();
for (i, name) in names.iter().enumerate() {
// Made by the thread, or a piece of a face the thread cut (it names the thread's faces
// around it).
let own = |p: &str, inside: &str| name.starts_with(p) || name.contains(inside) || name.contains(&format!(",{p}"));
if let Some((id, _, _)) = marks.iter().find(|(_, p, inside)| own(p, inside)) {
keyed.entry((0, *id)).or_default().push(i);
}
}
}
// Pieces of one face of an imported file.
if let Some(sources) = b.body.face_sources() {
for (i, s) in sources.iter().enumerate() {
if let Some(solidcraft_kernel::FaceSource::Imported { face, .. }) = s
&& !keyed.values().any(|faces| faces.contains(&i))
{
keyed.entry((1, *face)).or_default().push(i);
}
}
}
let mut out = FaceGroups { of_face: vec![None; b.body.face_count()], groups: Vec::new() };
let mut groups: Vec<Vec<usize>> = keyed.into_values().filter(|faces| faces.len() > 1).collect();
groups.sort();
for faces in groups {
let g = u32::try_from(out.groups.len()).unwrap_or(u32::MAX);
for &f in &faces {
if let Some(slot) = out.of_face.get_mut(f) {
*slot = Some(g);
}
}
out.groups.push(Arc::new(faces));
}
out
}
}
/// The modelled threads of `doc`, by feature id.
pub fn thread_features(doc: &Document) -> Vec<u64> {
doc.features.iter().filter(|f| matches!(f.kind, FeatureKind::Thread { modeled: true, .. })).map(|f| f.id).collect()
}
/// Groups worked out (or being worked out: `None`), by the identity of a body's names and the
/// threads they were grouped by.
type Cache = HashMap<(usize, Vec<u64>), Option<Arc<FaceGroups>>>;
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
/// Bodies whose groups are kept; the oldest go first.
const KEPT: usize = 256;
/// The groups of `b` in `doc` if they are worked out; otherwise they are worked out on a worker
/// thread (once) and `None` comes back meanwhile. The browser build has no threads: there they
/// are worked out now.
pub fn face_groups_soon(doc: &Document, b: &ModelBody) -> Option<Arc<FaceGroups>> {
let threads = thread_features(doc);
if threads.is_empty() && b.body.face_sources().is_none() {
return Some(Arc::new(FaceGroups::default()));
}
let key = (b.names.id(), threads.clone());
{
let Ok(mut cache) = CACHE.lock() else { return None };
let cache = cache.get_or_insert_with(HashMap::new);
if let Some(entry) = cache.get(&key) {
return entry.clone();
}
if cache.len() >= KEPT {
cache.clear();
}
cache.insert(key.clone(), None);
}
let work = move |b: ModelBody| {
let groups = Arc::new(FaceGroups::of_body(&threads, &b));
if let Ok(mut cache) = CACHE.lock()
&& let Some(slot) = cache.as_mut().and_then(|c| c.get_mut(&key))
{
*slot = Some(groups.clone());
}
groups
};
#[cfg(not(target_arch = "wasm32"))]
{
let body = b.clone();
let work = work.clone();
if std::thread::Builder::new().name("face-groups".into()).spawn(move || drop(work(body))).is_ok() {
return None;
}
}
Some(work(b.clone()))
}
+1
View File
@@ -22,6 +22,7 @@ mod document;
mod eval;
pub mod expr;
pub mod face_anchor;
pub mod face_groups;
mod feature_geometry;
mod feature_refs;
pub use face_anchor::{FaceAnchor, FaceAttachmentStatus};
+64
View File
@@ -2173,3 +2173,67 @@ fn hovering_a_large_body_does_not_wait_for_its_pick_index() {
assert!(matches!(h.app.viewport.hover, Some(crate::viewport::Hit::Face { .. })), "{:?}", h.app.viewport.hover);
assert!(hover_ms < index_ms / 3.0, "the first hover took {hover_ms:.1} ms; making the index takes {index_ms:.1} ms");
}
/// A modelled thread is thousands of small faces but one surface to point at: the pointer over
/// it lands on a face (not on the vertices and edges between the thread's own faces), the face
/// lights up with the whole thread, and a click selects the thread as one item, which stands for
/// every one of its faces when it is shown and when a command takes faces. No hover waits for the
/// thread's faces to be worked out.
#[test]
fn a_modelled_thread_hovers_and_selects_as_one_surface() {
let mut h = crate::scenario::Harness::new();
h.app.run("solid.cylinder", json!({"diameter": 8, "height": 16, "base": [0, 0, 0], "axis": [0, 0, 1]})).unwrap();
h.app.run("solid.thread", json!({"face": [4, 0, 8], "designation": "M8", "length": 10, "modeled": true})).unwrap();
let st = h.app.session.world_state();
let b = &st.bodies[0];
let names = solidcraft_engine::doc::naming::face_names(b);
let thread = h.app.session.doc.features.iter().find(|f| matches!(f.kind, solidcraft_engine::doc::FeatureKind::Thread { .. })).unwrap().id;
let made = names.iter().filter(|n| n.starts_with(&format!("F{thread}:"))).count();
assert!(made > 500, "{made} thread faces");
// Close in on the threaded part, side on.
h.app.cam.target = Vec3::new(0.0, 0.0, 5.0);
h.app.cam.distance = 30.0;
h.frames(3);
let mut slowest = 0.0f64;
let mut grouped = None;
for k in 0..40 {
let at = h.at(&json!({"world": [4.0 * (0.3 * k as f64).cos(), 4.0 * (0.3 * k as f64).sin(), 2.0 + (k % 6) as f64]})).unwrap();
let t = std::time::Instant::now();
h.step(&json!({"move": at})).unwrap();
slowest = slowest.max(t.elapsed().as_secs_f64() * 1000.0);
if let Some(crate::viewport::Hit::Face { body, index, point }) = h.app.viewport.hover.clone() {
let sel = Sel::Face { body, index, point };
let pieces = crate::viewport::grouped_selection(&h.app.session, &sel);
if pieces.len() >= made {
grouped = Some(sel);
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
assert!(slowest < 250.0, "a hover took {slowest:.0} ms");
let sel = grouped.expect("hovering the thread lands on its faces, and they are worked out as one surface");
// Over the thread, the pointer finds the surface, not the vertices and edges inside it.
let faces = (0..12)
.filter(|k| {
let a = 0.5 * *k as f64;
let at = h.at(&json!({"world": [4.0 * a.cos(), 4.0 * a.sin(), 3.0 + (*k % 4) as f64]})).unwrap();
h.step(&json!({"move": at})).unwrap();
matches!(h.app.viewport.hover, Some(crate::viewport::Hit::Face { .. }))
})
.count();
assert!(faces >= 9, "{faces} of 12 hovers over the thread landed on its surface");
// A click selects it as one item, which stands for every face of the thread.
let point = match &sel {
Sel::Face { point, .. } => *point,
_ => Vec3::ZERO,
};
let at = h.at(&json!({"world": [point.x, point.y, point.z]})).unwrap();
let t = std::time::Instant::now();
h.step(&json!({"click": at})).unwrap();
let click_ms = t.elapsed().as_secs_f64() * 1000.0;
assert_eq!(h.app.session.selection.len(), 1, "one item: {:?}", h.app.session.selection.len());
let pieces = crate::viewport::grouped_selection(&h.app.session, &h.app.session.selection[0]);
assert!(pieces.len() >= made, "the thread stands for {} faces of {made}", pieces.len());
assert!(click_ms < 250.0, "the click took {click_ms:.0} ms");
}
+185 -29
View File
@@ -1365,12 +1365,41 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
// The edges seen near the cursor, when the GPU pick buffer has them for this view (Select
// Other wants hidden ones too).
let seen = if all { None } else { crate::pick_buffer::edges_near(app, proj, pos, &st) };
// The face under the pointer wins over the vertices and edges inside it when they are not
// the boundary the pointer means: the edges between faces picked as one with it (a modelled
// thread's thousands of faces), and, when the face is small on screen (a finely faceted
// surface), any vertex or edge further than a couple of points away.
let inside = match bestf.as_ref().map(|(_, h)| h) {
Some(Hit::Face { body, index, .. }) if !all => st.body(body).map(|b| {
let m = b.mesh();
let groups = solidcraft_engine::doc::face_groups::face_groups_soon(&app.session.doc, b);
let group = groups.as_ref().and_then(|g| g.group_of(*index));
// Small on screen: only worth the look for a face picked alone.
let small = group.is_none() && {
let mut lo = egui::pos2(f32::INFINITY, f32::INFINITY);
let mut hi = egui::pos2(f32::NEG_INFINITY, f32::NEG_INFINITY);
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
if *f as usize == *index {
for p in m.tri(t).into_iter().flatten().filter_map(|p| proj.to_screen(p)) {
lo = lo.min(p);
hi = hi.max(p);
}
}
}
lo.x <= hi.x && (hi - lo).length() < 21.0
};
(body.clone(), groups.zip(group), small)
}),
_ => None,
};
let mut bestv = Near::new(all);
let mut beste = Near::new(all);
for (i, b) in st.bodies.iter().enumerate() {
if !app.session.body_visible(&b.name) {
continue;
}
let inside = inside.as_ref().filter(|(body, _, _)| *body == b.name);
let (vertex_px, edge_px) = if inside.is_some_and(|(_, _, small)| *small) { (2.0, 2.0) } else { (7.0, 6.0) };
// Only bodies whose outline on screen is near the cursor can have an edge under it.
if let Some((_, bb)) = bounds.get(i)
&& screen_box(proj, bb, 8.0).is_some_and(|r| !r.contains(pos))
@@ -1391,12 +1420,18 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
if !m.edge_pickable(ei) {
continue;
}
// Between two faces picked as one with the face under the pointer: not a boundary.
if let Some((_, Some((groups, group)), _)) = inside
&& m.edge_faces.get(ei).is_some_and(|fs| !fs.is_empty() && fs.iter().all(|f| groups.group_of(*f as usize) == Some(*group)))
{
continue;
}
if last_edge != Some(ei) {
last_edge = Some(ei);
for v in [e.first(), e.last()].into_iter().flatten() {
if let Some(sp) = proj.to_screen(*v) {
let dd = sp.distance(pos);
if dd < 7.0 && visible(*v) && bestv.wants(dd) {
if dd < vertex_px && visible(*v) && bestv.wants(dd) {
bestv.offer(dd, Hit::Vertex { body: b.name.clone(), point: *v });
}
}
@@ -1407,7 +1442,7 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
let dd = seg_dist(pos, a, c);
// Visible where the cursor is: the edge's point under the cursor, not an end or
// the middle, which can lie deeper than the face the cursor is on.
if dd < 6.0 && beste.wants(dd) && visible(ray_segment_closest(o, d, *p0, *p1)) {
if dd < edge_px && beste.wants(dd) && visible(ray_segment_closest(o, d, *p0, *p1)) {
beste.offer(dd, Hit::Edge { body: b.name.clone(), index: ei, mid: polyline_mid(e) });
}
}
@@ -1832,6 +1867,7 @@ fn edge_of(m: &solidcraft_engine::geom::Mesh, index: usize, point: Vec3) -> Opti
m.edges.iter().min_by(|a, b| near(a).total_cmp(&near(b))).filter(|e| near(e) < tol * 1e3)
}
#[cfg(test)]
fn face_tris(sc: &mut GpuScene, m: &solidcraft_engine::geom::Mesh, face: usize, col: [u8; 4], lit: bool) {
mesh_tris(sc, m, col, lit, |f| f as usize == face);
}
@@ -1866,9 +1902,25 @@ fn edge_lines(sc: &mut GpuScene, e: &[Vec3], core: Color32, halo: Color32, w: f3
}
/// The outline of a highlighted face where the model hides it (dimmed).
fn hidden_face_outline(sc: &mut GpuScene, m: &solidcraft_engine::geom::Mesh, face: usize, col: Color32) {
/// Whether an edge (between `faces`) lies inside one thread's faces: no boundary to draw or pick.
fn inside_group(groups: Option<&solidcraft_engine::doc::face_groups::FaceGroups>, faces: &[u32]) -> bool {
let Some(groups) = groups else { return false };
let mut of = faces.iter().map(|f| groups.group_of(*f as usize));
match of.next() {
Some(Some(g)) => faces.len() > 1 && of.all(|x| x == Some(g)),
_ => false,
}
}
fn hidden_faces_outline(
sc: &mut GpuScene,
m: &solidcraft_engine::geom::Mesh,
faces: &std::collections::HashSet<usize>,
groups: Option<&solidcraft_engine::doc::face_groups::FaceGroups>,
col: Color32,
) {
for (ei, e) in m.edges.iter().enumerate() {
let on = m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| *x as usize == face));
let on = m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| faces.contains(&(*x as usize))) && !inside_group(groups, f));
if on && m.edge_pickable(ei) {
for p in e.windows(2) {
sc.hidden_line(p[0].to_f32(), p[1].to_f32(), c4(col.gamma_multiply(0.45)), 1.4);
@@ -1936,8 +1988,32 @@ fn highlight_geometry_key(app: &SolveApp) -> u64 {
let selected = app.highlighted();
app.session.revision.hash(&mut h);
(std::sync::Arc::as_ptr(&crate::open_meshing::shown_world(app)) as usize).hash(&mut h);
serde_json::to_string(&selected).unwrap_or_default().hash(&mut h);
format!("{:?}", app.viewport.hover).hash(&mut h);
// A thread selected is thousands of faces: they are hashed by body and index, not written
// out every frame.
selected.len().hash(&mut h);
for item in &selected {
match item {
Sel::Face { body, index, .. } => (0u8, body, index).hash(&mut h),
other => serde_json::to_string(other).unwrap_or_default().hash(&mut h),
}
}
// A face hovered stands for the faces lit with it: moving over them (or within one) lights
// the same, and the highlight is not built again.
let group = match &app.viewport.hover {
Some(Hit::Face { body, index, .. }) => app
.session
.world_state()
.body(body)
.and_then(|b| solidcraft_engine::doc::face_groups::face_groups_soon(&app.session.doc, b))
.and_then(|g| g.of(*index))
.and_then(|g| g.first().copied())
.map(|first| (body.clone(), first)),
_ => None,
};
match group {
Some(group) => group.hash(&mut h),
None => format!("{:?}", app.viewport.hover).hash(&mut h),
}
app.viewport.hover_feature.hash(&mut h);
app.viewport.hover_bodies.hash(&mut h);
// Which mesh lights a face follows how the model draws its body (see `shown_mesh`).
@@ -1993,9 +2069,15 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
// Faces lit over the model use the triangles the model shows them with.
let shown = |b: &solidcraft_engine::doc::ModelBody| shown_mesh(app, b, &active_bodies);
let mut sel = Vec::new();
// Faces by body and index: a selected thread stands for thousands of them.
let mut seen_faces: std::collections::HashSet<(String, usize)> = std::collections::HashSet::new();
for item in app.highlighted() {
for piece in displayed_selection(app, &item) {
if !sel.iter().any(|x| crate::selection::same_item(x, &piece)) {
let fresh = match &piece {
Sel::Face { body, index, .. } => seen_faces.insert((body.clone(), *index)),
_ => !sel.iter().any(|x| crate::selection::same_item(x, &piece)),
};
if fresh {
sel.push(piece);
}
}
@@ -2076,16 +2158,31 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
return sc;
}
let mut feedback = GpuScene::default();
// Selected faces, a pass per body however many of its faces are selected (a modelled
// thread is thousands).
let mut faces_of: Vec<(&str, std::collections::HashSet<usize>)> = Vec::new();
for x in &sel {
if let Sel::Face { body, index, .. } = x {
match faces_of.iter_mut().find(|(b, _)| *b == body.as_str()) {
Some((_, faces)) => {
faces.insert(*index);
}
None => faces_of.push((body.as_str(), std::collections::HashSet::from([*index]))),
}
}
}
for (body, faces) in &faces_of {
// A live preview stands in for the body: its old faces would fight with it.
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.effective_replaced().iter().any(|r| r == *body)) {
mesh_tris(&mut feedback, &shown(b), c4(t.sel_face), false, |f| faces.contains(&(f as usize)));
let groups = solidcraft_engine::doc::face_groups::face_groups_soon(&s.doc, b);
hidden_faces_outline(&mut feedback, &drawn_mesh(app, b), faces, groups.as_deref(), t.sel_edge);
}
}
// Selected things.
for x in &sel {
match x {
Sel::Face { body, index, .. } => {
// A live preview stands in for the body: its old faces would fight with it.
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.effective_replaced().contains(body)) {
face_tris(&mut feedback, &shown(b), *index, c4(t.sel_face), false);
hidden_face_outline(&mut feedback, &drawn_mesh(app, b), *index, t.sel_edge);
}
}
Sel::Face { .. } => {}
Sel::Body { name } => {
if let Some(b) = st.body(name)
&& app.session.body_visible(name)
@@ -2211,12 +2308,21 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
let k = t.hover_face_lift;
let c = body_rgb(app, body);
let m = drawn_mesh(app, b);
let patch = m.surface_patch(*index);
let lit = shown(b);
// The face with the faces picked as one with it (a modelled thread), in one pass.
let patch = face_patch(s, b, &m, *index);
let mut in_patch = vec![false; m.tri_face.iter().map(|f| *f as usize + 1).max().unwrap_or(0).max(*index + 1)];
for &f in &patch {
face_tris(&mut feedback, &lit, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
if let Some(slot) = in_patch.get_mut(f) {
*slot = true;
}
}
for (e, _) in m.edge_faces.iter().enumerate().filter(|(_, fs)| fs.iter().any(|f| patch.contains(&(*f as usize)))) {
let lit = shown(b);
let col = [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255];
mesh_tris(&mut feedback, &lit, col, true, |f| in_patch.get(f as usize).copied().unwrap_or(false));
// Its edges, but not those between two faces of one thread: no boundary there.
let groups = solidcraft_engine::doc::face_groups::face_groups_soon(&s.doc, b);
let lit_face = |f: &u32| in_patch.get(*f as usize).copied().unwrap_or(false);
for (e, _) in m.edge_faces.iter().enumerate().filter(|(_, fs)| fs.iter().any(lit_face) && !inside_group(groups.as_deref(), fs)) {
if let Some(p) = m.edges.get(e)
&& m.edge_pickable(e)
{
@@ -3137,12 +3243,49 @@ pub(crate) fn picked_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Vec
{
return letters.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
}
grouped_selection(s, sel)
selection_pieces(s, sel)
}
/// Expand a visible rim or surface into the kernel pieces a command needs. The original
/// face pick keeps its point (used by tools such as Hole and construction geometry).
/// The faces picked as one with `face` of `b` (`m` its mesh): the pieces of its surface joined
/// across hidden seams, and every face of a modelled thread (or of one imported face) it is part
/// of. The thread's faces are worked out on a worker; until then the face goes with its surface.
fn face_patch(s: &solidcraft_engine::Session, b: &solidcraft_engine::doc::ModelBody, m: &solidcraft_engine::geom::Mesh, face: usize) -> Vec<usize> {
face_pieces(s, b, m, face, true)
}
/// [`face_patch`], with the faces of the thread it is part of when `groups`.
fn face_pieces(
s: &solidcraft_engine::Session,
b: &solidcraft_engine::doc::ModelBody,
m: &solidcraft_engine::geom::Mesh,
face: usize,
groups: bool,
) -> Vec<usize> {
let mut patch = m.surface_patch(face);
if !groups {
return patch;
}
if let Some(group) = solidcraft_engine::doc::face_groups::face_groups_soon(&s.doc, b).and_then(|g| g.of(face)) {
let mut seen: std::collections::HashSet<usize> = patch.iter().copied().collect();
patch.extend(group.iter().copied().filter(|f| seen.insert(*f)));
}
patch
}
pub(crate) fn grouped_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Vec<Sel> {
pieces(s, sel, true)
}
/// What a click or a box puts in the selection for `sel`: as [`grouped_selection`], but a face
/// of a modelled thread stays one item (the thread's thousands of faces are what it stands for
/// when shown or given to a command).
pub(crate) fn selection_pieces(s: &solidcraft_engine::Session, sel: &Sel) -> Vec<Sel> {
pieces(s, sel, false)
}
fn pieces(s: &solidcraft_engine::Session, sel: &Sel, groups: bool) -> Vec<Sel> {
let st = s.world_state();
match sel {
Sel::Edge { body, index, .. } => {
@@ -3158,21 +3301,28 @@ pub(crate) fn grouped_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Ve
Sel::Face { body, index, .. } => {
if let Some(b) = st.body(body) {
let m = b.mesh();
let mut patch = m.surface_patch(*index);
let mut patch = face_pieces(s, b, &m, *index, groups);
patch.sort_by_key(|i| usize::from(i != index));
// A point on each face, from one pass over the triangles.
let wanted: std::collections::HashSet<usize> = patch.iter().copied().collect();
let mut point: std::collections::HashMap<usize, Vec3> = std::collections::HashMap::new();
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
let f = *f as usize;
if f != *index
&& !point.contains_key(&f)
&& wanted.contains(&f)
&& let Some([a, b, c]) = m.tri(t)
{
point.insert(f, (a + b + c) / 3.0);
}
}
return patch
.into_iter()
.filter_map(|i| {
if i == *index {
return Some(sel.clone());
}
m.triangles.iter().zip(&m.tri_face).find_map(|(t, f)| {
if *f as usize != i {
return None;
}
let [a, b, c] = m.tri(t)?;
Some(Sel::Face { body: body.clone(), index: i, point: (a + b + c) / 3.0 })
})
point.get(&i).map(|p| Sel::Face { body: body.clone(), index: i, point: *p })
})
.collect();
}
@@ -3347,9 +3497,15 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
}
}
let mut grouped = Vec::new();
// Faces by body and index: a box over a thread takes thousands of them.
let mut faces: std::collections::HashSet<(String, usize)> = std::collections::HashSet::new();
for item in out {
for piece in grouped_selection(&app.session, &item) {
if !grouped.iter().any(|x| crate::selection::same_item(x, &piece)) {
for piece in selection_pieces(&app.session, &item) {
let fresh = match &piece {
Sel::Face { body, index, .. } => faces.insert((body.clone(), *index)),
_ => !grouped.iter().any(|x| crate::selection::same_item(x, &piece)),
};
if fresh {
grouped.push(piece);
}
}