forked from 1b-agent/opencadstudio-upstream
Merge pull request #1469 from yztink:feat/plugin-interactive-preview
This commit is contained in:
commit
8ae804d921
5 files changed
+416
-3
No files matched your search
@@ -603,6 +603,103 @@ pub trait InteractiveCommand: Send {
|
|||||||
fn on_object_pick(&mut self, _handle: Handle, _pt: [f64; 3]) -> CommandStep {
|
fn on_object_pick(&mut self, _handle: Handle, _pt: [f64; 3]) -> CommandStep {
|
||||||
CommandStep::Cancel
|
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 rendered in real-time during interactive commands.
|
||||||
|
/// Supports straight polylines as well as analytical circles and arcs
|
||||||
|
/// that render via GPU shaders with infinite smoothness.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "host", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub enum PreviewWire {
|
||||||
|
/// A connected sequence of straight line segments in world space.
|
||||||
|
Polyline {
|
||||||
|
points: Vec<[f64; 3]>,
|
||||||
|
color: Option<[f32; 4]>,
|
||||||
|
},
|
||||||
|
/// An analytical circle rendered with sub-pixel GPU anti-aliasing.
|
||||||
|
Circle {
|
||||||
|
center: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
color: Option<[f32; 4]>,
|
||||||
|
},
|
||||||
|
/// An analytical circular arc swept counter-clockwise from `start_angle_rad` to `end_angle_rad`.
|
||||||
|
Arc {
|
||||||
|
center: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
start_angle_rad: f64,
|
||||||
|
end_angle_rad: f64,
|
||||||
|
color: Option<[f32; 4]>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreviewWire {
|
||||||
|
/// Create a polyline preview wire with default host cyan color.
|
||||||
|
pub fn new(points: Vec<[f64; 3]>) -> Self {
|
||||||
|
Self::Polyline { points, color: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional RGBA color (0.0 to 1.0) specified for this preview wire.
|
||||||
|
pub fn color(&self) -> Option<[f32; 4]> {
|
||||||
|
match self {
|
||||||
|
Self::Polyline { color, .. } => *color,
|
||||||
|
Self::Circle { color, .. } => *color,
|
||||||
|
Self::Arc { color, .. } => *color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a polyline preview wire with a custom RGBA color.
|
||||||
|
pub fn with_color(points: Vec<[f64; 3]>, color: [f32; 4]) -> Self {
|
||||||
|
Self::Polyline {
|
||||||
|
points,
|
||||||
|
color: Some(color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a straight line segment preview between two points.
|
||||||
|
pub fn line(from: [f64; 3], to: [f64; 3], color: Option<[f32; 4]>) -> Self {
|
||||||
|
Self::Polyline {
|
||||||
|
points: vec![from, to],
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an analytical circle preview.
|
||||||
|
pub fn circle(center: [f64; 3], radius: f64, color: Option<[f32; 4]>) -> Self {
|
||||||
|
Self::Circle {
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an analytical circular arc preview.
|
||||||
|
pub fn arc(
|
||||||
|
center: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
start_angle_rad: f64,
|
||||||
|
end_angle_rad: f64,
|
||||||
|
color: Option<[f32; 4]>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Arc {
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
start_angle_rad,
|
||||||
|
end_angle_rad,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<[f64; 3]>> for PreviewWire {
|
||||||
|
fn from(points: Vec<[f64; 3]>) -> Self {
|
||||||
|
Self::new(points)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The outcome of an [`InteractiveCommand`] step.
|
/// The outcome of an [`InteractiveCommand`] step.
|
||||||
@@ -613,8 +710,12 @@ pub enum CommandStep {
|
|||||||
NeedPoint,
|
NeedPoint,
|
||||||
/// Commit an entity to the document and keep collecting points.
|
/// Commit an entity to the document and keep collecting points.
|
||||||
Commit(EntityType),
|
Commit(EntityType),
|
||||||
|
/// Commit multiple entities to the document and keep collecting points.
|
||||||
|
CommitMany(Vec<EntityType>),
|
||||||
/// Commit an entity and end the command.
|
/// Commit an entity and end the command.
|
||||||
CommitAndEnd(EntityType),
|
CommitAndEnd(EntityType),
|
||||||
|
/// Commit multiple entities to the document and end the command.
|
||||||
|
CommitManyAndEnd(Vec<EntityType>),
|
||||||
/// End the command without committing.
|
/// End the command without committing.
|
||||||
Done,
|
Done,
|
||||||
/// Cancel the command.
|
/// Cancel the command.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use crate::ribbon::owned::{OwnedPluginManifest, OwnedRibbonGroup};
|
|||||||
|
|
||||||
pub use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
pub use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||||
pub use acadrust::{CadDocument, EntityType, Handle};
|
pub use acadrust::{CadDocument, EntityType, Handle};
|
||||||
|
pub use crate::host::PreviewWire;
|
||||||
|
|
||||||
/// Events the host forwards to an active plugin `InteractiveCommand`.
|
/// Events the host forwards to an active plugin `InteractiveCommand`.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -83,6 +84,11 @@ pub enum HostRequest {
|
|||||||
},
|
},
|
||||||
/// V7: release an interactive command after completion or cancellation.
|
/// V7: release an interactive command after completion or cancellation.
|
||||||
DropInteractive { command_id: u64 },
|
DropInteractive { command_id: u64 },
|
||||||
|
/// V7: cursor move preview update during an interactive command.
|
||||||
|
CursorMove {
|
||||||
|
command_id: u64,
|
||||||
|
pt: [f64; 3],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Responses the plugin runner sends back for `HostRequest`.
|
/// Responses the plugin runner sends back for `HostRequest`.
|
||||||
@@ -95,6 +101,7 @@ pub enum HostResponse {
|
|||||||
Manifest(OwnedPluginManifest),
|
Manifest(OwnedPluginManifest),
|
||||||
Error(String),
|
Error(String),
|
||||||
CodeExecutionResult(crate::host::ExecutionResult),
|
CodeExecutionResult(crate::host::ExecutionResult),
|
||||||
|
PreviewWires(Vec<PreviewWire>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests the plugin runner sends to the host.
|
/// Requests the plugin runner sends to the host.
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ fn base_max_floor(base: Duration, kind: &'static str) -> Duration {
|
|||||||
"GetManifest" | "GetRibbon" => Duration::from_secs(5),
|
"GetManifest" | "GetRibbon" => Duration::from_secs(5),
|
||||||
"Dispatch" => Duration::from_secs(10),
|
"Dispatch" => Duration::from_secs(10),
|
||||||
"InteractiveEvent" | "GetPrompt" | "NeedsEntityPick" => Duration::from_secs(2),
|
"InteractiveEvent" | "GetPrompt" | "NeedsEntityPick" => Duration::from_secs(2),
|
||||||
|
"CursorMove" => Duration::from_millis(500),
|
||||||
"ExecuteCode" => execute_code_timeout(),
|
"ExecuteCode" => execute_code_timeout(),
|
||||||
_ => Duration::from_secs(1),
|
_ => Duration::from_secs(1),
|
||||||
};
|
};
|
||||||
@@ -238,6 +239,7 @@ pub(crate) fn request_kind(req: &HostRequest) -> &'static str {
|
|||||||
HostRequest::NeedsEntityPick { .. } => "NeedsEntityPick",
|
HostRequest::NeedsEntityPick { .. } => "NeedsEntityPick",
|
||||||
HostRequest::ExecuteCode { .. } => "ExecuteCode",
|
HostRequest::ExecuteCode { .. } => "ExecuteCode",
|
||||||
HostRequest::DropInteractive { .. } => "DropInteractive",
|
HostRequest::DropInteractive { .. } => "DropInteractive",
|
||||||
|
HostRequest::CursorMove { .. } => "CursorMove",
|
||||||
HostRequest::Shutdown => "Shutdown",
|
HostRequest::Shutdown => "Shutdown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,6 +683,35 @@ impl PluginProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask the plugin process for real-time preview wires for the cursor position.
|
||||||
|
pub fn on_cursor_move(
|
||||||
|
&self,
|
||||||
|
command_id: u64,
|
||||||
|
pt: [f64; 3],
|
||||||
|
) -> Result<Vec<crate::host::PreviewWire>, PluginError> {
|
||||||
|
if let Some(v4) = &self.v4 {
|
||||||
|
let resp = v4.call(
|
||||||
|
&mut NullHost,
|
||||||
|
HostRequest::CursorMove { command_id, pt },
|
||||||
|
&mut |_| {},
|
||||||
|
)?;
|
||||||
|
match resp {
|
||||||
|
HostResponse::PreviewWires(w) => Ok(w),
|
||||||
|
other => Err(PluginError::UnexpectedResponse(Box::new(other))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.call_simple(
|
||||||
|
HostRequest::CursorMove { command_id, pt },
|
||||||
|
"CursorMove",
|
||||||
|
"cursor_move",
|
||||||
|
|resp| match resp {
|
||||||
|
HostResponse::PreviewWires(w) => Ok(w),
|
||||||
|
other => Err(Box::new(other)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_alive(&self) -> bool {
|
pub fn is_alive(&self) -> bool {
|
||||||
if let Some(v4) = &self.v4 {
|
if let Some(v4) = &self.v4 {
|
||||||
return v4.is_alive();
|
return v4.is_alive();
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ fn handle_host_request(
|
|||||||
CommandStep::Done
|
CommandStep::Done
|
||||||
| CommandStep::Cancel
|
| CommandStep::Cancel
|
||||||
| CommandStep::CommitAndEnd(_)
|
| CommandStep::CommitAndEnd(_)
|
||||||
|
| CommandStep::CommitManyAndEnd(_)
|
||||||
);
|
);
|
||||||
(Ok(s), is_done)
|
(Ok(s), is_done)
|
||||||
}
|
}
|
||||||
@@ -243,6 +244,21 @@ fn handle_host_request(
|
|||||||
None => HostResponse::Error(format!("unknown interactive command {command_id}")),
|
None => HostResponse::Error(format!("unknown interactive command {command_id}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HostRequest::CursorMove { command_id, pt } => {
|
||||||
|
let result = {
|
||||||
|
let mut registry = interactive.borrow_mut();
|
||||||
|
registry.get_mut(&command_id).map(|cmd| {
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
cmd.on_cursor_move(pt)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Some(Ok(wires)) => HostResponse::PreviewWires(wires),
|
||||||
|
Some(Err(_)) => HostResponse::PreviewWires(Vec::new()),
|
||||||
|
None => HostResponse::PreviewWires(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
HostRequest::ExecuteCode { .. } => {
|
HostRequest::ExecuteCode { .. } => {
|
||||||
HostResponse::Error("ExecuteCode requires V4 protocol".to_string())
|
HostResponse::Error("ExecuteCode requires V4 protocol".to_string())
|
||||||
}
|
}
|
||||||
@@ -320,6 +336,7 @@ fn handle_host_request_v4(
|
|||||||
CommandStep::Done
|
CommandStep::Done
|
||||||
| CommandStep::Cancel
|
| CommandStep::Cancel
|
||||||
| CommandStep::CommitAndEnd(_)
|
| CommandStep::CommitAndEnd(_)
|
||||||
|
| CommandStep::CommitManyAndEnd(_)
|
||||||
);
|
);
|
||||||
(Ok(s), is_done)
|
(Ok(s), is_done)
|
||||||
}
|
}
|
||||||
@@ -366,6 +383,21 @@ fn handle_host_request_v4(
|
|||||||
None => Some(HostResponse::Error(format!("unknown interactive command {command_id}"))),
|
None => Some(HostResponse::Error(format!("unknown interactive command {command_id}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HostRequest::CursorMove { command_id, pt } => {
|
||||||
|
let result = {
|
||||||
|
let mut registry = interactive.borrow_mut();
|
||||||
|
registry.get_mut(&command_id).map(|cmd| {
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
cmd.on_cursor_move(pt)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Some(Ok(wires)) => Some(HostResponse::PreviewWires(wires)),
|
||||||
|
Some(Err(_)) => Some(HostResponse::PreviewWires(Vec::new())),
|
||||||
|
None => Some(HostResponse::PreviewWires(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
HostRequest::ExecuteCode {
|
HostRequest::ExecuteCode {
|
||||||
command_id,
|
command_id,
|
||||||
source,
|
source,
|
||||||
|
|||||||
+245
-3
@@ -2710,6 +2710,13 @@ impl crate::command::CadCommand for PluginInteractiveAdapter {
|
|||||||
.map(plugin_step_to_result)
|
.map(plugin_step_to_result)
|
||||||
.unwrap_or(crate::command::CmdResult::Cancel)
|
.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();
|
||||||
|
convert_preview_wires(raw_wires)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bridges an out-of-process plugin's interactive command to the host's
|
/// Bridges an out-of-process plugin's interactive command to the host's
|
||||||
@@ -2777,7 +2784,12 @@ impl crate::command::CadCommand for PluginProcessInteractiveAdapter {
|
|||||||
.interactive_event(self.command_id, InteractiveEvent::Point([pt.x, pt.y, pt.z]))
|
.interactive_event(self.command_id, InteractiveEvent::Point([pt.x, pt.y, pt.z]))
|
||||||
.map(plugin_step_to_result)
|
.map(plugin_step_to_result)
|
||||||
.unwrap_or(crate::command::CmdResult::Cancel);
|
.unwrap_or(crate::command::CmdResult::Cancel);
|
||||||
if matches!(result, crate::command::CmdResult::CommitAndExit(_) | crate::command::CmdResult::Cancel) {
|
if matches!(
|
||||||
|
result,
|
||||||
|
crate::command::CmdResult::CommitAndExit(_)
|
||||||
|
| crate::command::CmdResult::CommitEntitiesAndExit(_)
|
||||||
|
| crate::command::CmdResult::Cancel
|
||||||
|
) {
|
||||||
self.is_done = true;
|
self.is_done = true;
|
||||||
}
|
}
|
||||||
self.refresh();
|
self.refresh();
|
||||||
@@ -2790,7 +2802,12 @@ impl crate::command::CadCommand for PluginProcessInteractiveAdapter {
|
|||||||
.interactive_event(self.command_id, InteractiveEvent::Enter)
|
.interactive_event(self.command_id, InteractiveEvent::Enter)
|
||||||
.map(plugin_step_to_result)
|
.map(plugin_step_to_result)
|
||||||
.unwrap_or(crate::command::CmdResult::Cancel);
|
.unwrap_or(crate::command::CmdResult::Cancel);
|
||||||
if matches!(result, crate::command::CmdResult::CommitAndExit(_) | crate::command::CmdResult::Cancel) {
|
if matches!(
|
||||||
|
result,
|
||||||
|
crate::command::CmdResult::CommitAndExit(_)
|
||||||
|
| crate::command::CmdResult::CommitEntitiesAndExit(_)
|
||||||
|
| crate::command::CmdResult::Cancel
|
||||||
|
) {
|
||||||
self.is_done = true;
|
self.is_done = true;
|
||||||
}
|
}
|
||||||
self.refresh();
|
self.refresh();
|
||||||
@@ -2816,12 +2833,101 @@ impl crate::command::CadCommand for PluginProcessInteractiveAdapter {
|
|||||||
)
|
)
|
||||||
.map(plugin_step_to_result)
|
.map(plugin_step_to_result)
|
||||||
.unwrap_or(crate::command::CmdResult::Cancel);
|
.unwrap_or(crate::command::CmdResult::Cancel);
|
||||||
if matches!(result, crate::command::CmdResult::CommitAndExit(_) | crate::command::CmdResult::Cancel) {
|
if matches!(
|
||||||
|
result,
|
||||||
|
crate::command::CmdResult::CommitAndExit(_)
|
||||||
|
| crate::command::CmdResult::CommitEntitiesAndExit(_)
|
||||||
|
| crate::command::CmdResult::Cancel
|
||||||
|
) {
|
||||||
self.is_done = true;
|
self.is_done = true;
|
||||||
}
|
}
|
||||||
self.refresh();
|
self.refresh();
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
fn on_preview_wires(&mut self, pt: glam::DVec3) -> Vec<crate::scene::model::wire_model::WireModel> {
|
||||||
|
let raw_wires = self
|
||||||
|
.process
|
||||||
|
.on_cursor_move(self.command_id, [pt.x as f64, pt.y as f64, pt.z as f64])
|
||||||
|
.unwrap_or_default();
|
||||||
|
convert_preview_wires(raw_wires)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_preview_wires(
|
||||||
|
raw_wires: Vec<ocs_plugin_api::host::PreviewWire>,
|
||||||
|
) -> Vec<crate::scene::model::wire_model::WireModel> {
|
||||||
|
use ocs_plugin_api::host::PreviewWire;
|
||||||
|
use crate::scene::model::wire_model::{TangentGeom, WireModel};
|
||||||
|
|
||||||
|
raw_wires
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|w| match w {
|
||||||
|
PreviewWire::Polyline { points, color } => {
|
||||||
|
if points.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let c = color.unwrap_or(WireModel::CYAN);
|
||||||
|
Some(WireModel::solid_f64("rubber_band".into(), points, c, false))
|
||||||
|
}
|
||||||
|
PreviewWire::Circle { center, radius, color } => {
|
||||||
|
if radius <= 0.0 || !radius.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let c = color.unwrap_or(WireModel::CYAN);
|
||||||
|
// Two opposite diameter points ensure OCS's viewport frame-cache
|
||||||
|
// (render_signature) detects cursor motion and triggers 60-120 FPS
|
||||||
|
// redraws without requiring polar snap or artificial guide lines.
|
||||||
|
let points = vec![
|
||||||
|
[center[0] - radius, center[1], center[2]],
|
||||||
|
[center[0] + radius, center[1], center[2]],
|
||||||
|
];
|
||||||
|
let mut wire = WireModel::solid_f64("rubber_band".into(), points, c, false);
|
||||||
|
wire.tangent_geoms.push(TangentGeom::PlanarCircle {
|
||||||
|
center,
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius,
|
||||||
|
});
|
||||||
|
Some(wire)
|
||||||
|
}
|
||||||
|
PreviewWire::Arc {
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
start_angle_rad,
|
||||||
|
end_angle_rad,
|
||||||
|
color,
|
||||||
|
} => {
|
||||||
|
if radius <= 0.0 || !radius.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let c = color.unwrap_or(WireModel::CYAN);
|
||||||
|
// Start and end points along the circumference ensure OCS's viewport
|
||||||
|
// frame-cache (render_signature) detects cursor motion and triggers
|
||||||
|
// 60-120 FPS redraws without requiring polar snap or artificial guide lines.
|
||||||
|
let p_start = [
|
||||||
|
center[0] + radius * start_angle_rad.cos(),
|
||||||
|
center[1] + radius * start_angle_rad.sin(),
|
||||||
|
center[2],
|
||||||
|
];
|
||||||
|
let p_end = [
|
||||||
|
center[0] + radius * end_angle_rad.cos(),
|
||||||
|
center[1] + radius * end_angle_rad.sin(),
|
||||||
|
center[2],
|
||||||
|
];
|
||||||
|
let points = vec![p_start, p_end];
|
||||||
|
let mut wire = WireModel::solid_f64("rubber_band".into(), points, c, false);
|
||||||
|
wire.tangent_geoms.push(TangentGeom::Arc {
|
||||||
|
center,
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius,
|
||||||
|
start_angle: start_angle_rad,
|
||||||
|
end_angle: end_angle_rad,
|
||||||
|
});
|
||||||
|
Some(wire)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plugin_step_to_result(step: ocs_plugin_api::host::CommandStep) -> crate::command::CmdResult {
|
fn plugin_step_to_result(step: ocs_plugin_api::host::CommandStep) -> crate::command::CmdResult {
|
||||||
@@ -2830,7 +2936,9 @@ fn plugin_step_to_result(step: ocs_plugin_api::host::CommandStep) -> crate::comm
|
|||||||
match step {
|
match step {
|
||||||
CommandStep::NeedPoint => CmdResult::NeedPoint,
|
CommandStep::NeedPoint => CmdResult::NeedPoint,
|
||||||
CommandStep::Commit(e) => CmdResult::CommitEntity(e),
|
CommandStep::Commit(e) => CmdResult::CommitEntity(e),
|
||||||
|
CommandStep::CommitMany(es) => CmdResult::CommitEntities(es),
|
||||||
CommandStep::CommitAndEnd(e) => CmdResult::CommitAndExit(e),
|
CommandStep::CommitAndEnd(e) => CmdResult::CommitAndExit(e),
|
||||||
|
CommandStep::CommitManyAndEnd(es) => CmdResult::CommitEntitiesAndExit(es),
|
||||||
CommandStep::Done | CommandStep::Cancel => CmdResult::Cancel,
|
CommandStep::Done | CommandStep::Cancel => CmdResult::Cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9226,6 +9334,140 @@ 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::line(self.start, pt, self.color)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plugin command that previews an analytical circle.
|
||||||
|
struct PreviewCircle {
|
||||||
|
center: [f64; 3],
|
||||||
|
color: Option<[f32; 4]>,
|
||||||
|
}
|
||||||
|
impl ocs_plugin_api::host::InteractiveCommand for PreviewCircle {
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
"Pick radius".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> {
|
||||||
|
let dx = pt[0] - self.center[0];
|
||||||
|
let dy = pt[1] - self.center[1];
|
||||||
|
let r = (dx * dx + dy * dy).sqrt();
|
||||||
|
vec![ocs_plugin_api::host::PreviewWire::circle(self.center, r, self.color)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plugin command that previews an analytical arc.
|
||||||
|
struct PreviewArc {
|
||||||
|
center: [f64; 3],
|
||||||
|
color: Option<[f32; 4]>,
|
||||||
|
}
|
||||||
|
impl ocs_plugin_api::host::InteractiveCommand for PreviewArc {
|
||||||
|
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> {
|
||||||
|
let dx = pt[0] - self.center[0];
|
||||||
|
let dy = pt[1] - self.center[1];
|
||||||
|
let r = (dx * dx + dy * dy).sqrt();
|
||||||
|
vec![ocs_plugin_api::host::PreviewWire::arc(
|
||||||
|
self.center,
|
||||||
|
r,
|
||||||
|
0.0,
|
||||||
|
std::f64::consts::PI,
|
||||||
|
self.color,
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_cursor_move_preview_wires() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.tabs[0].is_start = false;
|
||||||
|
|
||||||
|
// 1. Polyline 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. Polyline 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]);
|
||||||
|
|
||||||
|
// 3. Analytical Circle with custom color
|
||||||
|
{
|
||||||
|
let mut host = HostSession::new(&mut app, 0);
|
||||||
|
host.start_interactive(Box::new(PreviewCircle {
|
||||||
|
center: [5.0, 5.0, 0.0],
|
||||||
|
color: Some([0.0, 1.0, 0.0, 1.0]),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
let cmd3 = app.tabs[0].active_cmd.as_mut().unwrap();
|
||||||
|
let circle_wires = cmd3.on_preview_wires(glam::DVec3::new(15.0, 5.0, 0.0));
|
||||||
|
assert_eq!(circle_wires.len(), 1);
|
||||||
|
assert_eq!(circle_wires[0].points.len(), 2);
|
||||||
|
assert_eq!(circle_wires[0].color, [0.0, 1.0, 0.0, 1.0]);
|
||||||
|
assert_eq!(circle_wires[0].tangent_geoms.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
circle_wires[0].tangent_geoms[0],
|
||||||
|
crate::scene::model::wire_model::TangentGeom::PlanarCircle { radius, .. } if (radius - 10.0).abs() < 1e-6
|
||||||
|
));
|
||||||
|
let instances = crate::scene::pipeline::circle_gpu::extract_circle_instances(&circle_wires[0], 0.0);
|
||||||
|
assert!(instances.is_some());
|
||||||
|
assert_eq!(instances.unwrap().len(), 1);
|
||||||
|
|
||||||
|
// 4. Analytical Arc
|
||||||
|
{
|
||||||
|
let mut host = HostSession::new(&mut app, 0);
|
||||||
|
host.start_interactive(Box::new(PreviewArc {
|
||||||
|
center: [0.0, 0.0, 0.0],
|
||||||
|
color: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
let cmd4 = app.tabs[0].active_cmd.as_mut().unwrap();
|
||||||
|
let arc_wires = cmd4.on_preview_wires(glam::DVec3::new(10.0, 0.0, 0.0));
|
||||||
|
assert_eq!(arc_wires.len(), 1);
|
||||||
|
assert_eq!(arc_wires[0].points.len(), 2);
|
||||||
|
assert_eq!(arc_wires[0].tangent_geoms.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
arc_wires[0].tangent_geoms[0],
|
||||||
|
crate::scene::model::wire_model::TangentGeom::Arc { radius, .. } if (radius - 10.0).abs() < 1e-6
|
||||||
|
));
|
||||||
|
let arc_instances = crate::scene::pipeline::circle_gpu::extract_circle_instances(&arc_wires[0], 0.0);
|
||||||
|
assert!(arc_instances.is_some());
|
||||||
|
assert_eq!(arc_instances.unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
/// A plugin command that picks an existing object, then marks it.
|
/// A plugin command that picks an existing object, then marks it.
|
||||||
struct PickThenMark;
|
struct PickThenMark;
|
||||||
impl ocs_plugin_api::host::InteractiveCommand for PickThenMark {
|
impl ocs_plugin_api::host::InteractiveCommand for PickThenMark {
|
||||||
|
|||||||
Reference in new issue
Block a user