forked from 1b-agent/opencadstudio-upstream
Groundwork for making the Properties panel conform to the per-entity group/row spec (PROPERTIES.md). The panel already renders a Vec of sections, but each entity could only express a single geometry section. Change PropertyEditable::geometry_properties (and the EntityTypeOps enum dispatch) to return Vec<PropSection>, update the two impl macros, and flatten in properties_sectioned so the panel shows [General] + the entity's groups. Every entity builder is wrapped to return its existing section as a one-element Vec — pure mechanical change, no rows or titles altered, panel output identical. Enables Phase 2: rewriting each entity's groups/rows to match the spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
374 lines
14 KiB
Rust
374 lines
14 KiB
Rust
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
|
use super::*;
|
|
|
|
impl Scene {
|
|
// ── Selection ─────────────────────────────────────────────────────────
|
|
|
|
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
|
|
if exclusive {
|
|
self.selected.clear();
|
|
}
|
|
self.selected.insert(handle);
|
|
self.bump_selection();
|
|
}
|
|
|
|
pub fn deselect_all(&mut self) {
|
|
self.selected.clear();
|
|
self.bump_selection();
|
|
}
|
|
|
|
/// Remove a single entity from the selection (Shift+click subtractive pick).
|
|
pub fn deselect_entity(&mut self, handle: Handle) {
|
|
if self.selected.remove(&handle) {
|
|
self.bump_selection();
|
|
}
|
|
}
|
|
|
|
pub fn selected_entities(&self) -> Vec<(Handle, &EntityType)> {
|
|
self.selected
|
|
.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()
|
|
}
|
|
|
|
/// 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<(&'static 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);
|
|
added += 1;
|
|
}
|
|
}
|
|
}
|
|
if added > 0 {
|
|
self.bump_selection();
|
|
}
|
|
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();
|
|
for h in all {
|
|
if !prev.contains(&h) {
|
|
self.selected.insert(h);
|
|
}
|
|
}
|
|
self.bump_selection();
|
|
self.selected.len()
|
|
}
|
|
|
|
/// Replaces (or extends, when `append` is true) the current
|
|
/// selection with every entity in the active layout that matches
|
|
/// the filter. Returns the number of newly-matching entities.
|
|
///
|
|
/// `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,
|
|
type_name: Option<&str>,
|
|
property_field: Option<&str>,
|
|
op: crate::app::QSelectOp,
|
|
value: &str,
|
|
append: bool,
|
|
) -> usize {
|
|
use crate::app::QSelectOp;
|
|
use crate::entities::traits::entity_type_name;
|
|
if !append {
|
|
self.selected.clear();
|
|
}
|
|
let handles = self.current_layout_entity_handles();
|
|
let mut matched = 0;
|
|
for h in handles {
|
|
let Some(e) = self.document.get_entity(h) else {
|
|
continue;
|
|
};
|
|
// Never quick-select objects on a locked layer.
|
|
if self
|
|
.document
|
|
.layers
|
|
.get(&e.common().layer)
|
|
.map(|l| l.is_locked())
|
|
.unwrap_or(false)
|
|
{
|
|
continue;
|
|
}
|
|
if let Some(t) = type_name {
|
|
if entity_type_name(e) != t {
|
|
continue;
|
|
}
|
|
}
|
|
let prop_ok = match (property_field, op) {
|
|
(None, _) | (_, QSelectOp::Any) => true,
|
|
(Some(field), op) => {
|
|
let Some(actual) = self.entity_property_value(e, field) else {
|
|
continue;
|
|
};
|
|
match op {
|
|
QSelectOp::Eq => actual.eq_ignore_ascii_case(value),
|
|
QSelectOp::Neq => !actual.eq_ignore_ascii_case(value),
|
|
QSelectOp::Gt | QSelectOp::Lt => {
|
|
let (Ok(a), Ok(b)) =
|
|
(actual.parse::<f64>(), value.parse::<f64>())
|
|
else {
|
|
continue;
|
|
};
|
|
if matches!(op, QSelectOp::Gt) {
|
|
a > b
|
|
} else {
|
|
a < b
|
|
}
|
|
}
|
|
QSelectOp::Any => true,
|
|
}
|
|
}
|
|
};
|
|
if prop_ok {
|
|
self.selected.insert(h);
|
|
matched += 1;
|
|
}
|
|
}
|
|
self.bump_selection();
|
|
matched
|
|
}
|
|
|
|
/// 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<&'static str> {
|
|
use crate::entities::traits::entity_type_name;
|
|
let mut names: std::collections::BTreeSet<&'static str> =
|
|
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));
|
|
}
|
|
}
|
|
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 the list of `(field, label)` pairs the Quick Select
|
|
/// "Properties" dropdown should show given the current type filter:
|
|
///
|
|
/// * Common properties (Layer, Color, Linetype, Lineweight) are
|
|
/// always included.
|
|
/// * When `type_name` names a specific entity type present in the
|
|
/// active layout, the first entity of that type contributes its
|
|
/// `geometry_properties()` rows (Start X, Length, Radius, …) so
|
|
/// type-specific filtering works.
|
|
pub fn qselect_properties(
|
|
&self,
|
|
type_name: Option<&str>,
|
|
) -> Vec<(String, String)> {
|
|
use crate::entities::traits::{entity_type_name, EntityTypeOps};
|
|
let mut out: Vec<(String, String)> = vec![
|
|
("layer".to_string(), "Layer".to_string()),
|
|
("color".to_string(), "Color".to_string()),
|
|
("linetype".to_string(), "Linetype".to_string()),
|
|
("lineweight".to_string(), "Lineweight".to_string()),
|
|
];
|
|
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 = self
|
|
.current_layout_entity_handles()
|
|
.into_iter()
|
|
.filter_map(|h| self.document.get_entity(h))
|
|
.find(|e| entity_type_name(e) == t);
|
|
if let Some(sample) = sample {
|
|
for section in sample.geometry_properties(&text_style_names) {
|
|
for prop in section.props {
|
|
// Skip rows that don't sensibly compare via
|
|
// `entity_property_value` (read-only labels are
|
|
// fine — users can still match against them).
|
|
out.push((prop.field.to_string(), prop.label.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
"layer" => Some(entity.common().layer.clone()),
|
|
"color" => Some(Self::format_color(entity.common().color)),
|
|
"linetype" => Some(entity.common().linetype.clone()),
|
|
"lineweight" => Some(Self::format_lineweight(entity.common().line_weight)),
|
|
_ => {
|
|
let text_style_names: Vec<String> = self
|
|
.document
|
|
.text_styles
|
|
.iter()
|
|
.map(|s| s.name.clone())
|
|
.collect();
|
|
let prop = entity
|
|
.geometry_properties(&text_style_names)
|
|
.into_iter()
|
|
.flat_map(|s| s.props)
|
|
.find(|p| p.field == field)?;
|
|
Some(match prop.value {
|
|
PropValue::ReadOnly(s) | PropValue::EditText(s) => s,
|
|
PropValue::LayerChoice(s) => s,
|
|
PropValue::Choice { selected, .. } => selected,
|
|
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::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::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]) {
|
|
for &h in handles {
|
|
// Objects on a locked layer can't be erased.
|
|
if self.is_layer_locked(h) {
|
|
continue;
|
|
}
|
|
self.document.remove_entity(h);
|
|
self.selected.remove(&h);
|
|
self.hatches.remove(&h);
|
|
self.meshes.remove(&h);
|
|
self.solid_models.remove(&h);
|
|
self.mark_entity_dirty(h);
|
|
}
|
|
// 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| !handles.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;
|
|
// the erased handles were already dropped from the memo above.
|
|
self.bump_geometry_no_blocks();
|
|
}
|
|
}
|