Merge pull request #1707 from yztink/feat/snap-overrides

feat(snap): add transparent object snap overrides in active commands typed on command-line

# Conflicts:
#	src/app/update/command.rs
This commit is contained in:
Hakan Seven committed 2026-10-11 23:39:07 +03:00
commit 45bc31f730
8 files changed
+287 -13

No files matched your search

+75
View File
@@ -3798,6 +3798,81 @@ mod tests {
);
}
#[test]
fn test_snap_override_typed_in_line_command() {
use crate::app::Message;
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
{
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
}
// Start LINE
let _ = app.update(Message::CommandInput("LINE".to_string()));
let _ = app.update(Message::CommandSubmit);
assert_eq!(
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
Some("LINE")
);
// Turn running osnap off to prove override temporarily enables snapping
app.snapper.snap_enabled = false;
assert!(!app.snapper.snap_enabled);
// Type MID at first point prompt
let _ = app.update(Message::CommandInput("MID".to_string()));
let _ = app.update(Message::CommandSubmit);
// LINE remains the active command
assert_eq!(
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
Some("LINE")
);
// Snapper is now forced on with only Midpoint enabled
assert!(app.snapper.snap_enabled);
assert_eq!(app.snapper.enabled.len(), 1);
assert!(app.snapper.enabled.contains(&crate::snap::SnapType::Midpoint));
// Submit first point (0,0)
let _ = app.update(Message::CommandInput("0,0".to_string()));
let _ = app.update(Message::CommandSubmit);
// Override is consumed by the point commit, restoring snap_enabled == false
assert!(!app.snapper.snap_enabled);
// At next point prompt, type END
let _ = app.update(Message::CommandInput("END".to_string()));
let _ = app.update(Message::CommandSubmit);
assert!(app.snapper.snap_enabled);
assert!(app.snapper.enabled.contains(&crate::snap::SnapType::Endpoint));
// Escape cancels LINE and clears unconsumed snap override
let _ = app.update(Message::CommandEscape);
assert!(app.tabs[0].active_cmd.is_none());
assert!(!app.snapper.snap_enabled);
}
#[test]
fn test_snap_override_feed_token() {
use crate::app::Message;
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let _ = app.update(Message::CommandInput("LINE".to_string()));
let _ = app.update(Message::CommandSubmit);
app.snapper.snap_enabled = false;
let _ = app.feed_active_cmd("CEN");
assert!(app.snapper.snap_enabled);
assert_eq!(app.snapper.enabled.len(), 1);
assert!(app.snapper.enabled.contains(&crate::snap::SnapType::Center));
let _ = app.feed_active_cmd("5,5");
assert!(!app.snapper.snap_enabled);
let _ = app.update(Message::CommandEscape);
}
#[test]
fn test_open_color_dropdown() {
use crate::app::Message;
+22
View File
@@ -602,6 +602,7 @@ impl OpenCADStudio {
if !self.record_accepted_snap(i, None, None, *point) {
return (Task::none(), true);
}
self.snapper.clear_override();
}
if default_start {
let StepInput::Point(point) = &input else {
@@ -1140,6 +1141,27 @@ impl OpenCADStudio {
return Task::none();
}
}
if let Some(override_kind) = crate::snap::parse_snap_override(token) {
let is_point_step = !self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.input_kind().wants_text())
.unwrap_or(true)
|| self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.point_step_accepts_keywords())
.unwrap_or(false);
let not_entity_pick = !self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.needs_entity_pick())
.unwrap_or(false);
if is_point_step && not_entity_pick {
self.apply_snap_override(override_kind);
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-
+1
View File
@@ -74,6 +74,7 @@ impl OpenCADStudio {
// A new command collects its own points, so the previous command's
// accepted snaps must not leak into it.
self.clear_accepted_snaps();
self.snapper.clear_override();
// Starting a command restarts the right-click cycle, so its first
// right-click acts as Enter rather than opening the context menu.
self.tabs[i]
+24
View File
@@ -835,6 +835,28 @@ impl OpenCADStudio {
return self.focus_cmd_input();
}
}
if let Some(override_kind) = crate::snap::parse_snap_override(&raw) {
let is_point_step = !self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.input_kind().wants_text())
.unwrap_or(true)
|| self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.point_step_accepts_keywords())
.unwrap_or(false);
let not_entity_pick = !self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.needs_entity_pick())
.unwrap_or(false);
if is_point_step && not_entity_pick {
self.command_line.input.clear();
self.apply_snap_override(override_kind);
return self.focus_cmd_input();
}
}
let text = if free_text {
raw
} else {
@@ -933,6 +955,7 @@ impl OpenCADStudio {
self.dyn_coord_absolute = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.snapper.clear_override();
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt));
if let Some(r) = result {
@@ -1048,6 +1071,7 @@ impl OpenCADStudio {
self.dyn_coord_absolute = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.snapper.clear_override();
if let Some((_, dir)) = distance_ray {
self.snapper.remember_distance_reference(dir);
}
+2
View File
@@ -682,6 +682,7 @@ impl OpenCADStudio {
self.dyn_coord_absolute = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.snapper.clear_override();
self.snapper.remember_distance_reference(dir);
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
@@ -755,6 +756,7 @@ impl OpenCADStudio {
self.dyn_coord_absolute = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.snapper.clear_override();
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
for f in self.tabs[i].dyn_fields.iter_mut() {
+2 -13
View File
@@ -947,23 +947,12 @@ impl OpenCADStudio {
}
Message::SnapOverridePick(t) => {
self.snap_override_popup = None;
self.snapper.set_override(t);
let label = crate::snap::ALL_SNAP_MODES
.iter()
.find(|(m, _, _)| *m == t)
.map(|(_, _, l)| *l)
.unwrap_or("Snap");
self.command_line
.push_info(crate::tf!("Snap override: {label} (next pick only).").as_ref());
self.apply_snap_override(crate::snap::SnapOverrideKind::Mode(t));
Task::none()
}
Message::SnapOverrideNone => {
self.snap_override_popup = None;
self.snapper.set_override_none();
self.command_line
.push_info(crate::t!("Snap override: None (next pick only).").as_ref());
self.apply_snap_override(crate::snap::SnapOverrideKind::None);
Task::none()
}
+24
View File
@@ -50,6 +50,30 @@ impl OpenCADStudio {
self.accepted_snaps.clear();
}
/// Apply a transparent one-shot snap override (from typed input or the context menu).
pub(crate) fn apply_snap_override(&mut self, kind: crate::snap::SnapOverrideKind) {
match kind {
crate::snap::SnapOverrideKind::Mode(t) => {
self.snap_override_popup = None;
self.snapper.set_override(t);
let label = crate::snap::ALL_SNAP_MODES
.iter()
.chain(crate::snap::ALL_3D_SNAP_MODES.iter())
.find(|(m, _, _)| *m == t)
.map(|(_, _, l)| *l)
.unwrap_or("Snap");
self.command_line
.push_info(crate::tf!("Snap override: {label} (next pick only).").as_ref());
}
crate::snap::SnapOverrideKind::None => {
self.snap_override_popup = None;
self.snapper.set_override_none();
self.command_line
.push_info(crate::t!("Snap override: None (next pick only).").as_ref());
}
}
}
/// Record the snap a point step just accepted. `snap` is the displayed snap
/// result (already in the current space's coordinates) or `None` for a free
/// / typed point; `committed` is the point the command actually received