feat(input): lock the next point to a typed <angle

Typing <angle at a point prompt (e.g. <30) now fixes the direction of
the next point from the last one until a point is placed: the cursor
moves along that direction, and a bare number entered next is the
distance along it. The angle is read in the drawing's angle base and
direction and turned into the current UCS. Works from the command line
and from scripts or the automation token input alike.

Fixes #1464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 14:21:50 +03:00
1 parent 29c0d1df9a
commit 0eec9c450f
28 files changed
+77 -1

No files matched your search

+12 -1
View File
@@ -98,6 +98,7 @@ impl OpenCADStudio {
self.otrack_cross = None;
self.otrack_kind = None;
self.axis_lock_dir = None;
self.angle_override = None;
self.dyn_user_reshaped = false;
self.dyn_coord_absolute = false;
self.grip_hover = None;
@@ -595,6 +596,7 @@ impl OpenCADStudio {
if !self.command_point_allowed(i, *point) {
return (Task::none(), true);
}
self.angle_override = None;
// Typed / dynamic-input / headless points carry no snap, but the
// accepted-snap list must stay index-parallel with the points the
// command collects. Interactive picks record themselves in
@@ -1130,6 +1132,9 @@ impl OpenCADStudio {
self.apply_snap_override(kind);
return Task::none();
}
if self.try_angle_override(token) {
return Task::none();
}
if let Some((coord, kind)) = crate::app::helpers::parse_coord(token) {
// Match the GUI command line: typed coordinates are in the active
// UCS (relative offsets are rotated by the UCS axes), so a multi-
@@ -1161,7 +1166,13 @@ impl OpenCADStudio {
// bare number is the step's own value, not a distance — ROTATE's
// angle, SCALE's factor, CIRCLE's radius. Only a token no step
// claims becomes direct distance entry, measured along the cursor
// direction from the command's anchor.
// direction from the command's anchor. Under a typed `<angle`
// the number is the distance along it first.
if self.angle_override.is_some() {
if let Some(distance) = self.try_direct_distance_entry(token) {
return distance;
}
}
let (task, consumed) = self.feed_command_consumed(StepInput::Text(token.to_string()));
if consumed {
return task;
+4
View File
@@ -1218,6 +1218,9 @@ pub(super) struct OpenCADStudio {
/// osnap hits included — projects onto this ray from `last_point` (or the
/// grip origin). Cleared when Shift releases.
axis_lock_dir: Option<glam::DVec3>,
/// A direction typed as `<angle` at a point prompt: the next point stays
/// on it until a point is placed.
angle_override: Option<glam::DVec3>,
/// Inline rename state: (original_name, current_edit_value).
layout_rename_state: Option<(String, String)>,
/// Timestamp of the previous viewport left-click release (for double-click detection).
@@ -4775,6 +4778,7 @@ impl OpenCADStudio {
text_inline: None,
snap_override_popup: None,
axis_lock_dir: None,
angle_override: None,
layout_rename_state: None,
last_vp_click_time: None,
last_vp_click_pos: None,
+30
View File
@@ -868,6 +868,13 @@ impl OpenCADStudio {
if let Some(task) = self.try_selection_keyword(&text) {
return task;
}
// Under a typed `<angle` a bare number is the distance along it,
// before the step reads it as a value of its own.
if self.angle_override.is_some() {
if let Some(task) = self.try_direct_distance_entry(&text) {
return task;
}
}
if self.tabs[i]
.active_cmd
.as_ref()
@@ -895,6 +902,10 @@ impl OpenCADStudio {
return self.feed_command(crate::command::StepInput::Enter);
}
if self.try_angle_override(&text) {
return Task::none();
}
// OTRACK and Extension both allow a bare scalar to act as a
// distance measured along the active reference ray.
if self.active_distance_ray(i).is_some() {
@@ -1001,6 +1012,25 @@ impl OpenCADStudio {
/// 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.
/// `<angle` at a point prompt locks the next point's direction from the
/// last point; a bare distance then runs along it. True when consumed.
pub(in crate::app) fn try_angle_override(&mut self, text: &str) -> bool {
let (Some(angle), Some(_)) = (text.trim().strip_prefix('<'), self.last_point) else {
return false;
};
let Some(angle) = crate::entities::common::parse_typed_direction(angle) else {
return false;
};
let local = glam::DVec3::new(angle.cos(), angle.sin(), 0.0);
self.angle_override = Some(match &self.tabs[self.active_tab].active_ucs {
Some(ucs) => ucs_rotate_vec(local, ucs),
None => local,
});
let shown = crate::entities::common::format_direction(angle);
self.command_line.push_output(crate::tf!("Angle Override: {shown}").as_ref());
true
}
pub(in crate::app) fn try_direct_distance_entry(
&mut self,
text: &str,
+4
View File
@@ -29,6 +29,10 @@ impl OpenCADStudio {
&self,
i: usize,
) -> Option<(glam::DVec3, glam::DVec3)> {
// A typed `<angle` locks the way from the last point.
if let (Some(dir), Some(base)) = (self.angle_override, self.last_point) {
return Some((base, dir));
}
// OTRACK already exposes its acquired world-space ray.
if let Some((base, dir)) = self.otrack_active {
return Some((base, dir));
+4
View File
@@ -340,6 +340,10 @@ impl OpenCADStudio {
base: glam::DVec3,
allowed: bool,
) -> Option<glam::DVec3> {
if let Some(direction) = self.angle_override.filter(|_| allowed) {
self.axis_lock_dir = Some(direction);
return self.axis_lock_dir;
}
if self.shift_down && allowed {
if self.axis_lock_dir.is_none() {
let ucs = self.tabs[tab].ucs_xform();
+1
View File
@@ -5844,6 +5844,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Requires 0 or 1 only." => Some(("count-feature", "requires-0-or-1")),
"Requires 0 or 1 only, or option keyword." => Some(("common", "requires-0-or-1-or-keyword")),
"Requires a number." => Some(("common", "requires-a-number")),
"Angle Override: {shown}" => Some(("common", "angle-override-shown")),
"{name} ...... {n}" => Some(("count-feature", "target-count")),
"MTEXT Current text style: \"{}\" Text height: {:.4}" => Some(("count-feature", "mtext-current-style")),
"{found} field(s) found." => Some(("count-feature", "fields-found")),