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            id,
113            ..PolylineItem::default()
114        }
115    }
116
117    /// Build a `GlyphItem` with sphere handles for each control point.
118    ///
119    /// Hovered and active handles are brightened via `use_default_colour` and `default_colour`.
120    /// For per-handle highlight, call with a separate GlyphItem for the active handle.
121    pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
122        let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
123        let radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 8.0);
124        GlyphItem {
125            positions: self.points.iter().map(|p| p.to_array()).collect(),
126            glyph_type: GlyphType::Sphere,
127            scale: radius,
128            use_default_colour: true,
129            default_colour: self.handle_colour,
130            id: id_base,
131            ..GlyphItem::default()
132        }
133    }
134
135    /// Evaluate the Catmull-Rom spline and return sampled world-space positions.
136    pub fn sampled_positions(&self) -> Vec<[f32; 3]> {
137        let n = self.points.len();
138        if n == 0 {
139            return Vec::new();
140        }
141        if n == 1 {
142            return vec![self.points[0].to_array()];
143        }
144        let res = self.resolution.max(1) as usize;
145        let mut out: Vec<[f32; 3]> = Vec::with_capacity((n - 1) * res + 1);
146        for seg in 0..(n - 1) {
147            let p0 = self.points[if seg > 0 { seg - 1 } else { seg }];
148            let p1 = self.points[seg];
149            let p2 = self.points[seg + 1];
150            let p3 = self.points[if seg + 2 < n { seg + 2 } else { seg + 1 }];
151            for s in 0..res {
152                let t = s as f32 / res as f32;
153                out.push(catmull_rom(p0, p1, p2, p3, t).to_array());
154            }
155        }
156        out.push(self.points[n - 1].to_array());
157        out
158    }
159}
160
161fn catmull_rom(
162    p0: glam::Vec3,
163    p1: glam::Vec3,
164    p2: glam::Vec3,
165    p3: glam::Vec3,
166    t: f32,
167) -> glam::Vec3 {
168    let t2 = t * t;
169    let t3 = t2 * t;
170    0.5 * ((-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
171        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
172        + (-p0 + p2) * t
173        + 2.0 * p1)
174}