897 lines
36 KiB
Rust
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);
|
|
}
|
|
}
|