diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 16be1a02c..74e23845b 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4462,7 +4462,16 @@ impl OpenCADStudio { self.command_line.push_info("HATCHEDIT: hatch is already associative."); self.tabs[i].active_cmd=None; }else{ - let command=crate::modules::draw::draw::hatchedit::HatcheditCommand::for_association(handle,name,scale,angle); + let plane=match self.tabs[i].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Hatch(h))=>{ + let storage=crate::entities::curve::ocs_plane(h.normal,h.elevation); + crate::command::WorkingPlane::new(glam::DVec3::from_array(storage.origin),glam::DVec3::from_array(storage.x_axis),glam::DVec3::from_array(storage.y_axis)) + }, + _=>self.tabs[i].ucs_xform().working_plane(), + }; + let mut sources=self.tabs[i].scene.boundary_sources_on_plane(plane,1e-6); + sources.remove(&handle); + let command=crate::modules::draw::draw::hatchedit::HatcheditCommand::for_association(handle,name,scale,angle,plane,sources); self.tabs[i].scene.deselect_all(); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd=Some(Box::new(command)); @@ -4632,6 +4641,14 @@ impl OpenCADStudio { .scene .edit_hatch_boundary_handles(handle, &handles, false); } + HatchEditOperation::AssociatePaths(paths) => { + if paths.is_empty()||paths.iter().any(|path|path.boundary_handles.is_empty()||path.boundary_handles.iter().any(|source|self.tabs[i].scene.document.get_entity(*source).is_none())) { + self.discard_last_undo_entry(i); + self.command_line.push_error("HATCHEDIT: associated boundary is no longer available."); + return Task::none(); + } + self.tabs[i].scene.replace_hatch_association(handle,paths); + } HatchEditOperation::AssociateBoundaries(handles) => { let plane=match self.tabs[i].scene.document.get_entity(handle) { Some(acadrust::EntityType::Hatch(h))=>{ diff --git a/src/command.rs b/src/command.rs index b5167f0d7..94ad82e6b 100644 --- a/src/command.rs +++ b/src/command.rs @@ -27,6 +27,7 @@ pub enum HatchEditOperation { RecreateBoundary { associate: bool, region: bool }, BeginAssociate, AssociateBoundaries(Vec), + AssociatePaths(Vec), DrawOrderBoundary { above: bool }, Separate, AddBoundaries(Vec), diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index 984965e0b..dc2cfa98f 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -37,6 +37,9 @@ pub struct HatcheditCommand { current_color: acadrust::types::Color, current_transparency: acadrust::types::Transparency, boundary_region: bool, + association_sources: Option<(crate::command::WorkingPlane,rustc_hash::FxHashMap)>, + association_paths: Vec, + association_missed: bool, } impl HatcheditCommand { @@ -54,6 +57,9 @@ impl HatcheditCommand { current_color: acadrust::types::Color::ByLayer, current_transparency: acadrust::types::Transparency::ByLayer, boundary_region: false, + association_sources: None, + association_paths: Vec::new(), + association_missed: false, } } @@ -82,6 +88,9 @@ impl HatcheditCommand { current_color: acadrust::types::Color::ByLayer, current_transparency: acadrust::types::Transparency::ByLayer, boundary_region: false, + association_sources: None, + association_paths: Vec::new(), + association_missed: false, } } @@ -104,9 +113,17 @@ impl HatcheditCommand { }) } - pub fn for_association(handle:Handle,name:String,scale:f32,angle:f32)->Self { + pub fn for_association(handle:Handle,name:String,scale:f32,angle:f32,plane:crate::command::WorkingPlane,sources:rustc_hash::FxHashMap)->Self { let mut command=Self::with_handle(handle,name,scale,angle,false); - command.input=Some("associate-select");command + command.input=Some("associate-point");command.association_sources=Some((plane,sources));command + } + fn add_association_rings(&mut self,rings:Vec>,sources:&rustc_hash::FxHashMap) { + let exterior=cadkernel::geom2d::ring_nesting_depths(&rings).into_iter().map(|depth|depth==0).collect::>(); + let paths=crate::scene::exact_hatch_paths(&rings,&exterior,sources,1e-6); + self.association_missed=paths.is_empty()||paths.len()!=rings.len()||paths.iter().any(|path|path.boundary_handles.is_empty()); + if !self.association_missed { + for path in paths {if !self.association_paths.contains(&path){self.association_paths.push(path);}} + } } pub fn with_appearance(mut self,entity:Option<&acadrust::EntityType>,current_color:acadrust::types::Color,current_transparency:acadrust::types::Transparency)->Self { self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)}); @@ -162,6 +179,8 @@ impl CadCommand for HatcheditCommand { "boundary-type"=>"Enter type of boundary object [Region/Polyline] :", "boundary-associate"=>"Reassociate hatch with new boundary? [Yes/No] :", "associate-select"=>"Select boundary objects:", + "associate-point" if self.association_missed=>"No closed boundary found. Specify internal point or [Select objects]:", + "associate-point"=>"Specify internal point or [Select objects]:", _=>"Specify value:", }.into(); } @@ -188,7 +207,13 @@ impl CadCommand for HatcheditCommand { } fn is_selection_gathering(&self)->bool {self.input==Some("associate-select")} fn on_selection_complete(&mut self,handles:Vec)->CmdResult { - self.boundary_selection=handles;CmdResult::NeedPoint + self.boundary_selection=handles.clone(); + if let Some((_,sources))=&self.association_sources { + let sources=sources.iter().filter(|(handle,_)|handles.contains(handle)).map(|(handle,source)|(*handle,source.clone())).collect(); + let rings=crate::scene::boundary_faces(&sources,1e-6); + self.add_association_rings(rings,&sources); + } + self.input=Some("associate-point");CmdResult::NeedPoint } fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { @@ -211,6 +236,7 @@ impl CadCommand for HatcheditCommand { } fn options(&self) -> Vec { + if self.input==Some("associate-point") {return vec![crate::command::CmdOption::new("Select objects","S")];} if self.input.is_some() {return Vec::new();} if !matches!(self.step, HatcheditStep::EditOptions { .. }) { return Vec::new(); @@ -238,6 +264,10 @@ impl CadCommand for HatcheditCommand { use acadrust::types::{Color,Transparency}; let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency}; match input { + "associate-point"=>{ + if matches!(keyword.as_str(),"S"|"SELECT"|"SELECT OBJECTS") {self.input=Some("associate-select");} + return Some(CmdResult::NeedPoint); + }, "color"=>{ if matches!(keyword.as_str(),"T"|"TRUECOLOR") {self.input=Some("truecolor");return Some(CmdResult::NeedPoint);} let color=match keyword.as_str(){"."=>Some(self.current_color),"BYLAYER"=>Some(Color::ByLayer),"BYBLOCK"=>Some(Color::ByBlock), @@ -403,7 +433,16 @@ impl CadCommand for HatcheditCommand { Some(CmdResult::NeedPoint) } - fn on_point(&mut self, _pt: DVec3) -> CmdResult { + fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.input==Some("associate-point") { + if let Some((plane,sources))=&self.association_sources { + let point=plane.to_local(pt); + let sources=sources.clone(); + if let Some(rings)=crate::scene::model::presspull_model::selected_rings(&sources,[point.x,point.y]) { + self.add_association_rings(rings,&sources); + }else{self.association_missed=true;} + } + } CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { @@ -420,8 +459,9 @@ impl CadCommand for HatcheditCommand { Some("angle")=>self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel), Some("boundary-type")=>{self.boundary_region=false;self.input=Some("boundary-associate");CmdResult::NeedPoint}, Some("boundary-associate")=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false,region:self.boundary_region}).unwrap_or(CmdResult::Cancel), - Some("associate-select")=>if self.boundary_selection.is_empty(){CmdResult::Cancel}else{ - self.apply_result(HatchEditOperation::AssociateBoundaries(self.boundary_selection.clone())).unwrap_or(CmdResult::Cancel) + Some("associate-select")=>{self.input=Some("associate-point");CmdResult::NeedPoint}, + Some("associate-point")=>if self.association_paths.is_empty(){CmdResult::Cancel}else{ + self.apply_result(HatchEditOperation::AssociatePaths(self.association_paths.clone())).unwrap_or(CmdResult::Cancel) }, _=>CmdResult::Cancel, } diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index c04691960..780e8b731 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -628,6 +628,14 @@ pub(crate) fn boundary_entities_from_sources( } impl Scene { + pub(crate) fn replace_hatch_association(&mut self,handle:Handle,paths:Vec) { + let Some(EntityType::Hatch(hatch))=self.document.get_entity_mut(handle) else{return;}; + hatch.paths=paths; + hatch.is_associative=true; + self.associative_hatch_source_cache.borrow_mut().take(); + self.refresh_fill_model(handle); + } + pub(crate) fn edit_hatch_boundary_handles( &mut self, hatch_handle: Handle, diff --git a/src/scene/model/presspull_model.rs b/src/scene/model/presspull_model.rs index 0218f03ab..cd9361a94 100644 --- a/src/scene/model/presspull_model.rs +++ b/src/scene/model/presspull_model.rs @@ -141,7 +141,7 @@ fn boundary_sources(scene: &Scene, plane: WorkingPlane) -> FxHashMap, point: [f64; 2]) -> Option>> { +pub(crate) fn selected_rings(sources: &FxHashMap, point: [f64; 2]) -> Option>> { let rings = boundary_faces(sources, BOUNDARY_TOLERANCE); let loops = boundary_loops(sources, &rings)?; let tolerance = Tolerance::new(BOUNDARY_TOLERANCE);