Skip to main content

viewport_lib/interaction/widgets/
polyline_widget.rs

1//! Polyline widget: N draggable waypoints connected by straight line segments.
2
3use crate::geometry::intersect::ray_plane_intersection;
4use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
5
6use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius, ray_point_dist};
7
8/// An interactive polyline widget with N draggable control points.
9///
10/// Unlike [`crate::SplineWidget`], this uses straight line segments between points (no
11/// interpolation). Control points can be added and removed programmatically, and via
12/// double-click when `ctx.double_clicked` is set by the host.
13///
14/// # Usage
15///
16/// ```rust,ignore
17/// let mut pw = PolylineWidget::new(vec![
18///     glam::Vec3::new(-2.0, 0.0, 0.0),
19///     glam::Vec3::new( 0.0, 1.0, 0.0),
20///     glam::Vec3::new( 2.0, 0.0, 0.0),
21/// ]);
22///
23/// // Each frame:
24/// let result = pw.update(&ctx);
25/// fd.scene.polylines.push(pw.polyline_item(PL_ID));
26/// fd.scene.glyphs.push(pw.handle_glyphs(HANDLE_ID, &ctx));
27/// ```
28pub struct PolylineWidget {
29    /// Control point positions in world space.
30    pub points: Vec<glam::Vec3>,
31    /// RGBA colour for the line segments.
32    pub colour: [f32; 4],
33    /// Line width in pixels.
34    pub line_width: f32,
35    /// RGBA colour for the drag handles.
36    pub handle_colour: [f32; 4],
37    /// Index of the currently hovered control point.
38    pub hovered_point: Option<usize>,
39    /// Index of the point actively being dragged.
40    pub active_point: Option<usize>,
41
42    drag_plane_normal: glam::Vec3,
43    drag_plane_d: f32,
44}
45
46impl PolylineWidget {
47    /// Create a new polyline widget. Must have at least two points; extras are ignored
48    /// if fewer are provided by adding a default second point.
49    pub fn new(mut points: Vec<glam::Vec3>) -> Self {
50        if points.is_empty() {
51            points.push(glam::Vec3::ZERO);
52        }
53        if points.len() < 2 {
54            points.push(points[0] + glam::Vec3::X);
55        }
56        Self {
57            points,
58            colour: [0.9, 0.5, 0.1, 1.0],
59            line_width: 2.0,
60            handle_colour: [0.0; 4],
61            hovered_point: None,
62            active_point: None,
63            drag_plane_normal: glam::Vec3::Y,
64            drag_plane_d: 0.0,
65        }
66    }
67
68    /// True while a control point drag is in progress.
69    pub fn is_active(&self) -> bool {
70        self.active_point.is_some()
71    }
72
73    /// Append a point at the end of the polyline.
74    pub fn add_point(&mut self, pos: glam::Vec3) {
75        self.points.push(pos);
76    }
77
78    /// Remove the point at `index`. No-op if the polyline would drop below two points.
79    pub fn remove_point(&mut self, index: usize) {
80        if self.points.len() > 2 && index < self.points.len() {
81            self.points.remove(index);
82            // Fix up hover/active indices to avoid dangling references.
83            if self.hovered_point == Some(index) {
84                self.hovered_point = None;
85            } else if let Some(h) = self.hovered_point {
86                if h > index {
87                    self.hovered_point = Some(h - 1);
88                }
89            }
90            if self.active_point == Some(index) {
91                self.active_point = None;
92            } else if let Some(a) = self.active_point {
93                if a > index {
94                    self.active_point = Some(a - 1);
95                }
96            }
97        }
98    }
99
100    /// Process input for this frame. Returns `Updated` if state changed.
101    ///
102    /// Double-click behavior (requires `ctx.double_clicked`):
103    /// - On a hovered control point: removes that point (minimum 2 enforced).
104    /// - On a line segment: inserts a new point at the projected position.
105    pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
106        let (ro, rd) = ctx_ray(ctx);
107        let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
108        let hit_radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 12.0);
109
110        // --- Hover detection ---
111        if !ctx.dragging {
112            self.hovered_point = None;
113            let mut best_dist = hit_radius;
114            for (i, &pt) in self.points.iter().enumerate() {
115                let d = ray_point_dist(ro, rd, pt);
116                if d < best_dist {
117                    best_dist = d;
118                    self.hovered_point = Some(i);
119                }
120            }
121        }
122
123        // --- Double-click: add or remove points ---
124        if ctx.double_clicked {
125            if let Some(idx) = self.hovered_point {
126                // Double-click on handle: remove that point.
127                if self.points.len() > 2 {
128                    self.points.remove(idx);
129                    self.hovered_point = None;
130                    return WidgetResult::Updated;
131                }
132            } else {
133                // Double-click on a segment: find the closest segment and insert a point.
134                let seg_threshold = hit_radius * 2.0;
135                if let Some((seg_idx, insert_pos)) = self.closest_segment(ro, rd, seg_threshold) {
136                    self.points.insert(seg_idx + 1, insert_pos);
137                    self.hovered_point = Some(seg_idx + 1);
138                    return WidgetResult::Updated;
139                }
140            }
141        }
142
143        // --- Drag start ---
144        if ctx.drag_started {
145            if let Some(idx) = self.hovered_point {
146                self.active_point = Some(idx);
147                self.drag_plane_normal = -glam::Vec3::from(ctx.camera.forward);
148                self.drag_plane_d = -self.drag_plane_normal.dot(self.points[idx]);
149            }
150        }
151
152        // --- Drag in progress ---
153        if ctx.dragging {
154            if let Some(idx) = self.active_point {
155                if let Some(hit) =
156                    ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
157                {
158                    if (hit - self.points[idx]).length_squared() > 1e-10 {
159                        self.points[idx] = hit;
160                        return WidgetResult::Updated;
161                    }
162                }
163            }
164        }
165
166        if ctx.released {
167            self.active_point = None;
168        }
169
170        WidgetResult::None
171    }
172
173    /// Build a `PolylineItem` through all control points.
174    pub fn polyline_item(&self, id: u64) -> PolylineItem {
175        let n = self.points.len() as u32;
176        PolylineItem {
177            positions: self.points.iter().map(|p| p.to_array()).collect(),
178            strip_lengths: if n > 0 { vec![n] } else { vec![] },
179            default_colour: self.colour,
180            line_width: self.line_width,
181
182            settings: crate::scene::material::ItemSettings {
183                pick_id: crate::renderer::PickId(id),
184                ..Default::default()
185            },
186            ..PolylineItem::default()
187        }
188    }
189
190    /// Build a `GlyphItem` with sphere handles for each control point.
191    ///
192    /// Hovered and active handles appear brighter (scalar = 1.0 vs 0.2).
193    /// `id_base` is the pick ID for the first point; each subsequent point uses `id_base + i`.
194    pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
195        let mut positions = Vec::with_capacity(self.points.len());
196        let mut vectors = Vec::with_capacity(self.points.len());
197        let mut scalars = Vec::with_capacity(self.points.len());
198
199        for (i, pt) in self.points.iter().enumerate() {
200            let r = handle_world_radius(*pt, &ctx.camera, ctx.viewport_size.y, 9.0);
201            let s = if self.hovered_point == Some(i) || self.active_point == Some(i) {
202                1.0_f32
203            } else {
204                0.2
205            };
206            positions.push(pt.to_array());
207            vectors.push([r, 0.0, 0.0]);
208            scalars.push(s);
209        }
210        GlyphItem {
211            positions,
212            vectors,
213            scale: 1.0,
214            scale_by_magnitude: true,
215            scalars,
216            scalar_range: Some((0.0, 1.0)),
217            glyph_type: GlyphType::Sphere,
218
219            settings: crate::scene::material::ItemSettings {
220                pick_id: crate::renderer::PickId(id_base),
221                ..Default::default()
222            },
223            default_colour: self.handle_colour,
224            use_default_colour: self.handle_colour[3] > 0.0,
225            ..GlyphItem::default()
226        }
227    }
228
229    // -----------------------------------------------------------------------
230    // Internal
231    // -----------------------------------------------------------------------
232
233    /// Find the closest segment to the ray, within `threshold` world units.
234    /// Returns `(segment_index, insertion_point_on_segment)` or `None`.
235    fn closest_segment(
236        &self,
237        ray_origin: glam::Vec3,
238        ray_dir: glam::Vec3,
239        threshold: f32,
240    ) -> Option<(usize, glam::Vec3)> {
241        let mut best: Option<(f32, usize, glam::Vec3)> = None;
242
243        for i in 0..self.points.len().saturating_sub(1) {
244            let a = self.points[i];
245            let b = self.points[i + 1];
246            let (pt, dist) = closest_point_on_segment_to_ray(ray_origin, ray_dir, a, b);
247            if dist < threshold {
248                if best.is_none() || dist < best.unwrap().0 {
249                    best = Some((dist, i, pt));
250                }
251            }
252        }
253
254        best.map(|(_, i, pt)| (i, pt))
255    }
256}
257
258/// Returns the closest point on segment [a, b] to the ray, plus the distance.
259fn closest_point_on_segment_to_ray(
260    ray_o: glam::Vec3,
261    ray_d: glam::Vec3,
262    seg_a: glam::Vec3,
263    seg_b: glam::Vec3,
264) -> (glam::Vec3, f32) {
265    let seg_d = seg_b - seg_a;
266    let r = ray_o - seg_a;
267    let b = ray_d.dot(seg_d);
268    let c = seg_d.dot(seg_d);
269    let d = ray_d.dot(r);
270    let e = seg_d.dot(r);
271
272    let denom = c - b * b;
273    let t_seg = if denom.abs() > 1e-7 {
274        // a * c - b * b  where a = ray_d.dot(ray_d) = 1.0
275        ((e - b * d) / denom).clamp(0.0, 1.0)
276    } else {
277        0.0
278    };
279
280    let seg_pt = seg_a + seg_d * t_seg;
281    let dist = ray_point_dist(ray_o, ray_d, seg_pt);
282    (seg_pt, dist)
283}