diff --git a/Cargo.toml b/Cargo.toml index a003cb3c3..5538dbb5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "H7CAD" -version = "0.1.0" +version = "0.1.1" edition = "2021" build = "build.rs" diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 8de0638de..9ed62c502 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -290,6 +290,7 @@ impl H7CAD { if self.tabs[i].active_cmd.is_some() { self.focus_cmd_input() } else { + self.ribbon.deactivate_tool(); self.blur_cmd_input() } } diff --git a/src/app/commands.rs b/src/app/commands.rs index 3ea63dc3e..5ce662694 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -12,6 +12,9 @@ impl H7CAD { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; } + // Reset the last committed point so the first click of the new command + // is not constrained by ortho/polar relative to a previous command's endpoint. + self.last_point = None; if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") { let path = PathBuf::from(path_str); diff --git a/src/modules/home/modify/fillet.rs b/src/modules/home/modify/fillet.rs index b0454155b..445e45e0c 100644 --- a/src/modules/home/modify/fillet.rs +++ b/src/modules/home/modify/fillet.rs @@ -263,7 +263,7 @@ fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> { ] } -fn arc_pts(cx: f64, cy: f64, r: f64, a0_deg: f64, a1_deg: f64, y: f64) -> Vec<[f32; 3]> { +fn arc_pts(cx: f64, cy: f64, r: f64, a0_deg: f64, a1_deg: f64, z: f64) -> Vec<[f32; 3]> { use std::f64::consts::TAU; let fn_norm = |a: f64| -> f64 { ((a % TAU) + TAU) % TAU }; let a0 = a0_deg.to_radians(); @@ -282,8 +282,8 @@ fn arc_pts(cx: f64, cy: f64, r: f64, a0_deg: f64, a1_deg: f64, y: f64) -> Vec<[f let ang = fn_norm(a0) + span * (i as f64 / steps as f64); [ (cx + r * ang.cos()) as f32, - y as f32, (cy + r * ang.sin()) as f32, + z as f32, ] }) .collect() @@ -298,7 +298,7 @@ fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> { a.radius, a.start_angle, a.end_angle, - a.center.y, + a.center.z, ), _ => vec![], } @@ -364,6 +364,7 @@ fn compute_chamfer( enum FilletStep { First, + WaitingForRadius, Second { h1: Handle, l1: LineEnt, @@ -395,7 +396,11 @@ impl CadCommand for FilletCommand { fn prompt(&self) -> String { match &self.step { FilletStep::First => format!( - "FILLET Select first line [R={:.4} | type R to change]:", + "FILLET Select first line [R={:.4} | type R to change]:", + self.radius + ), + FilletStep::WaitingForRadius => format!( + "FILLET Enter fillet radius <{:.4}>:", self.radius ), FilletStep::Second { .. } => { @@ -405,27 +410,58 @@ impl CadCommand for FilletCommand { } fn wants_text_input(&self) -> bool { - matches!(self.step, FilletStep::First) + matches!(self.step, FilletStep::WaitingForRadius) } fn on_text_input(&mut self, text: &str) -> Option { - let t = text.trim(); - let body = if t.to_uppercase().starts_with('R') { - t[1..].trim() - } else { - t - }; - if let Ok(v) = body.replace(',', ".").parse::() { - if v >= 0.0 { - self.radius = v; - defaults::set_fillet_radius(v as f32); + match self.step { + FilletStep::WaitingForRadius => { + let t = text.trim(); + if t.is_empty() { + // Keep current radius, return to First + self.step = FilletStep::First; + return Some(CmdResult::NeedPoint); + } + if let Ok(v) = t.replace(',', ".").parse::() { + if v >= 0.0 { + self.radius = v; + defaults::set_fillet_radius(v as f32); + } + self.step = FilletStep::First; + return Some(CmdResult::NeedPoint); + } + // Invalid — stay and re-prompt + Some(CmdResult::NeedPoint) + } + FilletStep::First | FilletStep::Second { .. } => { + let t = text.trim(); + let upper = t.to_uppercase(); + // "R" alone → enter sub-step to collect radius + if upper == "R" { + self.step = FilletStep::WaitingForRadius; + return Some(CmdResult::NeedPoint); + } + // "R 5.0" inline shorthand + if upper.starts_with('R') { + let body = t[1..].trim(); + if let Ok(v) = body.replace(',', ".").parse::() { + if v >= 0.0 { + self.radius = v; + defaults::set_fillet_radius(v as f32); + } + return Some(CmdResult::NeedPoint); + } + // "R" + invalid body → enter sub-step + self.step = FilletStep::WaitingForRadius; + return Some(CmdResult::NeedPoint); + } + None } } - None } fn needs_entity_pick(&self) -> bool { - true + !matches!(self.step, FilletStep::WaitingForRadius) } fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult { @@ -435,6 +471,7 @@ impl CadCommand for FilletCommand { let click = [pt.x as f64, pt.y as f64]; match &self.step { + FilletStep::WaitingForRadius => return CmdResult::NeedPoint, FilletStep::First => { // Must be a line let l1 = self @@ -510,6 +547,7 @@ impl CadCommand for FilletCommand { let click = [pt.x as f64, pt.y as f64]; match &self.step { + FilletStep::WaitingForRadius => vec![], FilletStep::First => { // Highlight hovered line in cyan let pts = self @@ -602,6 +640,8 @@ impl CadCommand for FilletCommand { enum ChamferStep { First, + WaitingForDist1, + WaitingForDist2, Second { h1: Handle, l1: LineEnt, @@ -635,9 +675,17 @@ impl CadCommand for ChamferCommand { fn prompt(&self) -> String { match &self.step { ChamferStep::First => format!( - "CHAMFER Select first line [D1={:.4} D2={:.4} | type D ]:", + "CHAMFER Select first line [D1={:.4} D2={:.4} | type D to change]:", self.dist1, self.dist2 ), + ChamferStep::WaitingForDist1 => format!( + "CHAMFER Enter first chamfer distance <{:.4}>:", + self.dist1 + ), + ChamferStep::WaitingForDist2 => format!( + "CHAMFER Enter second chamfer distance <{:.4}>:", + self.dist2 + ), ChamferStep::Second { .. } => format!( "CHAMFER Select second line [D1={:.4} D2={:.4}]:", self.dist1, self.dist2 @@ -646,36 +694,89 @@ impl CadCommand for ChamferCommand { } fn wants_text_input(&self) -> bool { - matches!(self.step, ChamferStep::First) + matches!( + self.step, + ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 + ) } fn on_text_input(&mut self, text: &str) -> Option { - let t = text.trim(); - let body = if t.to_uppercase().starts_with('D') { - t[1..].trim() - } else { - t - }; - let parts: Vec = body - .split_whitespace() - .filter_map(|s| s.replace(',', ".").parse::().ok()) - .collect(); - if let Some(&v) = parts.first() { - self.dist1 = v.max(0.0); - defaults::set_chamfer_dist1(self.dist1 as f32); + match self.step { + ChamferStep::WaitingForDist1 => { + let t = text.trim(); + if t.is_empty() { + // Keep current dist1, move on to dist2 + self.step = ChamferStep::WaitingForDist2; + return Some(CmdResult::NeedPoint); + } + if let Ok(v) = t.replace(',', ".").parse::() { + self.dist1 = v.max(0.0); + defaults::set_chamfer_dist1(self.dist1 as f32); + self.step = ChamferStep::WaitingForDist2; + return Some(CmdResult::NeedPoint); + } + // Invalid — stay and re-prompt + Some(CmdResult::NeedPoint) + } + ChamferStep::WaitingForDist2 => { + let t = text.trim(); + if t.is_empty() { + // Keep current dist2, return to First + self.step = ChamferStep::First; + return Some(CmdResult::NeedPoint); + } + if let Ok(v) = t.replace(',', ".").parse::() { + self.dist2 = v.max(0.0); + defaults::set_chamfer_dist2(self.dist2 as f32); + self.step = ChamferStep::First; + return Some(CmdResult::NeedPoint); + } + // Invalid — stay and re-prompt + Some(CmdResult::NeedPoint) + } + ChamferStep::First | ChamferStep::Second { .. } => { + let t = text.trim(); + let upper = t.to_uppercase(); + // "D" alone → enter sub-step to collect distances + if upper == "D" { + self.step = ChamferStep::WaitingForDist1; + return Some(CmdResult::NeedPoint); + } + // "D 5.0" or "D 5.0 3.0" inline shorthand + if upper.starts_with('D') { + let body = t[1..].trim(); + let parts: Vec = body + .split_whitespace() + .filter_map(|s| s.replace(',', ".").parse::().ok()) + .collect(); + if !parts.is_empty() { + if let Some(&v) = parts.first() { + self.dist1 = v.max(0.0); + defaults::set_chamfer_dist1(self.dist1 as f32); + } + if let Some(&v) = parts.get(1) { + self.dist2 = v.max(0.0); + defaults::set_chamfer_dist2(self.dist2 as f32); + } else { + self.dist2 = self.dist1; + defaults::set_chamfer_dist2(self.dist2 as f32); + } + return Some(CmdResult::NeedPoint); + } + // "D" + invalid body → enter sub-step + self.step = ChamferStep::WaitingForDist1; + return Some(CmdResult::NeedPoint); + } + None + } } - if let Some(&v) = parts.get(1) { - self.dist2 = v.max(0.0); - defaults::set_chamfer_dist2(self.dist2 as f32); - } else if parts.len() == 1 { - self.dist2 = self.dist1; - defaults::set_chamfer_dist2(self.dist2 as f32); - } - None } fn needs_entity_pick(&self) -> bool { - true + !matches!( + self.step, + ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 + ) } fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult { @@ -685,6 +786,9 @@ impl CadCommand for ChamferCommand { let click = [pt.x as f64, pt.y as f64]; match &self.step { + ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => { + return CmdResult::NeedPoint; + } ChamferStep::First => { let l1 = self .all_entities @@ -750,6 +854,7 @@ impl CadCommand for ChamferCommand { let click = [pt.x as f64, pt.y as f64]; match &self.step { + ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => return vec![], ChamferStep::First => { let pts = self .all_entities diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 39dfaa516..51ff7c383 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -103,6 +103,9 @@ impl Ribbon { pub fn activate_tool(&mut self, id: &str) { self.active_tool = Some(id.to_string()); } + pub fn deactivate_tool(&mut self) { + self.active_tool = None; + } pub fn set_wireframe(&mut self, w: bool) { self.wireframe = w; } diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 545c6df3f..a1926a1b6 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -211,7 +211,10 @@ pub(super) fn render_small<'a>( let tip_text = format!("{}\nCommand: {}", cur_label, last); let icon_btn = button(make_icon(cur_icon, SMALL_ICON)) - .on_press(Message::Command(last.to_string())) + .on_press(Message::RibbonToolClick { + tool_id: last.to_string(), + event: ModuleEvent::Command(last.to_string()), + }) .style(move |_: &Theme, status| tool_btn_style(active, status)) .width(Length::Fixed(SMALL_W)) .height(ROW_H) @@ -332,7 +335,10 @@ pub(super) fn render_large<'a>( .align_x(iced::Center) .spacing(3), ) - .on_press(Message::Command(last.to_string())) + .on_press(Message::RibbonToolClick { + tool_id: last.to_string(), + event: ModuleEvent::Command(last.to_string()), + }) .style(move |_: &Theme, status| tool_btn_style(active, status)) .width(Length::Fixed(LARGE_W)) .height(Fill)