From 33dfbf923090d34a3b45bf59ef5307bcc55e1648 Mon Sep 17 00:00:00 2001 From: yztink Date: Mon, 21 Sep 2026 22:52:29 -0700 Subject: [PATCH] fix(command): support direct distance entry when dynamic input is disabled --- src/app/command_driver.rs | 10 ++++ src/app/update/command.rs | 89 ++++++++++++++++++++++++++-------- src/app/update/context_menu.rs | 69 ++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 19 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 75493c3dd..7454d1b0e 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -929,6 +929,16 @@ impl OpenCADStudio { self.push_ucs_to_cmd(i); return self.feed_command(StepInput::Point(wcs)); } else { + let consumed = self.tabs[i] + .active_cmd + .as_mut() + .and_then(|c| c.on_text_input(token)); + if let Some(r) = consumed { + return self.apply_cmd_result(r); + } + if let Some(task) = self.try_direct_distance_entry(token) { + return task; + } return self.feed_command(StepInput::Text(token.to_string())); } } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index fc7ba2ebb..a61ebb3a7 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -778,25 +778,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // OTRACK and Extension both allow a bare scalar to act as a // distance measured along the active reference ray. - if let Some((base, dir)) = self.active_distance_ray(i) { - if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) { - let pt = base + dir * dist; - if !self.command_point_allowed(i, pt) { - return Task::none(); - } - self.last_point = Some(pt); - self.dyn_user_reshaped = false; - self.dyn_coord_absolute = false; - self.sync_dyn_fields(); - self.reset_tracking_after_point(); - self.push_ucs_to_cmd(i); - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); - if let Some(r) = result { - let task = self.apply_cmd_result(r); - self.refresh_active_cmd_preview(i); - return task; - } - return Task::none(); + if self.active_distance_ray(i).is_some() { + if let Some(task) = self.try_direct_distance_entry(&text) { + return task; } } @@ -861,6 +845,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { return self.apply_cmd_result(result); } + // Direct distance entry: if an anchor exists and a scalar distance was entered, + // project along the active reference ray (if any) or the current cursor direction + // in the active UCS plane. + if let Some(task) = self.try_direct_distance_entry(&text) { + return task; + } + self.command_line.push_error(crate::tf!( "Expected Cartesian, polar, cylindrical or spherical coordinates, or a number; got: \"{text}\"" ).as_ref()); @@ -879,6 +870,66 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { Task::none() } + /// Attempt direct distance entry: if an active command is expecting a point, + /// has an established anchor, and `text` parses as a scalar distance, + /// project along the active reference ray (if any) or the current cursor + /// direction in the active UCS plane. + pub(in crate::app) fn try_direct_distance_entry(&mut self, text: &str) -> Option> { + let i = self.active_tab; + let not_entity_pick = !self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.needs_entity_pick()) + .unwrap_or(false); + if !not_entity_pick { + return None; + } + + let anchor = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|c| c.resolved_anchor()) + .or(self.tabs[i].dyn_anchor) + .or(self.last_point)?; + + let dist = crate::entities::common::parse_length(text.trim()) + .or_else(|| crate::app::expr_eval::eval_number(text.trim()))?; + + let pt = if let Some((base, dir)) = self.active_distance_ray(i) { + base + dir * dist + } else { + let w = self.tabs[i].last_cursor_world; + let xf = self.tabs[i].ucs_xform(); + let d_ucs = xf.vec_to_ucs(w - anchor); + let dx = d_ucs.x; + let dy = d_ucs.y; + let dir_ucs = if (dx * dx + dy * dy) > 1e-12 { + glam::DVec3::new(dx, dy, 0.0).normalize() + } else if d_ucs.length_squared() > 1e-12 { + d_ucs.normalize() + } else { + glam::DVec3::X + }; + anchor + xf.vec_to_wcs(dir_ucs * dist) + }; + + if !self.command_point_allowed(i, pt) { + return Some(Task::none()); + } + + self.last_point = Some(pt); + self.dyn_user_reshaped = false; + self.dyn_coord_absolute = false; + self.sync_dyn_fields(); + self.reset_tracking_after_point(); + self.push_ucs_to_cmd(i); + + let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt))?; + let task = self.apply_cmd_result(result); + self.refresh_active_cmd_preview(i); + Some(task) + } + /// The active command's current step collects free-form prose from the /// command line. Single decision point for the routes that type into /// the buffer (CommandInput, Shift+Enter, the view's on_submit wiring). diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs index ea609b439..912f8fb68 100644 --- a/src/app/update/context_menu.rs +++ b/src/app/update/context_menu.rs @@ -940,4 +940,73 @@ mod transparent_tests { assert!(app.snapper.snap_enabled); }); } + + #[test] + fn test_direct_distance_entry_dyn_off() { + with_stack(|| { + let mut app = line_app(); + app.dyn_input = false; + assert_eq!(active(&app), Some("LINE")); + + // Point cursor in +X direction from origin + app.tabs[0].last_cursor_world = glam::DVec3::new(10.0, 0.0, 0.0); + + // Type "50" into command line and submit + app.command_line.input = "50".to_string(); + let _ = app.on_command_submit(); + + // Line segment should be created from (0,0) to (50,0) + let lines: Vec<_> = app.tabs[0] + .scene + .document + .entities() + .filter_map(|e| match e { + acadrust::EntityType::Line(l) => Some(l.clone()), + _ => None, + }) + .collect(); + assert_eq!(lines.len(), 1); + assert!((lines[0].start.x - 0.0).abs() < 1e-6 && (lines[0].start.y - 0.0).abs() < 1e-6); + assert!((lines[0].end.x - 50.0).abs() < 1e-6 && (lines[0].end.y - 0.0).abs() < 1e-6); + + // Command should still be active for the next line segment + assert_eq!(active(&app), Some("LINE")); + + // Point cursor in +Y direction from (50, 0) + app.tabs[0].last_cursor_world = glam::DVec3::new(50.0, 20.0, 0.0); + + // Type "25" and submit + app.command_line.input = "25".to_string(); + let _ = app.on_command_submit(); + + let lines: Vec<_> = app.tabs[0] + .scene + .document + .entities() + .filter_map(|e| match e { + acadrust::EntityType::Line(l) => Some(l.clone()), + _ => None, + }) + .collect(); + assert_eq!(lines.len(), 2); + assert!((lines[1].start.x - 50.0).abs() < 1e-6 && (lines[1].start.y - 0.0).abs() < 1e-6); + assert!((lines[1].end.x - 50.0).abs() < 1e-6 && (lines[1].end.y - 25.0).abs() < 1e-6); + + // Point cursor in -X direction from (50, 25) using feed_active_cmd + app.tabs[0].last_cursor_world = glam::DVec3::new(0.0, 25.0, 0.0); + let _ = app.feed_active_cmd("50"); + let lines: Vec<_> = app.tabs[0] + .scene + .document + .entities() + .filter_map(|e| match e { + acadrust::EntityType::Line(l) => Some(l.clone()), + _ => None, + }) + .collect(); + assert_eq!(lines.len(), 3); + assert!((lines[2].start.x - 50.0).abs() < 1e-6 && (lines[2].start.y - 25.0).abs() < 1e-6); + assert!((lines[2].end.x - 0.0).abs() < 1e-6 && (lines[2].end.y - 25.0).abs() < 1e-6); + }); + } }