From 8050fc79fd172b373eec204323c058d50981a2c3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 17:35:21 +0300 Subject: [PATCH] fix(scene): clear selection after erase --- src/app/commands/dim.rs | 4 +++- src/app/commands/view.rs | 2 +- src/app/history.rs | 20 ++++++++++++++------ src/app/update/file.rs | 4 +++- src/scene/entity.rs | 2 +- src/scene/mod.rs | 9 +++++++++ src/scene/selection.rs | 20 +++++++++++++++++++- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 6c507b43c..2b4c1e577 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -334,7 +334,9 @@ impl OpenCADStudio { n += 1; } // Draw the text in front of its newly-added masks. - self.tabs[i].scene.selected = handles.iter().cloned().collect(); + self.tabs[i] + .scene + .replace_selection(handles.iter().cloned().collect()); self.tabs[i].dirty = true; self.command_line .push_output(&format!("TEXTMASK: masked {n} text object(s).")); diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index bb41e3940..d96f4125e 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -754,7 +754,7 @@ impl OpenCADStudio { .push_info(&format!("{cmd}: no text or dimension objects.")); return Some(Task::none()); } - self.tabs[i].scene.selected = handles; + self.tabs[i].scene.replace_selection(handles); return self.dispatch_view( if to_front { "DRAWORDER FRONT" diff --git a/src/app/history.rs b/src/app/history.rs index 4f36ad6f0..c71bb589a 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -875,7 +875,13 @@ impl OpenCADStudio { .copied() .filter(|h| scene.document.get_entity(*h).is_some()) .collect(); - scene.selected = restored; + scene.replace_selection(restored); + if scene + .hover_highlight + .is_some_and(|handle| scene.document.get_entity(handle).is_none()) + { + scene.set_hover_highlight(None); + } // Entity deltas currently preserve the layout; direct assignment avoids // clearing layout render caches between steps and keeps future widened // deltas batchable. @@ -903,11 +909,13 @@ impl OpenCADStudio { let mut affected = scene.object_isolation.hidden.clone(); affected.extend(state.hidden.iter().copied()); scene.object_isolation = state.clone(); - scene.selected = selected - .iter() - .copied() - .filter(|handle| scene.document.get_entity(*handle).is_some()) - .collect(); + scene.replace_selection( + selected + .iter() + .copied() + .filter(|handle| scene.document.get_entity(*handle).is_some()) + .collect(), + ); let changes: Vec<_> = affected .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 8cb7cd456..81f04e7b2 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -801,7 +801,9 @@ pub(super) fn on_open_file(&mut self) -> Task { if let Some(prepared) = prepared_geometry { self.tabs[i].scene.install_prepared_open_geometry(prepared); } - self.tabs[i].scene.selected = rustc_hash::FxHashSet::default(); + self.tabs[i] + .scene + .replace_selection(rustc_hash::FxHashSet::default()); self.tabs[i].scene.preview_wires = vec![]; // Reopen in whichever space the file was saved in — the CTAB // tab name when recorded, else the $TILEMODE model/paper flag — diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 1d53346c1..243256571 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -2473,7 +2473,7 @@ impl Scene { pub fn clear(&mut self) { self.document.record_all_entities_for_transaction(); self.document = CadDocument::new(); - self.selected = HashSet::default(); + self.replace_selection(HashSet::default()); self.preview_wires = vec![]; self.preview_text = vec![]; self.current_layout = "Model".to_string(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 30a86a4f5..2dacb4dd2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -8715,6 +8715,9 @@ mod delta_undo_tests { let orig1 = scene.document.get_entity(h1).cloned().unwrap(); let orig2 = scene.document.get_entity(h2).cloned().unwrap(); let count = scene.document.entities().count(); + scene.select_entity(h2, false); + scene.set_hover_highlight(Some(h2)); + let selection_generation = scene.selection_generation; scene.begin_undo_recording(); scene.erase_entities(&[h2]); @@ -8725,6 +8728,12 @@ mod delta_undo_tests { ); let delta = build_delta(&scene, rec); assert!(scene.document.get_entity(h2).is_none(), "h2 must be erased"); + assert!(!scene.selected.contains(&h2)); + assert_eq!(scene.hover_highlight, None); + assert_ne!( + scene.selection_generation, selection_generation, + "erase must invalidate the stale selection overlay" + ); // Undo re-inserts h2 with its ORIGINAL handle and image, exactly once in // the block record (the strip prevents a duplicated dangling entry), diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 5ac8a2112..482852fe2 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -17,6 +17,16 @@ impl Scene { self.bump_selection(); } + /// Replace the complete selection and invalidate the GPU highlight overlay + /// only when its contents actually changed. History/file/command paths must + /// use this instead of assigning `selected` directly. + pub(crate) fn replace_selection(&mut self, selected: HashSet) { + if self.selected != selected { + self.selected = selected; + self.bump_selection(); + } + } + /// Remove a single entity from the selection (Shift+click subtractive pick). pub fn deselect_entity(&mut self, handle: Handle) { if self.selected.remove(&handle) { @@ -333,6 +343,7 @@ impl Scene { pub fn erase_entities(&mut self, handles: &[Handle]) { let mut handle_set: HashSet = HashSet::default(); let mut erased: Vec<(Handle, ChangeKind)> = Vec::new(); + let mut highlight_changed = false; for &h in handles { // Objects on a locked layer can't be erased. if self.is_layer_locked(h) { @@ -345,7 +356,11 @@ impl Scene { } self.remember_removed_cache_categories(h); self.document.remove_entity_arc(h); - self.selected.remove(&h); + highlight_changed |= self.selected.remove(&h); + if self.hover_highlight == Some(h) { + self.hover_highlight = None; + highlight_changed = true; + } self.hatches.remove(&h); self.images.remove(&h); self.meshes.remove(&h); @@ -354,6 +369,9 @@ impl Scene { handle_set.insert(h); erased.push((h, ChangeKind::Removed)); } + if highlight_changed { + self.bump_selection(); + } // Capture exactly the group objects that this erase will rewrite, plus // the group dictionary when an emptied group will be removed. Do this // before taking mutable object-map borrows below.