forked from 1b-agent/opencadstudio-upstream
feat(plugin): InteractiveCommand hook for click-to-place (API v2)
Plugins could dispatch commands but not register interactive (click-to- place) tools — the gap mf4633 flagged on #100 for Storm Sewer's SS_INLET / SS_PIPE. Add an InteractiveCommand trait + CommandStep to ocs_plugin_api and HostApi::start_interactive; a host adapter bridges it to the internal CadCommand, so a plugin tool drives the host's point-collection flow. Hybrid by construction: the same command works by clicking in the viewport AND by feeding coordinates over --serve (run "CMD x,y x,y"). Adding a HostApi method changes the contract vtable, so API_VERSION bumps to 2 — v1 plugin binaries are now refused at load. Part of the #100 extensibility epic. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
8d3d0c30cc
commit
931eb908a9
5 files changed
+141
-4
No files matched your search
@@ -179,6 +179,13 @@ impl HostApi for HostSession<'_> {
|
||||
fn push_error(&mut self, msg: &str) {
|
||||
self.push_error(msg)
|
||||
}
|
||||
fn start_interactive(
|
||||
&mut self,
|
||||
command: Box<dyn ocs_plugin_api::host::InteractiveCommand>,
|
||||
) {
|
||||
self.app.tabs[self.tab].active_cmd =
|
||||
Some(Box::new(PluginInteractiveAdapter { inner: command }));
|
||||
}
|
||||
fn plugin_state_any(&self, plugin_id: &str) -> Option<&(dyn Any + Send + Sync)> {
|
||||
self.app.tabs[self.tab]
|
||||
.plugin_state
|
||||
@@ -207,6 +214,42 @@ impl HostApi for HostSession<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridges a plugin's [`InteractiveCommand`](ocs_plugin_api::host::InteractiveCommand)
|
||||
/// to the host's internal `CadCommand`, so a plugin tool drives the host's
|
||||
/// point-collection flow (viewport clicks or `--serve` coordinates) just like a
|
||||
/// built-in tool.
|
||||
struct PluginInteractiveAdapter {
|
||||
inner: Box<dyn ocs_plugin_api::host::InteractiveCommand>,
|
||||
}
|
||||
|
||||
impl crate::command::CadCommand for PluginInteractiveAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"PLUGIN"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
self.inner.prompt()
|
||||
}
|
||||
fn on_point(&mut self, pt: glam::Vec3) -> crate::command::CmdResult {
|
||||
plugin_step_to_result(self.inner.on_point([pt.x as f64, pt.y as f64, pt.z as f64]))
|
||||
}
|
||||
fn on_enter(&mut self) -> crate::command::CmdResult {
|
||||
plugin_step_to_result(self.inner.on_enter())
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_step_to_result(
|
||||
step: ocs_plugin_api::host::CommandStep,
|
||||
) -> crate::command::CmdResult {
|
||||
use crate::command::CmdResult;
|
||||
use ocs_plugin_api::host::CommandStep;
|
||||
match step {
|
||||
CommandStep::NeedPoint => CmdResult::NeedPoint,
|
||||
CommandStep::Commit(e) => CmdResult::CommitEntity(e),
|
||||
CommandStep::CommitAndEnd(e) => CmdResult::CommitAndExit(e),
|
||||
CommandStep::Done | CommandStep::Cancel => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -258,4 +301,43 @@ mod tests {
|
||||
*host::plugin_state_mut::<u32>(host, "opencad.demo").unwrap() = 100;
|
||||
assert_eq!(*host::plugin_state::<u32>(&*host, "opencad.demo").unwrap(), 100);
|
||||
}
|
||||
|
||||
/// A plugin command: second point commits a Point and ends.
|
||||
struct PlacePoint {
|
||||
got_first: bool,
|
||||
}
|
||||
impl ocs_plugin_api::host::InteractiveCommand for PlacePoint {
|
||||
fn prompt(&self) -> String {
|
||||
"Pick a point".into()
|
||||
}
|
||||
fn on_point(&mut self, pt: [f64; 3]) -> ocs_plugin_api::host::CommandStep {
|
||||
use ocs_plugin_api::host::CommandStep;
|
||||
if self.got_first {
|
||||
let p = acadrust::entities::Point::at(acadrust::types::Vector3::new(
|
||||
pt[0], pt[1], pt[2],
|
||||
));
|
||||
CommandStep::CommitAndEnd(acadrust::EntityType::Point(p))
|
||||
} else {
|
||||
self.got_first = true;
|
||||
CommandStep::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_interactive_command_drives_host_flow() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.tabs[0].is_start = false;
|
||||
{
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
host.start_interactive(Box::new(PlacePoint { got_first: false }));
|
||||
}
|
||||
assert!(app.tabs[0].active_cmd.is_some());
|
||||
for pt in [glam::Vec3::new(0.0, 0.0, 0.0), glam::Vec3::new(5.0, 5.0, 0.0)] {
|
||||
let r = app.tabs[0].active_cmd.as_mut().unwrap().on_point(pt);
|
||||
let _ = app.apply_cmd_result(r);
|
||||
}
|
||||
assert_eq!(app.tabs[0].scene.document.entities().count(), 1);
|
||||
assert!(app.tabs[0].active_cmd.is_none(), "command should have ended");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user