Skip to main content

viewport_lib/interaction/widgets/
spline.rs

1//! Spline widget: N draggable control points connected by a Catmull-Rom spline.
2
3use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius, ray_point_dist};
4use crate::interaction::clip_plane::ray_plane_intersection;
5use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
6
7/// An interactive spline widget with N draggable Catmull-Rom control points.
8///
9/// Each frame call `update()` to advance state, then push `polyline_item()` into
10/// `fd.scene.polylines` and `handle_glyphs()` into `fd.scene.glyphs`.
11pub struct SplineWidget {
12    /// Control point positions.
13    pub points: Vec<glam::Vec3>,
14    /// Colour of the spline curve.
15    pub colour: [f32; 4],
16    /// Width of the spline curve in pixels.
17    pub line_width: f32,
18    /// Colour of the control point handles.
19    pub handle_colour: [f32; 4],
20    /// Number of samples between each pair of adjacent control points.
21    pub resolution: u32,
22    hovered_point: Option<usize>,
23    active_point: Option<usize>,
24    drag_plane_normal: glam::Vec3,
25    drag_plane_d: f32,
26}
27
28impl SplineWidget {
29    /// Create a new spline widget with the given control points.
30    pub fn new(points: Vec<glam::Vec3>) -> Self {
31        Self {
32            points,
33            colour: [0.4, 0.8, 1.0, 1.0],
34            line_width: 2.0,
35            handle_colour: [1.0, 0.8, 0.2, 1.0],
36            resolution: 16,
37            hovered_point: None,
38            active_point: None,
39            drag_plane_normal: glam::Vec3::Y,
40            drag_plane_d: 0.0,
41        }
42    }
43
44    /// Returns the index of the currently hovered control point, if any.
45    pub fn hovered_point(&self) -> Option<usize> {
46        self.hovered_point
47    }
48
49    /// Returns true when a control point drag is in progress.
50    pub fn is_active(&self) -> bool {
51        self.active_point.is_some()
52    }
53
54    /// Advance widget state from cursor input. Call once per frame before pushing render items.
55    pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
56        let (ray_origin, ray_dir) = ctx_ray(ctx);
57        // Use the first control point (or origin) to compute a reference world radius.
58        let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
59        let hit_radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 12.0);
60
61        if !ctx.dragging {
62            self.hovered_point = None;
63            let mut best_dist = hit_radius;
64            for (i, &pt) in self.points.iter().enumerate() {
65                let d = ray_point_dist(ray_origin, ray_dir, pt);
66                if d < best_dist {
67                    best_dist = d;
68                    self.hovered_point = Some(i);
69                }
70            }
71        }
72
73        if ctx.drag_started {
74            if let Some(idx) = self.hovered_point {
75                self.active_point = Some(idx);
76                self.drag_plane_normal = -glam::Vec3::from(ctx.camera.forward);
77                let pt = self.points[idx];
78                self.drag_plane_d = -self.drag_plane_normal.dot(pt);
79            }
80        }
81
82        if ctx.dragging {
83            if let Some(idx) = self.active_point {
84                if let Some(hit) = ray_plane_intersection(
85                    ray_origin,
86                    ray_dir,
87                    self.drag_plane_normal,
88                    self.drag_plane_d,
89                ) {
90                    self.points[idx] = hit;
91                    return WidgetResult::Updated;
92                }
93            }
94        }
95
96        if ctx.released {
97            self.active_point = None;
98        }
99
100        WidgetResult::None
101    }
102
103    /// Build a `PolylineItem` with the sampled Catmull-Rom spline.
104    pub fn polyline_item(&self, id: u64) -> PolylineItem {
105        let sampled = self.sampled_positions();
106        let n = sampled.len() as u32;
107        PolylineItem {
108            positions: sampled,
109            strip_lengths: if n > 0 { vec![n] } else { vec![] },
110            default_colour: self.colour,
111            line_width: self.line_width,
112
113            settings: crate::scene::material::ItemSettings {
114                pick_id: crate::renderer::PickId(id),
115                ..Default::default()
116            },
117            ..PolylineItem::default()
118        }
119    }
120
121    /// Build a `GlyphItem` with sphere handles for each control point.
122    ///
123    /// Hovered and active handles are brightened via `use_default_colour` and `default_colour`.
124    /// For per-handle highlight, call with a separate GlyphItem for the active handle.
125    pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
126        let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
127        let radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 8.0);
128        GlyphItem {
129            positions: self.points.iter().map(|p| p.to_array()).collect(),
130            glyph_type: GlyphType::Sphere,
131            scale: radius,
132            use_default_colour: true,
133            default_colour: self.handle_colour,
134
135            settings: crate::scene::material::ItemSettings {
136                pick_id: crate::renderer::PickId(id_base),
137                ..Default::default()
138            },
139            ..GlyphItem::default()
140        }
141    }
142
143    /// Evaluate the Catmull-Rom spline and return sampled world-space positions.
144    pub fn sampled_positions(&self) -> Vec<[f32; 3]> {
145        let n = self.points.len();
146        if n == 0 {
147            return Vec::new();
148        }
149        if n == 1 {
150            return vec![self.points[0].to_array()];
151        }
152        let res = self.resolution.max(1) as usize;
153        let mut out: Vec<[f32; 3]> = Vec::with_capacity((n - 1) * res + 1);
154        for seg in 0..(n - 1) {
155            let p0 = self.points[if seg > 0 { seg - 1 } else { seg }];
156            let p1 = self.points[seg];
157            let p2 = self.points[seg + 1];
158            let p3 = self.points[if seg + 2 < n { seg + 2 } else { seg + 1 }];
159            for s in 0..res {
160                let t = s as f32 / res as f32;
161                out.push(catmull_rom(p0, p1, p2, p3, t).to_array());
162            }
163        }
164        out.push(self.points[n - 1].to_array());
165        out
166    }
167}
168
169fn catmull_rom(
170    p0: glam::Vec3,
171    p1: glam::Vec3,
172    p2: glam::Vec3,
173    p3: glam::Vec3,
174    t: f32,
175) -> glam::Vec3 {
176    let t2 = t * t;
177    let t3 = t2 * t;
178    0.5 * ((-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
179        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
180        + (-p0 + p2) * t
181        + 2.0 * p1)
182}