diff --git a/src/app/automation.rs b/src/app/automation.rs index 585c3ba3a..6ea9c4f3d 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -372,12 +372,35 @@ impl OpenCADStudio { let _ = self.dispatch_command(cmd); return; } + // Plugin commands parse their own inline arguments from the whole line + // (e.g. `HC_PIPE 2B 2C 1.25 0.013`), so offer the full command to plugin + // dispatch first. A built-in interactive tool matches only its bare name + // (`LINE`), so the full line is not a plugin command and falls through to + // the first-word + fed-tokens path below. (#162) + if crate::plugin::try_dispatch(self, i, cmd) { + // The plugin either committed immediately (inline args consumed) or + // installed an interactive command — feed any remaining tokens and + // finish it on Enter, as for a built-in tool. + self.finish_headless_interactive(&tokens); + return; + } let _ = self.dispatch_command(tokens[0]); if self.tabs[i].active_cmd.is_none() { // Not an interactive tool — an inline-argument command. let _ = self.dispatch_command(cmd); return; } + self.finish_headless_interactive(&tokens); + } + + /// Feed `tokens[1..]` to the active interactive command as points / option + /// keywords, then terminate it as if Enter were pressed (LINE / PLINE finish + /// on Enter). No-op when no command is active. + fn finish_headless_interactive(&mut self, tokens: &[&str]) { + let i = self.active_tab; + if self.tabs[i].active_cmd.is_none() { + return; + } self.last_point = None; for tok in &tokens[1..] { if self.tabs[i].active_cmd.is_none() { @@ -386,17 +409,15 @@ impl OpenCADStudio { self.feed_active_cmd(tok); } // Terminate a still-open command (LINE / PLINE finish on Enter). - if self.tabs[i].active_cmd.is_some() { - if let Some(r) = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()) { - let _ = self.apply_cmd_result(r); - } - } + let _ = self.feed_command(crate::command::StepInput::Enter); } - /// Feed one token to the active command. When the command is picking an - /// existing entity, the token is a hex handle; otherwise a coordinate point - /// or an option keyword. + /// Classify one headless token into a [`StepInput`] and route it through the + /// shared [`OpenCADStudio::feed_command`]. When the command is picking an + /// existing entity the token is a hex handle; otherwise it is a coordinate + /// point or an option keyword / value. fn feed_active_cmd(&mut self, token: &str) { + use crate::command::StepInput; let i = self.active_tab; // Object-pick step: the token is a handle (as returned by `query`). if self.tabs[i] @@ -419,13 +440,7 @@ impl OpenCADStudio { ) }) .unwrap_or(glam::Vec3::ZERO); - if let Some(r) = self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_entity_pick(handle, pt.as_dvec3())) - { - let _ = self.apply_cmd_result(r); - } + let _ = self.feed_command(StepInput::EntityPick(handle, pt.as_dvec3())); } return; } @@ -436,15 +451,9 @@ impl OpenCADStudio { } } self.last_point = Some(pt); - if let Some(r) = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3())) { - let _ = self.apply_cmd_result(r); - } - } else if let Some(r) = self.tabs[i] - .active_cmd - .as_mut() - .and_then(|c| c.on_text_input(token)) - { - let _ = self.apply_cmd_result(r); + let _ = self.feed_command(StepInput::Point(pt.as_dvec3())); + } else { + let _ = self.feed_command(StepInput::Text(token.to_string())); } } diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 3c5b4f256..39cba5ede 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -1,9 +1,38 @@ use super::{Message, OpenCADStudio}; -use crate::command::CmdResult; +use crate::command::{CmdResult, StepInput}; use acadrust::Handle; use iced::Task; impl OpenCADStudio { + /// Drive the active command's step machine with one [`StepInput`], then + /// apply the result. This is the single entry point every input source + /// (command line, headless, dynamic input, plugin API, viewport) funnels + /// through, so the routing from input → `on_*` method → `apply_cmd_result` + /// lives in exactly one place. No-op when no command is active. + pub(super) fn feed_command(&mut self, input: StepInput) -> Task { + let i = self.active_tab; + let result: Option = { + let Some(cmd) = self.tabs[i].active_cmd.as_mut() else { + return Task::none(); + }; + match input { + StepInput::Point(p) => Some(cmd.on_point(p)), + StepInput::Text(s) => cmd.on_text_input(&s), + StepInput::EntityPick(h, p) => Some(cmd.on_entity_pick(h, p)), + StepInput::StructurePick(h, p) => Some(cmd.on_structure_pick(h, p)), + StepInput::SelectionComplete(hs) => Some(cmd.on_selection_complete(hs)), + StepInput::Tangent(o, p) => Some(cmd.on_tangent_point(o, p)), + StepInput::EditorClosed(c) => Some(cmd.on_editor_closed(c)), + StepInput::Enter => Some(cmd.on_enter()), + StepInput::Escape => Some(cmd.on_escape()), + } + }; + match result { + Some(r) => self.apply_cmd_result(r), + None => Task::none(), + } + } + pub(super) fn apply_cmd_result(&mut self, result: CmdResult) -> Task { let i = self.active_tab; match result { diff --git a/src/command/mod.rs b/src/command/mod.rs index b2bc52694..fc3c20cb6 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -46,6 +46,39 @@ pub enum TangentObject { Circle { center: DVec3, radius: f64 }, } +/// One unit of input to the active command's step machine. +/// +/// Every input source — the GUI command line, the headless automation feeder, +/// dynamic input, the plugin API, and the viewport (clicks / picks / selection +/// / tangent) — translates its raw input into one of these and routes it +/// through `OpenCADStudio::feed_command`, so a single place drives the command +/// regardless of where the step came from. +/// +/// Variants are wired up incrementally as each source is migrated onto +/// `feed_command`; `#[allow(dead_code)]` covers those not yet constructed. +#[allow(dead_code)] +pub enum StepInput { + /// A coordinate (`on_point`). + Point(DVec3), + /// A typed token: keyword, option letter, distance, or value + /// (`on_text_input`). + Text(String), + /// An object pick (`on_entity_pick`). + EntityPick(Handle, DVec3), + /// A sub-structure pick — vertex / edge / face (`on_structure_pick`). + StructurePick(Handle, DVec3), + /// A completed selection set (`on_selection_complete`). + SelectionComplete(Vec), + /// A tangent-snap target (`on_tangent_point`). + Tangent(TangentObject, DVec3), + /// The text / MText editor closed, with its commit flag (`on_editor_closed`). + EditorClosed(bool), + /// Advance / finish the current step — Enter, or Space-as-Enter (`on_enter`). + Enter, + /// Cancel the command (`on_escape`). + Escape, +} + // ── Result token ────────────────────────────────────────────────────────── /// Returned by every `CadCommand` method to tell main.rs what to do.