forked from 1b-agent/opencadstudio-upstream
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
2baabdf4a0
commit
bddc050b3e
39 files changed
+26674
-26113
No files matched your search
@@ -0,0 +1,355 @@
|
||||
// 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;
|
||||
};
|
||||
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 {
|
||||
if let Some(section) = 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 section = entity.geometry_properties(&text_style_names)?;
|
||||
let prop = section.props.into_iter().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::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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user