forked from 1b-agent/opencadstudio-upstream
feat(modify): commercial solutions keyword options for MOVE, COPY, FILLET, CHAMFER, OFFSET
MOVE: Displacement, Enter at the second point uses the base as the vector. COPY: Displacement, mOde (Single/Multiple), Undo of the last copy. FILLET: Undo of the last fillet (the command refreshes its entity cache through the new CadCommand::on_document_undone hook after the host undoes the document step). CHAMFER: Multiple and Undo. OFFSET: Erase (the offset replaces its source), Layer (Current/Source), Undo, Exit. POLY3D, TRACE, MIRROR and MEASUREGEOM expose the keywords they already accepted as options, so the context menu and option buttons list them. The pinned prompt now keeps informational bracket groups such as "[3 objects]" and only drops real option listings when the buttons take over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0bc44c4f6
commit
d753874fe4
32 files changed
+760
-54
No files matched your search
@@ -28,6 +28,8 @@ pub fn tool() -> ToolDef {
|
||||
enum Step {
|
||||
Base,
|
||||
Placing(DVec3),
|
||||
/// Displacement option: the next point / typed coordinate is the vector.
|
||||
Displacement,
|
||||
}
|
||||
|
||||
pub struct CopyCommand {
|
||||
@@ -39,6 +41,10 @@ pub struct CopyCommand {
|
||||
array_count: Option<usize>,
|
||||
/// True while the next typed value is captured as the array item count.
|
||||
awaiting_count: bool,
|
||||
/// True while the mOde prompt (Single / Multiple) is up.
|
||||
awaiting_mode: bool,
|
||||
/// COPYMODE: Single ends the command after the first copy.
|
||||
single: bool,
|
||||
}
|
||||
|
||||
impl CopyCommand {
|
||||
@@ -50,6 +56,8 @@ impl CopyCommand {
|
||||
count: 0,
|
||||
array_count: None,
|
||||
awaiting_count: false,
|
||||
awaiting_mode: false,
|
||||
single: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +71,9 @@ impl CadCommand for CopyCommand {
|
||||
if self.awaiting_count {
|
||||
return crate::tr!("command-copy", "array-count");
|
||||
}
|
||||
if self.awaiting_mode {
|
||||
return crate::tr!("command-copy", "mode");
|
||||
}
|
||||
match &self.step {
|
||||
Step::Base => crate::tr!(
|
||||
"command-copy", "base",
|
||||
@@ -85,6 +96,35 @@ impl CadCommand for CopyCommand {
|
||||
)
|
||||
}
|
||||
}
|
||||
Step::Displacement => crate::tr!("command-copy", "displacement"),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
use crate::command::CmdOption;
|
||||
if self.awaiting_count {
|
||||
return Vec::new();
|
||||
}
|
||||
if self.awaiting_mode {
|
||||
return vec![
|
||||
CmdOption::new("Single", "S"),
|
||||
CmdOption::new("Multiple", "M"),
|
||||
];
|
||||
}
|
||||
match self.step {
|
||||
Step::Base => vec![
|
||||
CmdOption::new("Displacement", "D"),
|
||||
CmdOption::new("mOde", "O"),
|
||||
],
|
||||
Step::Placing(_) => {
|
||||
let mut opts = vec![CmdOption::new("Array", "A")];
|
||||
if self.count > 0 {
|
||||
opts.push(CmdOption::new("Undo", "U"));
|
||||
}
|
||||
opts.push(CmdOption::enter("Exit"));
|
||||
opts
|
||||
}
|
||||
Step::Displacement => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +146,12 @@ impl CadCommand for CopyCommand {
|
||||
.map(|k| EntityTransform::Translate(delta * k as f64))
|
||||
.collect();
|
||||
CmdResult::BatchCopy(self.handles.clone(), transforms)
|
||||
} else if self.single {
|
||||
// Single mode: one copy, then done.
|
||||
CmdResult::BatchCopy(
|
||||
self.handles.clone(),
|
||||
vec![EntityTransform::Translate(delta)],
|
||||
)
|
||||
} else {
|
||||
self.count += 1;
|
||||
CmdResult::CopySelected(
|
||||
@@ -114,15 +160,24 @@ impl CadCommand for CopyCommand {
|
||||
)
|
||||
}
|
||||
}
|
||||
// The point (typically typed as `dx,dy`) is the displacement itself.
|
||||
Step::Displacement => CmdResult::BatchCopy(
|
||||
self.handles.clone(),
|
||||
vec![EntityTransform::Translate(pt)],
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Cancel an in-progress Array count entry; otherwise finish.
|
||||
// Cancel an in-progress Array count / mode entry; otherwise finish.
|
||||
if self.awaiting_count {
|
||||
self.awaiting_count = false;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.awaiting_mode {
|
||||
self.awaiting_mode = false;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
@@ -130,13 +185,15 @@ impl CadCommand for CopyCommand {
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::Placing(_))
|
||||
matches!(self.step, Step::Placing(_) | Step::Base)
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
// Accept the Array keyword while placing, but route the typed item
|
||||
// count straight through on_text_input.
|
||||
matches!(self.step, Step::Placing(_)) && !self.awaiting_count
|
||||
// Keywords ride along with the point picks, but a typed item count
|
||||
// or mode answer goes straight through on_text_input.
|
||||
matches!(self.step, Step::Placing(_) | Step::Base)
|
||||
&& !self.awaiting_count
|
||||
&& !self.awaiting_mode
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
@@ -149,15 +206,46 @@ impl CadCommand for CopyCommand {
|
||||
self.awaiting_count = false;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
match text.trim().to_uppercase().as_str() {
|
||||
"A" | "ARRAY" => {
|
||||
let upper = text.trim().to_uppercase();
|
||||
if self.awaiting_mode {
|
||||
match upper.as_str() {
|
||||
"S" | "SINGLE" => self.single = true,
|
||||
"M" | "MULTIPLE" => self.single = false,
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
}
|
||||
self.awaiting_mode = false;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
match (&self.step, upper.as_str()) {
|
||||
(Step::Base, "D" | "DISPLACEMENT") => {
|
||||
self.step = Step::Displacement;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::Base, "O" | "MODE") => {
|
||||
self.awaiting_mode = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::Placing(_), "A" | "ARRAY") => {
|
||||
self.awaiting_count = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
// Undo: take back the last copy placed by this command.
|
||||
(Step::Placing(_), "U" | "UNDO") if self.count > 0 => {
|
||||
self.count -= 1;
|
||||
Some(CmdResult::UndoDocument)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if matches!(self.step, Step::Placing(_)) && self.count > 0 {
|
||||
self.on_text_input("U")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
||||
let Step::Placing(base) = &self.step else {
|
||||
return vec![];
|
||||
@@ -180,3 +268,53 @@ impl CadCommand for CopyCommand {
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn keywords(cmd: &CopyCommand) -> Vec<String> {
|
||||
cmd.options().into_iter().map(|o| o.keyword).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_single_exits_after_one_copy() {
|
||||
let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]);
|
||||
assert_eq!(keywords(&cmd), ["D", "O"]);
|
||||
assert!(matches!(cmd.on_text_input("O"), Some(CmdResult::NeedPoint)));
|
||||
assert_eq!(keywords(&cmd), ["S", "M"]);
|
||||
assert!(matches!(cmd.on_text_input("S"), Some(CmdResult::NeedPoint)));
|
||||
cmd.on_point(DVec3::ZERO);
|
||||
match cmd.on_point(DVec3::new(10.0, 0.0, 0.0)) {
|
||||
CmdResult::BatchCopy(_, transforms) => assert_eq!(transforms.len(), 1),
|
||||
_ => panic!("single mode should copy once and end"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_mode_keeps_copying_and_undo_takes_one_back() {
|
||||
let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]);
|
||||
cmd.on_point(DVec3::ZERO);
|
||||
assert_eq!(keywords(&cmd), ["A", ""], "no Undo before a copy is placed");
|
||||
assert!(matches!(
|
||||
cmd.on_point(DVec3::new(10.0, 0.0, 0.0)),
|
||||
CmdResult::CopySelected(..)
|
||||
));
|
||||
assert_eq!(keywords(&cmd), ["A", "U", ""]);
|
||||
assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::UndoDocument)));
|
||||
assert_eq!(keywords(&cmd), ["A", ""]);
|
||||
assert!(cmd.on_text_input("U").is_none(), "nothing left to undo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn displacement_option_copies_by_vector() {
|
||||
let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]);
|
||||
cmd.on_text_input("D");
|
||||
match cmd.on_point(DVec3::new(1.0, 2.0, 0.0)) {
|
||||
CmdResult::BatchCopy(_, transforms) => {
|
||||
assert!(matches!(transforms[0], EntityTransform::Translate(d) if d == DVec3::new(1.0, 2.0, 0.0)));
|
||||
}
|
||||
_ => panic!("expected one copy by the displacement"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user