Merge PR #1177: Copy picked nested objects with placement and array options

This commit is contained in:
Hakan Seven committed 2026-09-11 11:55:43 +03:00
commit bfadf1c04e
8 files changed
+77 -13

No files matched your search

+28 -4
View File
@@ -1274,6 +1274,12 @@ impl OpenCADStudio {
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
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);
+1 -1
View File
@@ -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));
+2
View File
@@ -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(),
+3
View File
@@ -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,
+2
View File
@@ -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
+3
View File
@@ -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<bool> { 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 {
+1
View File
@@ -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);
+37 -8
View File
@@ -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<Handle, Vec<EntityType>>,
hit_paths: HashMap<Handle, Vec<Vec<[f64; 3]>>>,
@@ -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<String>, output: &mut Vec<EntityType>) {
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] <Displacement>:".into(),
Step::Target(_) => "Specify second point or [Array] <use first point as displacement>:".into(),
Step::ArrayCount(_) => "Enter number of items to array:".into(),
@@ -71,6 +80,8 @@ impl CadCommand for NcopyCommand {
}
fn options(&self) -> Vec<CmdOption> {
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<bool> { 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<CmdResult> {
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::<usize>() {
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") };