Add hatch editing option stages and appearance association workflows

This commit is contained in:
ramox81 committed 2026-09-11 10:22:55 +03:00
1 parent 7d0c059b30
commit c947e1ce50
4 files changed
+220 -11

No files matched your search

+65 -4
View File
@@ -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
+1 -1
View File
@@ -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 {
+9 -1
View File
@@ -13,13 +13,21 @@ use glam::DVec3;
#[derive(Clone, Debug)]
pub enum HatchEditOperation {
Appearance {
color: Option<acadrust::types::Color>,
layer: Option<String>,
transparency: Option<acadrust::types::Transparency>,
},
Update {
origin: Option<(f64, f64)>,
disassociate: bool,
style: Option<acadrust::entities::HatchStyleType>,
annotative: Option<bool>,
},
RecreateBoundary,
RecreateBoundary { associate: bool },
BeginAssociate,
AssociateBoundaries(Vec<Handle>),
DrawOrderBoundary { above: bool },
Separate,
AddBoundaries(Vec<Handle>),
RemoveBoundaries(Vec<Handle>),
+145 -5
View File
@@ -31,6 +31,10 @@ pub struct HatcheditCommand {
style: Option<acadrust::entities::HatchStyleType>,
annotative: Option<bool>,
annotative_current: bool,
input: Option<&'static str>,
boundary_selection: Vec<Handle>,
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] <ByLayer>:",
"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] <do Not change>:",
"boundary-type"=>"Enter type of boundary object [Polyline] <Polyline>:",
"boundary-associate"=>"Reassociate hatch with new boundary? [Yes/No] <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] <Properties>:",
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<Handle>)->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<crate::command::CmdOption> {
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<CmdResult> {
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::<i16>().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<Vec<u8>>=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::<u8>().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::<f32>() {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