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 {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue