feat(plugin): object-pick acquisition for interactive commands
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 <noreply@anthropic.com>
This commit is contained in:
parent
f5ac0a90cf
commit
707c143212
4 changed files with 114 additions and 2 deletions
|
|
@ -45,6 +45,19 @@ pub trait InteractiveCommand: Send {
|
||||||
fn on_enter(&mut self) -> CommandStep {
|
fn on_enter(&mut self) -> CommandStep {
|
||||||
CommandStep::Cancel
|
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.
|
/// The outcome of an [`InteractiveCommand`] step.
|
||||||
|
|
|
||||||
|
|
@ -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
|
API to **v2** — the added `HostApi` method changes the contract's vtable, so v1
|
||||||
binaries are refused at load.
|
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
|
### XDATA — domain persistence
|
||||||
|
|
||||||
Store domain data on entities as XDATA (under your `xdata_apps` ids), not in a
|
Store domain data on entities as XDATA (under your `xdata_apps` ids), not in a
|
||||||
|
|
|
||||||
|
|
@ -249,10 +249,42 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feed one token to the active command: a coordinate becomes a point, any
|
/// Feed one token to the active command. When the command is picking an
|
||||||
/// other token an option keyword.
|
/// existing entity, the token is a hex handle; otherwise a coordinate point
|
||||||
|
/// or an option keyword.
|
||||||
fn feed_active_cmd(&mut self, token: &str) {
|
fn feed_active_cmd(&mut self, token: &str) {
|
||||||
let i = self.active_tab;
|
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 let Some((mut pt, kind)) = super::helpers::parse_coord(token) {
|
||||||
if matches!(kind, super::helpers::CoordKind::Relative) {
|
if matches!(kind, super::helpers::CoordKind::Relative) {
|
||||||
if let Some(base) = self.last_point {
|
if let Some(base) = self.last_point {
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,15 @@ impl crate::command::CadCommand for PluginInteractiveAdapter {
|
||||||
fn on_enter(&mut self) -> crate::command::CmdResult {
|
fn on_enter(&mut self) -> crate::command::CmdResult {
|
||||||
plugin_step_to_result(self.inner.on_enter())
|
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(
|
fn plugin_step_to_result(
|
||||||
|
|
@ -340,4 +349,56 @@ mod tests {
|
||||||
assert_eq!(app.tabs[0].scene.document.entities().count(), 1);
|
assert_eq!(app.tabs[0].scene.document.entities().count(), 1);
|
||||||
assert!(app.tabs[0].active_cmd.is_none(), "command should have ended");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue