//! The GPU pick buffer in the viewport ([`solidcraft_gpu::pick`]): which model edges are seen at //! each pixel, so a pick over a large model tests only the edges near the cursor that are not //! hidden, instead of every edge the cursor's ray passes near. //! //! The pass is drawn for a view once it holds still (the same view two frames running), and a //! tile around the cursor is read back; the tile arrives a frame or two later, and until it //! does (and while the view moves) the picker answers from the CPU indexes //! ([`crate::pick_index`]). The tile only narrows the candidates: the picker's exact tests //! still decide. Not used for Select Other (every target along the ray), in wireframe (nothing //! hides an edge there), without a GPU, or for a model past [`MAX_TRIANGLES`]. use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use egui::Pos2; use solidcraft_engine::doc::ModelState; use solidcraft_gpu::pick::{PickBuffer, PickOut, PickScene, PickView}; use crate::SolveApp; use crate::viewport::Proj; /// Most surface triangles the pass draws (beyond them the CPU indexes answer alone). pub const MAX_TRIANGLES: usize = 6_000_000; /// How near the cursor an edge's pixels are looked for (points): vertices pick within 7, edges 6. const WINDOW: f32 = 8.0; /// Frames kept coming after a tile is asked for, so its read-back lands. const FRAMES: u8 = 4; /// The pass's scene for one model state: the ids it draws stand for (body, edge). struct Scene { state: Arc, key: u64, scene: Arc, /// Per id − 1: (body index in the state, edge index). edges: Vec<(u32, u32)>, /// One-point edges (no line to draw): always candidates. points: Vec<(u32, u32)>, } #[derive(Default)] pub(crate) struct State { scene: Mutex>>, out: PickOut, /// A pick found no tile for its cursor: the view it was in and the pixel. want: Mutex>, /// The view of the last frame (the pass is drawn for a view that held still). last_view: u64, /// Frames still to request so read-backs land. frames: u8, } /// The edges a tile names near the cursor, per body index of the world state. pub(crate) struct NearEdges { by_body: HashMap>, } impl NearEdges { /// Every segment of the named edges of body `index` of `mesh`, as (edge, first point), in /// edge order. pub(crate) fn segments(&self, index: usize, mesh: &solidcraft_engine::geom::Mesh) -> Vec<(usize, usize)> { let mut v = Vec::new(); for &ei in self.by_body.get(&index).map_or(&[][..], |e| &e[..]) { let n = mesh.edges.get(ei).map_or(0, |e| e.len().max(2) - 1); v.extend((0..n).map(|si| (ei, si))); } v } } /// What the pass's scene depends on: the world state, and which bodies show. fn model_key(app: &SolveApp, st: &Arc) -> u64 { let mut h = std::collections::hash_map::DefaultHasher::new(); (Arc::as_ptr(st) as usize).hash(&mut h); app.session.revision.hash(&mut h); app.session.visibility.hidden_bodies.hash(&mut h); app.session.visibility.hidden_occurrences.hash(&mut h); h.finish() } /// The view's pixels: the viewport's size in physical pixels. fn view_size(app: &SolveApp, proj: &Proj) -> [u32; 2] { let ppp = app.viewport.pixels_per_point(); [(proj.rect.width() * ppp).round().max(0.0) as u32, (proj.rect.height() * ppp).round().max(0.0) as u32] } /// How far edges are lifted toward the eye: the picker's slack for an edge behind a face. fn lift(app: &SolveApp) -> f64 { crate::viewport::scene_radius(app) * 0.02 } /// Everything the drawn ids depend on. fn view_key(app: &SolveApp, proj: &Proj, model: u64) -> u64 { let mut h = std::collections::hash_map::DefaultHasher::new(); model.hash(&mut h); for col in proj.vp.to_f32() { for x in col { x.to_bits().hash(&mut h); } } view_size(app, proj).hash(&mut h); lift(app).to_bits().hash(&mut h); h.finish() } fn usable(app: &SolveApp) -> bool { app.viewport.gpu.is_some() && app.ui.visual_style != 2 } /// The edges seen within reach of `pos`, when a tile of this view is in; otherwise `None`, and /// a tile around `pos` is asked for. pub(crate) fn edges_near(app: &SolveApp, proj: &Proj, pos: Pos2, st: &Arc) -> Option { if !usable(app) { return None; } let model = model_key(app, st); let scene = app.viewport.pick_buffer.scene.lock().ok()?.clone().filter(|s| s.key == model && Arc::ptr_eq(&s.state, st)); let key = view_key(app, proj, model); let size = view_size(app, proj); let ppp = app.viewport.pixels_per_point(); let (x, y) = ((pos.x - proj.rect.left()) * ppp, (pos.y - proj.rect.top()) * ppp); if !(x >= 0.0 && y >= 0.0 && x < size[0] as f32 && y < size[1] as f32) { return None; } let (x, y) = (x as i64, y as i64); let r = (WINDOW * ppp).ceil() as i64; let window = [x - r, y - r, x + r, y + r]; let ids = scene.as_ref().and_then(|_| app.viewport.pick_buffer.out.lock().ok()?.tile(key, size, window).map(|t| t.ids_in(window))); let (Some(scene), Some(ids)) = (scene, ids) else { if let Ok(mut w) = app.viewport.pick_buffer.want.lock() { *w = Some((key, [x as u32, y as u32])); } return None; }; let mut by_body: HashMap> = HashMap::new(); for (b, e) in ids.iter().filter_map(|id| scene.edges.get((*id as usize).checked_sub(1)?)).chain(&scene.points) { by_body.entry(*b as usize).or_default().push(*e as usize); } for v in by_body.values_mut() { v.sort_unstable(); v.dedup(); } Some(NearEdges { by_body }) } /// The pass's scene for the world state: visible bodies' triangles, and their pickable edges. /// `None` past [`MAX_TRIANGLES`] or without edges. fn scene(app: &SolveApp, st: &Arc, key: u64) -> Option> { let f = |p: solidcraft_engine::geom::Vec3| [p.x as f32, p.y as f32, p.z as f32]; let mut scene = PickScene::default(); let (mut edges, mut points, mut triangles) = (Vec::new(), Vec::new(), 0usize); for (bi, b) in st.bodies.iter().enumerate() { if !app.session.body_visible(&b.name) { continue; } let m = b.mesh(); triangles += m.triangles.len(); if triangles > MAX_TRIANGLES { return None; } for t in &m.triangles { if let Some([a, b, c]) = m.tri(t) { scene.triangle([f(a), f(b), f(c)]); } } for (ei, e) in m.edges.iter().enumerate() { if !m.edge_pickable(ei) || edges.len() >= u32::MAX as usize - 1 { continue; } if e.len() == 1 { points.push((bi as u32, ei as u32)); continue; } edges.push((bi as u32, ei as u32)); let id = edges.len() as u32; for w in e.windows(2) { scene.segment(f(w[0]), f(w[1]), id); } } } (!scene.is_empty()).then(|| Arc::new(Scene { state: st.clone(), key, scene: Arc::new(scene), edges, points })) } /// In the frame's GPU paint: draw the pass for a view that held still when a pick asked for a /// tile, and keep frames coming until the read-back lands. pub(crate) fn paint(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter, proj: &Proj) { if !usable(app) { return; } let st = app.session.world_state(); let model = model_key(app, &st); let key = view_key(app, proj, model); let state = &mut app.viewport.pick_buffer; let still = std::mem::replace(&mut state.last_view, key) == key; let want = state.want.lock().ok().and_then(|mut w| w.take()).filter(|(k, _)| *k == key && still); if want.is_none() && state.frames == 0 { return; } state.frames = if want.is_some() { FRAMES } else { state.frames.saturating_sub(1) }; let cached = state.scene.lock().ok().and_then(|s| s.clone()).filter(|s| s.key == model && Arc::ptr_eq(&s.state, &st)); let scene = match cached { Some(s) => s, None => { let Some(s) = scene(app, &st, model) else { return }; if let Ok(mut slot) = app.viewport.pick_buffer.scene.lock() { *slot = Some(s.clone()); } s } }; let e = proj.cam.eye().to_f32(); let view = PickView { key, view_proj: proj.vp.to_f32(), eye: [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }], back: proj.cam.back().to_f32(), lift: lift(app) as f32, size: view_size(app, proj), }; let cb = PickCallback { scene_key: scene.key, scene: scene.scene.clone(), view, centre: want.map(|w| w.1), out: app.viewport.pick_buffer.out.clone() }; painter.add(egui_wgpu::Callback::new_paint_callback(proj.rect, cb)); ctx.request_repaint(); } /// The pass as an egui paint callback: all its work is in `prepare` (it draws into its own /// textures, not the frame). struct PickCallback { scene_key: u64, scene: Arc, view: PickView, centre: Option<[u32; 2]>, out: PickOut, } impl egui_wgpu::CallbackTrait for PickCallback { fn prepare( &self, device: &egui_wgpu::wgpu::Device, queue: &egui_wgpu::wgpu::Queue, _screen: &egui_wgpu::ScreenDescriptor, encoder: &mut egui_wgpu::wgpu::CommandEncoder, resources: &mut egui_wgpu::CallbackResources, ) -> Vec { if resources.get::().is_none() { resources.insert(PickBuffer::new(device)); } if let Some(pb) = resources.get_mut::() { pb.prepare(device, queue, encoder, self.scene_key, &self.scene, &self.view, self.centre, &self.out); } Vec::new() } fn paint(&self, _info: egui::PaintCallbackInfo, _pass: &mut egui_wgpu::wgpu::RenderPass<'static>, _resources: &egui_wgpu::CallbackResources) {} } #[cfg(test)] #[path = "pick_buffer_tests.rs"] mod tests;