From c947e1ce50be8ea41bc1e9c786bfc099c633abc3 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:22:55 +0300 Subject: [PATCH] Add hatch editing option stages and appearance association workflows --- src/app/command_driver.rs | 69 ++++++++++++- src/app/commands/draw.rs | 2 +- src/command.rs | 10 +- src/modules/draw/draw/hatchedit.rs | 150 ++++++++++++++++++++++++++++- 4 files changed, 220 insertions(+), 11 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 87635b15f..af1a2b1de 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4456,6 +4456,31 @@ impl OpenCADStudio { .push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref()); } else { use crate::command::HatchEditOperation; + if matches!(&operation,HatchEditOperation::BeginAssociate) { + let associative=matches!(self.tabs[i].scene.document.get_entity(handle),Some(acadrust::EntityType::Hatch(h)) if h.is_associative); + if associative { + 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); + self.tabs[i].scene.deselect_all(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd=Some(Box::new(command)); + } + return Task::none(); + } + if let HatchEditOperation::DrawOrderBoundary{above}=&operation { + let references:Vec<_>=match self.tabs[i].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Hatch(h))=>h.paths.iter().flat_map(|p|p.boundary_handles.iter()) + .filter(|h|self.tabs[i].scene.document.get_entity(**h).is_some()).map(|h|format!("{:X}",h.value())).collect(), + _=>Vec::new(), + }; + self.tabs[i].active_cmd=None; + if references.is_empty(){self.command_line.push_info("HATCHEDIT: no associated boundary objects.");return Task::none();} + self.tabs[i].scene.deselect_all();self.tabs[i].scene.select_entity(handle,false); + let command=format!("DRAWORDER {} {}",if *above{"ABOVE"}else{"UNDER"},references.join(" ")); + return self.dispatch_view(&command,i).unwrap_or_else(Task::none); + } if matches!( &operation, HatchEditOperation::DrawOrderFront | HatchEditOperation::DrawOrderBack @@ -4472,6 +4497,22 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "HATCHEDIT"); match operation { + HatchEditOperation::Appearance { color, layer, transparency } => { + let layer=layer.map(|name|if name=="."{self.tabs[i].active_layer.clone()}else{name}); + if layer.as_ref().is_some_and(|name|self.tabs[i].scene.document.layers.get(name).is_none()) { + self.discard_last_undo_entry(i); + self.command_line.push_error("HATCHEDIT: layer not found."); + return Task::none(); + } + if let Some(entity)=self.tabs[i].scene.document.get_entity_mut(handle) { + let common=entity.common_mut(); + if let Some(value)=color {common.color=value;common.color_name=None;common.color_book_handle=None;} + if let Some(value)=layer {common.layer=value;} + if let Some(value)=transparency {common.transparency=value;} + } + self.tabs[i].scene.bump_entities(&[(handle,crate::scene::ChangeKind::Modified)]); + self.refresh_properties(); + } HatchEditOperation::Update { origin, disassociate, @@ -4591,7 +4632,25 @@ impl OpenCADStudio { .scene .edit_hatch_boundary_handles(handle, &handles, false); } - HatchEditOperation::RecreateBoundary => { + HatchEditOperation::AssociateBoundaries(handles) => { + 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.retain(|h,_|handles.contains(h)&&*h!=handle); + if crate::scene::boundary_faces(&sources,1e-6).is_empty() { + self.discard_last_undo_entry(i); + self.command_line.push_error("HATCHEDIT: selected objects do not form a closed boundary."); + return Task::none(); + } + let valid_handles:Vec<_>=sources.keys().copied().collect(); + self.tabs[i].scene.edit_hatch_boundary_handles(handle,&valid_handles,true); + } + HatchEditOperation::RecreateBoundary { associate } => { let source = self.tabs[i].scene.document.get_entity(handle).cloned(); if let Some(acadrust::EntityType::Hatch(source)) = source { let storage = crate::entities::curve::ocs_plane( @@ -4611,7 +4670,7 @@ impl OpenCADStudio { handles.push(boundary); } } - if let Some(acadrust::EntityType::Hatch(hatch)) = + if associate { if let Some(acadrust::EntityType::Hatch(hatch)) = self.tabs[i].scene.document.get_entity_mut(handle) { for (path, boundary) in @@ -4621,7 +4680,7 @@ impl OpenCADStudio { path.flags.set_external(true); } hatch.is_associative = !handles.is_empty(); - } + } } self.tabs[i].scene.bump_entities(&[( handle, crate::scene::ChangeKind::Modified, @@ -4650,7 +4709,9 @@ impl OpenCADStudio { } } HatchEditOperation::DrawOrderFront - | HatchEditOperation::DrawOrderBack => unreachable!(), + | HatchEditOperation::DrawOrderBack + | HatchEditOperation::DrawOrderBoundary {..} + | HatchEditOperation::BeginAssociate => unreachable!(), } self.tabs[i].dirty = true; self.command_line diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index cf848ff7b..fc707b846 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -808,7 +808,7 @@ impl OpenCADStudio { scale, angle, annotative, - ); + ).with_appearance(entity,self.tabs[i].scene.document.header.current_entity_color); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { diff --git a/src/command.rs b/src/command.rs index d69859462..b3fbe6de8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -13,13 +13,21 @@ use glam::DVec3; #[derive(Clone, Debug)] pub enum HatchEditOperation { + Appearance { + color: Option, + layer: Option, + transparency: Option, + }, Update { origin: Option<(f64, f64)>, disassociate: bool, style: Option, annotative: Option, }, - RecreateBoundary, + RecreateBoundary { associate: bool }, + BeginAssociate, + AssociateBoundaries(Vec), + DrawOrderBoundary { above: bool }, Separate, AddBoundaries(Vec), RemoveBoundaries(Vec), diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index d88ed407d..825b64ad3 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -31,6 +31,10 @@ pub struct HatcheditCommand { style: Option, annotative: Option, annotative_current: bool, + input: Option<&'static str>, + boundary_selection: Vec, + source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>, + current_color: acadrust::types::Color, } impl HatcheditCommand { @@ -42,6 +46,10 @@ impl HatcheditCommand { style: None, annotative: None, annotative_current: false, + input: None, + boundary_selection: Vec::new(), + source_appearance: None, + current_color: acadrust::types::Color::ByLayer, } } @@ -64,6 +72,10 @@ impl HatcheditCommand { style: None, annotative: None, annotative_current: annotative, + input: None, + boundary_selection: Vec::new(), + source_appearance: None, + current_color: acadrust::types::Color::ByLayer, } } @@ -86,6 +98,15 @@ impl HatcheditCommand { }) } + pub fn for_association(handle:Handle,name:String,scale:f32,angle:f32)->Self { + let mut command=Self::with_handle(handle,name,scale,angle,false); + command.input=Some("associate-select");command + } + pub fn with_appearance(mut self,entity:Option<&acadrust::EntityType>,current_color:acadrust::types::Color)->Self { + self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)}); + self.current_color=current_color;self + } + fn update_operation(&self) -> HatchEditOperation { HatchEditOperation::Update { origin: self.origin, @@ -102,6 +123,42 @@ impl CadCommand for HatcheditCommand { } fn prompt(&self) -> String { + if let Some(input)=self.input { + if let Some((color,layer,transparency))=&self.source_appearance { + match input { + "color"=>return format!("New color [Truecolor/. (for use current)] <{color:?}>:"), + "layer"=>return format!("Specify layer or [. (for use current)] <{layer}>:"), + "transparency"=>return format!("Specify transparency (0-90) or ByLayer/ByBlock <{}>:",match transparency { + acadrust::types::Transparency::ByLayer=>"ByLayer".into(), + acadrust::types::Transparency::ByBlock=>"ByBlock".into(), + value=>format!("{:.0}",value.as_percent()*100.0), + }), + _=>{}, + } + } + if let HatcheditStep::EditOptions{name,scale,angle,..}=&self.step { + match input { + "pattern"=>return format!("Enter a pattern name or [Solid] <{name}>:"), + "scale"=>return format!("Specify a scale for the pattern <{scale:.4}>:"), + "angle"=>return format!("Specify an angle for the pattern <{angle:.4}>:"), + _=>{}, + } + } + return match input { + "pattern"=>"Enter a pattern name or [Solid]:", + "scale"=>"Specify a scale for the pattern:", + "angle"=>"Specify an angle for the pattern:", + "color"=>"New color [Truecolor] :", + "truecolor"=>"Specify RGB color (red,green,blue):", + "layer"=>"Specify layer or [. (for use current)]:", + "transparency"=>"Specify transparency value (0-90) or ByLayer/ByBlock:", + "draworder"=>"Enter draw order [do Not change/send to Back/bring to Front/send beHind boundary/bring in front of bounDary] :", + "boundary-type"=>"Enter type of boundary object [Polyline] :", + "boundary-associate"=>"Reassociate hatch with new boundary? [Yes/No] :", + "associate-select"=>"Select boundary objects:", + _=>"Specify value:", + }.into(); + } match &self.step { HatcheditStep::PickHatch => t!("HATCHEDIT Select hatch:").into_owned(), HatcheditStep::EditOptions { @@ -110,7 +167,7 @@ impl CadCommand for HatcheditCommand { let scale = format!("{scale:.4}"); let angle = format!("{angle:.1}"); t!( - "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [P pattern / S scale / A angle / O x,y / D disassociate / Y style / N annotative / R recreate / E separate / + handles / - handles | Enter]:", + "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Properties/COlor/LAyer/Transparency/DRaw order/Disassociate/Annotative/Recreate/Separate] :", name = name, scale = scale, angle = angle @@ -123,6 +180,10 @@ impl CadCommand for HatcheditCommand { fn needs_entity_pick(&self) -> bool { matches!(self.step, HatcheditStep::PickHatch) } + 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 + } fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { if handle.is_null() { @@ -140,14 +201,21 @@ impl CadCommand for HatcheditCommand { } fn wants_text_input(&self) -> bool { - matches!(self.step, HatcheditStep::EditOptions { .. }) + matches!(self.step, HatcheditStep::EditOptions { .. })&&!self.is_selection_gathering() } fn options(&self) -> Vec { + if self.input.is_some() {return Vec::new();} if !matches!(self.step, HatcheditStep::EditOptions { .. }) { return Vec::new(); } vec![ + crate::command::CmdOption::new("Properties", "P"), + crate::command::CmdOption::new("Color", "CO"), + crate::command::CmdOption::new("Layer", "LA"), + crate::command::CmdOption::new("Transparency", "T"), + crate::command::CmdOption::new("Draw order", "DR"), + crate::command::CmdOption::new("Associate", "AS"), crate::command::CmdOption::new("Disassociate", "D"), crate::command::CmdOption::new("Annotative", "N"), crate::command::CmdOption::new("Recreate boundary", "R"), @@ -159,6 +227,62 @@ impl CadCommand for HatcheditCommand { } fn on_text_input(&mut self, text: &str) -> Option { + let keyword=text.trim().to_ascii_uppercase(); + if let Some(input)=self.input { + use acadrust::types::{Color,Transparency}; + let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency}; + match input { + "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), + "RED"=>Some(Color::Index(1)),"YELLOW"=>Some(Color::Index(2)),"GREEN"=>Some(Color::Index(3)), + "CYAN"=>Some(Color::Index(4)),"BLUE"=>Some(Color::Index(5)),"MAGENTA"=>Some(Color::Index(6)),"WHITE"=>Some(Color::Index(7)), + n=>n.parse::().ok().filter(|v|(0..=256).contains(v)).map(Color::from_index)}; + return Some(color.and_then(|v|self.apply_result(appearance(Some(v),None,None))).unwrap_or(CmdResult::NeedPoint)); + } + "truecolor"=>{ + let rgb:Option>=keyword.split(',').map(|s|s.trim().parse().ok()).collect(); + if let Some(rgb)=rgb.filter(|rgb|rgb.len()==3) {return self.apply_result(appearance(Some(Color::from_rgb(rgb[0],rgb[1],rgb[2])),None,None));} + } + "layer"=>if !text.trim().is_empty(){return self.apply_result(appearance(None,Some(text.trim().to_owned()),None));}, + "transparency"=>{ + let value=match keyword.as_str(){"BYLAYER"=>Some(Transparency::BY_LAYER),"BYBLOCK"=>Some(Transparency::BY_BLOCK), + n=>n.parse::().ok().filter(|v|*v<=90).map(|v|Transparency::from_percent(v as f64 / 100.0))}; + if let Some(value)=value{return self.apply_result(appearance(None,None,Some(value)));} + } + "draworder"=>return match keyword.as_str(){"F"|"FRONT"=>self.apply_result(HatchEditOperation::DrawOrderFront), + "H"|"BEHIND"=>self.apply_result(HatchEditOperation::DrawOrderBoundary{above:false}), + "D"=>self.apply_result(HatchEditOperation::DrawOrderBoundary{above:true}), + "B"|"BACK"=>self.apply_result(HatchEditOperation::DrawOrderBack),"N"|"NOT"=>Some(CmdResult::Cancel),_=>Some(CmdResult::NeedPoint)}, + "boundary-type"=>if matches!(keyword.as_str(),"P"|"POLYLINE"){self.input=Some("boundary-associate");}, + "boundary-associate"=>return match keyword.as_str(){ + "Y"|"YES"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:true}), + "N"|"NO"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false}), + _=>Some(CmdResult::NeedPoint), + }, + "pattern"=>{ + if crate::scene::model::hatch_patterns::find(&keyword).is_some() { + if let HatcheditStep::EditOptions{name,..}=&mut self.step{*name=keyword.clone();} + if keyword=="SOLID" {return self.apply_result(self.update_operation());} + self.input=Some("scale"); + } + } + "scale"|"angle"=>if let Ok(value)=keyword.parse::() {if value.is_finite()&&(input=="angle"||value>0.0){ + if let HatcheditStep::EditOptions{scale,angle,..}=&mut self.step{if input=="scale"{*scale=value;}else{*angle=value;}} + if input=="scale"{self.input=Some("angle");}else{return self.apply_result(self.update_operation());} + }}, + _=>{}, + } + return Some(CmdResult::NeedPoint); + } + let next=match keyword.as_str(){"P"|"PROPERTIES"=>Some("pattern"),"CO"|"COLOR"=>Some("color"),"LA"|"LAYER"=>Some("layer"), + "T"|"TRANSPARENCY"=>Some("transparency"),"DR"|"DRAW"|"DRAW ORDER"=>Some("draworder"), + "B"|"BOUNDARY"|"R"|"RECREATE"=>Some("boundary-type"),_=>None}; + if let Some(input)=next {self.input=Some(input);return Some(CmdResult::NeedPoint);} + if matches!(keyword.as_str(),"AS"|"ASSOCIATE"){return self.apply_result(HatchEditOperation::BeginAssociate);} + if matches!(keyword.as_str(),"DI"|"DISASSOCIATE"){ + self.disassociate=true;return self.apply_result(self.update_operation()); + } let (_handle, name, scale, angle) = match &mut self.step { HatcheditStep::EditOptions { handle, @@ -236,7 +360,7 @@ impl CadCommand for HatcheditCommand { return Some(CmdResult::NeedPoint); } if text == "R" || text == "RECREATE" { - return self.apply_result(HatchEditOperation::RecreateBoundary); + self.input=Some("boundary-type");return Some(CmdResult::NeedPoint); } if text == "E" || text == "SEPARATE" { return self.apply_result(HatchEditOperation::Separate); @@ -273,8 +397,24 @@ impl CadCommand for HatcheditCommand { CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { - // Enter without text → apply current settings - self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel) + match self.input { + None if matches!(self.step,HatcheditStep::EditOptions{..})=>{ + self.input=Some("pattern");CmdResult::NeedPoint + } + Some("pattern")=>{ + if matches!(&self.step,HatcheditStep::EditOptions{name,..} if name.eq_ignore_ascii_case("SOLID")) { + self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel) + }else{self.input=Some("scale");CmdResult::NeedPoint} + } + Some("scale")=>{self.input=Some("angle");CmdResult::NeedPoint} + Some("angle")=>self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel), + Some("boundary-type")=>{self.input=Some("boundary-associate");CmdResult::NeedPoint}, + Some("boundary-associate")=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false}).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) + }, + _=>CmdResult::Cancel, + } } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel