From ffdde7105797362fc164e0804c71831d327093e7 Mon Sep 17 00:00:00 2001 From: yztink Date: Wed, 23 Sep 2026 17:19:56 -0700 Subject: [PATCH] feat(plugin): support analytical GPU preview wires for circles and arcs --- crates/ocs_plugin_api/src/host.rs | 81 ++++++++++-- src/app/plugin_host.rs | 205 +++++++++++++++++++++++++----- 2 files changed, 244 insertions(+), 42 deletions(-) diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index 88a764ac2..9208f91fd 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -611,23 +611,88 @@ pub trait InteractiveCommand: Send { } } -/// A preview wire/polyline rendered in real-time during interactive commands. +/// 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 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]>, +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 { points, color: None } + 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 { points, color: Some(color) } + 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, + } } } diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 1bba6f6d7..5aa145dc7 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -2715,20 +2715,7 @@ impl crate::command::CadCommand for PluginInteractiveAdapter { 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() + convert_preview_wires(raw_wires) } } @@ -2862,23 +2849,87 @@ impl crate::command::CadCommand for PluginProcessInteractiveAdapter { .process .on_cursor_move(self.command_id, [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() + convert_preview_wires(raw_wires) } } +fn convert_preview_wires( + raw_wires: Vec, +) -> Vec { + 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 { use crate::command::CmdResult; use ocs_plugin_api::host::CommandStep; @@ -9338,10 +9389,53 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0))) ocs_plugin_api::host::CommandStep::Done } fn on_cursor_move(&mut self, pt: [f64; 3]) -> Vec { - vec![ocs_plugin_api::host::PreviewWire { - points: vec![self.start, pt], - color: self.color, - }] + 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 { + 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 { + 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, + )] } } @@ -9350,7 +9444,7 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0))) let mut app = OpenCADStudio::new_for_test(); app.tabs[0].is_start = false; - // 1. Default color (None -> CYAN) + // 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 })); @@ -9362,7 +9456,7 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0))) 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]) + // 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]) })); @@ -9371,6 +9465,49 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0))) 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.