forked from 1b-agent/opencadstudio-upstream
Selecting a whole drawing was unusable on a 186 468-entity file. Four costs scaled with the selection where they did not have to. Every selected handle triggered a walk of the entire document to find LEADERs pointing at it. On the file this came from, selecting 471 583 entities in a 585 786-entity drawing did not finish inside eight minutes. A reverse index built once per geometry epoch answers the same question; a test asserts it returns exactly what the walk returned, including the case of two leaders on one annotation. Group expansion asked the selection about every group: for each selected handle it scanned each group's own member list. Asking each group once whether it holds anything selected turns that around. Selecting and deselecting went one entity at a time, and `deselect_entity` rebuilt the ordered list with a `retain` per handle, so removing N of M walked the order list N times. Both now take a slice. Grips were built for every selected entity. AutoCAD stops at GRIPOBJLIMIT, and the DWG header does not carry the variable, so its default of 100 stands in for it. The remaining two commits are measurement: `select-commit` reports the phases of a selection separately, which is what showed the bulk paths had been added to the press-drag branch while a click-move-click window goes through a different one entirely.
1343 lines
54 KiB
Rust
1343 lines
54 KiB
Rust
use super::*;
|
|
|
|
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.
|
|
/// Every LEADER that points at `annotation`, resolved once per
|
|
/// `geometry_epoch` rather than by walking the document per handle.
|
|
fn leaders_by_annotation(
|
|
&self,
|
|
) -> std::cell::Ref<'_, (u64, HashMap<Handle, Vec<Handle>>)> {
|
|
{
|
|
let cache = self.leaders_by_annotation_cache.borrow();
|
|
if cache.as_ref().is_some_and(|(epoch, _)| *epoch == self.geometry_epoch) {
|
|
drop(cache);
|
|
return std::cell::Ref::map(
|
|
self.leaders_by_annotation_cache.borrow(),
|
|
|c| c.as_ref().unwrap(),
|
|
);
|
|
}
|
|
}
|
|
let mut by_annotation: HashMap<Handle, Vec<Handle>> = HashMap::default();
|
|
for entity in self.document.entities() {
|
|
if let EntityType::Leader(leader) = entity {
|
|
if !leader.annotation_handle.is_null() {
|
|
by_annotation
|
|
.entry(leader.annotation_handle)
|
|
.or_default()
|
|
.push(entity.common().handle);
|
|
}
|
|
}
|
|
}
|
|
*self.leaders_by_annotation_cache.borrow_mut() =
|
|
Some((self.geometry_epoch, by_annotation));
|
|
std::cell::Ref::map(self.leaders_by_annotation_cache.borrow(), |c| {
|
|
c.as_ref().unwrap()
|
|
})
|
|
}
|
|
|
|
/// The handles plus their leader/annotation partners, in the order given.
|
|
///
|
|
/// Kept separate from the sorted, deduplicated form because a bulk
|
|
/// selection has to preserve pick order — `selected_handles_in_order`
|
|
/// reports it — and because sorting per handle is what made selecting
|
|
/// 186 000 entities one call at a time expensive.
|
|
fn expanded_with_leaders(&self, handles: &[Handle]) -> Vec<Handle> {
|
|
let leaders = self.leaders_by_annotation();
|
|
let mut expanded = Vec::with_capacity(handles.len());
|
|
for &handle in handles {
|
|
expanded.push(handle);
|
|
if let Some(EntityType::Leader(leader)) = self.document.get_entity(handle) {
|
|
if !leader.annotation_handle.is_null() {
|
|
expanded.push(leader.annotation_handle);
|
|
}
|
|
}
|
|
if let Some(pointing) = leaders.1.get(&handle) {
|
|
expanded.extend(pointing.iter().copied());
|
|
}
|
|
}
|
|
expanded
|
|
}
|
|
|
|
/// Select many handles in one pass.
|
|
///
|
|
/// The single-handle path allocates, sorts and deduplicates for every
|
|
/// entity, and a box selection called it once per entity — most of the
|
|
/// remaining 786 ms of a 186 460-entity selection.
|
|
pub fn select_entities(&mut self, handles: &[Handle]) {
|
|
if handles.is_empty() {
|
|
return;
|
|
}
|
|
let expanded = self.expanded_with_leaders(handles);
|
|
let mut changed = false;
|
|
for handle in expanded {
|
|
if self.selected.insert(handle) {
|
|
self.selected_order.push(handle);
|
|
changed = true;
|
|
}
|
|
}
|
|
if changed {
|
|
self.bump_selection_set();
|
|
}
|
|
}
|
|
|
|
/// Deselect many handles in one pass.
|
|
///
|
|
/// `deselect_entity` rebuilds `selected_order` with a `retain` per handle,
|
|
/// so removing N of M selected entities walked the order list N times.
|
|
pub fn deselect_entities(&mut self, handles: &[Handle]) {
|
|
if handles.is_empty() {
|
|
return;
|
|
}
|
|
let doomed: HashSet<Handle> =
|
|
self.expanded_with_leaders(handles).into_iter().collect();
|
|
let mut changed = false;
|
|
for handle in &doomed {
|
|
changed |= self.selected.remove(handle);
|
|
}
|
|
if changed {
|
|
self.selected_order.retain(|handle| !doomed.contains(handle));
|
|
self.bump_selection_set();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handles_expanded_for_leader_annotations(
|
|
&self,
|
|
handles: &[Handle],
|
|
) -> Vec<Handle> {
|
|
let mut expanded = handles.to_vec();
|
|
let leaders = self.leaders_by_annotation();
|
|
|
|
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.
|
|
if let Some(pointing) = leaders.1.get(&handle) {
|
|
expanded.extend(pointing.iter().copied());
|
|
}
|
|
}
|
|
|
|
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 fn select_all_visible(&mut self) -> usize {
|
|
let block = self.interaction_block_handle();
|
|
let frozen: Option<HashSet<Handle>> = self
|
|
.interaction_viewport_frozen_layers()
|
|
.map(|layers| layers.iter().copied().collect());
|
|
let annotation_scale = self.displayed_annotation_scale_handle();
|
|
let all_visible = self.annotation_all_visible();
|
|
let handles = self
|
|
.document
|
|
.block_records
|
|
.iter()
|
|
.find(|record| record.handle == block)
|
|
.map(|record| record.entity_handles.clone())
|
|
.unwrap_or_default();
|
|
let selected = handles
|
|
.into_iter()
|
|
.filter(|handle| self.passes_selection_filter(*handle))
|
|
.filter(|handle| {
|
|
self.document.get_entity(*handle).is_some_and(|entity| {
|
|
self.resident_entity_visible(
|
|
entity,
|
|
block,
|
|
frozen.as_ref(),
|
|
annotation_scale,
|
|
all_visible,
|
|
)
|
|
})
|
|
})
|
|
.collect();
|
|
self.replace_selection(selected);
|
|
self.selected.len()
|
|
}
|
|
|
|
#[cfg(any(test, not(target_arch = "wasm32")))]
|
|
pub(crate) fn selection_fingerprint(&mut self) -> u64 {
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
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()
|
|
}
|
|
|
|
/// Entity type names in the current layout for the selection-filter menu.
|
|
/// Pure additions are folded into the cached set; other changes rebuild it.
|
|
pub fn entity_type_names_in_layout(&self) -> std::sync::Arc<Vec<String>> {
|
|
use crate::entities::traits::entity_type_name;
|
|
let block = self.current_layout_block_handle();
|
|
let mut cached_epoch = None;
|
|
{
|
|
let cache = self.layout_type_names_cache.borrow();
|
|
if let Some((epoch, cached_block, _, names)) = cache.as_ref() {
|
|
if *cached_block == block {
|
|
if *epoch == self.geometry_epoch {
|
|
return std::sync::Arc::clone(names);
|
|
}
|
|
cached_epoch = Some(*epoch);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Incremental: fold the changes since the cached epoch into the set.
|
|
if let Some(since) = cached_epoch {
|
|
if let Some(deltas) = self.replay_since(since) {
|
|
if deltas
|
|
.iter()
|
|
.all(|(_, kind)| *kind == ChangeKind::Added)
|
|
{
|
|
let mut cache = self.layout_type_names_cache.borrow_mut();
|
|
if let Some((epoch, _, present, names)) = cache.as_mut() {
|
|
let mut added = false;
|
|
for (handle, _) in &deltas {
|
|
if let Some(entity) = self.document.get_entity(*handle) {
|
|
if entity.common().owner_handle == block {
|
|
// `contains` first: the common case is a
|
|
// type already present, and that path must
|
|
// not allocate.
|
|
let name = entity_type_name(entity);
|
|
if !present.contains(name) {
|
|
present.insert(name.to_string());
|
|
added = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Only rebuild the list when the set actually moved;
|
|
// otherwise the existing `Arc` is still the answer.
|
|
if added {
|
|
*names =
|
|
std::sync::Arc::new(present.iter().cloned().collect());
|
|
}
|
|
*epoch = self.geometry_epoch;
|
|
return std::sync::Arc::clone(names);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut present: std::collections::BTreeSet<String> =
|
|
std::collections::BTreeSet::new();
|
|
if let Some(record) = self
|
|
.document
|
|
.block_records
|
|
.iter()
|
|
.find(|record| record.handle == block)
|
|
{
|
|
for &handle in &record.entity_handles {
|
|
if let Some(entity) = self.document.get_entity(handle) {
|
|
let name = entity_type_name(entity);
|
|
if !present.contains(name) {
|
|
present.insert(name.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let names: std::sync::Arc<Vec<String>> =
|
|
std::sync::Arc::new(present.iter().cloned().collect());
|
|
*self.layout_type_names_cache.borrow_mut() = Some((
|
|
self.geometry_epoch,
|
|
block,
|
|
present,
|
|
std::sync::Arc::clone(&names),
|
|
));
|
|
names
|
|
}
|
|
|
|
/// 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(_)
|
|
| PropValue::NamedColorChoice { .. } => QSelectValueEditor::Choice(vec![
|
|
"ByLayer".into(),
|
|
"ByBlock".into(),
|
|
]),
|
|
PropValue::LwChoice(_)
|
|
| PropValue::FieldLwChoice { .. } => 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
|
|
| PropValue::FieldLwVaries { .. } => 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(
|
|
entity
|
|
.common()
|
|
.color_name
|
|
.clone()
|
|
.unwrap_or_else(|| 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(match entity.common().transparency {
|
|
acadrust::types::Transparency::ByLayer => "ByLayer".to_string(),
|
|
acadrust::types::Transparency::ByBlock => "ByBlock".to_string(),
|
|
acadrust::types::Transparency::Explicit(alpha) => {
|
|
((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::NamedColorChoice { name, .. } => name,
|
|
PropValue::LwChoice(lw)
|
|
| PropValue::FieldLwChoice { value: 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
|
|
| PropValue::FieldLwVaries { .. } => 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]) {
|
|
self.remove_entities(handles, true);
|
|
}
|
|
|
|
/// Roll back entities created by the current command after a downstream
|
|
/// registration failure. These entities were never successful command
|
|
/// results, so locked-layer edit policy must not prevent their removal.
|
|
/// The shared removal path keeps first-touch undo state, history objects,
|
|
/// groups, selection, and derived caches coherent.
|
|
pub(crate) fn rollback_new_entities(&mut self, handles: &[Handle]) {
|
|
self.remove_entities(handles, false);
|
|
}
|
|
|
|
fn remove_entities(&mut self, handles: &[Handle], respect_layer_locks: bool) {
|
|
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 respect_layer_locks && 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::*;
|
|
|
|
/// A box selection used to call `select_entity` once per entity. The bulk
|
|
/// path has to land on the same selection, in the same pick order —
|
|
/// `selected_handles_in_order` is read by commands such as DIM, where the
|
|
/// order the user picked things in decides what they mean.
|
|
#[test]
|
|
fn bulk_selection_matches_selecting_one_at_a_time() {
|
|
use acadrust::entities::Line;
|
|
use acadrust::types::Vector3;
|
|
|
|
let build = || {
|
|
let mut scene = Scene::new();
|
|
let handles: Vec<_> = (0..40)
|
|
.map(|k| {
|
|
let x = k as f64;
|
|
scene.add_entity(EntityType::Line(Line::from_points(
|
|
Vector3::new(x, 0.0, 0.0),
|
|
Vector3::new(x + 1.0, 0.0, 0.0),
|
|
)))
|
|
})
|
|
.collect();
|
|
(scene, handles)
|
|
};
|
|
|
|
// Picked in an order that is neither creation nor handle order.
|
|
let picked = |handles: &[Handle]| -> Vec<Handle> {
|
|
let mut order: Vec<_> = handles.iter().copied().collect();
|
|
order.reverse();
|
|
order.retain(|h| h.value() % 3 != 0);
|
|
order
|
|
};
|
|
|
|
let (mut one_at_a_time, handles) = build();
|
|
let order = picked(&handles);
|
|
for &handle in &order {
|
|
one_at_a_time.select_entity(handle, false);
|
|
}
|
|
|
|
let (mut in_bulk, handles) = build();
|
|
in_bulk.select_entities(&picked(&handles));
|
|
|
|
assert_eq!(
|
|
in_bulk.selected_handles_in_order(),
|
|
one_at_a_time.selected_handles_in_order(),
|
|
"the bulk path must select the same entities in the same order",
|
|
);
|
|
|
|
// And removing a subset must agree too.
|
|
let drop: Vec<_> = order.iter().copied().take(7).collect();
|
|
for &handle in &drop {
|
|
one_at_a_time.deselect_entity(handle);
|
|
}
|
|
in_bulk.deselect_entities(&drop);
|
|
assert_eq!(
|
|
in_bulk.selected_handles_in_order(),
|
|
one_at_a_time.selected_handles_in_order(),
|
|
"the bulk removal must leave the same selection in the same order",
|
|
);
|
|
}
|
|
|
|
/// Selecting an entity pulls in any LEADER pointing at it, and that was
|
|
/// answered by walking the whole document once per selected handle —
|
|
/// selecting 471 583 entities in a 585 786-entity drawing did not finish
|
|
/// inside eight minutes. The reverse index has to give exactly what the
|
|
/// walk gave.
|
|
#[test]
|
|
fn the_leader_index_matches_a_document_walk() {
|
|
use acadrust::entities::Leader;
|
|
use acadrust::types::Vector3;
|
|
|
|
let mut scene = Scene::new();
|
|
let line = |x: f64| {
|
|
EntityType::Line(acadrust::entities::Line::from_points(
|
|
Vector3::new(x, 0.0, 0.0),
|
|
Vector3::new(x + 1.0, 0.0, 0.0),
|
|
))
|
|
};
|
|
let annotation = scene.add_entity(line(0.0));
|
|
let unrelated = scene.add_entity(line(5.0));
|
|
let mut leader_on = |target: Handle| {
|
|
let mut leader = Leader::default();
|
|
leader.annotation_handle = target;
|
|
scene.add_entity(EntityType::Leader(leader))
|
|
};
|
|
// Two leaders share one annotation; a third points nowhere.
|
|
let first = leader_on(annotation);
|
|
let second = leader_on(annotation);
|
|
let dangling = leader_on(Handle::NULL);
|
|
|
|
// The walk this replaced, written out so the index is compared against
|
|
// behaviour rather than against itself.
|
|
let by_walk = |handle: Handle| -> Vec<Handle> {
|
|
let mut out = vec![handle];
|
|
if let Some(EntityType::Leader(leader)) = scene.document.get_entity(handle) {
|
|
if !leader.annotation_handle.is_null() {
|
|
out.push(leader.annotation_handle);
|
|
}
|
|
}
|
|
out.extend(scene.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,
|
|
}));
|
|
out.sort_unstable_by_key(Handle::value);
|
|
out.dedup();
|
|
out
|
|
};
|
|
|
|
for handle in [annotation, unrelated, first, second, dangling] {
|
|
assert_eq!(
|
|
scene.handles_expanded_for_leader_annotations(&[handle]),
|
|
by_walk(handle),
|
|
"the index must answer exactly what the walk answered",
|
|
);
|
|
}
|
|
let expanded = scene.handles_expanded_for_leader_annotations(&[annotation]);
|
|
assert!(
|
|
expanded.contains(&first) && expanded.contains(&second),
|
|
"both leaders on one annotation must come back, not just one",
|
|
);
|
|
}
|
|
|
|
/// The reason the cache carries a set as well as a list: drawing into a
|
|
/// drawing that already has that type must not rebuild either, because the
|
|
/// rebuild walks every entity the layout owns — 444 937 of them on the
|
|
/// reproducer, ~100 ms, on every single drawn point.
|
|
#[test]
|
|
fn adding_a_type_already_present_reuses_the_list() {
|
|
use acadrust::entities::{Circle, EntityType, Line};
|
|
use acadrust::types::Vector3;
|
|
use std::sync::Arc;
|
|
|
|
let line = || {
|
|
EntityType::Line(Line::from_points(
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(1.0, 0.0, 0.0),
|
|
))
|
|
};
|
|
let mut scene = Scene::new();
|
|
scene.add_entity(line());
|
|
let first = scene.entity_type_names_in_layout();
|
|
|
|
scene.add_entity(line());
|
|
let second = scene.entity_type_names_in_layout();
|
|
assert!(
|
|
Arc::ptr_eq(&first, &second),
|
|
"a second line changes no type name, so the list must be reused",
|
|
);
|
|
|
|
scene.add_entity(EntityType::Circle(Circle::new()));
|
|
let third = scene.entity_type_names_in_layout();
|
|
assert_eq!(third.as_slice(), ["Circle", "Line"], "a new type must appear");
|
|
|
|
// The incremental answer has to be the answer a full walk gives.
|
|
scene.layout_type_names_cache.borrow_mut().take();
|
|
assert_eq!(
|
|
scene.entity_type_names_in_layout().as_slice(),
|
|
third.as_slice(),
|
|
"folding edits in must match rebuilding from scratch",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn changing_an_entity_type_rebuilds_the_type_names() {
|
|
use acadrust::entities::{Circle, EntityType, Line};
|
|
use acadrust::types::Vector3;
|
|
|
|
let mut scene = Scene::new();
|
|
let handle = scene.add_entity(EntityType::Line(Line::from_points(
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(1.0, 0.0, 0.0),
|
|
)));
|
|
assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Line"]);
|
|
|
|
let mut circle = Circle::new();
|
|
circle.common.handle = handle;
|
|
assert!(scene.update_entity(EntityType::Circle(circle)));
|
|
assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Circle"]);
|
|
}
|
|
|
|
#[test]
|
|
fn layout_type_cache_reuses_and_invalidates_on_edits_undo_and_layout() {
|
|
use acadrust::entities::{Circle, EntityType, Line};
|
|
use acadrust::types::Vector3;
|
|
use std::sync::Arc;
|
|
|
|
let mut scene = Scene::new();
|
|
let empty = scene.entity_type_names_in_layout();
|
|
assert!(empty.is_empty());
|
|
assert!(Arc::ptr_eq(&empty, &scene.entity_type_names_in_layout()));
|
|
scene.add_entity(EntityType::Line(Line::from_points(
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(1.0, 0.0, 0.0),
|
|
)));
|
|
let lines = scene.entity_type_names_in_layout();
|
|
assert_eq!(lines.as_slice(), ["Line"]);
|
|
assert!(Arc::ptr_eq(&lines, &scene.entity_type_names_in_layout()));
|
|
let circle = scene.add_entity(EntityType::Circle(Circle::new()));
|
|
let before = scene.document.get_entity_arc(circle);
|
|
assert_eq!(
|
|
scene.entity_type_names_in_layout().as_slice(),
|
|
["Circle", "Line"]
|
|
);
|
|
scene.erase_entities(&[circle]);
|
|
assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Line"]);
|
|
let changes = scene.apply_entity_delta(&[(circle, before, None)], true);
|
|
scene.bump_entities(&changes);
|
|
assert_eq!(
|
|
scene.entity_type_names_in_layout().as_slice(),
|
|
["Circle", "Line"]
|
|
);
|
|
scene.set_current_layout("Layout1".to_owned());
|
|
assert!(scene.entity_type_names_in_layout().is_empty());
|
|
scene.set_current_layout("Model".to_owned());
|
|
assert_eq!(
|
|
scene.entity_type_names_in_layout().as_slice(),
|
|
["Circle", "Line"]
|
|
);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|