forked from 1b-agent/opencadstudio-upstream
227 lines
12 KiB
Rust
227 lines
12 KiB
Rust
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<Handle, Vec<EntityType>>,
|
|
hit_paths: HashMap<Handle, Vec<Vec<[f64; 3]>>>,
|
|
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<String>, output: &mut Vec<EntityType>) {
|
|
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] <Displacement>:".into(),
|
|
Step::Target(_) => "Specify second point or [Array] <use first point as displacement>:".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<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")],
|
|
Step::ArrayTarget(_, _, false) => vec![CmdOption::new("Fit", "F")],
|
|
_ => 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) }
|
|
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<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); }
|
|
}
|
|
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));
|
|
}
|
|
}
|