mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 133569e695 |
@@ -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()))
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user