feat(plugin): support analytical GPU preview wires for circles and arcs

This commit is contained in:
yztink committed 2026-09-23 17:19:56 -07:00
1 parent d218ec1451
commit ffdde71057
2 files changed
+244 -42

No files matched your search

+73 -8
View File
@@ -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,
}
}
}
+171 -34
View File
@@ -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<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 {
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<ocs_plugin_api::host::PreviewWire> {
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<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,
)]
}
}
@@ -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.