Merge pull request #1184 from Ramox81/codex/hatchedit-command-parity

This commit is contained in:
Hakan Seven committed 2026-09-11 15:14:43 +03:00
commit 753fa17b68
24 files changed
+926 -132

No files matched your search

+19 -1
View File
@@ -196,6 +196,7 @@ pub struct HatchCommand {
pattern_override: Option<(String, HatchPattern)>,
angle_override: Option<f32>,
scale_override: Option<f32>,
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<Handle>) {
let mut segments = Vec::new();
for handle in &handles {
@@ -383,6 +387,7 @@ impl HatchCommand {
}
}
return HatchModel {
pattern_origin: source.pattern_origin,
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern,
@@ -425,11 +430,23 @@ 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 {
pattern_origin: Some(self.default_origin),
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern,
@@ -954,6 +971,7 @@ impl GradientCommand {
1.0e-6,
);
HatchModel {
pattern_origin: None,
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern: HatchPattern::Gradient {
+575 -13
View File
@@ -28,9 +28,23 @@ pub struct HatcheditCommand {
step: HatcheditStep,
origin: Option<(f64, f64)>,
disassociate: bool,
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<acadrust::entities::HatchStyleType>,
annotative: Option<bool>,
annotative_current: bool,
style_current: acadrust::entities::HatchStyleType,
input: Option<&'static str>,
source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>,
current_color: acadrust::types::Color,
current_transparency: acadrust::types::Transparency,
boundary_region: bool,
association_sources: Option<(crate::command::WorkingPlane,rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>)>,
association_paths: Vec<acadrust::entities::BoundaryPath>,
association_missed: bool,
}
impl HatcheditCommand {
@@ -39,9 +53,23 @@ impl HatcheditCommand {
step: HatcheditStep::PickHatch,
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,
annotative: None,
annotative_current: false,
style_current: acadrust::entities::HatchStyleType::Normal,
input: None,
source_appearance: None,
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,
}
}
@@ -61,9 +89,23 @@ 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,
annotative: None,
annotative_current: annotative,
style_current: acadrust::entities::HatchStyleType::Normal,
input: None,
source_appearance: None,
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,
}
}
@@ -86,9 +128,57 @@ impl HatcheditCommand {
})
}
pub fn for_association(handle:Handle,name:String,scale:f32,angle:f32,plane:crate::command::WorkingPlane,sources:rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>)->Self {
let mut command=Self::with_handle(handle,name,scale,angle,false);
command.input=Some("associate-point");command.association_sources=Some((plane,sources));command
}
fn add_association_rings(&mut self,rings:Vec<Vec<[f64;2]>>,sources:&rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>) {
let exterior=cadkernel::geom2d::ring_nesting_depths(&rings).into_iter().map(|depth|depth==0).collect::<Vec<_>>();
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 {
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::<Option<Vec<_>>>()
.and_then(|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)});
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,
@@ -102,6 +192,58 @@ 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:?}>:"),
"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 {
"annotative"=>if self.annotative_current {"Make hatch annotative [Yes/No] <Y>:"} else {"Make hatch annotative [Yes/No] <N>:"},
"origin" if self.origin_bounds_unavailable=>"Boundary extents unavailable for this geometry. [Use current origin/Set new origin/Default to boundary extents] <Use current origin>:",
"origin"=>"[Use current origin/Set new origin/Default to boundary extents] <Use current origin>:",
"origin-extents"=>"[bottom Left/bottom Right/top rIght/top lEft/Center] <bottom Left>:",
"origin-point"=>"Select point:",
"origin-store"=>"Store as default origin? [Yes/No] <N>:",
"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 [Region/Polyline] <Polyline>:",
"boundary-associate"=>"Reassociate hatch with new boundary? [Yes/No] <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();
}
match &self.step {
HatcheditStep::PickHatch => t!("HATCHEDIT Select hatch:").into_owned(),
HatcheditStep::EditOptions {
@@ -110,7 +252,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} [Style/Origin/Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] <Properties>:",
name = name,
scale = scale,
angle = angle
@@ -123,6 +265,15 @@ 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 {
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 {
if handle.is_null() {
@@ -140,25 +291,159 @@ 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==Some("associate-point") {return vec![crate::command::CmdOption::new("Select objects","S")];}
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"), 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")],
"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();
}
vec![
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("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"),
crate::command::CmdOption::new("Transparency", "T"),
crate::command::CmdOption::new("Draw order", "DR"),
crate::command::CmdOption::new("Associate", "AS"),
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"),
]
}
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 {
"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<Vec<f64>> = 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),
"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);
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);
},
"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(){"."=>Some(self.current_transparency),"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"=>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,region:self.boundary_region}),
"N"|"NO"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false,region:self.boundary_region}),
_=>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(){"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);}
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());
}
let (_handle, name, scale, angle) = match &mut self.step {
HatcheditStep::EditOptions {
handle,
@@ -236,7 +521,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);
@@ -269,18 +554,295 @@ 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("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);
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 {
// 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("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)},
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)
}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.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")=>{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,
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::hatch::{
BoundaryEdge, BoundaryPath, EllipticArcEdge, LineEdge,
};
use acadrust::entities::{Hatch, HatchStyleType};
use acadrust::types::{Color, Transparency, Vector2};
use acadrust::EntityType;
fn hatch_command() -> HatcheditCommand {
let mut hatch = Hatch::new();
hatch.common.handle = Handle::new(7);
hatch.common.layer = "Boundary".into();
hatch.common.color = Color::Index(3);
hatch.common.transparency = Transparency::T_20;
hatch.style = HatchStyleType::Outer;
let mut path = BoundaryPath::new();
for (start, end) in [
([1.0, 2.0], [5.0, 2.0]),
([5.0, 2.0], [5.0, 6.0]),
([5.0, 6.0], [1.0, 6.0]),
([1.0, 6.0], [1.0, 2.0]),
] {
path.edges.push(BoundaryEdge::Line(LineEdge {
start: Vector2::new(start[0], start[1]),
end: Vector2::new(end[0], end[1]),
}));
}
hatch.paths.push(path);
let entity = EntityType::Hatch(hatch);
HatcheditCommand::with_handle(Handle::new(7), "ANSI31".into(), 1.0, 0.0, false)
.with_appearance(Some(&entity), Color::Index(6), Transparency::T_40)
.with_origin([9.0, 8.0])
}
#[test]
fn properties_stages_validate_and_apply_pattern_values() {
let mut command = hatch_command();
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
assert!(command.prompt().contains("ANSI31"));
assert!(matches!(
command.on_text_input("missing"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
command.on_text_input("ANSI31"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
command.on_text_input("0"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
command.on_text_input("2.5"),
Some(CmdResult::NeedPoint)
));
let Some(CmdResult::HatcheditApply {
name,
scale,
angle,
operation: HatchEditOperation::Update { .. },
..
}) = command.on_text_input("30")
else {
panic!("expected a validated hatch update");
};
assert_eq!(name, "ANSI31");
assert_eq!(scale, 2.5);
assert_eq!(angle, 30.0);
}
#[test]
fn appearance_stages_keep_exact_color_layer_and_transparency_choices() {
let mut color = hatch_command();
assert!(matches!(
color.on_text_input("CO"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
color.on_text_input("red"),
Some(CmdResult::HatcheditApply {
operation: HatchEditOperation::Appearance {
color: Some(Color::Index(1)),
..
},
..
})
));
let mut layer = hatch_command();
assert!(matches!(
layer.on_text_input("LA"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
layer.on_text_input("CaseSensitiveLayer"),
Some(CmdResult::HatcheditApply {
operation: HatchEditOperation::Appearance {
layer: Some(value),
..
},
..
}) if value == "CaseSensitiveLayer"
));
let mut transparency = hatch_command();
assert!(matches!(
transparency.on_text_input("T"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
transparency.on_text_input("91"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
transparency.on_text_input("25"),
Some(CmdResult::HatcheditApply {
operation: HatchEditOperation::Appearance {
transparency: Some(Transparency::Explicit(64)),
..
},
..
})
));
}
#[test]
fn origin_choices_use_drawing_default_or_analytic_boundary_extents() {
let mut drawing_default = hatch_command();
assert!(matches!(
drawing_default.on_text_input("O"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
drawing_default.on_text_input("U"),
Some(CmdResult::HatcheditApply {
operation: HatchEditOperation::Update {
origin: Some((9.0, 8.0)),
store_origin: false,
..
},
..
})
));
let mut extents = hatch_command();
assert!(matches!(
extents.on_text_input("O"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
extents.on_text_input("D"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(extents.on_enter(), CmdResult::NeedPoint));
assert!(matches!(
extents.on_text_input("Y"),
Some(CmdResult::HatcheditApply {
operation: HatchEditOperation::Update {
origin: Some((1.0, 2.0)),
store_origin: true,
..
},
..
})
));
}
#[test]
fn style_enter_keeps_the_selected_hatch_default() {
let mut command = hatch_command();
assert!(matches!(
command.on_text_input("S"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
command.on_enter(),
CmdResult::HatcheditApply {
operation: HatchEditOperation::Update {
style: Some(HatchStyleType::Outer),
..
},
..
}
));
}
#[test]
fn full_ellipse_origin_uses_axis_anchor_bounds() {
let mut hatch = Hatch::new();
let mut path = BoundaryPath::new();
path.edges.push(BoundaryEdge::EllipticArc(EllipticArcEdge {
center: Vector2::new(10.0, 20.0),
major_axis_endpoint: Vector2::new(3.0, 4.0),
minor_axis_ratio: 0.4,
start_angle: 0.0,
end_angle: std::f64::consts::TAU,
counter_clockwise: true,
}));
hatch.paths.push(path);
let entity = EntityType::Hatch(hatch);
let mut command = HatcheditCommand::with_handle(
Handle::new(7),
"ANSI31".into(),
1.0,
0.0,
false,
)
.with_appearance(Some(&entity), Color::ByLayer, Transparency::ByLayer);
assert!(matches!(
command.on_text_input("O"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(
command.on_text_input("D"),
Some(CmdResult::NeedPoint)
));
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
assert!(matches!(
command.on_enter(),
CmdResult::HatcheditApply {
operation: HatchEditOperation::Update {
origin: Some((7.0, 16.0)),
..
},
..
}
));
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["HATCHEDIT"] }); // HatcheditCommand