diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 41d3a0fe3..c36d5fc93 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1274,6 +1274,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] @@ -1413,7 +1419,7 @@ impl OpenCADStudio { } } } - CmdResult::CommitEntities(entities) => { + CmdResult::CommitEntities(mut entities) => { let locked_source = entities.iter().find_map(|entity| { let handle = entity.common().handle; (!handle.is_null() @@ -1426,10 +1432,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); @@ -1449,12 +1464,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 a11f2d273..a0f5d2583 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 2e398b72f..733feefbd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -509,6 +509,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. @@ -3526,6 +3527,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 fb4e5d34f..14f223aa3 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, @@ -386,6 +388,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 20c3547fc..a0157ecf5 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, @@ -469,6 +470,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 35902916c..80e2aa78b 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1996,6 +1996,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 bcb2eebe7..6dcdd08c3 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) @@ -184,7 +210,10 @@ mod tests { let mut nested = HashMap::new(); nested.insert(handle, vec![EntityType::Line(line)]); let command = NcopyCommand { nested, hit_paths: HashMap::new(), selected: vec![(handle, 0)], - step: Step::Base, multiple: false, placements: 0 }; + step: Step::Base, multiple: false, placements: 0, + mode: NestedCopyMode::Insert, + insert_names: NestedCopySymbolNames::default(), + bind_names: NestedCopySymbolNames::default() }; let CmdResult::CommitEntitiesAndExit(entities) = command.place(DVec3::new(3.0, 4.0, 5.0)) else { panic!("expected copied entity") };