Complete hatch creation and editing workflow

This commit is contained in:
ramox81 committed 2026-08-21 12:57:19 +03:00
1 parent 573ed3245e
commit f83037271c
18 files changed
+1248 -338

No files matched your search

+293 -36
View File
@@ -206,14 +206,23 @@ fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64;
pub struct HatchCommand {
outlines: Vec<Vec<[f64; 2]>>,
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
point_regions: Vec<Vec<Vec<[f64; 2]>>>,
object_regions: Vec<Vec<Vec<[f64; 2]>>>,
selected_objects: Vec<Handle>,
mode: HatchMode,
manual_pts: Vec<DVec3>,
manual_bulges: Vec<f64>,
manual_arc_mode: bool,
manual_arc_midpoint: Option<DVec3>,
missed: bool,
retain_boundaries: bool,
pattern_override: Option<(String, HatchPattern)>,
angle_override: Option<f32>,
scale_override: Option<f32>,
associative: bool,
separate_hatches: bool,
island_style: acadrust::entities::HatchStyleType,
inherited: Option<(
HatchModel,
acadrust::types::Color,
@@ -224,7 +233,7 @@ pub struct HatchCommand {
impl HatchCommand {
pub fn new(
outlines: Vec<Vec<[f64; 2]>>,
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
selected_objects: Vec<Handle>,
inherited: Option<(
HatchModel,
@@ -249,8 +258,20 @@ impl HatchCommand {
HatchMode::PickInside
},
manual_pts: vec![],
manual_bulges: vec![],
manual_arc_mode: false,
manual_arc_midpoint: None,
missed: false,
retain_boundaries: false,
pattern_override: None,
angle_override: None,
scale_override: None,
associative: true,
separate_hatches: false,
island_style: inherited
.as_ref()
.map(|(model, _, _)| model.style)
.unwrap_or(acadrust::entities::HatchStyleType::Normal),
inherited,
};
command.set_object_selection(selected_objects);
@@ -261,7 +282,7 @@ impl HatchCommand {
let mut segments = Vec::new();
for handle in &handles {
if let Some(source) = self.boundary_sources.get(handle) {
segments.extend(source.iter().copied());
segments.extend(source.segments.iter().copied());
}
}
self.object_regions = bounded_faces(&segments, Tolerance::new(1.0e-6))
@@ -287,6 +308,14 @@ impl HatchCommand {
self.point_regions.len() + self.object_regions.len()
}
fn island_style_label(&self) -> &'static str {
match self.island_style {
acadrust::entities::HatchStyleType::Normal => "Normal",
acadrust::entities::HatchStyleType::Outer => "Outer",
acadrust::entities::HatchStyleType::Ignore => "Ignore",
}
}
fn combined_rings(&self) -> Vec<Vec<[f64; 2]>> {
let mut rings = Vec::new();
for ring in self
@@ -304,23 +333,37 @@ impl HatchCommand {
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
let (rel, origin, wcs) = pack_rings(&rings);
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
let exterior: Vec<bool> = cadkernel::geom2d::ring_nesting_depths(&rings)
.into_iter()
.map(|depth| depth == 0)
.collect();
let boundary_sources = rings
let mut boundary_sources: Vec<Vec<Handle>> = rings
.iter()
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
.collect();
let mut boundary_paths = crate::scene::exact_hatch_paths(
&rings,
&exterior,
&self.boundary_sources,
1.0e-6,
);
if !self.associative {
for handles in &mut boundary_sources {
handles.clear();
}
for path in &mut boundary_paths {
path.boundary_handles.clear();
}
}
if let Some((source, _, _)) = &self.inherited {
let mut pattern = source.pattern.clone();
let (name, mut pattern) = self
.pattern_override
.clone()
.unwrap_or_else(|| (source.name.clone(), source.pattern.clone()));
let angle = self.angle_override.unwrap_or(source.angle_offset);
let scale = self.scale_override.unwrap_or(source.scale).max(1.0e-6);
if let HatchPattern::Pattern(families) = &mut pattern {
let scale = if source.scale.abs() > 1.0e-6 {
source.scale
} else {
1.0
};
let (sin, cos) = source.angle_offset.sin_cos();
let (sin, cos) = angle.sin_cos();
for family in families {
let base_x = source.world_origin[0]
+ (family.x0 as f64 * cos as f64
@@ -340,27 +383,29 @@ impl HatchCommand {
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern,
name: source.name.clone(),
name,
color: source.color,
aci: source.aci,
line_weight_px: source.line_weight_px,
angle_offset: source.angle_offset,
scale: source.scale,
angle_offset: angle,
scale,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: Some(std::sync::Arc::new(exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
style: self.island_style,
draw_depth: source.draw_depth,
};
}
// Default: ANSI31 from catalog; fallback to a single 45° family.
let pat_name = "ANSI31";
let families = crate::scene::model::hatch_patterns::find(pat_name)
let default_pattern = crate::scene::model::hatch_patterns::find(pat_name)
.and_then(|e| {
if let HatchPattern::Pattern(f) = &e.gpu {
Some(f.clone())
Some(HatchPattern::Pattern(f.clone()))
} else {
None
}
@@ -368,34 +413,97 @@ impl HatchCommand {
.unwrap_or_else(|| {
// 45° lines, perpendicular spacing ≈ 5 world units.
let dy = 5.0_f32 / (45.0_f32.to_radians().cos());
vec![PatFamily {
HatchPattern::Pattern(vec![PatFamily {
angle_deg: 45.0,
x0: 0.0,
y0: 0.0,
dx: 0.0,
dy,
dashes: vec![],
}]
}])
});
let (name, pattern) = self
.pattern_override
.clone()
.unwrap_or_else(|| (pat_name.to_string(), default_pattern));
HatchModel {
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern: HatchPattern::Pattern(families),
name: pat_name.into(),
pattern,
name,
color: [0.75, 0.75, 0.75, 0.85],
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
scale: 1.0,
angle_offset: self.angle_override.unwrap_or(0.0),
scale: self.scale_override.unwrap_or(1.0).max(1.0e-6),
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: Some(std::sync::Arc::new(exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
style: self.island_style,
draw_depth: 0.0,
}
}
fn manual_boundary_path(&self) -> Option<acadrust::entities::BoundaryPath> {
use acadrust::entities::{BoundaryEdge, BoundaryPath, PolylineEdge};
use acadrust::types::Vector3;
if self.manual_pts.len() < 3 {
return None;
}
let vertices = self
.manual_pts
.iter()
.enumerate()
.map(|(index, point)| {
Vector3::new(
point.x,
point.y,
self.manual_bulges.get(index).copied().unwrap_or(0.0),
)
})
.collect();
let mut path = BoundaryPath::new();
path.add_edge(BoundaryEdge::Polyline(PolylineEdge {
vertices,
is_closed: true,
}));
Some(path)
}
}
fn arc_bulge(start: DVec3, middle: DVec3, end: DVec3) -> Option<f64> {
let d = 2.0
* (start.x * (middle.y - end.y)
+ middle.x * (end.y - start.y)
+ end.x * (start.y - middle.y));
if d.abs() <= 1.0e-12 {
return None;
}
let s2 = start.x * start.x + start.y * start.y;
let m2 = middle.x * middle.x + middle.y * middle.y;
let e2 = end.x * end.x + end.y * end.y;
let center_x = (s2 * (middle.y - end.y)
+ m2 * (end.y - start.y)
+ e2 * (start.y - middle.y))
/ d;
let center_y = (s2 * (end.x - middle.x)
+ m2 * (start.x - end.x)
+ e2 * (middle.x - start.x))
/ d;
let angle = |point: DVec3| (point.y - center_y).atan2(point.x - center_x);
let first = angle(start);
let through = (angle(middle) - first).rem_euclid(std::f64::consts::TAU);
let ccw = (angle(end) - first).rem_euclid(std::f64::consts::TAU);
let sweep = if through <= ccw + 1.0e-12 {
ccw
} else {
ccw - std::f64::consts::TAU
};
Some((sweep * 0.25).tan())
}
impl CadCommand for HatchCommand {
@@ -412,7 +520,7 @@ impl CadCommand for HatchCommand {
String::new()
};
t!(
"HATCH Pick internal point (%{count} regions selected, Enter to apply):%{miss}",
"HATCH Pick internal point (%{count} regions selected; P <pattern> / A <angle> / L <scale>; Enter to apply):%{miss}",
count = self.region_count(),
miss = miss
)
@@ -425,7 +533,7 @@ impl CadCommand for HatchCommand {
String::new()
};
t!(
"HATCH Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}",
"HATCH Select boundary objects (%{objects} objects, %{count} regions; P <pattern> / A <angle> / L <scale>; Enter to apply):%{miss}",
objects = self.selected_objects.len(),
count = self.region_count(),
miss = miss
@@ -458,6 +566,18 @@ impl CadCommand for HatchCommand {
},
"B",
),
CmdOption::new(
if self.associative { "Associative: on" } else { "Associative: off" },
"N",
),
CmdOption::new(
if self.separate_hatches { "Separate hatches: on" } else { "Separate hatches: off" },
"D",
),
CmdOption::new(
&format!("Island style: {}", self.island_style_label()),
"Y",
),
];
if self.region_count() > 0 {
options.push(CmdOption::enter(t!("Accept").as_ref()));
@@ -476,6 +596,18 @@ impl CadCommand for HatchCommand {
},
"B",
),
CmdOption::new(
if self.associative { "Associative: on" } else { "Associative: off" },
"N",
),
CmdOption::new(
if self.separate_hatches { "Separate hatches: on" } else { "Separate hatches: off" },
"D",
),
CmdOption::new(
&format!("Island style: {}", self.island_style_label()),
"Y",
),
];
if self.region_count() > 0 {
options.push(CmdOption::enter(t!("Accept").as_ref()));
@@ -483,12 +615,17 @@ impl CadCommand for HatchCommand {
options
}
HatchMode::Manual => {
// Enter accepts the boundary once at least 3 points are picked.
let mut options = vec![
CmdOption::new(
if self.manual_arc_mode { "Line" } else { "Arc" },
if self.manual_arc_mode { "L" } else { "A" },
),
];
if self.manual_pts.len() >= 3 {
vec![CmdOption::enter(t!("Accept").as_ref())]
} else {
vec![]
options.push(CmdOption::new("Close", "C"));
options.push(CmdOption::enter(t!("Accept").as_ref()));
}
options
}
}
}
@@ -511,8 +648,19 @@ impl CadCommand for HatchCommand {
}
HatchMode::SelectObjects => CmdResult::NeedPoint,
HatchMode::Manual => {
// Keep the typed/snapped point exact (issue #311).
self.manual_pts.push(pt);
if self.manual_pts.is_empty() || !self.manual_arc_mode {
if !self.manual_pts.is_empty() {
self.manual_bulges.push(0.0);
}
self.manual_pts.push(pt);
} else if let Some(middle) = self.manual_arc_midpoint.take() {
let start = *self.manual_pts.last().unwrap();
self.manual_bulges
.push(arc_bulge(start, middle, pt).unwrap_or(0.0));
self.manual_pts.push(pt);
} else {
self.manual_arc_midpoint = Some(pt);
}
CmdResult::NeedPoint
}
}
@@ -526,6 +674,35 @@ impl CadCommand for HatchCommand {
let rings = self.combined_rings();
if rings.is_empty() {
CmdResult::Cancel
} else if matches!(self.mode, HatchMode::Manual) {
let mut hatch = self.make_hatch(rings);
if let Some(path) = self.manual_boundary_path() {
hatch.boundary_paths = Some(std::sync::Arc::new(vec![path]));
}
if let Some((_, color, transparency)) = &self.inherited {
CmdResult::CommitStyledHatch {
hatch,
color: color.clone(),
transparency: *transparency,
}
} else {
CmdResult::CommitHatch(hatch)
}
} else if self.separate_hatches && !self.retain_boundaries {
let hatches = self
.point_regions
.iter()
.chain(self.object_regions.iter())
.cloned()
.map(|region| self.make_hatch(region))
.collect();
CmdResult::CommitHatches {
hatches,
entity_style: self
.inherited
.as_ref()
.map(|(_, color, transparency)| (color.clone(), *transparency)),
}
} else if self.retain_boundaries {
CmdResult::CommitHatchWithBoundaries {
hatch: self.make_hatch(rings.clone()),
@@ -564,6 +741,15 @@ impl CadCommand for HatchCommand {
}
fn on_undo_step(&mut self) -> Option<CmdResult> {
if matches!(self.mode, HatchMode::Manual) {
if self.manual_arc_midpoint.take().is_some() {
return Some(CmdResult::NeedPoint);
}
if self.manual_pts.pop().is_some() {
self.manual_bulges.pop();
return Some(CmdResult::NeedPoint);
}
}
if matches!(self.mode, HatchMode::PickInside) && self.point_regions.pop().is_some() {
Some(CmdResult::NeedPoint)
} else {
@@ -590,11 +776,52 @@ impl CadCommand for HatchCommand {
}
fn wants_text_input(&self) -> bool {
!matches!(self.mode, HatchMode::Manual)
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match text.trim().to_ascii_uppercase().as_str() {
let input = text.trim();
let upper = input.to_ascii_uppercase();
if matches!(self.mode, HatchMode::Manual) {
return match upper.as_str() {
"A" | "ARC" => {
self.manual_arc_mode = true;
self.manual_arc_midpoint = None;
Some(CmdResult::NeedPoint)
}
"L" | "LINE" => {
self.manual_arc_mode = false;
self.manual_arc_midpoint = None;
Some(CmdResult::NeedPoint)
}
"C" | "CLOSE" if self.manual_pts.len() >= 3 => Some(self.on_enter()),
_ => None,
};
}
if let Some(rest) = upper.strip_prefix('P') {
let name = rest.trim();
if !name.is_empty() {
if let Some(entry) = crate::scene::model::hatch_patterns::find(name) {
self.pattern_override = Some((entry.name.clone(), entry.gpu.clone()));
}
}
return Some(CmdResult::NeedPoint);
}
if let Some(rest) = upper.strip_prefix('A') {
if let Ok(value) = rest.trim().replace(',', ".").parse::<f32>() {
self.angle_override = Some(value.to_radians());
}
return Some(CmdResult::NeedPoint);
}
if let Some(rest) = upper.strip_prefix('L') {
if let Ok(value) = rest.trim().replace(',', ".").parse::<f32>() {
if value > 0.0 {
self.scale_override = Some(value);
}
}
return Some(CmdResult::NeedPoint);
}
match upper.as_str() {
"O" | "OBJECT" | "OBJECTS" => {
self.mode = HatchMode::SelectObjects;
self.missed = false;
@@ -614,6 +841,28 @@ impl CadCommand for HatchCommand {
self.retain_boundaries = !self.retain_boundaries;
Some(CmdResult::NeedPoint)
}
"N" | "ASSOCIATIVE" => {
self.associative = !self.associative;
Some(CmdResult::NeedPoint)
}
"D" | "SEPARATE" => {
self.separate_hatches = !self.separate_hatches;
Some(CmdResult::NeedPoint)
}
"Y" | "ISLAND" => {
self.island_style = match self.island_style {
acadrust::entities::HatchStyleType::Normal => {
acadrust::entities::HatchStyleType::Outer
}
acadrust::entities::HatchStyleType::Outer => {
acadrust::entities::HatchStyleType::Ignore
}
acadrust::entities::HatchStyleType::Ignore => {
acadrust::entities::HatchStyleType::Normal
}
};
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
@@ -649,7 +898,7 @@ impl CadCommand for HatchCommand {
pub struct GradientCommand {
outlines: Vec<Vec<[f64; 2]>>,
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
mode: Mode,
manual_pts: Vec<DVec3>,
missed: bool,
@@ -662,7 +911,7 @@ pub struct GradientCommand {
impl GradientCommand {
pub fn new(
outlines: Vec<Vec<[f64; 2]>>,
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
) -> Self {
Self {
outlines,
@@ -677,7 +926,7 @@ impl GradientCommand {
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
let (rel, origin, wcs) = pack_rings(&rings);
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
let exterior: Vec<bool> = cadkernel::geom2d::ring_nesting_depths(&rings)
.into_iter()
.map(|depth| depth == 0)
.collect();
@@ -685,6 +934,12 @@ impl GradientCommand {
.iter()
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
.collect();
let boundary_paths = crate::scene::exact_hatch_paths(
&rings,
&exterior,
&self.boundary_sources,
1.0e-6,
);
HatchModel {
render_instance: None,
boundary: std::sync::Arc::new(rel),
@@ -707,6 +962,8 @@ impl GradientCommand {
fill_plane_boundary: None,
boundary_exterior: Some(std::sync::Arc::new(exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
style: acadrust::entities::HatchStyleType::Normal,
draw_depth: 0.0,
}
}
+129 -26
View File
@@ -12,7 +12,7 @@ use acadrust::Handle;
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult};
use crate::command::{CadCommand, CmdResult, HatchEditOperation};
enum HatcheditStep {
PickHatch,
@@ -26,16 +26,32 @@ enum HatcheditStep {
pub struct HatcheditCommand {
step: HatcheditStep,
origin: Option<(f64, f64)>,
disassociate: bool,
style: Option<acadrust::entities::HatchStyleType>,
annotative: Option<bool>,
annotative_current: bool,
}
impl HatcheditCommand {
pub fn new() -> Self {
Self {
step: HatcheditStep::PickHatch,
origin: None,
disassociate: false,
style: None,
annotative: None,
annotative_current: false,
}
}
pub fn with_handle(handle: Handle, name: String, scale: f32, angle: f32) -> Self {
pub fn with_handle(
handle: Handle,
name: String,
scale: f32,
angle: f32,
annotative: bool,
) -> Self {
Self {
step: HatcheditStep::EditOptions {
handle,
@@ -43,6 +59,39 @@ impl HatcheditCommand {
scale,
angle,
},
origin: None,
disassociate: false,
style: None,
annotative: None,
annotative_current: annotative,
}
}
fn apply_result(&self, operation: HatchEditOperation) -> Option<CmdResult> {
let HatcheditStep::EditOptions {
handle,
name,
scale,
angle,
} = &self.step
else {
return None;
};
Some(CmdResult::HatcheditApply {
handle: *handle,
name: name.clone(),
scale: *scale,
angle: *angle,
operation,
})
}
fn update_operation(&self) -> HatchEditOperation {
HatchEditOperation::Update {
origin: self.origin,
disassociate: self.disassociate,
style: self.style,
annotative: self.annotative,
}
}
}
@@ -61,7 +110,7 @@ impl CadCommand for HatcheditCommand {
let scale = format!("{scale:.4}");
let angle = format!("{angle:.1}");
t!(
"HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [P <pat> / S <scale> / A <angle> | Enter to apply]:",
"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]:",
name = name,
scale = scale,
angle = angle
@@ -94,8 +143,23 @@ impl CadCommand for HatcheditCommand {
matches!(self.step, HatcheditStep::EditOptions { .. })
}
fn options(&self) -> Vec<crate::command::CmdOption> {
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"),
]
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let (handle, name, scale, angle) = match &mut self.step {
let (_handle, name, scale, angle) = match &mut self.step {
HatcheditStep::EditOptions {
handle,
name,
@@ -108,13 +172,7 @@ impl CadCommand for HatcheditCommand {
let text = text.trim().to_uppercase();
if text.is_empty() {
// Apply and exit
return Some(CmdResult::HatcheditApply {
handle,
name: name.clone(),
scale: *scale,
angle: *angle,
});
return self.apply_result(self.update_operation());
}
// Parse option: P/S/A followed by value
@@ -140,6 +198,65 @@ impl CadCommand for HatcheditCommand {
return Some(CmdResult::NeedPoint);
}
if let Some(rest) = text.strip_prefix('O') {
let values: Vec<_> = rest
.trim()
.split([',', ';', ' '])
.filter(|part| !part.is_empty())
.filter_map(|part| part.replace(',', ".").parse::<f64>().ok())
.collect();
if values.len() >= 2 {
self.origin = Some((values[0], values[1]));
}
return Some(CmdResult::NeedPoint);
}
if text == "D" || text == "DISASSOCIATE" {
self.disassociate = true;
return Some(CmdResult::NeedPoint);
}
if let Some(rest) = text.strip_prefix('Y') {
self.style = match rest.trim() {
"NORMAL" | "N" => Some(acadrust::entities::HatchStyleType::Normal),
"OUTER" | "O" => Some(acadrust::entities::HatchStyleType::Outer),
"IGNORE" | "I" => Some(acadrust::entities::HatchStyleType::Ignore),
_ => self.style,
};
return Some(CmdResult::NeedPoint);
}
if text == "N" || text == "ANNOTATIVE" {
self.annotative = Some(!self.annotative.unwrap_or(self.annotative_current));
return Some(CmdResult::NeedPoint);
}
if text == "R" || text == "RECREATE" {
return self.apply_result(HatchEditOperation::RecreateBoundary);
}
if text == "E" || text == "SEPARATE" {
return self.apply_result(HatchEditOperation::Separate);
}
if text == "F" || text == "FRONT" {
return self.apply_result(HatchEditOperation::DrawOrderFront);
}
if text == "B" || text == "BACK" {
return self.apply_result(HatchEditOperation::DrawOrderBack);
}
let parse_handles = |source: &str| {
source
.split([',', ';', ' '])
.filter(|part| !part.is_empty())
.filter_map(|part| {
u64::from_str_radix(part.trim_start_matches("0X"), 16)
.ok()
.map(Handle::new)
})
.collect::<Vec<_>>()
};
if let Some(rest) = text.strip_prefix('+') {
return self.apply_result(HatchEditOperation::AddBoundaries(parse_handles(rest)));
}
if let Some(rest) = text.strip_prefix('-') {
return self.apply_result(HatchEditOperation::RemoveBoundaries(parse_handles(rest)));
}
// Unrecognized — stay and re-prompt
Some(CmdResult::NeedPoint)
}
@@ -149,21 +266,7 @@ impl CadCommand for HatcheditCommand {
}
fn on_enter(&mut self) -> CmdResult {
// Enter without text → apply current settings
let (handle, name, scale, angle) = match &self.step {
HatcheditStep::EditOptions {
handle,
name,
scale,
angle,
} => (*handle, name.clone(), *scale, *angle),
_ => return CmdResult::Cancel,
};
CmdResult::HatcheditApply {
handle,
name,
scale,
angle,
}
self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel