diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 789fd97d5..5bcf660fd 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -42,6 +42,12 @@ impl OpenCADStudio { if let Some(p) = prompt { self.command_line.push_info(&p); } + // The command may have advanced to a step with a different + // dynamic-input shape (e.g. FILLET object-pick → radius entry). + // Rebuild the fields now so the matching box appears immediately + // and typed digits land in it rather than the command line, + // instead of waiting for the next cursor move to resync. + self.sync_dyn_fields(); } CmdResult::Preview(wire) => { self.tabs[i].scene.set_preview_wires(vec![wire]); diff --git a/src/app/update.rs b/src/app/update.rs index b6ec6ca15..0ce27ff3a 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1382,6 +1382,15 @@ impl OpenCADStudio { let idx = self.grip_popup.as_ref().map(|p| p.selected).unwrap_or(0); return Task::done(Message::GripMenuPick(idx)); } + // Pending typed command-line text (e.g. an option keyword like + // "R") must be submitted rather than finalising the command. + // The focused-input Enter routes through CommandSubmit, but a + // command started from the ribbon never focuses the field, so + // its Enter arrives here — forward to the same submit path. + let i = self.active_tab; + if self.tabs[i].active_cmd.is_some() && !self.command_line.input.trim().is_empty() { + return self.update(Message::CommandSubmit); + } // A typed dynamic-input value commits as a point pick // before the plain-Enter (on_enter) path runs. if let Some(task) = self.try_dyn_commit() { @@ -7327,7 +7336,7 @@ impl OpenCADStudio { /// command-state changes. The field set only changes shape when the /// command's `dyn_field()` or the presence of a base point changes; /// existing typed buffers survive an unchanged shape. - fn sync_dyn_fields(&mut self) { + pub(super) fn sync_dyn_fields(&mut self) { use super::document::{DynComponent, DynFieldEntry}; let i = self.active_tab; if !self.dyn_input || self.tabs[i].active_cmd.is_none() { @@ -7351,6 +7360,15 @@ impl OpenCADStudio { .as_ref() .map(|c| c.wants_text_input()) .unwrap_or(false); + // A step that hit-tests for an object (entity / structure pick) has no + // coordinate to enter — clicks select, they don't place a point. Show + // no coordinate box so the cursor stays clean and typed option keywords + // (e.g. FILLET's "R") reach the command line instead of an X/Y field. + let picks_object = self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.needs_entity_pick() || c.needs_structure_point_pick()) + .unwrap_or(false); let has_base = self.last_point.is_some(); // While aligned to an OTRACK ray, the point step reads a single // distance along the ray (issue #69) — show one Distance box. @@ -7366,7 +7384,7 @@ impl OpenCADStudio { // a name / a keyword (which may itself contain digits) from the // command line — show no scalar box, so digits reach the command // line instead of being captured into a dyn buffer. - crate::command::DynField::Point if wants_text => vec![], + crate::command::DynField::Point if wants_text || picks_object => vec![], crate::command::DynField::Point if has_base => { vec![DynComponent::Distance, DynComponent::Angle] } @@ -7388,7 +7406,7 @@ impl OpenCADStudio { // the command's default so e.g. clicking the first point of LINE // flips a stale `[X, Y]` (from before there was a base) over to // the polar `[Distance, Angle]` the prompt actually wants. - let current_is_acceptable = if self.dyn_user_reshaped && !wants_text { + let current_is_acceptable = if self.dyn_user_reshaped && !wants_text && !picks_object { match field { crate::command::DynField::Distance => { matches!(current.as_slice(), [DynComponent::Distance]) diff --git a/src/app/view.rs b/src/app/view.rs index ba1b06b07..e059cdfe1 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -691,8 +691,18 @@ impl OpenCADStudio { // between boxes; typing locks a box to a fixed value while the // rest keep tracking the cursor. The field set is maintained in // `tab.dyn_fields` by `sync_dyn_fields`. + // A pick step (object selection) has no input box, but still shows + // its prompt ("Select first object …") near the cursor as a hint. + let dyn_picks_object = tab + .active_cmd + .as_ref() + .map(|c| c.needs_entity_pick() || c.needs_structure_point_pick()) + .unwrap_or(false); let dyn_input_overlay: Option> = - if self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty() { + if self.dyn_input + && tab.active_cmd.is_some() + && (!tab.dyn_fields.is_empty() || dyn_picks_object) + { let w = tab.last_cursor_world; let base = self.last_point; // A command may drive a typed scalar by mouse (e.g. a diff --git a/src/modules/home/modify/fillet.rs b/src/modules/home/modify/fillet.rs index a140ec175..287bf0c71 100644 --- a/src/modules/home/modify/fillet.rs +++ b/src/modules/home/modify/fillet.rs @@ -1196,6 +1196,10 @@ pub struct FilletCommand { radius: f64, step: FilletStep, all_entities: Vec, + /// First-object pick to restore after a radius entry made mid-selection + /// (i.e. "R" pressed after the first object was already picked), so the + /// command resumes at the second pick instead of restarting selection. + resume_second: Option<(Handle, FilletEntity, [f64; 2])>, } impl FilletCommand { @@ -1204,8 +1208,29 @@ impl FilletCommand { radius: radius as f64, step: FilletStep::First, all_entities, + resume_second: None, } } + + /// Switch to the radius sub-step, remembering the first pick (if any) so + /// it can be restored afterwards. + fn enter_radius_substep(&mut self) { + self.resume_second = if let FilletStep::Second { h1, e1, click1 } = &self.step { + Some((*h1, e1.clone(), *click1)) + } else { + None + }; + self.step = FilletStep::WaitingForRadius; + } + + /// Leave the radius sub-step, resuming the second pick when a first object + /// was already chosen, otherwise restarting at the first pick. + fn resume_after_radius(&mut self) { + self.step = match self.resume_second.take() { + Some((h1, e1, click1)) => FilletStep::Second { h1, e1, click1 }, + None => FilletStep::First, + }; + } } impl CadCommand for FilletCommand { @@ -1248,8 +1273,8 @@ impl CadCommand for FilletCommand { FilletStep::WaitingForRadius => { let t = text.trim(); if t.is_empty() { - // Keep current radius, return to First - self.step = FilletStep::First; + // Keep current radius, resume where the radius was requested. + self.resume_after_radius(); return Some(CmdResult::NeedPoint); } if let Ok(v) = t.replace(',', ".").parse::() { @@ -1257,7 +1282,7 @@ impl CadCommand for FilletCommand { self.radius = v; defaults::set_fillet_radius(v as f32); } - self.step = FilletStep::First; + self.resume_after_radius(); return Some(CmdResult::NeedPoint); } // Invalid — stay and re-prompt @@ -1268,7 +1293,7 @@ impl CadCommand for FilletCommand { let upper = t.to_uppercase(); // "R" alone → enter sub-step to collect radius if upper == "R" { - self.step = FilletStep::WaitingForRadius; + self.enter_radius_substep(); return Some(CmdResult::NeedPoint); } // "R 5.0" inline shorthand @@ -1279,10 +1304,11 @@ impl CadCommand for FilletCommand { self.radius = v; defaults::set_fillet_radius(v as f32); } + // Stay in the current step (keeps any first pick). return Some(CmdResult::NeedPoint); } // "R" + invalid body → enter sub-step - self.step = FilletStep::WaitingForRadius; + self.enter_radius_substep(); return Some(CmdResult::NeedPoint); } None @@ -1533,6 +1559,10 @@ pub struct ChamferCommand { dist2: f64, step: ChamferStep, all_entities: Vec, + /// First-object pick (line or polyline segment) to restore after a + /// distance entry made mid-selection, so the command resumes at the + /// second pick instead of restarting selection. + resume_pick: Option, } impl ChamferCommand { @@ -1542,8 +1572,33 @@ impl ChamferCommand { dist2: defaults::get_chamfer_dist2() as f64, step: ChamferStep::First, all_entities, + resume_pick: None, } } + + /// Switch to the distance sub-step, remembering the first pick (if any). + fn enter_dist_substep(&mut self) { + self.resume_pick = match &self.step { + ChamferStep::Second { h1, l1, click1 } => Some(ChamferStep::Second { + h1: *h1, + l1: l1.clone(), + click1: *click1, + }), + ChamferStep::SecondPoly { h1, poly, click1 } => Some(ChamferStep::SecondPoly { + h1: *h1, + poly: poly.clone(), + click1: *click1, + }), + _ => None, + }; + self.step = ChamferStep::WaitingForDist1; + } + + /// Leave the distance sub-step, resuming the second pick when a first + /// object was already chosen, otherwise restarting at the first pick. + fn resume_after_dist(&mut self) { + self.step = self.resume_pick.take().unwrap_or(ChamferStep::First); + } } impl CadCommand for ChamferCommand { @@ -1614,14 +1669,14 @@ impl CadCommand for ChamferCommand { ChamferStep::WaitingForDist2 => { let t = text.trim(); if t.is_empty() { - // Keep current dist2, return to First - self.step = ChamferStep::First; + // Keep current dist2, resume where the distance was requested. + self.resume_after_dist(); 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; + self.resume_after_dist(); return Some(CmdResult::NeedPoint); } // Invalid — stay and re-prompt @@ -1634,7 +1689,7 @@ impl CadCommand for ChamferCommand { let upper = t.to_uppercase(); // "D" alone → enter sub-step to collect distances if upper == "D" { - self.step = ChamferStep::WaitingForDist1; + self.enter_dist_substep(); return Some(CmdResult::NeedPoint); } // "D 5.0" or "D 5.0 3.0" inline shorthand @@ -1659,7 +1714,7 @@ impl CadCommand for ChamferCommand { return Some(CmdResult::NeedPoint); } // "D" + invalid body → enter sub-step - self.step = ChamferStep::WaitingForDist1; + self.enter_dist_substep(); return Some(CmdResult::NeedPoint); } None diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 452616c25..0fbe3676e 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -1251,9 +1251,6 @@ impl canvas::Program for DynInputCanvas { _cursor: mouse::Cursor, ) -> Vec { let mut frame = canvas::Frame::new(renderer, bounds.size()); - if self.boxes.is_empty() { - return vec![frame.into_geometry()]; - } // Offset the row 14 px right and 20 px below the cursor. const OFFSET_X: f32 = 14.0; @@ -1264,6 +1261,44 @@ impl canvas::Program for DynInputCanvas { const CHAR_W: f32 = FONT_SIZE * 0.62; // monospace-ish width estimate const BOX_H: f32 = FONT_SIZE + PAD * 2.0; + // No input box (a pick step) — draw just the prompt pill at the cursor, + // so object-selection steps still get their hint without a field. + if self.boxes.is_empty() { + if !self.prompt.is_empty() { + let pw = (self.prompt.len() as f32 * CHAR_W) + PAD * 2.0; + let mut px = self.cursor_screen.x + OFFSET_X; + let mut py = self.cursor_screen.y + OFFSET_Y; + if px + pw > bounds.width { + px = (self.cursor_screen.x - pw - 4.0).max(0.0); + } + if py + BOX_H > bounds.height { + py = (self.cursor_screen.y - BOX_H - 4.0).max(0.0); + } + let prect = canvas::Path::rectangle( + Point { x: px, y: py }, + Size { + width: pw, + height: BOX_H, + }, + ); + frame.fill(&prect, Color { r: 0.10, g: 0.10, b: 0.12, a: 1.0 }); + frame.stroke( + &prect, + canvas::Stroke::default() + .with_color(Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 }) + .with_width(1.0), + ); + frame.fill_text(canvas::Text { + content: self.prompt.clone(), + position: Point { x: px + PAD, y: py + PAD }, + color: Color { r: 0.70, g: 0.85, b: 0.70, a: 1.0 }, + size: iced::Pixels(FONT_SIZE), + ..Default::default() + }); + } + return vec![frame.into_geometry()]; + } + // Each box is "