From 5d0ba9cef218800d73067eb2b9e1992971719428 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:39:21 +0300 Subject: [PATCH] Add persistent nested-copy symbol settings and transactional imports --- src/app/command_driver.rs | 32 +++++++++++++++++++++---- src/app/commands/blocks.rs | 2 +- src/app/mod.rs | 2 ++ src/app/settings.rs | 3 +++ src/app/update/file.rs | 2 ++ src/command.rs | 3 +++ src/io/xref.rs | 1 + src/modules/draw/modify/ncopy.rs | 40 ++++++++++++++++++++++++++------ 8 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 341e98ca6..3dfeaeef3 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -929,6 +929,12 @@ impl OpenCADStudio { fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task { let i = self.active_tab; + if let Some(bind) = self.tabs[i].active_cmd.as_ref().and_then(|command| command.nested_copy_bind_setting()) { + if self.ncopy_bind != bind { + self.ncopy_bind = bind; + self.persist_settings_if_changed(); + } + } let preserve_commit_style = self.tabs[i].active_cmd.as_ref() .is_some_and(|command| command.preserve_commit_style()); let preserve_commit_layer = self.tabs[i] @@ -1054,7 +1060,7 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } - CmdResult::CommitEntities(entities) => { + CmdResult::CommitEntities(mut entities) => { let locked_source = entities.iter().find_map(|entity| { let handle = entity.common().handle; (!handle.is_null() @@ -1067,10 +1073,19 @@ impl OpenCADStudio { return Task::none(); } let label = self.history_label_from_active_cmd(i, "ENTITY"); - let delta_safe = entities + let symbol_names = self.tabs[i].active_cmd.as_ref() + .and_then(|command| command.nested_copy_symbol_names()).cloned(); + let delta_safe = symbol_names.is_none() && entities .iter() .all(|entity| self.delta_add_safe(i, entity)); let pending = self.begin_undo(i, label, entities.len(), delta_safe); + if let Some(names) = symbol_names { + let retained = self.tabs[i].scene.document.localize_nested_copy_symbols(&mut entities, &names); + if retained > 0 { + self.command_line.push_info(&format!("{} copied object(s) retain imported style references whose dependencies cannot be localized.", retained)); + } + self.refresh_layer_panel(); + } for entity in entities { if preserve_commit_style { let _ = self.commit_entity_handle_preserve_style(entity); @@ -1090,12 +1105,21 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } - CmdResult::CommitEntitiesAndExit(entities) => { + CmdResult::CommitEntitiesAndExit(mut entities) => { let label = self.history_label_from_active_cmd(i, "ENTITY"); - let delta_safe = entities + let symbol_names = self.tabs[i].active_cmd.as_ref() + .and_then(|command| command.nested_copy_symbol_names()).cloned(); + let delta_safe = symbol_names.is_none() && entities .iter() .all(|entity| self.delta_add_safe(i, entity)); let pending = self.begin_undo(i, label, entities.len(), delta_safe); + if let Some(names) = symbol_names { + let retained = self.tabs[i].scene.document.localize_nested_copy_symbols(&mut entities, &names); + if retained > 0 { + self.command_line.push_info(&format!("{} copied object(s) retain imported style references whose dependencies cannot be localized.", retained)); + } + self.refresh_layer_panel(); + } for entity in entities { if preserve_commit_style { let _ = self.commit_entity_handle_preserve_style(entity); diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index 3b884049f..1073501ba 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -656,7 +656,7 @@ impl OpenCADStudio { "NCOPY" | "NCOPYALL" => { let command = crate::modules::draw::modify::ncopy::NcopyCommand::new( - &self.tabs[i].scene.document, + &self.tabs[i].scene.document, self.ncopy_bind, ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); diff --git a/src/app/mod.rs b/src/app/mod.rs index 0c2cd3e73..abd29821f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -511,6 +511,7 @@ pub(super) struct OpenCADStudio { /// Selected-object count past which grips stop being generated /// (GRIPOBJLIMIT, 0..=32767; 0 = no limit). grip_object_limit: i32, + ncopy_bind: bool, /// Drawing viewport cursor style (CURSORTYPE). cursor_type: settings::CursorType, /// Explicit crosshair colour; `None` retains automatic contrast. @@ -3461,6 +3462,7 @@ impl OpenCADStudio { double_click_block_refedit: false, double_click_block_attedit: true, grip_object_limit: settings::DEFAULT_GRIP_OBJECT_LIMIT, + ncopy_bind: false, cursor_type: settings::CursorType::Crosshair, crosshair_color: None, crosshair_color_input: String::new(), diff --git a/src/app/settings.rs b/src/app/settings.rs index f40d41d54..24674771c 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -203,6 +203,8 @@ pub struct UserSettings { /// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at /// all. 0 means no limit. The drawing header carries no slot for it. pub grip_object_limit: i32, + /// Nested-copy symbol handling: false inserts, true binds. + pub ncopy_bind: bool, /// Last Options page; unknown saved names fall back without rejecting the config. #[serde(default, deserialize_with = "deserialize_options_tab")] pub options_tab: crate::ui::window::options::OptionsTab, @@ -372,6 +374,7 @@ impl Default for UserSettings { double_click_block_refedit: false, double_click_block_attedit: true, grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT, + ncopy_bind: false, cursor_type: CursorType::Crosshair, crosshair_color: None, lineweight_display_scale: 100, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 518d5c4a5..4beaedfb4 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -404,6 +404,7 @@ impl OpenCADStudio { double_click_block_refedit: self.double_click_block_refedit, double_click_block_attedit: self.double_click_block_attedit, grip_object_limit: self.grip_object_limit, + ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, crosshair_color: self.crosshair_color, lineweight_display_scale: self.lineweight_display_scale, @@ -467,6 +468,7 @@ impl OpenCADStudio { self.double_click_block_refedit = s.double_click_block_refedit; self.double_click_block_attedit = s.double_click_block_attedit; self.grip_object_limit = s.grip_object_limit.clamp(0, 32767); + self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; self.crosshair_color = s.crosshair_color; self.crosshair_color_input = s diff --git a/src/command.rs b/src/command.rs index 5e3098f3a..3454b018c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1942,6 +1942,9 @@ impl InputKind { pub trait CadCommand: Send { /// Preserve source appearance for commands that extract existing entities. fn preserve_commit_style(&self) -> bool { false } + fn nested_copy_bind_setting(&self) -> Option { None } + /// Symbol localization is committed together with the extracted entities. + fn nested_copy_symbol_names(&self) -> Option<&acadrust::nested_copy::NestedCopySymbolNames> { None } /// Keep the layer already carried by entities committed by this command /// instead of replacing it with the current drawing layer. fn preserve_commit_layer(&self) -> bool { diff --git a/src/io/xref.rs b/src/io/xref.rs index 2aee489d0..fd054a784 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -347,6 +347,7 @@ fn merge_xref_into_block( let old = layer.name.clone(); let new = format!("{}|{}", prefix, old); let mut cloned = layer.clone(); + xref_doc.normalize_imported_layer_defaults(&mut cloned); cloned.name = new.clone(); cloned.set_handle(doc.allocate_handle()); doc.layers.add_or_replace(cloned); diff --git a/src/modules/draw/modify/ncopy.rs b/src/modules/draw/modify/ncopy.rs index 4221a4e94..aa3aa0eaf 100644 --- a/src/modules/draw/modify/ncopy.rs +++ b/src/modules/draw/modify/ncopy.rs @@ -1,11 +1,12 @@ use acadrust::{CadDocument, EntityType, Handle}; use glam::DVec3; +use acadrust::nested_copy::{NestedCopyMode, NestedCopySymbolNames}; use std::collections::{HashMap, HashSet}; use crate::command::{CadCommand, CmdOption, CmdResult, EntityTransform}; use crate::entities::traits::EntityTypeOps; use crate::scene::convert::acad_to_render::RenderObject; -enum Step { Select, Base, Target(DVec3), ArrayCount(DVec3), ArrayTarget(DVec3, usize, bool) } +enum Step { Select, Settings, Base, Target(DVec3), ArrayCount(DVec3), ArrayTarget(DVec3, usize, bool) } pub struct NcopyCommand { nested: HashMap>, hit_paths: HashMap>>, @@ -13,9 +14,12 @@ pub struct NcopyCommand { step: Step, multiple: bool, placements: usize, + mode: NestedCopyMode, + insert_names: NestedCopySymbolNames, + bind_names: NestedCopySymbolNames, } impl NcopyCommand { - pub fn new(document: &CadDocument) -> Self { + pub fn new(document: &CadDocument, bind: bool) -> Self { fn leaves(entity: &EntityType, document: &CadDocument, path: &mut HashSet, output: &mut Vec) { let EntityType::Insert(insert) = entity else { output.push(entity.clone()); return; }; let name = insert.block_name.to_ascii_uppercase(); @@ -43,7 +47,11 @@ impl NcopyCommand { }).collect(); (handle, paths) }).collect(); - Self { nested, hit_paths, selected: Vec::new(), step: Step::Select, multiple: false, placements: 0 } + Self { nested, hit_paths, selected: Vec::new(), step: Step::Select, multiple: false, placements: 0, + mode: if bind { NestedCopyMode::Bind } else { NestedCopyMode::Insert }, + insert_names: document.nested_copy_symbol_names(NestedCopyMode::Insert), + bind_names: document.nested_copy_symbol_names(NestedCopyMode::Bind), + } } fn place(&self, delta: DVec3) -> CmdResult { if !delta.is_finite() { return CmdResult::NeedPoint; } @@ -61,7 +69,8 @@ impl CadCommand for NcopyCommand { fn name(&self) -> &'static str { "NCOPY" } fn prompt(&self) -> String { match self.step { - Step::Select => "Select nested objects:".into(), + Step::Select => format!("Current setting: {}. Select nested objects or [Settings]:", if self.mode == NestedCopyMode::Bind { "Bind" } else { "Insert" }), + Step::Settings => format!("Enter setting [Insert/Bind] <{}>:", if self.mode == NestedCopyMode::Bind { "Bind" } else { "Insert" }), Step::Base => "Specify base point or [Displacement/Multiple] :".into(), Step::Target(_) => "Specify second point or [Array] :".into(), Step::ArrayCount(_) => "Enter number of items to array:".into(), @@ -71,6 +80,8 @@ impl CadCommand for NcopyCommand { } fn options(&self) -> Vec { match self.step { + Step::Select => vec![CmdOption::new("Settings", "S")], + Step::Settings => vec![CmdOption::new("Insert", "I"), CmdOption::new("Bind", "B")], Step::Base => vec![CmdOption::new("Displacement", "D"), CmdOption::new("Multiple", "M")], Step::Target(_) if self.multiple && self.placements > 0 => vec![CmdOption::new("Array", "A"), CmdOption::new("Exit", "E"), CmdOption::new("Undo", "U")], Step::Target(_) => vec![CmdOption::new("Array", "A")], @@ -78,6 +89,10 @@ impl CadCommand for NcopyCommand { _ => Vec::new(), } } + fn nested_copy_bind_setting(&self) -> Option { Some(self.mode == NestedCopyMode::Bind) } + fn nested_copy_symbol_names(&self) -> Option<&NestedCopySymbolNames> { + Some(if self.mode == NestedCopyMode::Bind { &self.bind_names } else { &self.insert_names }) + } fn preserve_commit_style(&self) -> bool { true } fn preserve_commit_layer(&self) -> bool { true } fn needs_entity_pick(&self) -> bool { matches!(self.step, Step::Select) } @@ -109,7 +124,7 @@ impl CadCommand for NcopyCommand { } fn on_point(&mut self, point: DVec3) -> CmdResult { match self.step { - Step::Select => CmdResult::NeedPoint, + Step::Select | Step::Settings => CmdResult::NeedPoint, Step::Base => { self.step = Step::Target(point); CmdResult::NeedPoint } Step::Target(base) => { if !point.is_finite() { return CmdResult::NeedPoint; } @@ -132,15 +147,25 @@ impl CadCommand for NcopyCommand { } fn on_enter(&mut self) -> CmdResult { match self.step { + Step::Settings => { self.step = Step::Select; CmdResult::NeedPoint } Step::Select if !self.selected.is_empty() => { self.step = Step::Base; CmdResult::NeedPoint } Step::Base => { self.step = Step::Target(DVec3::ZERO); CmdResult::NeedPoint } Step::Target(base) if !self.multiple => self.place(base), _ => CmdResult::Cancel, } } - fn point_step_accepts_keywords(&self) -> bool { matches!(self.step, Step::Base | Step::Target(_) | Step::ArrayTarget(..)) } - fn wants_text_input(&self) -> bool { !matches!(self.step, Step::Select) } + fn point_step_accepts_keywords(&self) -> bool { matches!(self.step, Step::Select | Step::Settings | Step::Base | Step::Target(_) | Step::ArrayTarget(..)) } + fn wants_text_input(&self) -> bool { true } fn on_text_input(&mut self, text: &str) -> Option { + if matches!(self.step, Step::Settings) { + match text.trim().to_uppercase().as_str() { + "I" | "INSERT" => self.mode = NestedCopyMode::Insert, + "B" | "BIND" => self.mode = NestedCopyMode::Bind, + _ => return Some(CmdResult::NeedPoint), + } + self.step = Step::Select; + return Some(CmdResult::NeedPoint); + } if let Step::ArrayCount(base) = self.step { if let Ok(count) = text.trim().parse::() { if (2..=32767).contains(&count) { self.step = Step::ArrayTarget(base, count, false); } @@ -148,6 +173,7 @@ impl CadCommand for NcopyCommand { return Some(CmdResult::NeedPoint); } match text.trim().to_uppercase().as_str() { + "S" | "SETTINGS" if matches!(self.step, Step::Select) => { self.step = Step::Settings; Some(CmdResult::NeedPoint) } "A" | "ARRAY" => { if let Step::Target(base) = self.step { self.step = Step::ArrayCount(base); } Some(CmdResult::NeedPoint)