From d06a1022f22c8218004664ce9cd0ffe2a4d1b8ed Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 2 Aug 2026 23:32:54 +0300 Subject: [PATCH] fix(input): handle @/# coordinate modes --- src/app/command_driver.rs | 2 ++ src/app/commands/mod.rs | 1 + src/app/mod.rs | 6 ++++ src/app/update/command.rs | 16 +++++++++ src/app/update/dynamic.rs | 68 +++++++++++++++++++++++++++++++++++--- src/app/update/viewport.rs | 1 + src/app/view/controls.rs | 17 ++++++---- src/app/view/mod.rs | 1 + 8 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 985e19830..f50ec8dad 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -79,6 +79,7 @@ impl OpenCADStudio { self.otrack_active = None; self.axis_lock_dir = None; self.dyn_user_reshaped = false; + self.dyn_coord_absolute = false; self.grip_hover = None; self.grip_popup = None; self.grip_pending = None; @@ -252,6 +253,7 @@ impl OpenCADStudio { }; self.last_point = Some(*point); 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); diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 46c78a354..aadc94dcc 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -96,6 +96,7 @@ impl OpenCADStudio { // default (LIMITS then compares an unintended lower-left point with // the displayed default upper-right). self.dyn_user_reshaped = false; + self.dyn_coord_absolute = false; self.tabs[i].dyn_fields.clear(); self.tabs[i].dyn_active = 0; diff --git a/src/app/mod.rs b/src/app/mod.rs index 8d094678e..905989570 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -416,6 +416,11 @@ pub(super) struct OpenCADStudio { /// reverting to the command-default when `has_base` flips. Cleared /// on point commit / command start. See #35. dyn_user_reshaped: bool, + /// Dynamic cartesian entry mode for the current point. `false` is the + /// usual relative-to-last-point mode; typing `#` selects absolute UCS + /// coordinates and typing `@` selects relative coordinates again. + /// Cleared after a point is committed or a new command starts. + dyn_coord_absolute: bool, /// Grip the cursor is currently dwelling on. Set when the cursor /// stops within `GRIP_THRESHOLD_PX` of a grip; cleared when it /// drifts away. The instant lets `ViewportMove` detect when the @@ -2730,6 +2735,7 @@ impl OpenCADStudio { ucs_grip_drag: None, pane_move_from: None, dyn_user_reshaped: false, + dyn_coord_absolute: false, grip_hover: None, grip_popup: None, grip_pending: None, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 51e20cdd7..a69551fec 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -157,6 +157,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.refresh_active_cmd_preview(i); return self.focus_cmd_input(); } + // In dynamic point entry, `@` and `#` are coordinate-mode + // switches rather than command-line text. Consuming them + // here keeps the prefix out of the command buffer while + // the following digits continue into the dynamic fields. + if matches!(s.as_str(), "@" | "#") + && self.command_line.input.is_empty() + && self.dyn_input + && self.dyn_has_coordinate_fields(i) + { + self.dyn_set_coordinate_mode(s == "#"); + self.command_line.autocomplete_cursor = None; + self.refresh_active_cmd_preview(i); + return self.focus_cmd_input(); + } // While dynamic input is showing fields, numeric and // expression glyphs edit the focused field instead of // the command line. Letters still go to the command line @@ -370,6 +384,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } 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); @@ -415,6 +430,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.last_point = Some(wcs_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); diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 4288c9789..0e9994bd9 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -267,6 +267,7 @@ impl OpenCADStudio { // (The delta is offset-invariant, so only the rotation matters.) let xf = self.tabs[i].ucs_xform(); let d_ucs = xf.vec_to_ucs(w - base); + let w_ucs = xf.to_ucs(w); let dx = d_ucs.x; let dy = d_ucs.y; let dz = d_ucs.z; @@ -319,20 +320,29 @@ impl OpenCADStudio { // (see #26 / #35). The live cartesian fallback is the cursor // position relative to base; typed values are relative deltas. let has_base = self.last_point.is_some(); + let relative = has_base && !self.dyn_coord_absolute; // Relative result: base + the typed UCS-frame offset mapped to world. let rel = |off_ucs: glam::DVec3| base + xf.vec_to_wcs(off_ucs); match comps.as_slice() { - [DynComponent::X, DynComponent::Y] if has_base => { + [DynComponent::X, DynComponent::Y] if relative => { Some(rel(glam::DVec3::new(val(0, dx), val(1, dy), 0.0))) } [DynComponent::X, DynComponent::Y] => { - Some(glam::DVec3::new(val(0, w.x), val(1, w.y), base.z)) + Some(xf.to_wcs(glam::DVec3::new( + val(0, w_ucs.x), + val(1, w_ucs.y), + 0.0, + ))) } - [DynComponent::X, DynComponent::Y, DynComponent::Z] if has_base => { + [DynComponent::X, DynComponent::Y, DynComponent::Z] if relative => { Some(rel(glam::DVec3::new(val(0, dx), val(1, dy), val(2, dz)))) } [DynComponent::X, DynComponent::Y, DynComponent::Z] => { - Some(glam::DVec3::new(val(0, w.x), val(1, w.y), val(2, base.z))) + Some(xf.to_wcs(glam::DVec3::new( + val(0, w_ucs.x), + val(1, w_ucs.y), + val(2, w_ucs.z), + ))) } [DynComponent::Distance, DynComponent::Angle] => { let d = val(0, live_d); @@ -411,6 +421,54 @@ impl OpenCADStudio { } } + /// Whether the current dynamic field set represents a point coordinate + /// that can be switched with `@` / `#`. + pub(in crate::app) fn dyn_has_coordinate_fields(&self, i: usize) -> bool { + use crate::app::document::DynComponent; + let comps: Vec = self.tabs[i] + .dyn_fields + .iter() + .map(|field| field.component) + .collect(); + comps.iter().any(|component| { + matches!( + component, + DynComponent::X | DynComponent::Y | DynComponent::Z + ) + }) || matches!( + comps.as_slice(), + [DynComponent::Distance, DynComponent::Angle] + ) + } + + /// Switch the current dynamic point entry between relative (`@`) and + /// absolute (`#`) UCS coordinates. Polar fields become cartesian because + /// the selected mode applies to coordinate components, not distance/angle. + pub(in crate::app) fn dyn_set_coordinate_mode(&mut self, absolute: bool) { + use crate::app::document::{DynComponent, DynFieldEntry}; + let i = self.active_tab; + self.dyn_coord_absolute = absolute; + + let comps: Vec = self.tabs[i] + .dyn_fields + .iter() + .map(|field| field.component) + .collect(); + if matches!( + comps.as_slice(), + [DynComponent::Distance, DynComponent::Angle] + ) { + let first_buffer = self.tabs[i].dyn_fields[0].buffer.clone(); + let mut x = DynFieldEntry::new(DynComponent::X); + x.buffer = first_buffer; + self.tabs[i].dyn_fields = vec![x, DynFieldEntry::new(DynComponent::Y)]; + self.tabs[i].dyn_active = usize::from(self.tabs[i].dyn_fields[0].locked()); + } + + self.dyn_user_reshaped = true; + self.sync_dyn_fields(); + } + /// If dynamic input has at least one locked (typed) field, resolve the /// implied point, feed it to the active command as a point pick, reset /// the field buffers, and return the resulting task. Returns `None` @@ -493,6 +551,7 @@ impl OpenCADStudio { } self.tabs[i].dyn_active = 0; 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); @@ -555,6 +614,7 @@ impl OpenCADStudio { } 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); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 7f549f548..fc7260b11 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2823,6 +2823,7 @@ impl OpenCADStudio { // restore the running osnap configuration (#337). self.snapper.clear_override(); self.dyn_user_reshaped = false; + self.dyn_coord_absolute = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); // A running Tangent snap may carry a tangent object; a diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 2fbae28f4..6c83a9c71 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -185,11 +185,13 @@ pub(super) fn dyn_component_value( base: Option, xf: &super::super::helpers::UcsXform, comma_cartesian: bool, + absolute: bool, ) -> String { if let Some(b) = &f.buffer { return b.clone(); } let b = base.unwrap_or(glam::DVec3::ZERO); + let p = xf.to_ucs(w); // Relative deltas and the polar angle read in the active UCS plane. The // delta is offset-invariant, so only the axis rotation matters (identity // xf reproduces the world-frame deltas). @@ -200,21 +202,22 @@ pub(super) fn dyn_component_value( // fields show relative deltas — matching the typed-value convention // in `dyn_resolve_point` so the live preview and the committed // coordinate use the same frame. See #35. - let has_base = base.is_some(); + let relative = base.is_some() && !absolute; // Width / Height read as unsigned magnitudes (the sign is taken from the // cursor side on commit), matching the rectangle's two-edge entry. But once // the user separates the values with `,` the entry is a cartesian // coordinate pair, so the fields read as signed X/Y deltas to match the // committed point (#269). let wh = matches!(f.role, crate::command::DynRole::Width | crate::command::DynRole::Height) + && relative && !comma_cartesian; match f.component { - DynComponent::X if has_base => format!("{:.4}", if wh { dx.abs() } else { dx }), - DynComponent::Y if has_base => format!("{:.4}", if wh { dy.abs() } else { dy }), - DynComponent::Z if has_base => "0.0000".to_string(), - DynComponent::X => format!("{:.4}", w.x), - DynComponent::Y => format!("{:.4}", w.y), - DynComponent::Z => format!("{:.4}", b.z), + DynComponent::X if relative => format!("{:.4}", if wh { dx.abs() } else { dx }), + DynComponent::Y if relative => format!("{:.4}", if wh { dy.abs() } else { dy }), + DynComponent::Z if relative => "0.0000".to_string(), + DynComponent::X => format!("{:.4}", p.x), + DynComponent::Y => format!("{:.4}", p.y), + DynComponent::Z => format!("{:.4}", p.z), // Scaled by the role so a diameter box reads twice the radius. DynComponent::Distance => { format!("{:.4}", (dx * dx + dy * dy).sqrt() * f.role.value_scale() as f64) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index ecee5dab8..386c0513f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -708,6 +708,7 @@ impl OpenCADStudio { base, &tab.ucs_xform(), self.dyn_user_reshaped, + self.dyn_coord_absolute, ), }; crate::ui::overlay::DynBox {