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 01/13] 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 From 14f6cae50895f4f08aa97ea23714eb6c3d566560 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:28:29 +0300 Subject: [PATCH 02/13] Import command interface for hatch association prompts --- src/app/command_driver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index af1a2b1de..2fa61e4f6 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4455,7 +4455,7 @@ impl OpenCADStudio { self.command_line .push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref()); } else { - use crate::command::HatchEditOperation; + use crate::command::{CadCommand, 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 { From 6e74cc48d89b58d76679883831e934a9100c598c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:57:23 +0300 Subject: [PATCH 03/13] Recreate hatch boundaries as exact regions --- src/app/command_driver.rs | 18 +++++++++++++++--- src/command.rs | 2 +- src/modules/draw/draw/hatchedit.rs | 19 +++++++++++++------ src/scene/model/presspull_model.rs | 6 +++++- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 2fa61e4f6..16be1a02c 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4650,7 +4650,7 @@ impl OpenCADStudio { let valid_handles:Vec<_>=sources.keys().copied().collect(); self.tabs[i].scene.edit_hatch_boundary_handles(handle,&valid_handles,true); } - HatchEditOperation::RecreateBoundary { associate } => { + HatchEditOperation::RecreateBoundary { associate, region } => { 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( @@ -4662,8 +4662,20 @@ impl OpenCADStudio { glam::DVec3::from_array(storage.x_axis), glam::DVec3::from_array(storage.y_axis), ); - let rings = crate::scene::hatch_boundary_rings(&source); - let entities = crate::scene::boundary_entities(&rings, plane); + let entities = if region { + let entities = source.paths.iter().map(|path| { + let curves = path.edges.iter().map(crate::entities::hatch::edge_curve).collect::>>()?; + crate::scene::model::presspull_model::region_from_loops(&[curves], plane) + }).collect::>>(); + let Some(entities) = entities else { + self.command_line.push_error("HATCHEDIT: boundary cannot form a region."); + return Task::none(); + }; + entities + } else { + let rings = crate::scene::hatch_boundary_rings(&source); + crate::scene::boundary_entities(&rings, plane) + }; let mut handles = Vec::new(); for entity in entities { if let Some(boundary) = self.commit_entity_handle(entity) { diff --git a/src/command.rs b/src/command.rs index b3fbe6de8..b5167f0d7 100644 --- a/src/command.rs +++ b/src/command.rs @@ -24,7 +24,7 @@ pub enum HatchEditOperation { style: Option, annotative: Option, }, - RecreateBoundary { associate: bool }, + RecreateBoundary { associate: bool, region: bool }, BeginAssociate, AssociateBoundaries(Vec), DrawOrderBoundary { above: bool }, diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index 825b64ad3..807babf52 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -35,6 +35,7 @@ pub struct HatcheditCommand { boundary_selection: Vec, source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>, current_color: acadrust::types::Color, + boundary_region: bool, } impl HatcheditCommand { @@ -50,6 +51,7 @@ impl HatcheditCommand { boundary_selection: Vec::new(), source_appearance: None, current_color: acadrust::types::Color::ByLayer, + boundary_region: false, } } @@ -76,6 +78,7 @@ impl HatcheditCommand { boundary_selection: Vec::new(), source_appearance: None, current_color: acadrust::types::Color::ByLayer, + boundary_region: false, } } @@ -153,7 +156,7 @@ impl CadCommand for HatcheditCommand { "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-type"=>"Enter type of boundary object [Region/Polyline] :", "boundary-associate"=>"Reassociate hatch with new boundary? [Yes/No] :", "associate-select"=>"Select boundary objects:", _=>"Specify value:", @@ -254,10 +257,14 @@ impl CadCommand for HatcheditCommand { "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-type"=>match keyword.as_str(){ + "P"|"POLYLINE"=>{self.boundary_region=false;self.input=Some("boundary-associate");}, + "R"|"REGION"=>{self.boundary_region=true;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}), + "Y"|"YES"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:true,region:self.boundary_region}), + "N"|"NO"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false,region:self.boundary_region}), _=>Some(CmdResult::NeedPoint), }, "pattern"=>{ @@ -408,8 +415,8 @@ impl CadCommand for HatcheditCommand { } 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("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) }, diff --git a/src/scene/model/presspull_model.rs b/src/scene/model/presspull_model.rs index 151b15e95..0218f03ab 100644 --- a/src/scene/model/presspull_model.rs +++ b/src/scene/model/presspull_model.rs @@ -179,8 +179,12 @@ fn boundary_region( sources: &FxHashMap, rings: &[Vec<[f64; 2]>], plane: WorkingPlane, ) -> Option { let loops = boundary_loops(sources, rings)?; + region_from_loops(&loops, plane) +} + +pub(crate) fn region_from_loops(loops: &[Vec], plane: WorkingPlane) -> Option { let kernel_plane = Plane::from_axes(plane.origin.to_array(), plane.x.to_array(), plane.y.to_array()); - let sheet = brep::planar_region(kernel_plane, &loops)?; + let sheet = brep::planar_region(kernel_plane, loops)?; let document = crate::scene::convert::acis_export::solid_to_sat(&sheet)?; let mut region = Region::new(); region.acis_data = AcisData::from_sat(&document.to_sat_string()); From b24feab498264cb7e1ca1671bf509f3ac815bfd3 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:06:52 +0300 Subject: [PATCH 04/13] Apply and edit drawing current transparency --- src/app/commands/draw.rs | 4 ++-- src/app/commands/mod.rs | 1 + src/app/commands/styleprops.rs | 30 +++++++++++++++++++++++++++++- src/app/properties.rs | 2 +- src/app/update/command.rs | 12 +++++++++++- src/modules/draw/draw/hatchedit.rs | 9 ++++++--- src/scene/creation_style.rs | 20 ++++++++++++++++++++ src/scene/entity.rs | 2 ++ 8 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index fc707b846..2ed934146 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); + ).with_appearance(entity,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { @@ -816,7 +816,7 @@ impl OpenCADStudio { .push_error(crate::t!("HATCHEDIT: selected entity is not a hatch.").as_ref()); } } else { - let cmd = HatcheditCommand::new(); + let cmd = HatcheditCommand::new().with_appearance(None,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index e3650b5d1..16412b2e0 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -368,6 +368,7 @@ inventory::submit!(crate::command::CommandRegistration { "COLOR", "COLOUR", "CECOLOR", + "CETRANSPARENCY", "DDCOLOR", "BYLAYER", // Synchronise block attributes. diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index b99da19b4..ac579628b 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -3,6 +3,10 @@ use super::*; impl OpenCADStudio { pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option> { match cmd { + "CETRANSPARENCY" => return self.dispatch_styleprops("SETVAR CETRANSPARENCY", i), + cmd if cmd.starts_with("CETRANSPARENCY ") => { + return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); + } "FRAMES0" => return self.dispatch_styleprops("SETVAR FRAME 0", i), "FRAMES1" => return self.dispatch_styleprops("SETVAR FRAME 1", i), "FRAMES2" => return self.dispatch_styleprops("SETVAR FRAME 2", i), @@ -941,9 +945,33 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - crate::t!("SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), + crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), ); } else { + if name == "CETRANSPARENCY" { + let current = self.tabs[i].scene.document.current_entity_transparency(); + if let Some(value) = &value { + match crate::scene::creation_style::parse_current_transparency(value) { + Some(transparency) => { + if current != transparency { + self.push_undo_snapshot(i, &name); + if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) { + self.command_line.push_error("CETRANSPARENCY: drawing variable dictionary is invalid."); + return Some(self.finish_dispatch(cmd)); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + self.command_line.push_output(&format!("CETRANSPARENCY = {}", crate::scene::creation_style::current_transparency_label(transparency))); + } + None => self.command_line.push_error("CETRANSPARENCY: expected ByLayer (-1), ByBlock (-2), or an integer from 0 to 90."), + } + } else { + self.command_line.push_output(&format!("Enter new value for CETRANSPARENCY <{}>:", crate::scene::creation_style::current_transparency_label(current))); + self.pending_setvar = Some(name.clone()); + } + return Some(self.finish_dispatch(cmd)); + } if matches!(name.as_str(), "SHOWHIST" | "SOLIDHIST") { let current = if name == "SHOWHIST" { self.tabs[i].scene.document.header.show_solid_history.clamp(0, 2) diff --git a/src/app/properties.rs b/src/app/properties.rs index 79bf7c7c8..49c0edaaa 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -306,7 +306,7 @@ impl OpenCADStudio { label: t!("Transparency").into_owned(), field: "transparency", value: PropValue::EditChoice { - value: "ByLayer".to_string(), + value: crate::scene::creation_style::current_transparency_label(self.tabs[i].scene.document.current_entity_transparency()), options: vec!["ByLayer".to_string(), "ByBlock".to_string()], }, }, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 23df084a7..0b927aabf 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -2808,7 +2808,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } else { match field { "transparency" => { - self.tabs[i].dirty = true; + let Some(transparency) = crate::scene::creation_style::parse_current_transparency(&value) else { + self.command_line.push_error("Transparency: expected ByLayer, ByBlock, or an integer from 0 to 90."); + self.refresh_properties(); + return Task::none(); + }; + self.push_undo_snapshot(i, "CETRANSPARENCY"); + if self.tabs[i].scene.document.set_current_entity_transparency(transparency) { + self.tabs[i].dirty = true; + } else { + self.command_line.push_error("CETRANSPARENCY: drawing variable dictionary is invalid."); + } self.refresh_properties(); } "material" => { diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index 807babf52..984965e0b 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -35,6 +35,7 @@ pub struct HatcheditCommand { boundary_selection: Vec, source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>, current_color: acadrust::types::Color, + current_transparency: acadrust::types::Transparency, boundary_region: bool, } @@ -51,6 +52,7 @@ impl HatcheditCommand { boundary_selection: Vec::new(), source_appearance: None, current_color: acadrust::types::Color::ByLayer, + current_transparency: acadrust::types::Transparency::ByLayer, boundary_region: false, } } @@ -78,6 +80,7 @@ impl HatcheditCommand { boundary_selection: Vec::new(), source_appearance: None, current_color: acadrust::types::Color::ByLayer, + current_transparency: acadrust::types::Transparency::ByLayer, boundary_region: false, } } @@ -105,9 +108,9 @@ impl HatcheditCommand { 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 { + 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)}); - self.current_color=current_color;self + self.current_color=current_color;self.current_transparency=current_transparency;self } fn update_operation(&self) -> HatchEditOperation { @@ -249,7 +252,7 @@ impl CadCommand for HatcheditCommand { } "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), + let value=match keyword.as_str(){"."=>Some(self.current_transparency),"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)));} } diff --git a/src/scene/creation_style.rs b/src/scene/creation_style.rs index 442d61189..fa2902f74 100644 --- a/src/scene/creation_style.rs +++ b/src/scene/creation_style.rs @@ -252,7 +252,27 @@ fn apply_object_defaults(doc: &CadDocument, entity: &mut EntityType) { } pub fn apply_current_creation_styles(doc: &CadDocument, entity: &mut EntityType) { + entity.common_mut().transparency = doc.current_entity_transparency(); apply_text_defaults(doc, entity); apply_dimension_defaults(doc, entity); apply_object_defaults(doc, entity); } + +pub(crate) fn parse_current_transparency(value: &str) -> Option { + use acadrust::types::Transparency; + match value.trim().to_ascii_uppercase().as_str() { + "BYLAYER" | "-1" => Some(Transparency::ByLayer), + "BYBLOCK" | "-2" => Some(Transparency::ByBlock), + value => value.parse::().ok().filter(|value| *value <= 90) + .map(|value| Transparency::from_percent(value as f64 / 100.0)), + } +} + +pub(crate) fn current_transparency_label(value: acadrust::types::Transparency) -> String { + use acadrust::types::Transparency; + match value { + Transparency::ByLayer => "ByLayer".into(), + Transparency::ByBlock => "ByBlock".into(), + Transparency::Explicit(_) => format!("{:.0}", value.as_percent() * 100.0), + } +} diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 02ed14956..2943e7458 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -2671,6 +2671,8 @@ impl Scene { if let Some((color, transparency)) = entity_style { entity.common_mut().color = color; entity.common_mut().transparency = transparency; + } else { + entity.common_mut().transparency = self.document.current_entity_transparency(); } self.add_entity(entity) From 49d1125530bc51649a53da33f26b9c1076bd2243 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:10:29 +0300 Subject: [PATCH 05/13] Adopt and restore template transparency for new matching objects --- src/app/commands/inquiry.rs | 12 +++++++++++- src/app/mod.rs | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 08f920696..8ef4d558d 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -1373,6 +1373,7 @@ impl OpenCADStudio { crate::entities::names::dxf_name(e).to_string(), c.layer.clone(), c.color, + c.transparency, c.linetype.clone(), c.linetype_scale, c.line_weight, @@ -1384,7 +1385,7 @@ impl OpenCADStudio { }, ) }); - let Some((verb, kind, layer, color, linetype, lt_scale, lw, template_dimstyle)) = info + let Some((verb, kind, layer, color, transparency, linetype, lt_scale, lw, template_dimstyle)) = info else { self.command_line .push_error(crate::t!("ADDSELECTED: selected object not found.").as_ref()); @@ -1408,6 +1409,7 @@ impl OpenCADStudio { layer_name: self.tabs[i].scene.document.header.current_layer_name.clone(), layer_handle: self.tabs[i].scene.document.header.current_layer_handle, color: self.tabs[i].scene.document.header.current_entity_color, + transparency: self.tabs[i].scene.document.current_entity_transparency(), linetype_name: self.tabs[i].scene.document.header.current_linetype_name.clone(), linetype_handle: self.tabs[i].scene.document.header.current_linetype_handle, line_weight: self.tabs[i].scene.document.header.current_line_weight, @@ -1422,6 +1424,11 @@ impl OpenCADStudio { ribbon_lineweight: self.ribbon.active_lineweight, }; self.add_selected_restore = Some(restore); + if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) { + self.add_selected_restore = None; + self.command_line.push_error("ADDSELECTED: template transparency cannot be adopted."); + return Task::none(); + } // Adopt the template's general properties as the current defaults. The // entity-creation path stamps new objects from the tab's active layer @@ -1504,6 +1511,9 @@ impl OpenCADStudio { h.current_dimstyle_name = r.dimstyle_name; h.current_dimstyle_handle = r.dimstyle_handle; } + if !self.tabs[i].scene.document.set_current_entity_transparency(r.transparency) { + self.command_line.push_error("ADDSELECTED: current transparency could not be restored."); + } self.tabs[i].active_layer = r.tab_active_layer; self.tabs[i].layers.current_layer = r.tab_layers_current; self.ribbon.active_layer = r.ribbon_layer; diff --git a/src/app/mod.rs b/src/app/mod.rs index 0c2cd3e73..ebee3b5a1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -307,6 +307,7 @@ struct AddSelectedRestore { layer_name: String, layer_handle: acadrust::types::Handle, color: AcadColor, + transparency: acadrust::types::Transparency, linetype_name: String, linetype_handle: acadrust::types::Handle, line_weight: i16, From 1039f51abff18a2a07ac3af4a9abf957c4d698b5 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:18:01 +0300 Subject: [PATCH 06/13] Associate hatch boundaries from internal points and selected sources --- src/app/command_driver.rs | 19 ++++++++++- src/command.rs | 1 + src/modules/draw/draw/hatchedit.rs | 52 ++++++++++++++++++++++++++---- src/scene/boundary.rs | 8 +++++ src/scene/model/presspull_model.rs | 2 +- 5 files changed, 74 insertions(+), 8 deletions(-) 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); From 7df252ad0436fac9ec3490f6276e5ab67ad03a84 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:50:47 +0300 Subject: [PATCH 07/13] Align hatch edit option buttons with command handlers --- src/modules/draw/draw/hatchedit.rs | 36 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index dc2cfa98f..ab879d7e3 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -168,6 +168,7 @@ impl CadCommand for HatcheditCommand { } } return match input { + "annotative"=>if self.annotative_current {"Make hatch annotative [Yes/No] :"} else {"Make hatch annotative [Yes/No] :"}, "pattern"=>"Enter a pattern name or [Solid]:", "scale"=>"Specify a scale for the pattern:", "angle"=>"Specify an angle for the pattern:", @@ -192,7 +193,7 @@ impl CadCommand for HatcheditCommand { let scale = format!("{scale:.4}"); let angle = format!("{angle:.1}"); t!( - "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Properties/COlor/LAyer/Transparency/DRaw order/Disassociate/Annotative/Recreate/Separate] :", + "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] :", name = name, scale = scale, angle = angle @@ -237,7 +238,17 @@ 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 let Some(input) = self.input { + use crate::command::CmdOption; + return match input { + "draworder" => vec![CmdOption::new("Do not change", "N"), CmdOption::new("Send to back", "B"), CmdOption::new("Bring to front", "F"), CmdOption::new("Behind boundary", "H"), CmdOption::new("In front of boundary", "D")], + "annotative" | "boundary-associate" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], + "boundary-type" => vec![CmdOption::new("Region", "R"), CmdOption::new("Polyline", "P")], + "color" => vec![CmdOption::new("Truecolor", "T")], + "pattern" => vec![CmdOption::new("Solid", "SOLID")], + _ => Vec::new(), + }; + } if !matches!(self.step, HatcheditStep::EditOptions { .. }) { return Vec::new(); } @@ -248,13 +259,11 @@ impl CadCommand for HatcheditCommand { 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"), - crate::command::CmdOption::new("Separate hatches", "E"), - crate::command::CmdOption::new("Draw front", "F"), - crate::command::CmdOption::new("Draw back", "B"), - crate::command::CmdOption::enter("Apply"), + crate::command::CmdOption::new("Disassociate", "DI"), + crate::command::CmdOption::new("Annotative", "AN"), + crate::command::CmdOption::new("Recreate boundary", "B"), + crate::command::CmdOption::new("Separate hatches", "H"), + crate::command::CmdOption::enter("Properties"), ] } @@ -264,6 +273,11 @@ impl CadCommand for HatcheditCommand { use acadrust::types::{Color,Transparency}; let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency}; match input { + "annotative" => { + let value = match keyword.as_str() { "Y" | "YES" => true, "N" | "NO" => false, _ => return Some(CmdResult::NeedPoint) }; + self.annotative = Some(value); + return self.apply_result(self.update_operation()); + } "associate-point"=>{ if matches!(keyword.as_str(),"S"|"SELECT"|"SELECT OBJECTS") {self.input=Some("associate-select");} return Some(CmdResult::NeedPoint); @@ -316,9 +330,10 @@ impl CadCommand for HatcheditCommand { 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"), + "AN"|"ANNOTATIVE"=>Some("annotative"),"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(),"H"|"HATCHES"|"SEPARATE") {return self.apply_result(HatchEditOperation::Separate);} 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()); @@ -450,6 +465,7 @@ impl CadCommand for HatcheditCommand { None if matches!(self.step,HatcheditStep::EditOptions{..})=>{ self.input=Some("pattern");CmdResult::NeedPoint } + Some("annotative")=>{self.annotative=Some(self.annotative_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, 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) From a44a13a5ad28b69b8d5fb1e580f15824a8368885 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:43:05 +0300 Subject: [PATCH 08/13] Expose hatch island style command choices and selected default --- src/modules/draw/draw/hatchedit.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index ab879d7e3..a884b659d 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -31,6 +31,7 @@ pub struct HatcheditCommand { style: Option, annotative: Option, annotative_current: bool, + style_current: acadrust::entities::HatchStyleType, input: Option<&'static str>, boundary_selection: Vec, source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>, @@ -51,6 +52,7 @@ impl HatcheditCommand { style: None, annotative: None, annotative_current: false, + style_current: acadrust::entities::HatchStyleType::Normal, input: None, boundary_selection: Vec::new(), source_appearance: None, @@ -82,6 +84,7 @@ impl HatcheditCommand { style: None, annotative: None, annotative_current: annotative, + style_current: acadrust::entities::HatchStyleType::Normal, input: None, boundary_selection: Vec::new(), source_appearance: None, @@ -126,6 +129,7 @@ impl HatcheditCommand { } } pub fn with_appearance(mut self,entity:Option<&acadrust::EntityType>,current_color:acadrust::types::Color,current_transparency:acadrust::types::Transparency)->Self { + if let Some(acadrust::EntityType::Hatch(hatch)) = entity { self.style_current = hatch.style; } self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)}); self.current_color=current_color;self.current_transparency=current_transparency;self } @@ -147,6 +151,14 @@ impl CadCommand for HatcheditCommand { fn prompt(&self) -> String { if let Some(input)=self.input { + if input == "style" { + let current = match self.style_current { + acadrust::entities::HatchStyleType::Normal => "Normal", + acadrust::entities::HatchStyleType::Outer => "Outer", + acadrust::entities::HatchStyleType::Ignore => "Ignore", + }; + return format!("Enter hatching style [Ignore/Outer/Normal] <{current}>:"); + } if let Some((color,layer,transparency))=&self.source_appearance { match input { "color"=>return format!("New color [Truecolor/. (for use current)] <{color:?}>:"), @@ -193,7 +205,7 @@ impl CadCommand for HatcheditCommand { let scale = format!("{scale:.4}"); let angle = format!("{angle:.1}"); t!( - "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] :", + "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Style/Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] :", name = name, scale = scale, angle = angle @@ -241,6 +253,7 @@ impl CadCommand for HatcheditCommand { if let Some(input) = self.input { use crate::command::CmdOption; return match input { + "style" => vec![CmdOption::new("Ignore", "I"), CmdOption::new("Outer", "O"), CmdOption::new("Normal", "N")], "draworder" => vec![CmdOption::new("Do not change", "N"), CmdOption::new("Send to back", "B"), CmdOption::new("Bring to front", "F"), CmdOption::new("Behind boundary", "H"), CmdOption::new("In front of boundary", "D")], "annotative" | "boundary-associate" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], "boundary-type" => vec![CmdOption::new("Region", "R"), CmdOption::new("Polyline", "P")], @@ -253,6 +266,7 @@ impl CadCommand for HatcheditCommand { return Vec::new(); } vec![ + crate::command::CmdOption::new("Style", "S"), crate::command::CmdOption::new("Properties", "P"), crate::command::CmdOption::new("Color", "CO"), crate::command::CmdOption::new("Layer", "LA"), @@ -273,6 +287,15 @@ impl CadCommand for HatcheditCommand { use acadrust::types::{Color,Transparency}; let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency}; match input { + "style" => { + self.style = match keyword.as_str() { + "I" | "IGNORE" => Some(acadrust::entities::HatchStyleType::Ignore), + "O" | "OUTER" => Some(acadrust::entities::HatchStyleType::Outer), + "N" | "NORMAL" => Some(acadrust::entities::HatchStyleType::Normal), + _ => return Some(CmdResult::NeedPoint), + }; + return self.apply_result(self.update_operation()); + } "annotative" => { let value = match keyword.as_str() { "Y" | "YES" => true, "N" | "NO" => false, _ => return Some(CmdResult::NeedPoint) }; self.annotative = Some(value); @@ -329,7 +352,7 @@ impl CadCommand for HatcheditCommand { } return Some(CmdResult::NeedPoint); } - let next=match keyword.as_str(){"P"|"PROPERTIES"=>Some("pattern"),"CO"|"COLOR"=>Some("color"),"LA"|"LAYER"=>Some("layer"), + let next=match keyword.as_str(){"S"|"STYLE"=>Some("style"),"P"|"PROPERTIES"=>Some("pattern"),"CO"|"COLOR"=>Some("color"),"LA"|"LAYER"=>Some("layer"), "AN"|"ANNOTATIVE"=>Some("annotative"),"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);} @@ -465,6 +488,7 @@ impl CadCommand for HatcheditCommand { None if matches!(self.step,HatcheditStep::EditOptions{..})=>{ self.input=Some("pattern");CmdResult::NeedPoint } + Some("style")=>{self.style=Some(self.style_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, Some("annotative")=>{self.annotative=Some(self.annotative_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, Some("pattern")=>{ if matches!(&self.step,HatcheditStep::EditOptions{name,..} if name.eq_ignore_ascii_case("SOLID")) { From 67f374c853a336218f2e1608ba6e5251540c595c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:52:30 +0300 Subject: [PATCH 09/13] Apply and persist hatch origin command choices --- src/app/command_driver.rs | 10 ++++++ src/app/commands/draw.rs | 6 ++-- src/command.rs | 1 + src/modules/draw/draw/hatch.rs | 17 +++++++++- src/modules/draw/draw/hatchedit.rs | 54 ++++++++++++++++++++++++++++-- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 74e23845b..b626b575e 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4524,10 +4524,20 @@ impl OpenCADStudio { } HatchEditOperation::Update { origin, + store_origin, disassociate, style, annotative, } => { + if store_origin { + if let Some((x, y)) = origin { + if !self.tabs[i].scene.document.set_hatch_origin([x, y]) { + self.discard_last_undo_entry(i); + self.command_line.push_error("HATCHEDIT: cannot store default origin."); + return Task::none(); + } + } + } if let Some(acadrust::EntityType::Hatch(hatch)) = self.tabs[i].scene.document.get_entity_mut(handle) { diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 2ed934146..55638e914 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -775,7 +775,7 @@ impl OpenCADStudio { selected, inherited, plane, - ); + ).with_origin(self.tabs[i].scene.document.hatch_origin()); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.refresh_area_preview(i); @@ -808,7 +808,7 @@ impl OpenCADStudio { scale, angle, annotative, - ).with_appearance(entity,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()); + ).with_appearance(entity,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()).with_origin(self.tabs[i].scene.document.hatch_origin()); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { @@ -816,7 +816,7 @@ impl OpenCADStudio { .push_error(crate::t!("HATCHEDIT: selected entity is not a hatch.").as_ref()); } } else { - let cmd = HatcheditCommand::new().with_appearance(None,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()); + let cmd = HatcheditCommand::new().with_appearance(None,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()).with_origin(self.tabs[i].scene.document.hatch_origin()); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } diff --git a/src/command.rs b/src/command.rs index 94ad82e6b..88a7be8d3 100644 --- a/src/command.rs +++ b/src/command.rs @@ -20,6 +20,7 @@ pub enum HatchEditOperation { }, Update { origin: Option<(f64, f64)>, + store_origin: bool, disassociate: bool, style: Option, annotative: Option, diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 7a51f986e..8fa7c244a 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -196,6 +196,7 @@ pub struct HatchCommand { pattern_override: Option<(String, HatchPattern)>, angle_override: Option, scale_override: Option, + default_origin: [f64; 2], associative: bool, separate_hatches: bool, island_style: acadrust::entities::HatchStyleType, @@ -244,6 +245,7 @@ impl HatchCommand { pattern_override: None, angle_override: None, scale_override: None, + default_origin: [0.0, 0.0], associative: true, separate_hatches: false, island_style: inherited @@ -257,6 +259,8 @@ impl HatchCommand { command } + pub fn with_origin(mut self, origin: [f64; 2]) -> Self { self.default_origin = origin; self } + fn set_object_selection(&mut self, handles: Vec) { let mut segments = Vec::new(); for handle in &handles { @@ -425,10 +429,21 @@ impl HatchCommand { dashes: vec![], }]) }); - let (name, pattern) = self + let (name, mut pattern) = self .pattern_override .clone() .unwrap_or_else(|| (pat_name.to_string(), default_pattern)); + let angle = self.angle_override.unwrap_or(0.0); + let scale = self.scale_override.unwrap_or(1.0).max(1.0e-6); + if let (HatchPattern::Pattern(families), Some(anchor)) = (&mut pattern, local_boundary.first()) { + let (sin, cos) = (angle as f64).sin_cos(); + let dx = self.default_origin[0] - anchor[0] as f64; + let dy = self.default_origin[1] - anchor[1] as f64; + for family in families { + family.x0 += ((dx * cos + dy * sin) / scale as f64) as f32; + family.y0 += ((-dx * sin + dy * cos) / scale as f64) as f32; + } + } HatchModel { render_instance: None, boundary: std::sync::Arc::new(rel), diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index a884b659d..1a104f1d4 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -28,6 +28,9 @@ pub struct HatcheditCommand { step: HatcheditStep, origin: Option<(f64, f64)>, disassociate: bool, + store_origin: bool, + current_origin: [f64; 2], + origin_plane: cadkernel::space::Plane, style: Option, annotative: Option, annotative_current: bool, @@ -49,6 +52,9 @@ impl HatcheditCommand { step: HatcheditStep::PickHatch, origin: None, disassociate: false, + store_origin: false, + current_origin: [0.0, 0.0], + origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]), style: None, annotative: None, annotative_current: false, @@ -81,6 +87,9 @@ impl HatcheditCommand { }, origin: None, disassociate: false, + store_origin: false, + current_origin: [0.0, 0.0], + origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]), style: None, annotative: None, annotative_current: annotative, @@ -129,14 +138,20 @@ impl HatcheditCommand { } } pub fn with_appearance(mut self,entity:Option<&acadrust::EntityType>,current_color:acadrust::types::Color,current_transparency:acadrust::types::Transparency)->Self { - if let Some(acadrust::EntityType::Hatch(hatch)) = entity { self.style_current = hatch.style; } + if let Some(acadrust::EntityType::Hatch(hatch)) = entity { + self.style_current = hatch.style; + self.origin_plane = crate::entities::curve::ocs_plane(hatch.normal, hatch.elevation); + } self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)}); self.current_color=current_color;self.current_transparency=current_transparency;self } + pub fn with_origin(mut self, origin: [f64; 2]) -> Self { self.current_origin = origin; self } + fn update_operation(&self) -> HatchEditOperation { HatchEditOperation::Update { origin: self.origin, + store_origin: self.store_origin, disassociate: self.disassociate, style: self.style, annotative: self.annotative, @@ -181,6 +196,9 @@ impl CadCommand for HatcheditCommand { } return match input { "annotative"=>if self.annotative_current {"Make hatch annotative [Yes/No] :"} else {"Make hatch annotative [Yes/No] :"}, + "origin"=>"[Use current origin/Set new origin] :", + "origin-point"=>"Select point:", + "origin-store"=>"Store as default origin? [Yes/No] :", "pattern"=>"Enter a pattern name or [Solid]:", "scale"=>"Specify a scale for the pattern:", "angle"=>"Specify an angle for the pattern:", @@ -205,7 +223,7 @@ impl CadCommand for HatcheditCommand { let scale = format!("{scale:.4}"); let angle = format!("{angle:.1}"); t!( - "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Style/Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] :", + "HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Style/Origin/Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] :", name = name, scale = scale, angle = angle @@ -253,6 +271,8 @@ impl CadCommand for HatcheditCommand { if let Some(input) = self.input { use crate::command::CmdOption; return match input { + "origin" => vec![CmdOption::new("Use current origin", "U"), CmdOption::new("Set new origin", "S")], + "origin-store" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], "style" => vec![CmdOption::new("Ignore", "I"), CmdOption::new("Outer", "O"), CmdOption::new("Normal", "N")], "draworder" => vec![CmdOption::new("Do not change", "N"), CmdOption::new("Send to back", "B"), CmdOption::new("Bring to front", "F"), CmdOption::new("Behind boundary", "H"), CmdOption::new("In front of boundary", "D")], "annotative" | "boundary-associate" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], @@ -267,6 +287,7 @@ impl CadCommand for HatcheditCommand { } vec![ crate::command::CmdOption::new("Style", "S"), + crate::command::CmdOption::new("Origin", "O"), crate::command::CmdOption::new("Properties", "P"), crate::command::CmdOption::new("Color", "CO"), crate::command::CmdOption::new("Layer", "LA"), @@ -287,6 +308,21 @@ impl CadCommand for HatcheditCommand { use acadrust::types::{Color,Transparency}; let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency}; match input { + "origin" => match keyword.as_str() { + "U" | "USE" => { self.origin = Some((self.current_origin[0], self.current_origin[1])); return self.apply_result(self.update_operation()); } + "S" | "SET" => { self.input = Some("origin-point"); } + _ => {}, + }, + "origin-point" => { + let values: Option> = text.split(',').map(|s| s.trim().parse().ok()).collect(); + if let Some(values) = values.filter(|v| (2..=3).contains(&v.len()) && v.iter().all(|n| n.is_finite())) { + return Some(self.on_point(DVec3::new(values[0], values[1], values.get(2).copied().unwrap_or(0.0)))); + } + } + "origin-store" => { + self.store_origin = match keyword.as_str() { "Y" | "YES" => true, "N" | "NO" => false, _ => return Some(CmdResult::NeedPoint) }; + return self.apply_result(self.update_operation()); + } "style" => { self.style = match keyword.as_str() { "I" | "IGNORE" => Some(acadrust::entities::HatchStyleType::Ignore), @@ -352,7 +388,7 @@ impl CadCommand for HatcheditCommand { } return Some(CmdResult::NeedPoint); } - let next=match keyword.as_str(){"S"|"STYLE"=>Some("style"),"P"|"PROPERTIES"=>Some("pattern"),"CO"|"COLOR"=>Some("color"),"LA"|"LAYER"=>Some("layer"), + let next=match keyword.as_str(){"O"|"ORIGIN"=>Some("origin"),"S"|"STYLE"=>Some("style"),"P"|"PROPERTIES"=>Some("pattern"),"CO"|"COLOR"=>Some("color"),"LA"|"LAYER"=>Some("layer"), "AN"|"ANNOTATIVE"=>Some("annotative"),"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);} @@ -472,6 +508,15 @@ impl CadCommand for HatcheditCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.input == Some("origin-point") { + if let Some(point) = self.origin_plane.project(pt.to_array()) { + if point.iter().all(|value| value.is_finite()) { + self.origin = Some((point[0], point[1])); + self.input = Some("origin-store"); + } + } + return CmdResult::NeedPoint; + } if self.input==Some("associate-point") { if let Some((plane,sources))=&self.association_sources { let point=plane.to_local(pt); @@ -488,6 +533,9 @@ impl CadCommand for HatcheditCommand { None if matches!(self.step,HatcheditStep::EditOptions{..})=>{ self.input=Some("pattern");CmdResult::NeedPoint } + Some("origin")=>{self.origin=Some((self.current_origin[0],self.current_origin[1]));self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, + Some("origin-point")=>CmdResult::NeedPoint, + Some("origin-store")=>self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel), Some("style")=>{self.style=Some(self.style_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, Some("annotative")=>{self.annotative=Some(self.annotative_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, Some("pattern")=>{ From dbd85c05594e53ab658333b03c3340923abdee71 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:12:18 +0300 Subject: [PATCH 10/13] Recreate hatch regions with holes and nested island grouping --- src/app/command_driver.rs | 55 +++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b626b575e..e651ef0fb 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4689,36 +4689,59 @@ impl OpenCADStudio { glam::DVec3::from_array(storage.x_axis), glam::DVec3::from_array(storage.y_axis), ); - let entities = if region { - let entities = source.paths.iter().map(|path| { - let curves = path.edges.iter().map(crate::entities::hatch::edge_curve).collect::>>()?; - crate::scene::model::presspull_model::region_from_loops(&[curves], plane) + let (entities, path_groups) = if region { + let loops = source.paths.iter().map(|path| { + path.edges.iter().map(crate::entities::hatch::edge_curve).collect::>>() }).collect::>>(); - let Some(entities) = entities else { + let Some(loops) = loops.filter(|loops| !loops.is_empty() && loops.iter().all(|ring| !ring.is_empty())) else { + self.discard_last_undo_entry(i); self.command_line.push_error("HATCHEDIT: boundary cannot form a region."); return Task::none(); }; - entities + let contained = loops.iter().enumerate().map(|(inner, ring)| { + let seed = ring[0].point_at(0.0); + loops.iter().enumerate().map(|(outer, boundary)| { + inner != outer && cadkernel::geom2d::contains(boundary, seed, cadkernel::geom2d::Tolerance::new(1e-6)) + }).collect::>() + }).collect::>(); + let depths = contained.iter().map(|row| row.iter().filter(|inside| **inside).count()).collect::>(); + let groups = depths.iter().enumerate().filter(|(_, depth)| **depth % 2 == 0).map(|(outer, depth)| { + let mut indices = vec![outer]; + indices.extend((0..loops.len()).filter(|inner| depths[*inner] == *depth + 1 && contained[*inner][outer])); + indices + }).collect::>(); + let entities = groups.iter().map(|indices| { + let profiles = indices.iter().map(|index| loops[*index].clone()).collect::>(); + crate::scene::model::presspull_model::region_from_loops(&profiles, plane) + }).collect::>>(); + let Some(entities) = entities else { + self.discard_last_undo_entry(i); + self.command_line.push_error("HATCHEDIT: boundary cannot form a region."); + return Task::none(); + }; + (entities, groups) } else { let rings = crate::scene::hatch_boundary_rings(&source); - crate::scene::boundary_entities(&rings, plane) + let entities = crate::scene::boundary_entities(&rings, plane); + let groups = (0..entities.len()).map(|index| vec![index]).collect(); + (entities, groups) }; - let mut handles = Vec::new(); - for entity in entities { + let mut path_handles = vec![None; source.paths.len()]; + for (entity, paths) in entities.into_iter().zip(path_groups) { if let Some(boundary) = self.commit_entity_handle(entity) { - handles.push(boundary); + for path in paths { if let Some(slot) = path_handles.get_mut(path) { *slot = Some(boundary); } } } } if associate { if let Some(acadrust::EntityType::Hatch(hatch)) = self.tabs[i].scene.document.get_entity_mut(handle) { - for (path, boundary) in - hatch.paths.iter_mut().zip(handles.iter().copied()) - { - path.boundary_handles = vec![boundary]; - path.flags.set_external(true); + for (path, boundary) in hatch.paths.iter_mut().zip(path_handles.iter().copied()) { + if let Some(boundary) = boundary { + path.boundary_handles = vec![boundary]; + path.flags.set_external(true); + } } - hatch.is_associative = !handles.is_empty(); + hatch.is_associative = path_handles.iter().all(Option::is_some); } } self.tabs[i].scene.bump_entities(&[( handle, From 6272722baa2b05cd30b9e86d746e1630d5d34795 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:40:40 +0300 Subject: [PATCH 11/13] Carry hatch origin through creation and pattern editing --- src/app/command_driver.rs | 37 +++------------------- src/app/update/command.rs | 16 ++-------- src/entities/hatch.rs | 58 +++++++++++----------------------- src/io/pdf_export.rs | 1 + src/modules/draw/draw/hatch.rs | 3 ++ src/scene/entity.rs | 6 ++++ src/scene/mod.rs | 1 + src/scene/model/hatch_model.rs | 2 ++ src/scene/preview.rs | 1 + src/scene/view/render.rs | 1 + src/ui/properties.rs | 1 + 11 files changed, 42 insertions(+), 85 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e651ef0fb..8ecf348ec 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4545,11 +4545,6 @@ impl OpenCADStudio { if let Some(entry) = crate::scene::model::hatch_patterns::find(&name) { - let old_origin = hatch - .pattern - .lines - .first() - .map(|line| line.base_point); let mut pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); crate::entities::hatch::scale_pattern_geometry( &mut pattern, @@ -4559,16 +4554,8 @@ impl OpenCADStudio { &mut pattern, (angle as f64).to_radians(), ); - if let (Some(old), Some(new)) = ( - old_origin, - pattern.lines.first().map(|line| line.base_point), - ) { - crate::entities::hatch::translate_pattern_geometry( - &mut pattern, - old.x - new.x, - old.y - new.y, - ); - } + let origin = hatch.pattern_origin(); + crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y); hatch.pattern = pattern; hatch.is_solid = matches!( entry.gpu, @@ -4582,30 +4569,16 @@ impl OpenCADStudio { let requested_scale = scale.max(1.0e-6) as f64; if hatch.pattern_scale > 1.0e-12 { let factor = requested_scale / hatch.pattern_scale; - crate::entities::hatch::scale_pattern_geometry( - &mut hatch.pattern, - factor, - ); + hatch.scale_pattern_about_origin(factor); } let requested_angle = (angle as f64).to_radians(); let delta = requested_angle - hatch.pattern_angle; - crate::entities::hatch::rotate_pattern_geometry( - &mut hatch.pattern, - delta, - ); + hatch.rotate_pattern_about_origin(delta); } hatch.pattern_scale = scale.max(1.0e-6) as f64; hatch.pattern_angle = (angle as f64).to_radians(); if let Some((x, y)) = origin { - if let Some(current) = - hatch.pattern.lines.first().map(|line| line.base_point) - { - crate::entities::hatch::translate_pattern_geometry( - &mut hatch.pattern, - x - current.x, - y - current.y, - ); - } + hatch.set_pattern_origin(acadrust::types::Vector2::new(x, y)); } if disassociate { for path in &mut hatch.paths { diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 0b927aabf..8aa11eb94 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1997,9 +1997,6 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if let Some(acadrust::EntityType::Hatch(dxf)) = self.tabs[i].scene.document.get_entity_mut(handle) { - let old_origin = dxf.pattern.lines.first().map(|line| { - (line.base_point.x, line.base_point.y) - }); let mut pattern = hatch_patterns::build_dxf_pattern(entry); // Stored pattern lines are final world-space // geometry. Preserve the selected hatch's scale, @@ -2013,17 +2010,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { &mut pattern, dxf.pattern_angle, ); - if let (Some((old_x, old_y)), Some(new_origin)) = - (old_origin, pattern.lines.first()) - { - let dx = old_x - new_origin.base_point.x; - let dy = old_y - new_origin.base_point.y; - crate::entities::hatch::translate_pattern_geometry( - &mut pattern, - dx, - dy, - ); - } + let origin = dxf.pattern_origin(); + crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y); dxf.pattern = pattern; dxf.is_solid = matches!( entry.gpu, diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 05036ee30..0acf8ebde 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -182,18 +182,14 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> { (n > 0.0).then(|| (sx / n, sy / n)) } -/// Scale the stored, final hatch-pattern geometry around the pattern origin. +/// Scale a catalog pattern about its intrinsic coordinate origin. /// Pattern lines loaded from DXF/DWG are rendered prebaked, so their metadata /// scale is not applied again by the renderer. pub(crate) fn scale_pattern_geometry( pattern: &mut acadrust::entities::HatchPattern, factor: f64, ) { - let (origin_x, origin_y) = pattern - .lines - .first() - .map(|line| (line.base_point.x, line.base_point.y)) - .unwrap_or((0.0, 0.0)); + let (origin_x, origin_y) = (0.0, 0.0); for line in pattern.lines.iter_mut() { line.base_point.x = origin_x + (line.base_point.x - origin_x) * factor; line.base_point.y = origin_y + (line.base_point.y - origin_y) * factor; @@ -205,18 +201,14 @@ pub(crate) fn scale_pattern_geometry( } } -/// Rotate the stored, final hatch-pattern geometry around the pattern origin. +/// Rotate a catalog pattern about its intrinsic coordinate origin. /// This keeps the line angle, base point and world-space offset in sync. pub(crate) fn rotate_pattern_geometry( pattern: &mut acadrust::entities::HatchPattern, angle: f64, ) { let (sin, cos) = angle.sin_cos(); - let (origin_x, origin_y) = pattern - .lines - .first() - .map(|line| (line.base_point.x, line.base_point.y)) - .unwrap_or((0.0, 0.0)); + let (origin_x, origin_y) = (0.0, 0.0); for line in pattern.lines.iter_mut() { line.angle += angle; let (x, y) = ( @@ -613,7 +605,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { _ => h.pattern_type, }; if requested != h.pattern_type { - let old_origin = h.pattern.lines.first().map(|line| line.base_point); + let old_origin = h.pattern_origin(); h.pattern_type = requested; h.is_solid = false; match requested { @@ -629,15 +621,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { crate::scene::model::hatch_patterns::build_dxf_pattern(entry); scale_pattern_geometry(&mut pattern, h.pattern_scale); rotate_pattern_geometry(&mut pattern, h.pattern_angle); - if let (Some(old), Some(new)) = - (old_origin, pattern.lines.first().map(|line| line.base_point)) - { - translate_pattern_geometry( - &mut pattern, - old.x - new.x, - old.y - new.y, - ); - } + translate_pattern_geometry(&mut pattern, old_origin.x, old_origin.y); h.pattern = pattern; } } @@ -695,7 +679,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { "pattern_angle" => { let angle = v.to_radians(); let delta = angle - h.pattern_angle; - rotate_pattern_geometry(&mut h.pattern, delta); + h.rotate_pattern_about_origin(delta); h.pattern_angle = angle; } // The stored pattern lines are the FINAL rendered geometry (offsets, @@ -706,7 +690,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { let old = h.pattern_scale; if old > 1e-12 { let k = v / old; - scale_pattern_geometry(&mut h.pattern, k); + h.scale_pattern_about_origin(k); } h.pattern_scale = v; } @@ -724,19 +708,17 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { } // Origin rows are relative offsets and return to zero after commit. "origin_x" => { - for line in h.pattern.lines.iter_mut() { - line.base_point.x += v; - } + let origin = h.pattern_origin(); + h.set_pattern_origin(acadrust::types::Vector2::new(origin.x + v, origin.y)); } "origin_y" => { - for line in h.pattern.lines.iter_mut() { - line.base_point.y += v; - } + let origin = h.pattern_origin(); + h.set_pattern_origin(acadrust::types::Vector2::new(origin.x, origin.y + v)); } "iso_pen_width" if v > 0.0 => { let old = h.pattern_scale; if old > 1e-12 { - scale_pattern_geometry(&mut h.pattern, v / old); + h.scale_pattern_about_origin(v / old); } h.pattern_scale = v; } @@ -876,10 +858,8 @@ impl Grippable for Hatch { GripApply::Absolute(point) => (point.x - gx, point.y - gy), GripApply::Translate(delta) => (delta.x, delta.y), }; - for line in self.pattern.lines.iter_mut() { - line.base_point.x += dx; - line.base_point.y += dy; - } + let origin = self.pattern_origin(); + self.set_pattern_origin(acadrust::types::Vector2::new(origin.x + dx, origin.y + dy)); return; } id += 1; @@ -1005,11 +985,11 @@ impl Grippable for Hatch { fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) { use crate::scene::model::object::GripMenuAction as A; if grip_id == 0 && matches!(action, A::OriginPoint) { - if let (Some((gx, gy)), Some((ox, oy))) = ( + if let (Some((gx, gy)), Some((_ox, _oy))) = ( boundary_centroid(self), self.pattern.lines.first().map(|line| (line.base_point.x, line.base_point.y)), ) { - translate_pattern_geometry(&mut self.pattern, gx - ox, gy - oy); + self.set_pattern_origin(acadrust::types::Vector2::new(gx, gy)); } } } @@ -1038,14 +1018,14 @@ impl Grippable for Hatch { A::HatchAngle => { let angle = value.to_radians(); let delta = angle - self.pattern_angle; - rotate_pattern_geometry(&mut self.pattern, delta); + self.rotate_pattern_about_origin(delta); self.pattern_angle = angle; } A::HatchScale => { if value > 0.0 { if self.pattern_scale > 1e-12 { let factor = value / self.pattern_scale; - scale_pattern_geometry(&mut self.pattern, factor); + self.scale_pattern_about_origin(factor); } self.pattern_scale = value; } diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 10b72da48..5dc8161e1 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -927,6 +927,7 @@ fn emit_wire_fills( } boundary.push(boundary[0]); let hatch = HatchModel { + pattern_origin: None, render_instance: wire.render_instance.clone(), world_origin: [0.0, 0.0], boundary: std::sync::Arc::new(boundary), diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 8fa7c244a..3ce2a4c1a 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -387,6 +387,7 @@ impl HatchCommand { } } return HatchModel { + pattern_origin: source.pattern_origin, render_instance: None, boundary: std::sync::Arc::new(rel), pattern, @@ -445,6 +446,7 @@ impl HatchCommand { } } HatchModel { + pattern_origin: Some(self.default_origin), render_instance: None, boundary: std::sync::Arc::new(rel), pattern, @@ -969,6 +971,7 @@ impl GradientCommand { 1.0e-6, ); HatchModel { + pattern_origin: None, render_instance: None, boundary: std::sync::Arc::new(rel), pattern: HatchPattern::Gradient { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2943e7458..4636b9390 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1674,6 +1674,7 @@ impl Scene { let fill_plane_boundary = Some(Arc::new(fill_plane.1)); let fill_plane = Some(fill_plane.0); models.push(HatchModel { + pattern_origin: None, render_instance, boundary: Arc::new(boundary), boundary_wcs: None, @@ -2107,6 +2108,7 @@ impl Scene { let storage = crate::entities::curve::ocs_plane(dxf.normal, dxf.elevation); Some(HatchModel { + pattern_origin: Some([dxf.pattern_origin().x, dxf.pattern_origin().y]), render_instance: None, boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, @@ -2429,6 +2431,7 @@ impl Scene { }) .collect(); HatchModel { + pattern_origin: None, render_instance: None, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, @@ -2663,6 +2666,9 @@ impl Scene { // carries `world_origin: [0, 0]`, which after the world_offset removal // leaves the fill mis-placed and effectively invisible until a later // edit rebuilds it from the DXF — so keep the seed, don't overwrite it. + if let Some(origin) = model.pattern_origin { + dxf.record_pattern_origin(Vector2::new(origin[0], origin[1])); + } let mut entity = EntityType::Hatch(dxf); if let Some(layer) = layer { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 83771658a..f1457f85f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3794,6 +3794,7 @@ impl Scene { let ((x0, y0), (x1, y1)) = self.paper_limits()?; let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); Some(HatchModel { + pattern_origin: None, render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 1ae480b4d..8165bcdbd 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -208,6 +208,8 @@ pub struct FillPlane { /// A hatched region defined by a closed polygon boundary. #[derive(Clone, Debug)] pub struct HatchModel { + /// Recorded hatch origin in its fill plane, independent of intrinsic PAT line bases. + pub pattern_origin: Option<[f64; 2]>, pub render_instance: Option, /// World XY anchor (in the same offset-relative coordinate space as /// the rest of the scene — `world_offset` already subtracted, but diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 93b91c9cf..20bd00d73 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -154,6 +154,7 @@ impl Scene { return None; } let mut model = HatchModel { + pattern_origin: None, render_instance: None, world_origin: origin, boundary: std::sync::Arc::new(boundary), diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 331a536f5..efde7319e 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2886,6 +2886,7 @@ impl Scene { } boundary.push(boundary[0]); hatches.push(HatchModel { + pattern_origin: None, render_instance: wire.render_instance.clone(), world_origin: [0.0, 0.0], boundary: Arc::new(boundary), diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 9033adb99..35b98867a 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -131,6 +131,7 @@ impl canvas::Program for HatchPatternPreview { } HatchPattern::Pattern(_) => { let model = HatchModel { + pattern_origin: None, render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(vec![ From ddf338e6f8304b93109d22c2b8262153f8feb581 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:02:56 +0300 Subject: [PATCH 12/13] Add hatch origin boundary corner choices --- src/modules/draw/draw/hatchedit.rs | 44 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index 1a104f1d4..d7f54f69c 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -31,6 +31,8 @@ pub struct HatcheditCommand { store_origin: bool, current_origin: [f64; 2], origin_plane: cadkernel::space::Plane, + origin_bounds: Option<([f64;2],[f64;2])>, + origin_bounds_unavailable: bool, style: Option, annotative: Option, annotative_current: bool, @@ -53,6 +55,8 @@ impl HatcheditCommand { origin: None, disassociate: false, store_origin: false, + origin_bounds: None, + origin_bounds_unavailable: false, current_origin: [0.0, 0.0], origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]), style: None, @@ -88,6 +92,8 @@ impl HatcheditCommand { origin: None, disassociate: false, store_origin: false, + origin_bounds: None, + origin_bounds_unavailable: false, current_origin: [0.0, 0.0], origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]), style: None, @@ -141,6 +147,15 @@ impl HatcheditCommand { if let Some(acadrust::EntityType::Hatch(hatch)) = entity { self.style_current = hatch.style; self.origin_plane = crate::entities::curve::ocs_plane(hatch.normal, hatch.elevation); + self.origin_bounds = hatch.paths.iter().flat_map(|path| &path.edges) + .map(crate::entities::hatch::edge_curve).collect::>>() + .and_then(|curves| { + // The origin command's elliptic boundary extents differ from + // geometric extrema; leave that case unavailable until its + // placement convention is represented by the kernel. + if curves.iter().any(|curve| matches!(curve, cadkernel::geom2d::Curve::Ellipse(_) | cadkernel::geom2d::Curve::Nurbs(_))) { return None; } + cadkernel::geom2d::analytic_curve_bounds(&curves) + }); } self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)}); self.current_color=current_color;self.current_transparency=current_transparency;self @@ -196,7 +211,9 @@ impl CadCommand for HatcheditCommand { } return match input { "annotative"=>if self.annotative_current {"Make hatch annotative [Yes/No] :"} else {"Make hatch annotative [Yes/No] :"}, - "origin"=>"[Use current origin/Set new origin] :", + "origin" if self.origin_bounds_unavailable=>"Boundary extents unavailable for this geometry. [Use current origin/Set new origin/Default to boundary extents] :", + "origin"=>"[Use current origin/Set new origin/Default to boundary extents] :", + "origin-extents"=>"[bottom Left/bottom Right/top rIght/top lEft/Center] :", "origin-point"=>"Select point:", "origin-store"=>"Store as default origin? [Yes/No] :", "pattern"=>"Enter a pattern name or [Solid]:", @@ -271,7 +288,8 @@ impl CadCommand for HatcheditCommand { if let Some(input) = self.input { use crate::command::CmdOption; return match input { - "origin" => vec![CmdOption::new("Use current origin", "U"), CmdOption::new("Set new origin", "S")], + "origin" => vec![CmdOption::new("Use current origin", "U"), CmdOption::new("Set new origin", "S"), CmdOption::new("Default to boundary extents", "D")], + "origin-extents" => vec![CmdOption::new("Bottom left", "L"), CmdOption::new("Bottom right", "R"), CmdOption::new("Top right", "I"), CmdOption::new("Top left", "E"), CmdOption::new("Center", "C")], "origin-store" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], "style" => vec![CmdOption::new("Ignore", "I"), CmdOption::new("Outer", "O"), CmdOption::new("Normal", "N")], "draworder" => vec![CmdOption::new("Do not change", "N"), CmdOption::new("Send to back", "B"), CmdOption::new("Bring to front", "F"), CmdOption::new("Behind boundary", "H"), CmdOption::new("In front of boundary", "D")], @@ -311,8 +329,26 @@ impl CadCommand for HatcheditCommand { "origin" => match keyword.as_str() { "U" | "USE" => { self.origin = Some((self.current_origin[0], self.current_origin[1])); return self.apply_result(self.update_operation()); } "S" | "SET" => { self.input = Some("origin-point"); } + "D" | "DEFAULT" => { + self.origin_bounds_unavailable = self.origin_bounds.is_none(); + if !self.origin_bounds_unavailable { self.input = Some("origin-extents"); } + } _ => {}, }, + "origin-extents" => { + if let Some((min,max)) = self.origin_bounds { + let origin = match keyword.as_str() { + "L" | "LEFT" => min, + "R" | "RIGHT" => [max[0],min[1]], + "I" | "TOP RIGHT" => max, + "E" | "TOP LEFT" => [min[0],max[1]], + "C" | "CENTER" => [min[0]*0.5+max[0]*0.5,min[1]*0.5+max[1]*0.5], + _ => return Some(CmdResult::NeedPoint), + }; + self.origin = Some((origin[0],origin[1])); + self.input = Some("origin-store"); + } + } "origin-point" => { let values: Option> = text.split(',').map(|s| s.trim().parse().ok()).collect(); if let Some(values) = values.filter(|v| (2..=3).contains(&v.len()) && v.iter().all(|n| n.is_finite())) { @@ -534,6 +570,10 @@ impl CadCommand for HatcheditCommand { self.input=Some("pattern");CmdResult::NeedPoint } Some("origin")=>{self.origin=Some((self.current_origin[0],self.current_origin[1]));self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, + Some("origin-extents")=>{ + if let Some((min,_)) = self.origin_bounds { self.origin=Some((min[0],min[1])); self.input=Some("origin-store"); } + CmdResult::NeedPoint + }, Some("origin-point")=>CmdResult::NeedPoint, Some("origin-store")=>self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel), Some("style")=>{self.style=Some(self.style_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)}, From d4c242c1b05e6e55f27eee5a59451d8c01bab3ee Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:04:54 +0300 Subject: [PATCH 13/13] Use full ellipse axis anchors for hatch origin placement --- src/modules/draw/draw/hatchedit.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index d7f54f69c..03ec64c6c 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -150,11 +150,26 @@ impl HatcheditCommand { self.origin_bounds = hatch.paths.iter().flat_map(|path| &path.edges) .map(crate::entities::hatch::edge_curve).collect::>>() .and_then(|curves| { - // The origin command's elliptic boundary extents differ from - // geometric extrema; leave that case unavailable until its - // placement convention is represented by the kernel. - if curves.iter().any(|curve| matches!(curve, cadkernel::geom2d::Curve::Ellipse(_) | cadkernel::geom2d::Curve::Nurbs(_))) { return None; } - cadkernel::geom2d::analytic_curve_bounds(&curves) + // Full ellipses use the four axis endpoints as origin anchors, + // not their geometric extrema. Keep the general bounds contract + // unchanged and aggregate these command-specific anchor segments. + let mut anchors = Vec::new(); + for curve in curves { + match curve { + cadkernel::geom2d::Curve::Ellipse(arc) => { + if (arc.end_parameter - arc.start_parameter).abs() < std::f64::consts::TAU { return None; } + for angle in [0.0, std::f64::consts::FRAC_PI_2] { + anchors.push(cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line { + start: arc.ellipse.point_at(angle), + end: arc.ellipse.point_at(angle + std::f64::consts::PI), + })); + } + } + cadkernel::geom2d::Curve::Nurbs(_) => return None, + other => anchors.push(other), + } + } + cadkernel::geom2d::analytic_curve_bounds(&anchors) }); } self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)});