forked from 1b-agent/opencadstudio-upstream
Merge pull request #1184 from Ramox81/codex/hatchedit-command-parity
This commit is contained in:
commit
753fa17b68
24 files changed
+926
-132
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user