fix(plan): show options on activation instead of flipping the view (#326)

PLAN acted like a ViewCube TOP click the moment it ran — already on Top
meant an immediate flip to Bottom, and the options never appeared. It
now prompts "[Current ucs / Ucs / World] <Current>:" right on
activation and only changes the view once an option runs: Current =
plan of the active UCS, World = world XY, Ucs = prompts for a named UCS
(case-insensitive lookup). The new plan_snap goes straight to the plan
view with no flip-to-opposite cube behaviour.

KeywordCommand gains with_default(): a bare Enter at the verb step
dispatches the default verb (PLAN's <Current>) instead of cancelling.

Closes #326

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-22 16:53:32 +03:00
commit b32a9b4083
2 changed files with 100 additions and 4 deletions

View file

@ -394,10 +394,69 @@ impl OpenCADStudio {
// PLAN — look straight down at the drawing (top view). The optional
// World/Ucs/Current keyword is accepted; all map to the world top
// view for now.
cmd if cmd == "PLAN" || cmd.starts_with("PLAN ") => {
return Some(Task::done(Message::ViewCubeSnapWorld(
crate::scene::CubeRegion::Face(crate::scene::pipeline::viewcube::FACE_TOP),
)));
// PLAN — plan view of a UCS (#326). Shows its options right on
// activation instead of flipping the view like a cube click; the
// view only changes once an option (or the Current default) runs.
"PLAN" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"PLAN",
"PLAN [Current ucs / Ucs / World] <Current>:",
vec![
("Current ucs", "CURRENT", None),
("Ucs", "UCS", Some("PLAN UCS ucs name:")),
("World", "WORLD", None),
],
)
.with_default("CURRENT");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("PLAN ") => {
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
match sub.as_str() {
"" | "CURRENT" | "C" => {
let r = self.tabs[i].scene.viewcube_ucs_mat();
self.plan_snap(i, r);
self.command_line.push_output("PLAN: current UCS plan view.");
}
"WORLD" | "W" => {
self.plan_snap(i, glam::Mat4::IDENTITY);
self.command_line.push_output("PLAN: world plan view.");
}
"UCS" | "U" => {
let name = parts.get(2).map(|s| s.trim()).unwrap_or("");
if name.is_empty() {
self.command_line.push_info("Usage: PLAN UCS <name>");
} else {
let ucs = self.tabs[i]
.scene
.document
.ucss
.iter()
.find(|u| u.name.eq_ignore_ascii_case(name))
.cloned();
match ucs {
Some(u) => {
let r = crate::app::helpers::UcsXform::from_ucs(&u)
.rotation_mat();
self.plan_snap(i, r);
self.command_line.push_output(&format!(
"PLAN: plan view of UCS '{}'.",
u.name
));
}
None => self
.command_line
.push_error(&format!("UCS '{name}' not found.")),
}
}
}
_ => self
.command_line
.push_info("Usage: PLAN [CURRENT / UCS <name> / WORLD]"),
}
}
"VIEW" => {
@ -1152,4 +1211,25 @@ impl OpenCADStudio {
}
Some(self.finish_dispatch(cmd))
}
/// Snap to the plan view of the UCS whose rotation is `r_ucs`, straight —
/// no "already there → flip to the opposite face" cube behaviour (#326).
fn plan_snap(&mut self, i: usize, r_ucs: glam::Mat4) {
let eye_dir = r_ucs.transform_vector3(
crate::scene::CubeRegion::Face(crate::scene::pipeline::viewcube::FACE_TOP)
.snap_direction(),
);
if self.tabs[i].scene.active_viewport.is_some() {
self.tabs[i]
.scene
.mutate_active_viewport_camera(|c| c.snap_to_face(eye_dir, r_ucs));
} else {
self.tabs[i]
.scene
.camera
.borrow_mut()
.snap_to_face(eye_dir, r_ucs);
}
self.tabs[i].scene.camera_generation += 1;
}
}

View file

@ -332,9 +332,18 @@ pub struct KeywordCommand {
options: Vec<(&'static str, &'static str, Option<&'static str>)>,
/// Set once a value-taking verb is chosen: `(keyword, value prompt)`.
pending: Option<(&'static str, &'static str)>,
/// Keyword a bare Enter dispatches at the verb step (`<Default>` prompts,
/// e.g. PLAN's `<Current>`). `None` = Enter cancels, as before.
default: Option<&'static str>,
}
impl KeywordCommand {
/// Set the verb a bare Enter dispatches (shown as `<Default>` in prompts).
pub fn with_default(mut self, kw: &'static str) -> Self {
self.default = Some(kw);
self
}
pub fn new(
name: &'static str,
prompt: &'static str,
@ -345,6 +354,7 @@ impl KeywordCommand {
prompt,
options,
pending: None,
default: None,
}
}
}
@ -411,6 +421,12 @@ impl CadCommand for KeywordCommand {
}
fn on_enter(&mut self) -> CmdResult {
// Bare Enter at the verb step runs the default verb when one is set.
if self.pending.is_none() {
if let Some(kw) = self.default {
return CmdResult::Dispatch(format!("{} {kw}", self.name));
}
}
CmdResult::Cancel
}
}