feat(ucs): interactive UCS command — UCS Z 90 typable everywhere (F2, #169)
Bare `UCS` now enters an interactive front-end (UcsCommand) that gathers the option then its value as separate steps, instead of the command line submitting `UCS` alone on the first Space. The front-end delegates execution back to the existing inline `UCS <option> …` handler via a new `CmdResult::Dispatch(String)`, so the coordinate-system math and the save/persist path are untouched. Now `UCS Z 90` works the same from the command line (Space-separated), headless automation, and any source that feeds the active command — closing the input half of #169. Inline `UCS Z 90` (one-shot / paste) still hits the inline handler directly. Second phase of routing input sources through the unified step driver: the interactive command is driven by feed_command like any tool. Adds headless test ucs_interactive_inline_args. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5fdd212592
commit
36d2f33301
6 changed files with 159 additions and 2 deletions
|
|
@ -565,6 +565,27 @@ mod tests {
|
||||||
assert_eq!(app.automation_op(r#"{"op":"run"}"#)["ok"], false);
|
assert_eq!(app.automation_op(r#"{"op":"run"}"#)["ok"], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ucs_interactive_inline_args() {
|
||||||
|
// `UCS Z 90` must drive the interactive UCS command step-by-step (option
|
||||||
|
// "Z" then value "90") and rotate the active UCS 90° about Z. (#169)
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app.automation_op(r#"{"op":"run","cmd":"UCS Z 90"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let ucs = app.tabs[i]
|
||||||
|
.active_ucs
|
||||||
|
.as_ref()
|
||||||
|
.expect("UCS Z 90 should set an active UCS");
|
||||||
|
// 90° about Z sends the X axis (1,0,0) → (0,1,0).
|
||||||
|
assert!(
|
||||||
|
ucs.x_axis.x.abs() < 1e-6 && (ucs.x_axis.y - 1.0).abs() < 1e-6,
|
||||||
|
"x_axis after UCS Z 90 = ({}, {})",
|
||||||
|
ucs.x_axis.x,
|
||||||
|
ucs.x_axis.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn save_then_open_round_trips() {
|
fn save_then_open_round_trips() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|
|
||||||
|
|
@ -567,6 +567,15 @@ impl OpenCADStudio {
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
let _ = self.dispatch_command(&cmd);
|
let _ = self.dispatch_command(&cmd);
|
||||||
}
|
}
|
||||||
|
CmdResult::Dispatch(cmd) => {
|
||||||
|
// End this interactive front-end, then run the assembled command
|
||||||
|
// through the normal dispatcher. Selection is left untouched.
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
let _ = self.dispatch_command(&cmd);
|
||||||
|
}
|
||||||
CmdResult::MatchEntityLayer { dest, src } => {
|
CmdResult::MatchEntityLayer { dest, src } => {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|
|
||||||
|
|
@ -3618,8 +3618,18 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── UCS management ───────────────────────────────────────────
|
// Bare UCS → interactive front-end (option then value as steps), so
|
||||||
cmd if cmd == "UCS" || cmd.starts_with("UCS ") => {
|
// `UCS Z 90` is typable in the command line and works headlessly.
|
||||||
|
// The front-end delegates back to the inline handler below. (#169)
|
||||||
|
"UCS" => {
|
||||||
|
use crate::modules::view::ucs_cmd::UcsCommand;
|
||||||
|
let cmd_obj = UcsCommand::new();
|
||||||
|
self.command_line.push_info(&cmd_obj.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UCS management (inline `UCS <option> …`) ─────────────────────
|
||||||
|
cmd if cmd.starts_with("UCS ") => {
|
||||||
use super::helpers::{ucs_rotated_z, ucs_to_wcs, ucs_z_axis};
|
use super::helpers::{ucs_rotated_z, ucs_to_wcs, ucs_z_axis};
|
||||||
use acadrust::tables::Ucs;
|
use acadrust::tables::Ucs;
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,12 @@ pub enum CmdResult {
|
||||||
/// End the selection-gather phase and re-dispatch the named command
|
/// End the selection-gather phase and re-dispatch the named command
|
||||||
/// with the gathered handles installed as the active scene selection.
|
/// with the gathered handles installed as the active scene selection.
|
||||||
Relaunch(String, Vec<Handle>),
|
Relaunch(String, Vec<Handle>),
|
||||||
|
/// End the command and dispatch the given command string. Used by an
|
||||||
|
/// interactive front-end that gathered its arguments step-by-step and
|
||||||
|
/// delegates execution to an existing inline command handler (e.g. UCS
|
||||||
|
/// collecting an option + value, then running `UCS Z 90`). Unlike
|
||||||
|
/// `Relaunch` it does not touch the selection.
|
||||||
|
Dispatch(String),
|
||||||
/// Move `dest` entities to the layer of the `src` entity; end command.
|
/// Move `dest` entities to the layer of the `src` entity; end command.
|
||||||
MatchEntityLayer { dest: Vec<Handle>, src: Handle },
|
MatchEntityLayer { dest: Vec<Handle>, src: Handle },
|
||||||
/// Copy all visual properties (layer/color/linetype/lineweight) from `src` to `dest`; end command.
|
/// Copy all visual properties (layer/color/linetype/lineweight) from `src` to `dest`; end command.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ mod solid;
|
||||||
mod tile_horiz;
|
mod tile_horiz;
|
||||||
mod tile_vert;
|
mod tile_vert;
|
||||||
mod tool_palettes;
|
mod tool_palettes;
|
||||||
|
pub mod ucs_cmd;
|
||||||
mod ucs_icon;
|
mod ucs_icon;
|
||||||
mod view_front;
|
mod view_front;
|
||||||
mod view_iso;
|
mod view_iso;
|
||||||
|
|
|
||||||
110
src/modules/view/ucs_cmd.rs
Normal file
110
src/modules/view/ucs_cmd.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
//! Interactive front-end for the UCS command.
|
||||||
|
//!
|
||||||
|
//! Bare `UCS` enters this command so the option and its value are entered
|
||||||
|
//! step-by-step — `UCS` ⏎ → option ⏎ → value ⏎ — and so a single line
|
||||||
|
//! `UCS Z 90` (typed in the command line with Space between tokens, or sent
|
||||||
|
//! headless as `{"op":"run","cmd":"UCS Z 90"}`) feeds the option then the value
|
||||||
|
//! as text steps. Execution is delegated to the existing inline `UCS …` handler
|
||||||
|
//! via [`CmdResult::Dispatch`], so the coordinate-system math and persistence
|
||||||
|
//! stay in one place. (#169)
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct UcsCommand {
|
||||||
|
/// The chosen option keyword (uppercased), once entered; `None` until then.
|
||||||
|
option: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UcsCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options that take no further argument and execute immediately.
|
||||||
|
fn is_zero_arg(opt: &str) -> bool {
|
||||||
|
matches!(opt, "W" | "WORLD" | "LIST" | "?")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options whose argument is a coordinate (so a click is accepted too).
|
||||||
|
fn takes_point(opt: &str) -> bool {
|
||||||
|
matches!(opt, "ORIGIN" | "O")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options that expect one more typed argument (angle or name).
|
||||||
|
fn takes_value(opt: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
opt,
|
||||||
|
"Z" | "X" | "Y" | "ORIGIN" | "O" | "SAVE" | "S" | "DELETE" | "DEL" | "D"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for UcsCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"UCS"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.option.as_deref() {
|
||||||
|
None => "UCS option [World/Z/X/Y/Origin/Save/Delete] or name:".into(),
|
||||||
|
Some("Z") => "UCS rotation angle about Z (degrees):".into(),
|
||||||
|
Some("X") => "UCS rotation angle about X (degrees):".into(),
|
||||||
|
Some("Y") => "UCS rotation angle about Y (degrees):".into(),
|
||||||
|
Some("ORIGIN") | Some("O") => "UCS new origin point:".into(),
|
||||||
|
Some("SAVE") | Some("S") => "UCS name to save current UCS as:".into(),
|
||||||
|
Some("DELETE") | Some("DEL") | Some("D") => "UCS name of UCS to delete:".into(),
|
||||||
|
Some(_) => "UCS value:".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let t = text.trim();
|
||||||
|
match self.option.take() {
|
||||||
|
// First token: the option keyword (or a named UCS to restore).
|
||||||
|
None => {
|
||||||
|
if t.is_empty() {
|
||||||
|
// Bare Enter → list (delegate to inline `UCS`).
|
||||||
|
return Some(CmdResult::Dispatch("UCS LIST".into()));
|
||||||
|
}
|
||||||
|
let up = t.to_uppercase();
|
||||||
|
if Self::is_zero_arg(&up) {
|
||||||
|
Some(CmdResult::Dispatch(format!("UCS {up}")))
|
||||||
|
} else if Self::takes_value(&up) {
|
||||||
|
// Needs a value next; keep the command active and re-prompt.
|
||||||
|
self.option = Some(up);
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
} else {
|
||||||
|
// Not a keyword → a named UCS to activate.
|
||||||
|
Some(CmdResult::Dispatch(format!("UCS {t}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Second token: the option's value → run the assembled command.
|
||||||
|
Some(opt) => Some(CmdResult::Dispatch(format!("UCS {opt} {t}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
// A clicked point only makes sense for the Origin option; otherwise a
|
||||||
|
// stray click is ignored (keep waiting for the typed keyword / value).
|
||||||
|
if matches!(self.option.as_deref(), Some(o) if Self::takes_point(o)) {
|
||||||
|
let opt = self.option.take().unwrap_or_default();
|
||||||
|
return CmdResult::Dispatch(format!("UCS {opt} {},{},{}", pt.x, pt.y, pt.z));
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.option.take() {
|
||||||
|
// No option chosen yet → behave like bare `UCS` (list).
|
||||||
|
None => CmdResult::Dispatch("UCS LIST".into()),
|
||||||
|
// Option chosen but no value supplied → cancel cleanly.
|
||||||
|
Some(_) => CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue