refactor(commands): unified StepInput → feed_command step router (F0)

Introduce the single place every input source funnels through to drive the
active command's step machine: a `StepInput` vocabulary (one variant per
`CadCommand::on_*` method) and `OpenCADStudio::feed_command`, which routes
the input to the matching `on_*` and applies the result. Sources keep their
own classification (raw input → which StepInput); only the routing is
centralized.

Migrate the headless automation feeder onto it: feed_active_cmd builds a
StepInput, the terminator sends StepInput::Enter. No behavior change —
automation_ops_round_trip still passes.

Also fixes headless inline-argument loss for plugin commands (#162):
run_headless now offers the whole line to plugin dispatch first (plugins
parse their own inline args, e.g. `HC_PIPE 2B 2C 1.25 0.013`), falling back
to the first-word + fed-tokens path for built-in interactive tools.

First phase of routing all input sources (command line, dynamic input,
plugin API, viewport clicks) through one driver.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-25 12:24:17 +03:00
1 parent 69ea050124
commit 5fdd212592
3 files changed
+96 -25

No files matched your search

+33 -24
View File
@@ -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()));
}
}
+30 -1
View File
@@ -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<Message> {
let i = self.active_tab;
let result: Option<CmdResult> = {
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<Message> {
let i = self.active_tab;
match result {
+33
View File
@@ -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<Handle>),
/// 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.