diff --git a/src/app/update.rs b/src/app/update.rs index 12c696f01..13fe2ceb1 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2639,8 +2639,28 @@ impl OpenCADStudio { }; let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds); let all_wires = self.tabs[i].scene.hit_test_wires(); + // Mirror the click-selection pick order so the rollover + // highlights every selectable object: wire → hatch → + // block-internal hatch → shaded 3D solid body. let hovered = scene::pick::hit_test::click_hit(p, &all_wires[..], view_proj, bounds) - .and_then(|s| Scene::handle_from_wire_name(s)); + .and_then(|s| Scene::handle_from_wire_name(s)) + .or_else(|| { + scene::pick::hit_test::click_hit_hatch( + p, + &self.tabs[i].scene.visible_hatches_for_click(), + view_proj, + bounds, + ) + }) + .or_else(|| { + scene::pick::hit_test::click_hit_insert_hatch( + p, + &self.tabs[i].scene.insert_hatches_for_click(), + view_proj, + bounds, + ) + }) + .or_else(|| self.tabs[i].scene.mesh_click_hit(p, view_proj, bounds)); self.tabs[i].scene.set_hover_highlight(hovered); } else { // Suppress the rollover during a command or a drag. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 3735e4c39..adc5a21db 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -849,7 +849,6 @@ pub struct Scene { /// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by /// `selection_generation`. Rebuilt only when the selection changes so /// `build_primitive` doesn't clone the set every frame. - highlight_set_cache: RefCell>)>>, /// Per-entity tessellation memo for the culled Model render path (Phase /// 2.2). Maps a top-level handle to its already-tessellated wires so a /// single-entity edit re-tessellates only the changed entity and reuses the @@ -928,7 +927,6 @@ impl Scene { last_model_wire_gen: std::cell::Cell::new(0), wire_force_nonce: std::cell::Cell::new(0), split_cache: RefCell::new(None), - highlight_set_cache: RefCell::new(None), tess_memo: RefCell::new(HashMap::default()), tess_memo_guard: std::cell::Cell::new(0), } @@ -1072,26 +1070,6 @@ impl Scene { self.selection_generation = self.selection_generation.wrapping_add(1); } - /// `selected ∪ hover` handles, cached per `selection_generation`. Drives the - /// GPU xray overlay that highlights the current selection without baking it - /// into the wire tessellation. - pub(super) fn highlight_handles(&self) -> Arc> { - { - let c = self.highlight_set_cache.borrow(); - if let Some((gen, arc)) = &*c { - if *gen == self.selection_generation { - return Arc::clone(arc); - } - } - } - let mut hl = self.selected.clone(); - if let Some(h) = self.hover_highlight { - hl.insert(h); - } - let arc = Arc::new(hl); - *self.highlight_set_cache.borrow_mut() = Some((self.selection_generation, Arc::clone(&arc))); - arc - } /// Re-evaluate every cached mesh's color through `render_style` so a /// Register a Model-tab solid: cache its truck B-rep (for boolean ops) and diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index f4228c9fd..9e82536f2 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -79,6 +79,8 @@ impl WireModel { pub const WHITE: [f32; 4] = [1.00, 1.00, 1.00, 1.0]; pub const CYAN: [f32; 4] = [0.25, 0.85, 1.00, 1.0]; pub const SELECTED: [f32; 4] = [0.15, 0.55, 1.00, 1.0]; + /// Rollover (hover) highlight — orange, distinct from the blue selection. + pub const HOVER: [f32; 4] = [0.95, 0.55, 0.10, 1.0]; /// Sentinel AABB that never rejects any snap query. pub const UNBOUNDED_AABB: [f32; 4] = [ f32::NEG_INFINITY, diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index 6e0405f1a..cc6b181f9 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -69,14 +69,35 @@ pub struct MeshLodGpu { pub world_aabb: [f32; 4], } +/// How a solid mesh is highlighted this frame. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Highlight { + None, + /// Hovered — light orange wash. + Hover, + /// Selected — stronger blue wash. + Selected, +} + +impl Highlight { + /// Blend colour and mix factor, or `None` when the mesh keeps its colour. + fn tint(self) -> Option<([f32; 4], f32)> { + match self { + Highlight::None => None, + Highlight::Hover => Some(([0.95, 0.55, 0.10, 1.0], 0.35)), + Highlight::Selected => Some(([0.15, 0.55, 1.0, 1.0], 0.60)), + } + } +} + impl MeshLodGpu { - pub fn new(device: &wgpu::Device, set: &MeshLodSet) -> Self { + pub fn new(device: &wgpu::Device, set: &MeshLodSet, highlight: Highlight) -> Self { Self { lods: set .lods .iter() .filter(|m| !m.indices.is_empty()) - .map(|m| MeshGpu::new(device, m)) + .map(|m| MeshGpu::new(device, m, highlight)) .collect(), world_aabb: set.world_aabb, } @@ -84,8 +105,20 @@ impl MeshLodGpu { } impl MeshGpu { - pub fn new(device: &wgpu::Device, mesh: &MeshModel) -> Self { + pub fn new(device: &wgpu::Device, mesh: &MeshModel, highlight: Highlight) -> Self { let has_normals = mesh.normals.len() == mesh.verts.len(); + // Blend the base colour toward the highlight so a selected / hovered + // solid reads clearly while keeping some shape shading. + let color = match highlight.tint() { + Some((hl, t)) => { + let mut c = [0.0f32; 4]; + for k in 0..4 { + c[k] = mesh.color[k] * (1.0 - t) + hl[k] * t; + } + c + } + None => mesh.color, + }; let vertices: Vec = mesh .verts .iter() @@ -99,7 +132,7 @@ impl MeshGpu { MeshVertex { position: pos, normal, - color: mesh.color, + color, } }) .collect(); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index f8e781e50..7cc3850da 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -135,6 +135,10 @@ pub struct Pipeline { /// a pick bumps only `selection_generation`, refreshing the overlay without /// touching the main wire buffers. pub cached_selection: (u64, u64), + /// `(geometry_epoch, selection_generation)` the resident mesh buffers were + /// uploaded for. Bumps on a selection/hover change so highlighted solids + /// re-upload with their tint. + pub cached_mesh_key: (u64, u64), /// Handle → indices into the resident wire set, built once per wire upload /// (when `cached_wire_id` changes). Lets the selection/hover xray overlay /// gather just the highlighted entity's wires (`O(highlighted)`) instead of @@ -923,6 +927,7 @@ impl Pipeline { cached_epoch: (u64::MAX, u64::MAX, u64::MAX), cached_wire_id: u64::MAX, cached_selection: (u64::MAX, u64::MAX), + cached_mesh_key: (u64::MAX, u64::MAX), wire_handle_index: rustc_hash::FxHashMap::default(), } } @@ -983,37 +988,39 @@ impl Pipeline { &mut self, device: &wgpu::Device, wires: &[WireModel], - highlight: &rustc_hash::FxHashSet, + selected: &rustc_hash::FxHashSet, + hover: Option, depth_map: &rustc_hash::FxHashMap, ) { - if highlight.is_empty() { + let hover = hover.filter(|h| !selected.contains(h)); + if selected.is_empty() && hover.is_none() { self.gpu_selected_wires = vec![]; return; } - // Gather only the highlighted entities' wires via the prebuilt index — - // O(highlighted), no per-wire string parse / full-set scan. Indices are - // collected then sorted so the xray run keeps the original draw order. - let mut slots: Vec = Vec::new(); - for h in highlight { - if let Some(idxs) = self.wire_handle_index.get(&h.value()) { - slots.extend_from_slice(idxs); + // Gather the highlighted entities' wires via the prebuilt index — + // O(highlighted), no per-wire string parse. Selection recolours blue, + // hover orange. Drawn on top (depth-compare Always) over the base pass. + let mut out: Vec = Vec::new(); + let mut push = |handle_val: u64, color: [f32; 4], wires: &[WireModel], out: &mut Vec| { + if let Some(idxs) = self.wire_handle_index.get(&handle_val) { + let mut slots = idxs.clone(); + slots.sort_unstable(); + for &i in &slots { + if let Some(w) = wires.get(i as usize) { + let mut c = w.clone(); + c.color = color; + out.push(c); + } + } } + }; + for h in selected { + push(h.value(), WireModel::SELECTED, wires, &mut out); } - slots.sort_unstable(); - // Recolor to the selection highlight: the xray pass uses the normal - // wire shader (no forced colour), so the highlight now lives here - // instead of being baked into the tessellation. Drawn on top with - // depth-compare Always, so it overrides the base-coloured main pass. - let selected: Vec = slots - .iter() - .filter_map(|&i| wires.get(i as usize)) - .map(|w| { - let mut c = w.clone(); - c.color = WireModel::SELECTED; - c - }) - .collect(); - self.gpu_selected_wires = WireGpu::from_run(device, &selected, depth_map, None, false); + if let Some(h) = hover { + push(h.value(), WireModel::HOVER, wires, &mut out); + } + self.gpu_selected_wires = WireGpu::from_run(device, &out, depth_map, None, false); } /// Upload the live overlay (command preview / interim / grip-drag) wires. @@ -1136,11 +1143,31 @@ impl Pipeline { } } - pub fn upload_meshes(&mut self, device: &wgpu::Device, meshes: &[MeshLodSet]) { + pub fn upload_meshes( + &mut self, + device: &wgpu::Device, + meshes: &[MeshLodSet], + selected: &rustc_hash::FxHashSet, + hover: Option, + ) { self.gpu_meshes = meshes .iter() .filter(|s| s.lods.iter().any(|m| !m.indices.is_empty())) - .map(|s| MeshLodGpu::new(device, s)) + .map(|s| { + // A mesh's name is its source entity handle (decimal). Selection + // wins over hover when both apply to the same solid. + let h = s + .lods + .first() + .and_then(|m| m.name.parse::().ok()) + .map(acadrust::Handle::new); + let mode = match h { + Some(h) if selected.contains(&h) => mesh_gpu::Highlight::Selected, + Some(h) if Some(h) == hover => mesh_gpu::Highlight::Hover, + _ => mesh_gpu::Highlight::None, + }; + MeshLodGpu::new(device, s, mode) + }) .collect(); } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 01c14e34b..649d34a63 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -85,10 +85,10 @@ pub struct ViewportData { /// world-space wire buffer when only the camera moved. Non-tile and /// preview/interim frames carry a fresh id each time → always re-upload. pub(in crate::scene) wire_content_id: u64, - /// `selected ∪ hover` handles for the GPU xray overlay. The highlight is no - /// longer baked into the wire tessellation, so `prepare` rebuilds the - /// selected-wire batch by filtering `wires` against this set. - pub(in crate::scene) highlight_handles: Arc>, + /// Selected handles only (no hover) — solid meshes tint these blue. + pub(in crate::scene) selected_handles: Arc>, + /// Currently hovered handle — solid meshes tint it orange. + pub(in crate::scene) hover_handle: Option, /// Bumped on selection / hover change. Paired with `wire_content_id` to /// decide when the xray overlay batch needs rebuilding. pub(in crate::scene) selection_generation: u64, @@ -252,7 +252,6 @@ impl shader::Primitive for Primitive { } if geo_changed { inner.upload_images(device, queue, &vp.images[..]); - inner.upload_meshes(device, &vp.meshes[..]); } inner.upload_face3d( device, @@ -283,11 +282,25 @@ impl shader::Primitive for Primitive { inner.upload_selected_wires( device, &vp.wires[..], - &vp.highlight_handles, + &vp.selected_handles, + vp.hover_handle, &vp.draw_depths, ); inner.cached_selection = sel_key; } + // Solid meshes carry their selection/hover tint baked into the + // vertex colours, so re-upload them when the geometry OR the + // highlight set changes. + let mesh_key = (vp.geometry_epoch, vp.selection_generation); + if mesh_key != inner.cached_mesh_key { + inner.upload_meshes( + device, + &vp.meshes[..], + &vp.selected_handles, + vp.hover_handle, + ); + inner.cached_mesh_key = mesh_key; + } // Live overlay (command preview / interim / grip drag) — small and // refreshed every frame it's present, so a drag never re-uploads // the resident base wire buffer. @@ -793,7 +806,8 @@ impl Scene { geometry_epoch: self.geometry_epoch, camera_generation: self.camera_generation, wire_content_id, - highlight_handles: self.highlight_handles(), + selected_handles: Arc::new(self.selected.iter().copied().collect()), + hover_handle: self.hover_highlight, selection_generation: self.selection_generation, selected_sig: self.selected_set_sig(), screen_rect,