feat(select): orange hover / blue selection highlight for all objects
Solids (meshes) now tint on rollover and selection: hover washes orange, selection washes blue, with selection winning when both apply. Meshes re-upload their tint when the highlight set changes, and the rollover pick now also hits shaded solid bodies and hatches (mirroring the click-select order: wire → hatch → block hatch → solid). Wires get the same scheme: the selection xray overlay recolours selected entities blue and the hovered entity orange (previously both blue). Drops the now-unused highlight_handles union helper and its cache. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c6b3975fd4
commit
e7eeff5945
6 changed files with 134 additions and 60 deletions
|
|
@ -2639,8 +2639,28 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
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)
|
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);
|
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||||
} else {
|
} else {
|
||||||
// Suppress the rollover during a command or a drag.
|
// Suppress the rollover during a command or a drag.
|
||||||
|
|
|
||||||
|
|
@ -849,7 +849,6 @@ pub struct Scene {
|
||||||
/// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by
|
/// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by
|
||||||
/// `selection_generation`. Rebuilt only when the selection changes so
|
/// `selection_generation`. Rebuilt only when the selection changes so
|
||||||
/// `build_primitive` doesn't clone the set every frame.
|
/// `build_primitive` doesn't clone the set every frame.
|
||||||
highlight_set_cache: RefCell<Option<(u64, Arc<HashSet<Handle>>)>>,
|
|
||||||
/// Per-entity tessellation memo for the culled Model render path (Phase
|
/// 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
|
/// 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
|
/// 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),
|
last_model_wire_gen: std::cell::Cell::new(0),
|
||||||
wire_force_nonce: std::cell::Cell::new(0),
|
wire_force_nonce: std::cell::Cell::new(0),
|
||||||
split_cache: RefCell::new(None),
|
split_cache: RefCell::new(None),
|
||||||
highlight_set_cache: RefCell::new(None),
|
|
||||||
tess_memo: RefCell::new(HashMap::default()),
|
tess_memo: RefCell::new(HashMap::default()),
|
||||||
tess_memo_guard: std::cell::Cell::new(0),
|
tess_memo_guard: std::cell::Cell::new(0),
|
||||||
}
|
}
|
||||||
|
|
@ -1072,26 +1070,6 @@ impl Scene {
|
||||||
self.selection_generation = self.selection_generation.wrapping_add(1);
|
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<HashSet<Handle>> {
|
|
||||||
{
|
|
||||||
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
|
/// 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
|
/// Register a Model-tab solid: cache its truck B-rep (for boolean ops) and
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ impl WireModel {
|
||||||
pub const WHITE: [f32; 4] = [1.00, 1.00, 1.00, 1.0];
|
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 CYAN: [f32; 4] = [0.25, 0.85, 1.00, 1.0];
|
||||||
pub const SELECTED: [f32; 4] = [0.15, 0.55, 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.
|
/// Sentinel AABB that never rejects any snap query.
|
||||||
pub const UNBOUNDED_AABB: [f32; 4] = [
|
pub const UNBOUNDED_AABB: [f32; 4] = [
|
||||||
f32::NEG_INFINITY,
|
f32::NEG_INFINITY,
|
||||||
|
|
|
||||||
|
|
@ -69,14 +69,35 @@ pub struct MeshLodGpu {
|
||||||
pub world_aabb: [f32; 4],
|
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 {
|
impl MeshLodGpu {
|
||||||
pub fn new(device: &wgpu::Device, set: &MeshLodSet) -> Self {
|
pub fn new(device: &wgpu::Device, set: &MeshLodSet, highlight: Highlight) -> Self {
|
||||||
Self {
|
Self {
|
||||||
lods: set
|
lods: set
|
||||||
.lods
|
.lods
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|m| !m.indices.is_empty())
|
.filter(|m| !m.indices.is_empty())
|
||||||
.map(|m| MeshGpu::new(device, m))
|
.map(|m| MeshGpu::new(device, m, highlight))
|
||||||
.collect(),
|
.collect(),
|
||||||
world_aabb: set.world_aabb,
|
world_aabb: set.world_aabb,
|
||||||
}
|
}
|
||||||
|
|
@ -84,8 +105,20 @@ impl MeshLodGpu {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MeshGpu {
|
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();
|
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<MeshVertex> = mesh
|
let vertices: Vec<MeshVertex> = mesh
|
||||||
.verts
|
.verts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -99,7 +132,7 @@ impl MeshGpu {
|
||||||
MeshVertex {
|
MeshVertex {
|
||||||
position: pos,
|
position: pos,
|
||||||
normal,
|
normal,
|
||||||
color: mesh.color,
|
color,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,10 @@ pub struct Pipeline {
|
||||||
/// a pick bumps only `selection_generation`, refreshing the overlay without
|
/// a pick bumps only `selection_generation`, refreshing the overlay without
|
||||||
/// touching the main wire buffers.
|
/// touching the main wire buffers.
|
||||||
pub cached_selection: (u64, u64),
|
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
|
/// Handle → indices into the resident wire set, built once per wire upload
|
||||||
/// (when `cached_wire_id` changes). Lets the selection/hover xray overlay
|
/// (when `cached_wire_id` changes). Lets the selection/hover xray overlay
|
||||||
/// gather just the highlighted entity's wires (`O(highlighted)`) instead of
|
/// 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_epoch: (u64::MAX, u64::MAX, u64::MAX),
|
||||||
cached_wire_id: u64::MAX,
|
cached_wire_id: u64::MAX,
|
||||||
cached_selection: (u64::MAX, u64::MAX),
|
cached_selection: (u64::MAX, u64::MAX),
|
||||||
|
cached_mesh_key: (u64::MAX, u64::MAX),
|
||||||
wire_handle_index: rustc_hash::FxHashMap::default(),
|
wire_handle_index: rustc_hash::FxHashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -983,37 +988,39 @@ impl Pipeline {
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
wires: &[WireModel],
|
wires: &[WireModel],
|
||||||
highlight: &rustc_hash::FxHashSet<acadrust::Handle>,
|
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
|
||||||
|
hover: Option<acadrust::Handle>,
|
||||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||||
) {
|
) {
|
||||||
if highlight.is_empty() {
|
let hover = hover.filter(|h| !selected.contains(h));
|
||||||
|
if selected.is_empty() && hover.is_none() {
|
||||||
self.gpu_selected_wires = vec![];
|
self.gpu_selected_wires = vec![];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Gather only the highlighted entities' wires via the prebuilt index —
|
// Gather the highlighted entities' wires via the prebuilt index —
|
||||||
// O(highlighted), no per-wire string parse / full-set scan. Indices are
|
// O(highlighted), no per-wire string parse. Selection recolours blue,
|
||||||
// collected then sorted so the xray run keeps the original draw order.
|
// hover orange. Drawn on top (depth-compare Always) over the base pass.
|
||||||
let mut slots: Vec<u32> = Vec::new();
|
let mut out: Vec<WireModel> = Vec::new();
|
||||||
for h in highlight {
|
let mut push = |handle_val: u64, color: [f32; 4], wires: &[WireModel], out: &mut Vec<WireModel>| {
|
||||||
if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
|
if let Some(idxs) = self.wire_handle_index.get(&handle_val) {
|
||||||
slots.extend_from_slice(idxs);
|
let mut slots = idxs.clone();
|
||||||
}
|
|
||||||
}
|
|
||||||
slots.sort_unstable();
|
slots.sort_unstable();
|
||||||
// Recolor to the selection highlight: the xray pass uses the normal
|
for &i in &slots {
|
||||||
// wire shader (no forced colour), so the highlight now lives here
|
if let Some(w) = wires.get(i as usize) {
|
||||||
// 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<WireModel> = slots
|
|
||||||
.iter()
|
|
||||||
.filter_map(|&i| wires.get(i as usize))
|
|
||||||
.map(|w| {
|
|
||||||
let mut c = w.clone();
|
let mut c = w.clone();
|
||||||
c.color = WireModel::SELECTED;
|
c.color = color;
|
||||||
c
|
out.push(c);
|
||||||
})
|
}
|
||||||
.collect();
|
}
|
||||||
self.gpu_selected_wires = WireGpu::from_run(device, &selected, depth_map, None, false);
|
}
|
||||||
|
};
|
||||||
|
for h in selected {
|
||||||
|
push(h.value(), WireModel::SELECTED, wires, &mut out);
|
||||||
|
}
|
||||||
|
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.
|
/// 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<acadrust::Handle>,
|
||||||
|
hover: Option<acadrust::Handle>,
|
||||||
|
) {
|
||||||
self.gpu_meshes = meshes
|
self.gpu_meshes = meshes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| s.lods.iter().any(|m| !m.indices.is_empty()))
|
.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::<u64>().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();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,10 @@ pub struct ViewportData {
|
||||||
/// world-space wire buffer when only the camera moved. Non-tile and
|
/// world-space wire buffer when only the camera moved. Non-tile and
|
||||||
/// preview/interim frames carry a fresh id each time → always re-upload.
|
/// preview/interim frames carry a fresh id each time → always re-upload.
|
||||||
pub(in crate::scene) wire_content_id: u64,
|
pub(in crate::scene) wire_content_id: u64,
|
||||||
/// `selected ∪ hover` handles for the GPU xray overlay. The highlight is no
|
/// Selected handles only (no hover) — solid meshes tint these blue.
|
||||||
/// longer baked into the wire tessellation, so `prepare` rebuilds the
|
pub(in crate::scene) selected_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
||||||
/// selected-wire batch by filtering `wires` against this set.
|
/// Currently hovered handle — solid meshes tint it orange.
|
||||||
pub(in crate::scene) highlight_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
pub(in crate::scene) hover_handle: Option<acadrust::Handle>,
|
||||||
/// Bumped on selection / hover change. Paired with `wire_content_id` to
|
/// Bumped on selection / hover change. Paired with `wire_content_id` to
|
||||||
/// decide when the xray overlay batch needs rebuilding.
|
/// decide when the xray overlay batch needs rebuilding.
|
||||||
pub(in crate::scene) selection_generation: u64,
|
pub(in crate::scene) selection_generation: u64,
|
||||||
|
|
@ -252,7 +252,6 @@ impl shader::Primitive for Primitive {
|
||||||
}
|
}
|
||||||
if geo_changed {
|
if geo_changed {
|
||||||
inner.upload_images(device, queue, &vp.images[..]);
|
inner.upload_images(device, queue, &vp.images[..]);
|
||||||
inner.upload_meshes(device, &vp.meshes[..]);
|
|
||||||
}
|
}
|
||||||
inner.upload_face3d(
|
inner.upload_face3d(
|
||||||
device,
|
device,
|
||||||
|
|
@ -283,11 +282,25 @@ impl shader::Primitive for Primitive {
|
||||||
inner.upload_selected_wires(
|
inner.upload_selected_wires(
|
||||||
device,
|
device,
|
||||||
&vp.wires[..],
|
&vp.wires[..],
|
||||||
&vp.highlight_handles,
|
&vp.selected_handles,
|
||||||
|
vp.hover_handle,
|
||||||
&vp.draw_depths,
|
&vp.draw_depths,
|
||||||
);
|
);
|
||||||
inner.cached_selection = sel_key;
|
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
|
// Live overlay (command preview / interim / grip drag) — small and
|
||||||
// refreshed every frame it's present, so a drag never re-uploads
|
// refreshed every frame it's present, so a drag never re-uploads
|
||||||
// the resident base wire buffer.
|
// the resident base wire buffer.
|
||||||
|
|
@ -793,7 +806,8 @@ impl Scene {
|
||||||
geometry_epoch: self.geometry_epoch,
|
geometry_epoch: self.geometry_epoch,
|
||||||
camera_generation: self.camera_generation,
|
camera_generation: self.camera_generation,
|
||||||
wire_content_id,
|
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,
|
selection_generation: self.selection_generation,
|
||||||
selected_sig: self.selected_set_sig(),
|
selected_sig: self.selected_set_sig(),
|
||||||
screen_rect,
|
screen_rect,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue