forked from 1b-agent/opencadstudio-upstream
feat(ui): feature-parity context menu — Snap Overrides, transparent Pan/Zoom, ARC keyword flow
The command menu now follows the commercial solutions' layout row for row: Enter, Cancel,
Recent Input ▸, the command's keywords, Snap Overrides ▸ (every object snap
with its glyph, Mid Between 2 Points, None, Osnap Settings…), then Pan and
Zoom. Pan / Zoom run transparently while a command is active ('PAN,
'ZOOM …): navigation modes leave the command untouched and an interactive
ZOOM Window parks it and resumes it afterwards. Rows carry an icon gutter
so labels align; the Enter row shows a default value ("<0.5000>") as its
hint instead of a second finish row.
ARC gets the commercial solutions' prompt flow with all keywords: start [Center], second
point [Center/End], end; after Center: end [Angle/chord Length]; after
End: center [Angle/Direction/Radius]; Enter continues tangent. The
previous default (Center/Start/End with method codes) stays as ARC_CSE.
Object snaps gain a one-shot "None" override. New strings in all locales.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
96df4928c7
commit
02cd98e790
34 files changed
+1305
-67
No files matched your search
@@ -140,6 +140,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
MenuAction::Command(cmd) => self.update(Message::Command(cmd)),
|
||||
MenuAction::Mtp => self.update(Message::SnapOverrideMtp),
|
||||
MenuAction::SnapOverride(t) => self.update(Message::SnapOverridePick(t)),
|
||||
MenuAction::SnapOverrideNone => self.update(Message::SnapOverrideNone),
|
||||
MenuAction::DeleteSelected => self.update(Message::DeleteSelected),
|
||||
MenuAction::SelectSimilar => self.update(Message::SelectSimilar),
|
||||
MenuAction::InvertSelection => self.update(Message::InvertSelection),
|
||||
@@ -455,7 +457,8 @@ mod tests {
|
||||
assert_eq!(acts[1], MenuAction::Cancel);
|
||||
assert!(acts.contains(&MenuAction::Option("C".into())));
|
||||
assert!(acts.contains(&MenuAction::Option("U".into())));
|
||||
assert!(acts.contains(&MenuAction::Mtp));
|
||||
assert!(acts.contains(&MenuAction::ToggleSubmenu(SubmenuId::SnapOverrides)));
|
||||
assert!(acts.contains(&MenuAction::Command("'PAN".into())));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -831,3 +834,110 @@ mod tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod transparent_tests {
|
||||
use super::*;
|
||||
use crate::command::StepInput;
|
||||
use glam::DVec3;
|
||||
|
||||
fn with_stack(f: impl FnOnce() + Send + 'static) {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(32 * 1024 * 1024)
|
||||
.spawn(f)
|
||||
.expect("spawn test thread")
|
||||
.join()
|
||||
.unwrap_or_else(|e| std::panic::resume_unwind(e));
|
||||
}
|
||||
|
||||
fn line_app() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||
let _ = app.dispatch_command("LINE");
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
|
||||
app
|
||||
}
|
||||
|
||||
fn active(app: &OpenCADStudio) -> Option<&'static str> {
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent_pan_keeps_the_command_running() {
|
||||
with_stack(|| {
|
||||
let mut app = line_app();
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::Command("'PAN".into())));
|
||||
assert!(app.tabs[0].pan_mode);
|
||||
assert_eq!(active(&app), Some("LINE"));
|
||||
// Esc leaves pan mode; the line is still being drawn.
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert!(!app.tabs[0].pan_mode);
|
||||
assert_eq!(active(&app), Some("LINE"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent_zoom_window_resumes_the_command() {
|
||||
with_stack(|| {
|
||||
let mut app = line_app();
|
||||
let _ = app.dispatch_command("'ZOOM W");
|
||||
assert_eq!(active(&app), Some("ZOOM WINDOW"));
|
||||
assert!(app.tabs[0].transparent_resume);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(-5.0, -5.0, 0.0)));
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(5.0, 5.0, 0.0)));
|
||||
assert_eq!(active(&app), Some("LINE"), "LINE resumes after the zoom");
|
||||
assert!(!app.tabs[0].transparent_resume);
|
||||
assert!(app.tabs[0].suspended_cmd.is_none());
|
||||
// The line still has its first point: the next pick draws a segment.
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(10.0, 0.0, 0.0)));
|
||||
let lines = app.tabs[0]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| matches!(e, acadrust::EntityType::Line(_)))
|
||||
.count();
|
||||
assert_eq!(lines, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_prompt_handoff_and_escape_both_resume() {
|
||||
with_stack(|| {
|
||||
let mut app = line_app();
|
||||
let _ = app.dispatch_command("'ZOOM");
|
||||
let _ = app.feed_active_cmd("E"); // → ZOOM EXTENTS, one-shot
|
||||
assert_eq!(active(&app), Some("LINE"));
|
||||
let _ = app.dispatch_command("'ZOOM");
|
||||
assert_eq!(active(&app), Some("ZOOM WINDOW"));
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(active(&app), Some("LINE"));
|
||||
assert!(!app.tabs[0].transparent_resume);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_command_drops_a_parked_one() {
|
||||
with_stack(|| {
|
||||
let mut app = line_app();
|
||||
let _ = app.dispatch_command("'ZOOM");
|
||||
let _ = app.dispatch_command("CIRCLE");
|
||||
assert_eq!(active(&app), Some("CIRCLE"));
|
||||
assert!(app.tabs[0].suspended_cmd.is_none());
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(active(&app), None, "no stale LINE comes back");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_override_none_is_one_shot() {
|
||||
with_stack(|| {
|
||||
let mut app = line_app();
|
||||
app.snapper.snap_enabled = true;
|
||||
let _ = app.update(Message::ContextMenuPick(MenuAction::SnapOverrideNone));
|
||||
assert!(!app.snapper.snap_enabled);
|
||||
app.snapper.clear_override();
|
||||
assert!(app.snapper.snap_enabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -754,6 +754,14 @@ impl OpenCADStudio {
|
||||
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());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SnapOverrideMtp => {
|
||||
self.snap_override_popup = None;
|
||||
self.tabs[self.active_tab]
|
||||
|
||||
Reference in new issue
Block a user