feat(plugin): add on_cursor_move and PreviewWire for interactive rubber-band previews

This commit is contained in:
yztink committed 2026-09-23 02:03:16 -07:00
1 parent 133bfddba9
commit 5eda52626e
2 files changed
+100

No files matched your search

+32
View File
@@ -603,6 +603,38 @@ pub trait InteractiveCommand: Send {
fn on_object_pick(&mut self, _handle: Handle, _pt: [f64; 3]) -> CommandStep {
CommandStep::Cancel
}
/// Real-time preview geometry (lines, arcs, polylines) to render as the cursor moves.
/// Each wire can specify custom vertices and an optional color (defaults to host cyan).
fn on_cursor_move(&mut self, _pt: [f64; 3]) -> Vec<PreviewWire> {
Vec::new()
}
}
/// A preview wire/polyline rendered in real-time during interactive commands.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "host", derive(serde::Serialize, serde::Deserialize))]
pub struct PreviewWire {
/// World-coordinate points along the polyline.
pub points: Vec<[f64; 3]>,
/// Optional RGBA color (0.0 to 1.0). If `None`, the host's default rubber-band color (cyan) is used.
pub color: Option<[f32; 4]>,
}
impl PreviewWire {
pub fn new(points: Vec<[f64; 3]>) -> Self {
Self { points, color: None }
}
pub fn with_color(points: Vec<[f64; 3]>, color: [f32; 4]) -> Self {
Self { points, color: Some(color) }
}
}
impl From<Vec<[f64; 3]>> for PreviewWire {
fn from(points: Vec<[f64; 3]>) -> Self {
Self::new(points)
}
}
/// The outcome of an [`InteractiveCommand`] step.
+68
View File
@@ -2710,6 +2710,26 @@ impl crate::command::CadCommand for PluginInteractiveAdapter {
.map(plugin_step_to_result)
.unwrap_or(crate::command::CmdResult::Cancel)
}
fn on_preview_wires(&mut self, pt: glam::DVec3) -> Vec<crate::scene::model::wire_model::WireModel> {
let raw_wires = crate::plugin::guard("InteractiveCommand::on_cursor_move", || {
self.inner.on_cursor_move([pt.x as f64, pt.y as f64, pt.z as f64])
})
.unwrap_or_default();
raw_wires
.into_iter()
.filter(|w| w.points.len() >= 2)
.map(|w| {
let color = w.color.unwrap_or(crate::scene::model::wire_model::WireModel::CYAN);
crate::scene::model::wire_model::WireModel::solid_f64(
"rubber_band".into(),
w.points,
color,
false,
)
})
.collect()
}
}
/// Bridges an out-of-process plugin's interactive command to the host's
@@ -9268,6 +9288,54 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
);
}
/// A plugin command that previews a rubber-band line to the cursor.
struct PreviewLine {
start: [f64; 3],
color: Option<[f32; 4]>,
}
impl ocs_plugin_api::host::InteractiveCommand for PreviewLine {
fn prompt(&self) -> String {
"Pick endpoint".to_string()
}
fn on_point(&mut self, _pt: [f64; 3]) -> ocs_plugin_api::host::CommandStep {
ocs_plugin_api::host::CommandStep::Done
}
fn on_cursor_move(&mut self, pt: [f64; 3]) -> Vec<ocs_plugin_api::host::PreviewWire> {
vec![ocs_plugin_api::host::PreviewWire {
points: vec![self.start, pt],
color: self.color,
}]
}
}
#[test]
fn plugin_cursor_move_preview_wires() {
let mut app = OpenCADStudio::new_for_test();
app.tabs[0].is_start = false;
// 1. Default color (None -> CYAN)
{
let mut host = HostSession::new(&mut app, 0);
host.start_interactive(Box::new(PreviewLine { start: [0.0, 0.0, 0.0], color: None }));
}
assert!(app.tabs[0].active_cmd.is_some());
let cmd = app.tabs[0].active_cmd.as_mut().unwrap();
let wires = cmd.on_preview_wires(glam::DVec3::new(10.0, 20.0, 0.0));
assert_eq!(wires.len(), 1);
assert_eq!(wires[0].name, "rubber_band");
assert_eq!(wires[0].color, crate::scene::model::wire_model::WireModel::CYAN);
// 2. Custom color ([1.0, 0.0, 0.0, 1.0])
{
let mut host = HostSession::new(&mut app, 0);
host.start_interactive(Box::new(PreviewLine { start: [0.0, 0.0, 0.0], color: Some([1.0, 0.0, 0.0, 1.0]) }));
}
let cmd2 = app.tabs[0].active_cmd.as_mut().unwrap();
let wires2 = cmd2.on_preview_wires(glam::DVec3::new(10.0, 20.0, 0.0));
assert_eq!(wires2.len(), 1);
assert_eq!(wires2[0].color, [1.0, 0.0, 0.0, 1.0]);
}
/// A plugin command that picks an existing object, then marks it.
struct PickThenMark;
impl ocs_plugin_api::host::InteractiveCommand for PickThenMark {