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, Settings, 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, mode: NestedCopyMode, insert_names: NestedCopySymbolNames, bind_names: NestedCopySymbolNames, } impl NcopyCommand { 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(); 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, 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; } 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 => 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(), Step::ArrayTarget(_, _, false) => "Specify second point or [Fit]:".into(), Step::ArrayTarget(_, _, true) => "Specify last point:".into(), } } 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")], Step::ArrayTarget(_, _, false) => vec![CmdOption::new("Fit", "F")], _ => 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) } 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 | Step::Settings => 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::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::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); } } 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) } "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, } } } #[cfg(test)] mod tests { use super::*; use acadrust::entities::Line; use acadrust::types::Vector3; #[test] fn placement_moves_only_geometry_and_keeps_source_style() { let handle = Handle::new(7); let mut line = Line::from_points(Vector3::ZERO, Vector3::new(2.0, 0.0, 0.0)); line.common.handle = handle; line.common.layer = "DETAIL".into(); line.common.linetype = "DASHED".into(); 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, 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") }; let EntityType::Line(line) = &entities[0] else { panic!("expected line") }; assert!(line.common.handle.is_null()); assert_eq!((line.common.layer.as_str(), line.common.linetype.as_str()), ("DETAIL", "DASHED")); assert_eq!(line.start, Vector3::new(3.0, 4.0, 5.0)); assert_eq!(line.end, Vector3::new(5.0, 4.0, 5.0)); } }