From 707c143212a99c47a63c3f56a790678b552690b0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 18 Jun 2026 01:43:38 +0300 Subject: [PATCH] feat(plugin): object-pick acquisition for interactive commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend InteractiveCommand with needs_object_pick / on_object_pick so a plugin tool can prompt the user to pick an existing entity (handle + point) rather than a free point — the structure-pick path Storm Sewer's SS_PIPE needs to connect existing structures. The host adapter delegates to the internal entity-pick flow; over --serve the pick is fed as a hex handle. Stays API v2 (no released plugin implements InteractiveCommand yet). Co-Authored-By: Claude Opus 4.8 --- crates/ocs_plugin_api/src/host.rs | 13 +++++++ docs/plugin-architecture.md | 6 +++ src/app/automation.rs | 36 +++++++++++++++++- src/app/plugin_host.rs | 61 +++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index 02f3ebf6..9b6ef0c7 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -45,6 +45,19 @@ pub trait InteractiveCommand: Send { fn on_enter(&mut self) -> CommandStep { CommandStep::Cancel } + + /// When `true`, the next input picks an existing **entity** (the user clicks + /// on it; over `--serve`, a handle is supplied) rather than a free point — + /// the host then calls [`on_object_pick`](Self::on_object_pick). Use this to + /// reference existing geometry (e.g. connect a pipe between two structures). + fn needs_object_pick(&self) -> bool { + false + } + /// An existing entity was picked: its `handle` and the pick point. Read the + /// entity's data (XDATA / geometry) via `HostApi`, keyed by the handle. + fn on_object_pick(&mut self, _handle: Handle, _pt: [f64; 3]) -> CommandStep { + CommandStep::Cancel + } } /// The outcome of an [`InteractiveCommand`] step. diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 1b5e68fe..ea02f5bc 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -243,6 +243,12 @@ command works by clicking in the viewport and by feeding coordinates over the API to **v2** — the added `HostApi` method changes the contract's vtable, so v1 binaries are refused at load. +To reference **existing** geometry (e.g. connect a pipe between two structures), +set `needs_object_pick() -> true`; the host then calls +`on_object_pick(handle, pt)` with the clicked entity's handle (read its +XDATA/geometry via `HostApi`). Over `--serve` the pick is supplied as a hex +handle: `run "MY_CMD 2F 30"`. + ### XDATA — domain persistence Store domain data on entities as XDATA (under your `xdata_apps` ids), not in a diff --git a/src/app/automation.rs b/src/app/automation.rs index dde192e0..b8a6224f 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -249,10 +249,42 @@ impl OpenCADStudio { } } - /// Feed one token to the active command: a coordinate becomes a point, any - /// other token an option keyword. + /// 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. fn feed_active_cmd(&mut self, token: &str) { let i = self.active_tab; + // Object-pick step: the token is a handle (as returned by `query`). + if self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|c| c.needs_entity_pick()) + { + if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) { + let handle = acadrust::Handle::new(v); + let pt = self.tabs[i] + .scene + .document + .get_entity(handle) + .map(|e| { + let bb = e.as_entity().bounding_box(); + glam::Vec3::new( + ((bb.min.x + bb.max.x) * 0.5) as f32, + ((bb.min.y + bb.max.y) * 0.5) as f32, + 0.0, + ) + }) + .unwrap_or(glam::Vec3::ZERO); + if let Some(r) = self.tabs[i] + .active_cmd + .as_mut() + .map(|c| c.on_entity_pick(handle, pt)) + { + let _ = self.apply_cmd_result(r); + } + } + return; + } if let Some((mut pt, kind)) = super::helpers::parse_coord(token) { if matches!(kind, super::helpers::CoordKind::Relative) { if let Some(base) = self.last_point { diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 353519d3..47c14377 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -235,6 +235,15 @@ impl crate::command::CadCommand for PluginInteractiveAdapter { fn on_enter(&mut self) -> crate::command::CmdResult { plugin_step_to_result(self.inner.on_enter()) } + fn needs_entity_pick(&self) -> bool { + self.inner.needs_object_pick() + } + fn on_entity_pick(&mut self, handle: Handle, pt: glam::Vec3) -> crate::command::CmdResult { + plugin_step_to_result( + self.inner + .on_object_pick(handle, [pt.x as f64, pt.y as f64, pt.z as f64]), + ) + } } fn plugin_step_to_result( @@ -340,4 +349,56 @@ mod tests { assert_eq!(app.tabs[0].scene.document.entities().count(), 1); assert!(app.tabs[0].active_cmd.is_none(), "command should have ended"); } + + /// A plugin command that picks an existing object, then marks it. + struct PickThenMark; + impl ocs_plugin_api::host::InteractiveCommand for PickThenMark { + fn prompt(&self) -> String { + "Pick an object".into() + } + fn on_point(&mut self, _pt: [f64; 3]) -> ocs_plugin_api::host::CommandStep { + ocs_plugin_api::host::CommandStep::Cancel + } + fn needs_object_pick(&self) -> bool { + true + } + fn on_object_pick( + &mut self, + _handle: acadrust::Handle, + pt: [f64; 3], + ) -> ocs_plugin_api::host::CommandStep { + let p = acadrust::entities::Point::at(acadrust::types::Vector3::new( + pt[0], pt[1], pt[2], + )); + ocs_plugin_api::host::CommandStep::CommitAndEnd(acadrust::EntityType::Point(p)) + } + } + + #[test] + fn plugin_object_pick_routes_to_command() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let target = { + let mut host = HostSession::new(&mut app, 0); + let h = host.add_entity(acadrust::EntityType::Point( + acadrust::entities::Point::at(acadrust::types::Vector3::new(3.0, 4.0, 0.0)), + )); + host.start_interactive(Box::new(PickThenMark)); + h + }; + // The command requested an entity pick, not a free point. + assert!(app.tabs[0] + .active_cmd + .as_ref() + .unwrap() + .needs_entity_pick()); + let r = app.tabs[0] + .active_cmd + .as_mut() + .unwrap() + .on_entity_pick(target, glam::Vec3::new(3.0, 4.0, 0.0)); + let _ = app.apply_cmd_result(r); + // Original point + the mark the command committed. + assert_eq!(app.tabs[0].scene.document.entities().count(), 2); + } } \ No newline at end of file