From 60da28265417a6004eaa227bc91221b4d5a82080 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:24:39 +0300 Subject: [PATCH] Add tolerant OVERKILL settings and geometric cleanup --- src/app/commands/styleprops.rs | 125 +++++++++++++++----------- src/modules/draw/modify/mod.rs | 1 + src/modules/draw/modify/overkill.rs | 133 ++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 51 deletions(-) create mode 100644 src/modules/draw/modify/overkill.rs diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index b99da19b4..068138552 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -682,61 +682,84 @@ impl OpenCADStudio { } } - // ── OVERKILL — delete duplicate (identical) objects ────────── - // Removes objects that are identical in geometry AND properties to - // another object (compared with the handle ignored). Operates on - // the current selection, or the whole drawing when nothing is - // selected. Conservative: only exact duplicates are removed. - "OVERKILL" => { - use acadrust::Handle; - let selected: std::collections::HashSet = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h.value()) - .collect(); - // Capture (handle, type-name, handle-normalized clone) for each - // candidate while the document is borrowed immutably. - let candidates: Vec<(Handle, String, acadrust::EntityType)> = self.tabs[i] - .scene - .document - .entities() - .filter(|e| { - (selected.is_empty() || selected.contains(&e.common().handle.value())) - && !self.tabs[i].scene.is_layer_locked(e.common().handle) - }) - .map(|e| { - let key = crate::entities::names::dxf_name(e).to_string(); - let mut norm = e.clone(); - norm.common_mut().handle = Handle::NULL; - (e.common().handle, key, norm) - }) - .collect(); - // Bucket by (type, layer) so only like objects are compared. - let mut kept: Vec<(String, acadrust::EntityType)> = Vec::new(); - let mut dups: Vec = Vec::new(); - for (h, key, norm) in &candidates { - let bucket = format!("{key}\u{0}{}", norm.common().layer); - if kept.iter().any(|(b, e)| b == &bucket && e == norm) { - dups.push(*h); - } else { - kept.push((bucket, norm.clone())); + // OVERKILL gathers objects, exposes cleanup settings, then applies one undo group. + "OVERKILL" | "-OVERKILL" => { + let handles=self.tabs[i].scene.selected_entities().iter().map(|(h,_)|*h).collect(); + let command=crate::modules::draw::modify::overkill::OverkillCommand::new(handles); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd=Some(Box::new(command)); + } + value if value.starts_with("OVERKILL_APPLY ") => { + use crate::modules::draw::modify::overkill::{normalized,optimize}; + let values:Vec<_>=value.split_whitespace().skip(1).collect(); + if values.len()!=6 { return Some(self.finish_dispatch(cmd)); } + let tolerance=values[0].parse::().unwrap_or(1e-6); + let ignore=values[1].parse::().unwrap_or(0); + let optimize_plines=values[2]=="1"; + let overlap=values[3]=="1"; + let end_to_end=values[4]=="1"; + let preserve_associative=values[5]=="1"; + let mut candidates:Vec<_>=self.tabs[i].scene.selected_entities().into_iter() + .filter(|(h,e)|!self.tabs[i].scene.is_layer_locked(*h) + && (!preserve_associative||e.common().reactors.is_empty())) + .map(|(h,e)|(h,e.clone())).collect(); + candidates.sort_by_key(|(h,_)|h.value()); + let mut changed=std::collections::HashSet::new(); + if optimize_plines { for (h,e) in &mut candidates { + let before=e.clone(); optimize(e,tolerance); if *e!=before {changed.insert(*h);} + } } + let mut removed=std::collections::HashSet::new(); + // Iterate to a fixed point: a bridge can join two previously disjoint intervals. + loop { + let mut progress=false; + for a in 0..candidates.len() { + if removed.contains(&candidates[a].0) {continue;} + for b in a+1..candidates.len() { + if removed.contains(&candidates[b].0) {continue;} + let left=normalized(&candidates[a].1,ignore); + let right=normalized(&candidates[b].1,ignore); + if left==right { + removed.insert(candidates[b].0);progress=true;continue; + } + let (acadrust::EntityType::Line(l),acadrust::EntityType::Line(r))=(&left,&right) + else {continue;}; + if l.common!=r.common||l.thickness!=r.thickness||l.normal!=r.normal {continue;} + let point=|p:acadrust::types::Vector3|[p.x,p.y,p.z]; + let Some(union)=cadkernel::space::line_union( + [point(l.start),point(l.end)],[point(r.start),point(r.end)],tolerance) + else {continue;}; + let allowed=match union.kind { + cadkernel::space::LineUnionKind::Duplicate=>true, + cadkernel::space::LineUnionKind::Overlap=>overlap, + cadkernel::space::LineUnionKind::EndToEnd=>end_to_end, + }; + if !allowed {continue;} + if let acadrust::EntityType::Line(line)=&mut candidates[a].1 { + line.start=acadrust::types::Vector3::new(union.start[0],union.start[1],union.start[2]); + line.end=acadrust::types::Vector3::new(union.end[0],union.end[1],union.end[2]); + } + changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true; + } } + if !progress {break;} } - if dups.is_empty() { - self.command_line - .push_output(crate::t!("OVERKILL: no duplicate objects found.").as_ref()); - } else { - let n = dups.len(); - self.push_undo_snapshot(i, "OVERKILL"); - self.tabs[i].scene.erase_entities(&dups); - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line - .push_output(crate::tf!("OVERKILL: deleted {n} duplicate object(s).").as_ref()); + if !changed.is_empty()||!removed.is_empty() { + self.push_undo_snapshot(i,"OVERKILL"); + let updates:Vec<_>=candidates.into_iter().filter(|(h,_)|changed.contains(h)&&!removed.contains(h)).collect(); + let mut changes = Vec::new(); + for (handle, entity) in updates { + if let Some(target) = self.tabs[i].scene.document.get_entity_mut(handle) { + *target = entity; + changes.push((handle, crate::scene::ChangeKind::Modified)); + } + } + self.tabs[i].scene.bump_entities(&changes); + let handles:Vec<_>=removed.iter().copied().collect(); + self.tabs[i].scene.erase_entities(&handles); + self.tabs[i].dirty=true;self.refresh_properties(); } + self.command_line.push_output(&format!("OVERKILL: removed {} objects; updated {} objects.",removed.len(),changed.len())); } - // ── PICKADD / PICKDRAG — selection UX (#226, app settings) ─── // Bare form reports; ` 0|1` sets and persists. Defaults keep // today's behaviour (PICKADD 1, PICKDRAG 0). diff --git a/src/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index a497c4c4e..c5317917f 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -14,6 +14,7 @@ pub mod lengthen; pub mod mirror; pub mod mledit; pub mod offset; +pub mod overkill; pub mod pedit; pub mod block_edit; pub mod refedit; diff --git a/src/modules/draw/modify/overkill.rs b/src/modules/draw/modify/overkill.rs new file mode 100644 index 000000000..842d84104 --- /dev/null +++ b/src/modules/draw/modify/overkill.rs @@ -0,0 +1,133 @@ +use acadrust::{EntityType, Handle}; +use glam::DVec3; +use crate::command::{CadCommand, CmdOption, CmdResult}; + +#[derive(Clone, Copy)] +pub struct Settings { + pub tolerance: f64, + pub ignore: u16, + pub optimize: bool, + pub overlap: bool, + pub end_to_end: bool, + pub associativity: bool, +} +impl Default for Settings { + fn default() -> Self { Self { tolerance: 1e-6, ignore: 0, optimize: true, + overlap: true, end_to_end: true, associativity: true } } +} +fn remembered() -> &'static std::sync::Mutex { + static SETTINGS: std::sync::OnceLock> = std::sync::OnceLock::new(); + SETTINGS.get_or_init(|| std::sync::Mutex::new(Settings::default())) +} +enum Step { Selection, Options, Ignore, Tolerance, Optimize, Overlap, EndToEnd, Associativity } +pub struct OverkillCommand { selected: Vec, step: Step, settings: Settings } +impl OverkillCommand { + pub fn new(selected: Vec) -> Self { + let step = if selected.is_empty() { Step::Selection } else { Step::Options }; + Self { selected, step, settings: *remembered().lock().unwrap_or_else(|e|e.into_inner()) } + } + fn finish(&self) -> CmdResult { + let s = self.settings; + *remembered().lock().unwrap_or_else(|e|e.into_inner())=s; + CmdResult::Relaunch(format!("OVERKILL_APPLY {} {} {} {} {} {}", s.tolerance, + s.ignore, u8::from(s.optimize), u8::from(s.overlap), u8::from(s.end_to_end), + u8::from(s.associativity)), self.selected.clone()) + } +} +impl CadCommand for OverkillCommand { + fn name(&self) -> &'static str { "OVERKILL" } + fn prompt(&self) -> String { + match self.step { + Step::Selection => "Select objects:".into(), + Step::Options => format!("Tolerance={} Ignore={} Optimize={} Overlap={} End-to-end={}\nEnter an option [Done/Ignore/tOlerance/optimize Plines/combine parTial overlap/combine Endtoend/Associativity] :", self.settings.tolerance, self.settings.ignore, self.settings.optimize, self.settings.overlap, self.settings.end_to_end), + Step::Ignore => "Properties to ignore [None/All/Color/LAyer/Ltype/ltScale/LWeight/Thickness/TRansparency/plotSTyle/Material]:".into(), + Step::Tolerance => format!("Specify tolerance <{}>:", self.settings.tolerance), + Step::Optimize => format!("Optimize segments within polylines [Yes/No] <{}>:",if self.settings.optimize{"Yes"}else{"No"}), + Step::Overlap => format!("Combine partially overlapping objects [Yes/No] <{}>:",if self.settings.overlap{"Yes"}else{"No"}), + Step::EndToEnd => format!("Combine end-to-end objects [Yes/No] <{}>:",if self.settings.end_to_end{"Yes"}else{"No"}), + Step::Associativity => format!("Maintain associative objects [Yes/No] <{}>:",if self.settings.associativity{"Yes"}else{"No"}), + } + } + fn options(&self) -> Vec { + match self.step { + Step::Options => [("Done","D"),("Ignore","I"),("Tolerance","O"),("Optimize polylines","P"),("Combine overlap","T"),("Combine end-to-end","E"),("Associativity","A")].into_iter().map(|(l,k)|CmdOption::new(l,k)).collect(), + Step::Optimize|Step::Overlap|Step::EndToEnd|Step::Associativity => vec![CmdOption::new("Yes","Y"),CmdOption::new("No","N")], + _ => Vec::new(), + } + } + fn is_selection_gathering(&self) -> bool { matches!(self.step,Step::Selection) } + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { self.selected=handles; CmdResult::NeedPoint } + fn wants_text_input(&self) -> bool { !self.is_selection_gathering() } + fn on_point(&mut self, _:DVec3) -> CmdResult { CmdResult::NeedPoint } + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::Selection if self.selected.is_empty() => CmdResult::Cancel, + Step::Options => self.finish(), + _ => { self.step=Step::Options; CmdResult::NeedPoint } + } + } + fn on_text_input(&mut self, text:&str) -> Option { + let k=text.trim().to_ascii_uppercase(); + match self.step { + Step::Options => match k.as_str() { + "D"|"DONE" => return Some(self.finish()), + "I"|"IGNORE" => self.step=Step::Ignore, + "O"|"TOLERANCE" => self.step=Step::Tolerance, + "P"|"PLINES" => self.step=Step::Optimize, + "T"|"PARTIAL" => self.step=Step::Overlap, + "E"|"ENDTOEND" => self.step=Step::EndToEnd, + "A"|"ASSOCIATIVITY" => self.step=Step::Associativity, + _=>{}, + }, + Step::Ignore => { + let flag=match k.as_str() { "N"|"NONE"=>{self.settings.ignore=0;0}, "A"|"ALL"=>511, + "C"|"COLOR"=>1,"LA"|"LAYER"=>2,"L"|"LTYPE"=>4,"S"|"LTSCALE"=>8, + "LW"|"LWEIGHT"=>16,"T"|"THICKNESS"=>32,"TR"|"TRANSPARENCY"=>64, + "ST"|"PLOTSTYLE"=>128,"M"|"MATERIAL"=>256,_=>return Some(CmdResult::NeedPoint)}; + self.settings.ignore|=flag; self.step=Step::Options; + } + Step::Tolerance => if let Ok(v)=k.parse::() { if v.is_finite()&&v>=0.0 { + self.settings.tolerance=v; self.step=Step::Options; + } }, + Step::Optimize|Step::Overlap|Step::EndToEnd|Step::Associativity => { + let yes=match k.as_str(){"Y"|"YES"=>true,"N"|"NO"=>false,_=>return Some(CmdResult::NeedPoint)}; + match self.step {Step::Optimize=>self.settings.optimize=yes,Step::Overlap=>self.settings.overlap=yes, + Step::EndToEnd=>self.settings.end_to_end=yes,Step::Associativity=>self.settings.associativity=yes,_=>{}} + self.step=Step::Options; + } + _=>{}, + } + Some(CmdResult::NeedPoint) + } +} + +pub fn normalized(entity:&EntityType, ignore:u16) -> EntityType { + let mut e=entity.clone(); + let common=e.common_mut(); + let default=acadrust::entities::EntityCommon::new(); + common.handle=Handle::NULL; + if ignore&1!=0 {common.color=default.color;common.color_name=None;common.color_book_handle=None;} + if ignore&2!=0 {common.layer.clear();} + if ignore&4!=0 {common.linetype.clear();common.linetype_handle=None;} + if ignore&8!=0 {common.linetype_scale=1.0;} + if ignore&16!=0 {common.line_weight=default.line_weight;} + if ignore&64!=0 {common.transparency=default.transparency;} + if ignore&128!=0 {common.plotstyle_flags=0;common.plotstyle_handle=None;} + if ignore&256!=0 {common.material_flags=0;common.material_handle=None;} + if ignore&32!=0 {match &mut e {EntityType::Line(v)=>v.thickness=0.0, + EntityType::Circle(v)=>v.thickness=0.0,EntityType::Arc(v)=>v.thickness=0.0, + EntityType::LwPolyline(v)=>v.thickness=0.0,_=>{}}} + e +} + +pub fn optimize(entity:&mut EntityType, tolerance:f64) { + if let EntityType::LwPolyline(p)=entity { + // Width changes and bulged spans carry geometry that must be retained. + if p.vertices.iter().any(|v|v.bulge!=0.0||v.start_width!=0.0||v.end_width!=0.0) {return;} + let points:Vec<_>=p.vertices.iter().map(|v|[v.location.x,v.location.y,p.elevation]).collect(); + let indices=cadkernel::space::simplify_linear_chain(&points,tolerance); + if indices.len()>=if p.is_closed{3}else{2} { + p.vertices=indices.into_iter().map(|i|p.vertices[i].clone()).collect(); + } + } +}