cad-editor/src/scene/selection.rs
2026-08-26 21:48:02 +03:00

897 lines
36 KiB
Rust

// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
impl Scene {
// ── Selection ─────────────────────────────────────────────────────────
/// Treat a classic LEADER and its attached annotation as one logical object.
/// Clicking/copying/deleting either side expands to the complete pair.
pub(crate) fn handles_expanded_for_leader_annotations(
&self,
handles: &[Handle],
) -> Vec<Handle> {
let mut expanded = handles.to_vec();
for &handle in handles {
// LEADER -> annotation.
if let Some(EntityType::Leader(leader)) = self.document.get_entity(handle) {
if !leader.annotation_handle.is_null() {
expanded.push(leader.annotation_handle);
}
}
// Annotation -> LEADER.
expanded.extend(self.document.entities().filter_map(|entity| match entity {
EntityType::Leader(leader)
if !leader.annotation_handle.is_null()
&& leader.annotation_handle == handle =>
{
Some(entity.common().handle)
}
_ => None,
}));
}
expanded.sort_unstable_by_key(|handle| handle.value());
expanded.dedup();
expanded
}
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
let mut changed = false;
if exclusive {
changed = self.selected.len() != handles.len()
|| handles.iter().any(|handle| !self.selected.contains(handle));
self.selected.clear();
self.selected_order.clear();
}
for handle in handles {
if self.selected.insert(handle) {
self.selected_order.push(handle);
changed = true;
}
}
if changed {
self.bump_selection_set();
}
}
pub fn deselect_all(&mut self) {
if self.selected.is_empty() {
return;
}
self.selected.clear();
self.selected_order.clear();
self.bump_selection_set();
}
pub(crate) fn selection_fingerprint(&mut self) -> u64 {
if self.selection_fingerprint_dirty {
let mut fingerprint = self.selected.len() as u64;
for handle in &self.selected {
let mut hasher = DefaultHasher::new();
handle.hash(&mut hasher);
fingerprint ^= hasher.finish();
}
self.selection_fingerprint_cache = fingerprint;
self.selection_fingerprint_dirty = false;
}
self.selection_fingerprint_cache
}
pub(crate) fn selected_handles_in_order(&self) -> Vec<Handle> {
let mut seen = HashSet::default();
let mut ordered: Vec<_> = self
.selected_order
.iter()
.copied()
.filter(|handle| self.selected.contains(handle) && seen.insert(*handle))
.collect();
let mut missing: Vec<_> = self
.selected
.iter()
.copied()
.filter(|handle| seen.insert(*handle))
.collect();
missing.sort_unstable_by_key(|handle| handle.value());
ordered.extend(missing);
ordered
}
/// 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<Handle>) {
let handles: Vec<Handle> = selected.iter().copied().collect();
let selected: HashSet<Handle> = self
.handles_expanded_for_leader_annotations(&handles)
.into_iter()
.collect();
if self.selected != selected {
let mut seen = HashSet::default();
let mut order: Vec<_> = self
.selected_order
.iter()
.copied()
.filter(|handle| selected.contains(handle) && seen.insert(*handle))
.collect();
let mut added: Vec<_> = selected
.iter()
.copied()
.filter(|handle| seen.insert(*handle))
.collect();
added.sort_unstable_by_key(|handle| handle.value());
order.extend(added);
self.selected = selected;
self.selected_order = order;
self.bump_selection_set();
}
}
/// Remove a single entity from the selection (Shift+click subtractive pick).
pub fn deselect_entity(&mut self, handle: Handle) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
let mut changed = false;
for handle in handles {
changed |= self.selected.remove(&handle);
self.selected_order.retain(|selected| *selected != handle);
}
if changed {
self.bump_selection_set();
}
}
pub fn selected_entities(&self) -> Vec<(Handle, &EntityType)> {
self.selected_handles_in_order()
.into_iter()
.filter_map(|h| self.document.get_entity(h).map(|e| (h, e)))
.collect()
}
/// Iterates every entity owned by the current layout's block-record.
/// Returns an empty vec when the block-record is missing or holds no
/// entity handles (legacy DXF without group-code 330 — we err on the
/// side of "no candidates" instead of scanning the whole document, so
/// model-block entities don't leak into a paper-layout selection).
fn current_layout_entity_handles(&self) -> Vec<Handle> {
let block = self.current_layout_block_handle();
self.document
.block_records
.iter()
.find(|br| br.handle == block)
.map(|br| br.entity_handles.clone())
.unwrap_or_default()
}
fn qselect_candidate_handles(&self, scope: crate::app::QSelectScope) -> Vec<Handle> {
match scope {
crate::app::QSelectScope::CurrentSpace => self.current_layout_entity_handles(),
crate::app::QSelectScope::CurrentSelection => {
self.selected_handles_in_order()
}
}
}
pub fn qselect_candidate_count(&self, scope: crate::app::QSelectScope) -> usize {
self.qselect_candidate_handles(scope).len()
}
pub fn qselect_entity_type_names(&self, scope: crate::app::QSelectScope) -> Vec<String> {
use crate::entities::traits::entity_type_name;
let mut names = std::collections::BTreeSet::new();
for h in self.qselect_candidate_handles(scope) {
if let Some(entity) = self.document.get_entity(h) {
names.insert(entity_type_name(entity).to_string());
}
}
names.into_iter().collect()
}
/// Extends the current selection with every entity in the active
/// layout that matches one of the selected entities by `(variant,
/// layer)`. The seed selection stays selected. No-op when nothing is
/// selected. Returns the number of newly-added entities.
pub fn select_similar(&mut self) -> usize {
use crate::entities::traits::entity_type_name;
if self.selected.is_empty() {
return 0;
}
let pairs: rustc_hash::FxHashSet<(&str, String)> = self
.selected
.iter()
.filter_map(|h| self.document.get_entity(*h))
.map(|e| (entity_type_name(e), e.as_entity().layer().to_string()))
.collect();
let handles = self.current_layout_entity_handles();
let mut added = 0;
for h in handles {
if self.selected.contains(&h) {
continue;
}
if let Some(e) = self.document.get_entity(h) {
let key = (entity_type_name(e), e.as_entity().layer().to_string());
if pairs.contains(&key) {
self.selected.insert(h);
self.selected_order.push(h);
added += 1;
}
}
}
if added > 0 {
self.bump_selection_set();
}
added
}
/// Replace the selection with its complement: every selectable object
/// in the active layout that isn't currently selected. The candidate
/// set is the visible wire set, so objects on off/frozen layers (which
/// can't be picked anyway) are excluded. Returns the new count.
pub fn invert_selection(&mut self) -> usize {
let prev: rustc_hash::FxHashSet<Handle> = self.selected.iter().copied().collect();
let all: Vec<Handle> = self
.entity_wires()
.iter()
.filter_map(|w| Self::handle_from_wire_name(&w.name))
.collect();
self.selected.clear();
self.selected_order.clear();
for h in all {
if !prev.contains(&h) {
self.selected.insert(h);
self.selected_order.push(h);
}
}
if self.selected != prev {
self.bump_selection_set();
}
self.selected.len()
}
/// Builds a selection from the chosen scope and filter. Exclude mode
/// keeps candidates that do not match. Append mode unions the result
/// with the selection that existed before the query.
///
/// `type_name` of `None` means "any type". `property_field` of
/// `None` skips the property test (only the type filter applies).
/// The operator's `Any` variant also skips the property test.
/// Numeric operators (`Gt` / `Lt`) parse both sides as `f64` and
/// reject anything non-numeric.
pub fn qselect(
&mut self,
scope: crate::app::QSelectScope,
type_name: Option<&str>,
property_field: Option<&str>,
op: crate::app::QSelectOp,
value: &str,
mode: crate::app::QSelectMode,
append: bool,
) -> usize {
use crate::app::{QSelectMode, QSelectOp};
use crate::entities::traits::entity_type_name;
let previous = self.selected.clone();
let handles = self.qselect_candidate_handles(scope);
let mut result = HashSet::default();
for h in handles {
let Some(e) = self.document.get_entity(h) else {
continue;
};
let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t);
let prop_ok = if !type_ok {
true
} else {
match (property_field, op) {
(None, _) | (_, QSelectOp::Any) => true,
(Some(field), op) => {
match self.entity_property_value(e, field) {
Some(actual) => match op {
QSelectOp::Eq => actual.eq_ignore_ascii_case(value),
QSelectOp::Neq => !actual.eq_ignore_ascii_case(value),
QSelectOp::Gt | QSelectOp::Lt => {
match (
crate::entities::common::parse_f64(&actual),
crate::entities::common::parse_f64(value),
) {
(Some(a), Some(b)) if matches!(op, QSelectOp::Gt) => a > b,
(Some(a), Some(b)) => a < b,
_ => false,
}
}
QSelectOp::Any => true,
},
None => false,
}
}
}
};
let matches_filter = type_ok && prop_ok;
let keep = match mode {
QSelectMode::Include => matches_filter,
QSelectMode::Exclude => !matches_filter,
};
if keep {
result.insert(h);
}
}
if append {
result.extend(previous);
}
self.replace_selection(result);
self.selected.len()
}
/// Returns the sorted set of entity-type names present in the active
/// layout. Used to populate the Quick Select "Object type" dropdown
/// with only the types that actually exist in the drawing.
pub fn entity_type_names_in_layout(&self) -> Vec<String> {
use crate::entities::traits::entity_type_name;
let mut names: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for h in self.current_layout_entity_handles() {
if let Some(e) = self.document.get_entity(h) {
names.insert(entity_type_name(e).to_string());
}
}
names.into_iter().collect()
}
/// True when `handle`'s entity type is allowed by the selection filter.
/// The filter stores excluded type names; empty = everything allowed.
pub fn passes_selection_filter(&self, handle: Handle) -> bool {
if self.selection_filter.is_empty() {
return true;
}
match self.document.get_entity(handle) {
Some(e) => !self
.selection_filter
.contains(crate::entities::traits::entity_type_name(e)),
None => true,
}
}
/// True when the selection filter is excluding at least one type.
pub fn selection_filter_active(&self) -> bool {
!self.selection_filter.is_empty()
}
/// Returns common and type-specific filter properties with the editor
/// each value needs. Choice editors are enriched from both document
/// tables and values present in the selected candidate scope.
pub fn qselect_properties(
&self,
type_name: Option<&str>,
scope: crate::app::QSelectScope,
) -> Vec<crate::app::QSelectPropertyChoice> {
use crate::entities::traits::{entity_type_name, EntityTypeOps};
use crate::scene::model::object::PropValue;
use crate::app::{QSelectPropertyChoice, QSelectValueEditor};
let candidate_handles: Vec<Handle> = self
.qselect_candidate_handles(scope)
.into_iter()
.filter(|h| {
self.document.get_entity(*h).is_some_and(|entity| {
type_name.is_none_or(|t| entity_type_name(entity) == t)
})
})
.collect();
let mut layer_options: Vec<String> = self
.document
.layers
.iter()
.map(|layer| layer.name.clone())
.collect();
layer_options.sort_by_key(|value| value.to_lowercase());
let mut linetype_options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
linetype_options.extend(
self.document
.line_types
.iter()
.map(|linetype| linetype.name.clone()),
);
let choice = |field: &str, label: String, editor: QSelectValueEditor| {
QSelectPropertyChoice {
field: field.to_string(),
label,
editor,
}
};
let mut out = vec![
choice("handle", crate::t!("Handle").into_owned(), QSelectValueEditor::Text),
choice(
"color",
crate::t!("Color").into_owned(),
QSelectValueEditor::Choice(vec!["ByLayer".into(), "ByBlock".into()]),
),
choice(
"layer",
crate::t!("Layer").into_owned(),
QSelectValueEditor::Choice(layer_options.clone()),
),
choice(
"linetype",
crate::t!("Linetype").into_owned(),
QSelectValueEditor::Choice(linetype_options.clone()),
),
choice(
"linetype_scale",
crate::t!("Linetype scale").into_owned(),
QSelectValueEditor::Number,
),
choice(
"plot_style",
crate::t!("Plot style").into_owned(),
QSelectValueEditor::Choice(vec![
"ByLayer".into(),
"ByBlock".into(),
"ByColor".into(),
]),
),
choice(
"lineweight",
crate::t!("Lineweight").into_owned(),
QSelectValueEditor::Choice(vec![
"ByLayer".into(),
"ByBlock".into(),
"Default".into(),
]),
),
choice(
"transparency",
crate::t!("Transparency").into_owned(),
QSelectValueEditor::Choice(vec!["ByLayer".into()]),
),
choice(
"hyperlink",
crate::t!("Hyperlink").into_owned(),
QSelectValueEditor::Text,
),
];
if let Some(t) = type_name {
let text_style_names: Vec<String> = self
.document
.text_styles
.iter()
.map(|s| s.name.clone())
.collect();
let sample = candidate_handles
.iter()
.copied()
.filter_map(|h| self.document.get_entity(h))
.find(|e| entity_type_name(e) == t);
if let Some(sample) = sample {
let mut sections = vec![crate::scene::cache::properties::general_section(sample)];
if let Some(section) =
crate::scene::cache::properties::visualization_section(sample)
{
sections.push(section);
}
sections.extend(sample.geometry_properties(&text_style_names));
for section in sections {
for prop in section.props {
if out.iter().any(|item| item.field == prop.field) {
continue;
}
let editor = if prop.field == "material" {
QSelectValueEditor::Choice(vec![
"ByLayer".into(),
"ByBlock".into(),
"Custom".into(),
])
} else {
match prop.value {
PropValue::PlainText(_) => QSelectValueEditor::Text,
PropValue::ReadOnly(ref value)
| PropValue::ReadOnlyWithTooltip { ref value, .. }
| PropValue::EditText(ref value) => {
let field = prop.field.to_ascii_lowercase();
let textual = [
"name",
"text",
"content",
"style",
"tag",
"prompt",
"value",
"description",
"format",
"font",
"path",
"file",
"url",
]
.iter()
.any(|part| field.contains(part));
if !textual
&& crate::entities::common::parse_f64(value).is_some()
{
QSelectValueEditor::Number
} else {
QSelectValueEditor::Text
}
}
PropValue::LayerChoice(_) => {
QSelectValueEditor::Choice(layer_options.clone())
}
PropValue::Choice { ref options, .. } => {
QSelectValueEditor::Choice(options.clone())
}
PropValue::EditChoice {
ref value,
ref options,
} => {
let mut values = options.clone();
if !values.iter().any(|item| item == value) {
values.push(value.clone());
}
QSelectValueEditor::Choice(values)
}
PropValue::ColorChoice(_) => QSelectValueEditor::Choice(vec![
"ByLayer".into(),
"ByBlock".into(),
]),
PropValue::LwChoice(_) => QSelectValueEditor::Choice(vec![
"ByLayer".into(),
"ByBlock".into(),
"Default".into(),
]),
PropValue::LinetypeChoice(_) => {
QSelectValueEditor::Choice(linetype_options.clone())
}
PropValue::HatchPatternChoice(value) => {
let mut patterns: Vec<String> =
crate::scene::model::hatch_patterns::catalog()
.iter()
.map(|entry| entry.name.clone())
.collect();
if !patterns.iter().any(|item| item == &value) {
patterns.push(value);
}
QSelectValueEditor::Choice(patterns)
}
PropValue::BoolToggle { .. } => {
QSelectValueEditor::Choice(vec![
"false".into(),
"true".into(),
])
}
PropValue::AttrText { .. } => QSelectValueEditor::Text,
PropValue::Stepper { .. }
| PropValue::ColorVaries
| PropValue::LwVaries => continue,
}
};
out.push(choice(prop.field, prop.label, editor));
}
}
}
}
for property in &mut out {
let QSelectValueEditor::Choice(options) = &mut property.editor else {
continue;
};
let common_choice = matches!(
property.field.as_str(),
"color"
| "layer"
| "linetype"
| "plot_style"
| "lineweight"
| "transparency"
| "material"
);
if !common_choice && !options.is_empty() {
continue;
}
for handle in &candidate_handles {
let Some(entity) = self.document.get_entity(*handle) else {
continue;
};
if let Some(value) = self.entity_property_value(entity, &property.field) {
if !options.iter().any(|option| option.eq_ignore_ascii_case(&value)) {
options.push(value);
}
}
}
options.sort_by_key(|value| value.to_lowercase());
}
out
}
/// Reads a property value from an entity for QSELECT comparison.
/// Returns the canonical string used as the left-hand side of the
/// operator test. Common properties have hand-rolled formatting so
/// `"ByLayer"` / `"7"` / `"0.30mm"` are stable; everything else
/// goes through `geometry_properties()` and pulls the matching
/// row's value out.
pub fn entity_property_value(
&self,
entity: &acadrust::EntityType,
field: &str,
) -> Option<String> {
use crate::entities::traits::EntityTypeOps;
use crate::scene::model::object::PropValue;
match field {
"handle" => Some(entity.common().handle.value().to_string()),
"layer" => Some(entity.common().layer.clone()),
"color" => Some(Self::format_color(entity.common().color)),
"linetype" => Some(if entity.common().linetype.is_empty() {
"ByLayer".to_string()
} else {
entity.common().linetype.clone()
}),
"linetype_scale" => Some(format!("{:.4}", entity.common().linetype_scale)),
"plot_style" => Some(
match entity.common().plotstyle_flags {
0 => "ByLayer",
1 => "ByBlock",
_ => "ByColor",
}
.to_string(),
),
"lineweight" => Some(Self::format_lineweight(entity.common().line_weight)),
"transparency" => Some(if entity.common().transparency.alpha() == 0 {
"ByLayer".to_string()
} else {
((entity.common().transparency.alpha() as f64 / 255.0 * 100.0).round() as u32)
.to_string()
}),
"hyperlink" => Some(
entity
.common()
.extended_data
.get_record("PE_URL")
.and_then(|record| {
record.values.iter().find_map(|value| match value {
acadrust::xdata::XDataValue::String(text) if !text.is_empty() => {
Some(text.clone())
}
_ => None,
})
})
.unwrap_or_default(),
),
"material" => Some(
match entity.common().material_flags {
0 => "ByLayer",
1 => "ByBlock",
_ => "Custom",
}
.to_string(),
),
_ => {
let text_style_names: Vec<String> = self
.document
.text_styles
.iter()
.map(|s| s.name.clone())
.collect();
let mut sections = vec![crate::scene::cache::properties::general_section(entity)];
if let Some(section) =
crate::scene::cache::properties::visualization_section(entity)
{
sections.push(section);
}
sections.extend(entity.geometry_properties(&text_style_names));
let prop = sections
.into_iter()
.flat_map(|s| s.props)
.find(|p| p.field == field)?;
Some(match prop.value {
PropValue::ReadOnly(s)
| PropValue::ReadOnlyWithTooltip { value: s, .. }
| PropValue::EditText(s)
| PropValue::PlainText(s) => s,
PropValue::LayerChoice(s) => s,
PropValue::Choice { selected, .. } => selected,
PropValue::EditChoice { value, .. } => value,
PropValue::ColorChoice(c) => Self::format_color(c),
PropValue::LwChoice(lw) => Self::format_lineweight(lw),
PropValue::LinetypeChoice(s) => s,
PropValue::HatchPatternChoice(s) => s,
PropValue::BoolToggle { value, .. } => value.to_string(),
PropValue::AttrText { value, .. } => value,
PropValue::Stepper { display, .. } => display,
PropValue::ColorVaries | PropValue::LwVaries => return None,
})
}
}
}
fn format_color(c: acadrust::types::Color) -> String {
use acadrust::types::Color;
match c {
Color::ByLayer => "ByLayer".to_string(),
Color::None => "None".to_string(),
Color::ByBlock => "ByBlock".to_string(),
Color::Index(i) => i.to_string(),
Color::Rgb { r, g, b } => format!("{},{},{}", r, g, b),
}
}
fn format_lineweight(lw: acadrust::types::LineWeight) -> String {
use acadrust::types::LineWeight;
match lw {
LineWeight::ByLayer => "ByLayer".to_string(),
LineWeight::ByBlock => "ByBlock".to_string(),
LineWeight::Default => "Default".to_string(),
LineWeight::Value(v) => format!("{:.2}mm", v as f64 / 100.0),
}
}
// ── Erase ─────────────────────────────────────────────────────────────
pub fn erase_entities(&mut self, handles: &[Handle]) {
let erase_handles = self.handles_expanded_for_leader_annotations(handles);
let mut handle_set: HashSet<Handle> = HashSet::default();
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
let mut selection_changed = false;
let mut hover_changed = false;
for &h in &erase_handles {
// Objects on a locked layer can't be erased.
if self.is_layer_locked(h) {
continue;
}
// Delta-undo: capture the removed entity so an undo can re-insert it.
if self.is_recording_undo() {
let before = self.document.get_entity_arc(h);
self.record_undo_before(h, before);
}
self.delete_solid_history(h);
self.remember_removed_cache_categories(h);
self.document.remove_entity_arc(h);
selection_changed |= self.selected.remove(&h);
self.selected_order.retain(|selected| *selected != h);
if self.hover_highlight == Some(h) {
self.hover_highlight = None;
hover_changed = true;
}
self.hatches.remove(&h);
self.images.remove(&h);
self.meshes.remove(&h);
self.block_meshes.remove(&h);
self.solid_models.remove(&h);
handle_set.insert(h);
erased.push((h, ChangeKind::Removed));
}
if selection_changed {
self.bump_selection_set();
} else if hover_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.
if self.is_recording_undo() && !handle_set.is_empty() {
let affected_groups: Vec<(Handle, ObjectType)> = self
.document
.objects
.iter()
.filter_map(|(&handle, object)| match object {
ObjectType::Group(group)
if group.entities.iter().any(|h| handle_set.contains(h)) =>
{
Some((handle, object.clone()))
}
_ => None,
})
.collect();
if !affected_groups.is_empty() {
let removes_group = affected_groups.iter().any(|(_, object)| {
matches!(
object,
ObjectType::Group(group)
if group.entities.iter().all(|h| handle_set.contains(h))
)
});
if removes_group {
let dictionary = self.document.header.acad_group_dict_handle;
let dictionary_before = self.document.objects.get(&dictionary).cloned();
self.record_undo_object_before(dictionary, dictionary_before);
}
for (handle, object) in affected_groups {
self.record_undo_object_before(handle, Some(object));
}
}
}
// Remove erased handles from all groups; delete groups that become empty.
let group_dict_handle = self.document.header.acad_group_dict_handle;
let to_remove: Vec<Handle> = self
.document
.objects
.values_mut()
.filter_map(|obj| match obj {
ObjectType::Group(g) => {
g.entities.retain(|h| !handle_set.contains(h));
if g.entities.is_empty() {
Some(g.handle)
} else {
None
}
}
_ => None,
})
.collect();
for gh in &to_remove {
if let Some(ObjectType::Dictionary(dict)) =
self.document.objects.get_mut(&group_dict_handle)
{
dict.entries.retain(|(_, h)| h != gh);
}
self.document.objects.remove(gh);
}
// Deleting top-level entities/inserts leaves block definitions intact.
// Report the exact erased handles so derived caches drop just those and
// the resident set removes only their wires (bump_entities drops them
// from the tessellation memos too).
if !erased.is_empty() {
self.invalidate_dependency_index();
self.bump_entities(&erased);
}
}
/// Restore erased Arc-backed entities without re-linking their still-present
/// block-record handles. Used by OOPS and history replay.
pub fn restore_erased_entities(&mut self, entities: Vec<Arc<EntityType>>) -> Vec<Handle> {
let mut restored = Vec::with_capacity(entities.len());
let mut changes = Vec::with_capacity(entities.len());
for entity in entities {
let handle = entity.common().handle;
if handle.is_null() || self.document.get_entity(handle).is_some() {
continue;
}
if self.is_recording_undo() {
self.record_undo_before(handle, None);
}
if self.document.restore_entity_arc(entity).is_some() {
self.reseed_derived_caches(handle);
restored.push(handle);
changes.push((handle, ChangeKind::Added));
}
}
if !changes.is_empty() {
self.invalidate_dependency_index();
self.bump_entities(&changes);
}
restored
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selection_fingerprint_tracks_final_set_only() {
let mut scene = Scene::default();
let first = Handle::new(1);
let second = Handle::new(2);
scene.select_entity(first, false);
scene.select_entity(second, false);
let fingerprint = scene.selection_fingerprint();
assert!(!scene.selection_fingerprint_dirty);
scene.deselect_all();
scene.select_entity(second, false);
scene.select_entity(first, false);
assert!(scene.selection_fingerprint_dirty);
assert_eq!(scene.selection_fingerprint(), fingerprint);
scene.set_hover_highlight(Some(Handle::new(3)));
assert!(!scene.selection_fingerprint_dirty);
assert_eq!(scene.selection_fingerprint(), fingerprint);
}
}