From f24fdc79653f40125ba329db6cbfb79b9dbb2ce6 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:23:38 +0300 Subject: [PATCH] Implement nested object selection and placement for NCOPY --- src/app/command_driver.rs | 14 ++- src/app/commands/blocks.rs | 39 +------ src/command.rs | 2 + src/modules/draw/modify/mod.rs | 1 + src/modules/draw/modify/ncopy.rs | 169 +++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 src/modules/draw/modify/ncopy.rs diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 87635b15f..341e98ca6 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -929,6 +929,8 @@ impl OpenCADStudio { fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task { let i = self.active_tab; + 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] .active_cmd .as_ref() @@ -1070,7 +1072,9 @@ impl OpenCADStudio { .all(|entity| self.delta_add_safe(i, entity)); let pending = self.begin_undo(i, label, entities.len(), delta_safe); for entity in entities { - if preserve_commit_layer { + if preserve_commit_style { + let _ = self.commit_entity_handle_preserve_style(entity); + } else if preserve_commit_layer { let _ = self.commit_entity_handle_preserve_layer(entity); } else { self.commit_entity(entity); @@ -1093,7 +1097,13 @@ impl OpenCADStudio { .all(|entity| self.delta_add_safe(i, entity)); let pending = self.begin_undo(i, label, entities.len(), delta_safe); for entity in entities { - self.commit_entity(entity); + if preserve_commit_style { + let _ = self.commit_entity_handle_preserve_style(entity); + } else if preserve_commit_layer { + let _ = self.commit_entity_handle_preserve_layer(entity); + } else { + self.commit_entity(entity); + } } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index d020e42cc..3b884049f 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -654,42 +654,13 @@ impl OpenCADStudio { } } - // NCOPY — copy the nested objects of the selected block reference(s) - // into model space, keeping the block (a non-destructive extraction; - // every nested object is copied — the block is not exploded). "NCOPY" | "NCOPYALL" => { - use crate::modules::draw::modify::explode::explode_entity; - let inserts: Vec = self.tabs[i] - .scene - .selected_entities() - .iter() - .filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_))) - .map(|(h, _)| *h) - .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) - .collect(); - if inserts.is_empty() { - self.command_line - .push_error(crate::t!("NCOPY: select a block reference first.").as_ref()); - return Some(Task::none()); - } - self.push_undo_snapshot(i, "NCOPY"); - let mut n = 0usize; - for h in &inserts { - let nested = match self.tabs[i].scene.document.get_entity(*h).cloned() { - Some(ins) => explode_entity(&ins, &self.tabs[i].scene.document), - None => Vec::new(), - }; - for e in nested { - self.tabs[i].scene.add_entity(e); - n += 1; - } - } - self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "NCOPY: copied {n} nested object(s) into model space (blocks kept)." - ).as_ref()); + let command = crate::modules::draw::modify::ncopy::NcopyCommand::new( + &self.tabs[i].scene.document, + ); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); } - _ => return None, } Some(self.finish_dispatch(cmd)) diff --git a/src/command.rs b/src/command.rs index d69859462..5e3098f3a 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1940,6 +1940,8 @@ impl InputKind { } pub trait CadCommand: Send { + /// Preserve source appearance for commands that extract existing entities. + fn preserve_commit_style(&self) -> bool { false } /// 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/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index a497c4c4e..4964db414 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -13,6 +13,7 @@ pub mod join; pub mod lengthen; pub mod mirror; pub mod mledit; +pub mod ncopy; pub mod offset; pub mod pedit; pub mod block_edit; diff --git a/src/modules/draw/modify/ncopy.rs b/src/modules/draw/modify/ncopy.rs new file mode 100644 index 000000000..4221a4e94 --- /dev/null +++ b/src/modules/draw/modify/ncopy.rs @@ -0,0 +1,169 @@ +use acadrust::{CadDocument, EntityType, Handle}; +use glam::DVec3; +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) } +pub struct NcopyCommand { + nested: HashMap>, + hit_paths: HashMap>>, + selected: Vec<(Handle, usize)>, + step: Step, + multiple: bool, + placements: usize, +} +impl NcopyCommand { + pub fn new(document: &CadDocument) -> 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(); + if !path.insert(name.clone()) { return; } + for child in super::explode::explode_entity(entity, document) { + leaves(&child, document, path, output); + } + path.remove(&name); + } + let nested: HashMap<_, _> = document.entities().filter(|e| matches!(e, EntityType::Insert(_))) + .map(|e| { + let mut entities = Vec::new(); + leaves(e, document, &mut HashSet::new(), &mut entities); + (e.common().handle, entities) + }).collect(); + let hit_paths = nested.iter().map(|(&handle, entities)| { + let paths = entities.iter().map(|entity| { + if crate::entities::curve::entity_curve(entity).is_some() { return Vec::new(); } + match entity.to_render_entity(document).map(|render| render.object) { + Some(RenderObject::Dot(point)) => vec![point], + Some(RenderObject::Lines(points) | RenderObject::SegmentedLines(points) + | RenderObject::TaperedLines(points, _) | RenderObject::BoundaryLines { points, .. }) => points, + _ => Vec::new(), + } + }).collect(); + (handle, paths) + }).collect(); + Self { nested, hit_paths, selected: Vec::new(), step: Step::Select, multiple: false, placements: 0 } + } + fn place(&self, delta: DVec3) -> CmdResult { + if !delta.is_finite() { return CmdResult::NeedPoint; } + let copies = self.selected.iter().filter_map(|(h, index)| self.nested.get(h)?.get(*index)).cloned() + .map(|mut entity| { + crate::scene::view::dispatch::apply_transform(&mut entity, &EntityTransform::Translate(delta)); + entity.common_mut().handle = Handle::NULL; + entity + }).collect(); + if self.multiple { CmdResult::CommitEntities(copies) } + else { CmdResult::CommitEntitiesAndExit(copies) } + } +} +impl CadCommand for NcopyCommand { + fn name(&self) -> &'static str { "NCOPY" } + fn prompt(&self) -> String { + match self.step { + Step::Select => "Select nested objects:".into(), + 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(), + Step::ArrayTarget(_, _, false) => "Specify second point or [Fit]:".into(), + Step::ArrayTarget(_, _, true) => "Specify last point:".into(), + } + } + fn options(&self) -> Vec { + match self.step { + 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")], + Step::ArrayTarget(_, _, false) => vec![CmdOption::new("Fit", "F")], + _ => Vec::new(), + } + } + 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) } + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if let Some(entities) = self.nested.get(&handle) { + let nearest = entities.iter().enumerate().filter_map(|(index, entity)| { + let distance = if let Some(curve) = crate::entities::curve::entity_curve(entity) { + let projected = curve.plane.project([point.x, point.y, point.z])?; + cadkernel::geom2d::closest_point(&curve.curve, projected).distance + .hypot(curve.plane.distance_to(point.to_array())?) + } else { + let path = self.hit_paths.get(&handle)?.get(index)?; + let point = cadkernel::space::Vec3::from(point.to_array()); + if path.len() == 1 { + point.distance(cadkernel::space::Vec3::from(path[0])) + } else { + path.windows(2).filter(|segment| segment.iter().flatten().all(|v| v.is_finite())) + .map(|segment| point.distance_to_segment(segment[0].into(), segment[1].into())) + .min_by(f64::total_cmp)? + } + }; + Some((index, distance)) + }).min_by(|a, b| a.1.total_cmp(&b.1)); + if let Some((index, _)) = nearest { + if !self.selected.contains(&(handle, index)) { self.selected.push((handle, index)); } + } + } + CmdResult::NeedPoint + } + fn on_point(&mut self, point: DVec3) -> CmdResult { + match self.step { + Step::Select => CmdResult::NeedPoint, + Step::Base => { self.step = Step::Target(point); CmdResult::NeedPoint } + Step::Target(base) => { + if !point.is_finite() { return CmdResult::NeedPoint; } + self.placements += 1; + self.place(point - base) + } + Step::ArrayCount(_) => CmdResult::NeedPoint, + Step::ArrayTarget(base, count, fit) => { + let delta = (point - base) / if fit { (count - 1) as f64 } else { 1.0 }; + if !delta.is_finite() { return CmdResult::NeedPoint; } + let mut copies = Vec::new(); + for index in 0..count { + if let CmdResult::CommitEntities(mut entities) | CmdResult::CommitEntitiesAndExit(mut entities) = self.place(delta * index as f64) { + copies.append(&mut entities); + } + } + CmdResult::CommitEntitiesAndExit(copies) + } + } + } + fn on_enter(&mut self) -> CmdResult { + match self.step { + 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 on_text_input(&mut self, text: &str) -> Option { + 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); } + } + return Some(CmdResult::NeedPoint); + } + match text.trim().to_uppercase().as_str() { + "A" | "ARRAY" => { + if let Step::Target(base) = self.step { self.step = Step::ArrayCount(base); } + Some(CmdResult::NeedPoint) + } + "F" | "FIT" => { + if let Step::ArrayTarget(base, count, _) = self.step { self.step = Step::ArrayTarget(base, count, true); } + Some(CmdResult::NeedPoint) + } + "U" | "UNDO" if self.multiple && self.placements > 0 => { + self.placements -= 1; + Some(CmdResult::UndoDocument) + } + "E" | "EXIT" if self.multiple => Some(CmdResult::Cancel), + "M" | "MULTIPLE" if matches!(self.step, Step::Base) => { self.multiple = true; Some(CmdResult::NeedPoint) } + "D" | "DISPLACEMENT" if matches!(self.step, Step::Base) => { self.step = Step::Target(DVec3::ZERO); Some(CmdResult::NeedPoint) } + _ => None, + } + } +}