forked from 1b-agent/opencadstudio-upstream
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:
commit
45bc31f730
8 files changed
+287
-13
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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-
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+137
@@ -115,6 +115,76 @@ pub const ALL_3D_SNAP_MODES: &[(SnapType, &str, &str)] = &[
|
||||
(SnapType::NearestFace, "✦", "Nearest to face"),
|
||||
];
|
||||
|
||||
/// Transparent object snap override kind (single-pick override).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapOverrideKind {
|
||||
Mode(SnapType),
|
||||
None,
|
||||
}
|
||||
|
||||
/// Parses a transparent object snap override keyword (e.g. `MID`, `END`, `CEN`, `NON`).
|
||||
///
|
||||
/// Requires at least 3 letters matching a supported snap mode or alias.
|
||||
/// Leading underscores (e.g. `_MID`) and case variations are permitted.
|
||||
pub fn parse_snap_override(input: &str) -> Option<SnapOverrideKind> {
|
||||
let s = input.trim().trim_start_matches('_').to_ascii_lowercase();
|
||||
if s.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
if s.starts_with("end") && "endpoint".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Endpoint));
|
||||
}
|
||||
if s.starts_with("mid") && "midpoint".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Midpoint));
|
||||
}
|
||||
if s.starts_with("cen") && ("center".starts_with(&s) || "centre".starts_with(&s)) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Center));
|
||||
}
|
||||
if (s.starts_with("gce") && "gcen".starts_with(&s))
|
||||
|| (s.starts_with("geo") && ("geocenter".starts_with(&s) || "geometriccenter".starts_with(&s)))
|
||||
{
|
||||
return Some(SnapOverrideKind::Mode(SnapType::GeometricCenter));
|
||||
}
|
||||
if s.starts_with("nod") && "node".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Node));
|
||||
}
|
||||
if s.starts_with("qua") && "quadrant".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Quadrant));
|
||||
}
|
||||
if s.starts_with("int") && "intersection".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Intersection));
|
||||
}
|
||||
if s.starts_with("ext") && "extension".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Extension));
|
||||
}
|
||||
if s.starts_with("ins") && ("insertion".starts_with(&s) || "insert".starts_with(&s)) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Insertion));
|
||||
}
|
||||
if s.starts_with("per") && "perpendicular".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Perpendicular));
|
||||
}
|
||||
if s.starts_with("tan") && "tangent".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Tangent));
|
||||
}
|
||||
if s.starts_with("nea") && "nearest".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Nearest));
|
||||
}
|
||||
if s.starts_with("app")
|
||||
&& ("apparent".starts_with(&s)
|
||||
|| "apparentintersection".starts_with(&s)
|
||||
|| "appint".starts_with(&s))
|
||||
{
|
||||
return Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection));
|
||||
}
|
||||
if s.starts_with("par") && "parallel".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::Mode(SnapType::Parallel));
|
||||
}
|
||||
if s.starts_with("non") && "none".starts_with(&s) {
|
||||
return Some(SnapOverrideKind::None);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ── Snap result ───────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -5514,4 +5584,71 @@ mod ext_tests {
|
||||
"cursor over ellipse arc break must NOT snap to tangent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_snap_override() {
|
||||
use super::{parse_snap_override, SnapOverrideKind, SnapType};
|
||||
|
||||
// 3-letter prefixes
|
||||
assert_eq!(parse_snap_override("end"), Some(SnapOverrideKind::Mode(SnapType::Endpoint)));
|
||||
assert_eq!(parse_snap_override("mid"), Some(SnapOverrideKind::Mode(SnapType::Midpoint)));
|
||||
assert_eq!(parse_snap_override("cen"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
||||
assert_eq!(parse_snap_override("nod"), Some(SnapOverrideKind::Mode(SnapType::Node)));
|
||||
assert_eq!(parse_snap_override("qua"), Some(SnapOverrideKind::Mode(SnapType::Quadrant)));
|
||||
assert_eq!(parse_snap_override("int"), Some(SnapOverrideKind::Mode(SnapType::Intersection)));
|
||||
assert_eq!(parse_snap_override("ext"), Some(SnapOverrideKind::Mode(SnapType::Extension)));
|
||||
assert_eq!(parse_snap_override("ins"), Some(SnapOverrideKind::Mode(SnapType::Insertion)));
|
||||
assert_eq!(parse_snap_override("per"), Some(SnapOverrideKind::Mode(SnapType::Perpendicular)));
|
||||
assert_eq!(parse_snap_override("tan"), Some(SnapOverrideKind::Mode(SnapType::Tangent)));
|
||||
assert_eq!(parse_snap_override("nea"), Some(SnapOverrideKind::Mode(SnapType::Nearest)));
|
||||
assert_eq!(parse_snap_override("app"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
||||
assert_eq!(parse_snap_override("par"), Some(SnapOverrideKind::Mode(SnapType::Parallel)));
|
||||
assert_eq!(parse_snap_override("non"), Some(SnapOverrideKind::None));
|
||||
assert_eq!(parse_snap_override("geo"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
||||
assert_eq!(parse_snap_override("gce"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
||||
|
||||
// Case insensitivity & leading underscores
|
||||
assert_eq!(parse_snap_override("_MID"), Some(SnapOverrideKind::Mode(SnapType::Midpoint)));
|
||||
assert_eq!(parse_snap_override("_Endpoint"), Some(SnapOverrideKind::Mode(SnapType::Endpoint)));
|
||||
assert_eq!(parse_snap_override(" _NON "), Some(SnapOverrideKind::None));
|
||||
assert_eq!(parse_snap_override("CEN"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
||||
|
||||
// Full names and aliases
|
||||
assert_eq!(parse_snap_override("endpoint"), Some(SnapOverrideKind::Mode(SnapType::Endpoint)));
|
||||
assert_eq!(parse_snap_override("midpoint"), Some(SnapOverrideKind::Mode(SnapType::Midpoint)));
|
||||
assert_eq!(parse_snap_override("center"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
||||
assert_eq!(parse_snap_override("centre"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
||||
assert_eq!(parse_snap_override("quadrant"), Some(SnapOverrideKind::Mode(SnapType::Quadrant)));
|
||||
assert_eq!(parse_snap_override("intersection"), Some(SnapOverrideKind::Mode(SnapType::Intersection)));
|
||||
assert_eq!(parse_snap_override("perpendicular"), Some(SnapOverrideKind::Mode(SnapType::Perpendicular)));
|
||||
assert_eq!(parse_snap_override("tangent"), Some(SnapOverrideKind::Mode(SnapType::Tangent)));
|
||||
assert_eq!(parse_snap_override("nearest"), Some(SnapOverrideKind::Mode(SnapType::Nearest)));
|
||||
assert_eq!(parse_snap_override("apparent"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
||||
assert_eq!(parse_snap_override("apparentintersection"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
||||
assert_eq!(parse_snap_override("appint"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
||||
assert_eq!(parse_snap_override("parallel"), Some(SnapOverrideKind::Mode(SnapType::Parallel)));
|
||||
assert_eq!(parse_snap_override("insert"), Some(SnapOverrideKind::Mode(SnapType::Insertion)));
|
||||
assert_eq!(parse_snap_override("insertion"), Some(SnapOverrideKind::Mode(SnapType::Insertion)));
|
||||
assert_eq!(parse_snap_override("none"), Some(SnapOverrideKind::None));
|
||||
assert_eq!(parse_snap_override("gcen"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
||||
assert_eq!(parse_snap_override("geometriccenter"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
||||
assert_eq!(parse_snap_override("geocenter"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
||||
|
||||
// Rejection of strings shorter than 3 chars
|
||||
assert_eq!(parse_snap_override("en"), None);
|
||||
assert_eq!(parse_snap_override("mi"), None);
|
||||
assert_eq!(parse_snap_override("ce"), None);
|
||||
assert_eq!(parse_snap_override("p"), None);
|
||||
assert_eq!(parse_snap_override("no"), None);
|
||||
assert_eq!(parse_snap_override("_m"), None);
|
||||
assert_eq!(parse_snap_override(""), None);
|
||||
|
||||
// Non-matching tokens
|
||||
assert_eq!(parse_snap_override("line"), None);
|
||||
assert_eq!(parse_snap_override("circle"), None);
|
||||
assert_eq!(parse_snap_override("10,20"), None);
|
||||
assert_eq!(parse_snap_override("@5<45"), None);
|
||||
assert_eq!(parse_snap_override("undo"), None);
|
||||
assert_eq!(parse_snap_override("close"), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user