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::interaction::clip_plane::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            id,
182            ..PolylineItem::default()
183        }
184    }
185
186    /// Build a `GlyphItem` with sphere handles for each control point.
187    ///
188    /// Hovered and active handles appear brighter (scalar = 1.0 vs 0.2).
189    /// `id_base` is the pick ID for the first point; each subsequent point uses `id_base + i`.
190    pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
191        let mut positions = Vec::with_capacity(self.points.len());
192        let mut vectors = Vec::with_capacity(self.points.len());
193        let mut scalars = Vec::with_capacity(self.points.len());
194
195        for (i, pt) in self.points.iter().enumerate() {
196            let r = handle_world_radius(*pt, &ctx.camera, ctx.viewport_size.y, 9.0);
197            let s = if self.hovered_point == Some(i) || self.active_point == Some(i) {
198                1.0_f32
199            } else {
200                0.2
201            };
202            positions.push(pt.to_array());
203            vectors.push([r, 0.0, 0.0]);
204            scalars.push(s);
205        }
206        GlyphItem {
207            positions,
208            vectors,
209            scale: 1.0,
210            scale_by_magnitude: true,
211            scalars,
212            scalar_range: Some((0.0, 1.0)),
213            glyph_type: GlyphType::Sphere,
214            id: id_base,
215            default_colour: self.handle_colour,
216            use_default_colour: self.handle_colour[3] > 0.0,
217            ..GlyphItem::default()
218        }
219    }
220
221    // -----------------------------------------------------------------------
222    // Internal
223    // -----------------------------------------------------------------------
224
225    /// Find the closest segment to the ray, within `threshold` world units.
226    /// Returns `(segment_index, insertion_point_on_segment)` or `None`.
227    fn closest_segment(
228        &self,
229        ray_origin: glam::Vec3,
230        ray_dir: glam::Vec3,
231        threshold: f32,
232    ) -> Option<(usize, glam::Vec3)> {
233        let mut best: Option<(f32, usize, glam::Vec3)> = None;
234
235        for i in 0..self.points.len().saturating_sub(1) {
236            let a = self.points[i];
237            let b = self.points[i + 1];
238            let (pt, dist) = closest_point_on_segment_to_ray(ray_origin, ray_dir, a, b);
239            if dist < threshold {
240                if best.is_none() || dist < best.unwrap().0 {
241                    best = Some((dist, i, pt));
242                }
243            }
244        }
245
246        best.map(|(_, i, pt)| (i, pt))
247    }
248}
249
250/// Returns the closest point on segment [a, b] to the ray, plus the distance.
251fn closest_point_on_segment_to_ray(
252    ray_o: glam::Vec3,
253    ray_d: glam::Vec3,
254    seg_a: glam::Vec3,
255    seg_b: glam::Vec3,
256) -> (glam::Vec3, f32) {
257    let seg_d = seg_b - seg_a;
258    let r = ray_o - seg_a;
259    let b = ray_d.dot(seg_d);
260    let c = seg_d.dot(seg_d);
261    let d = ray_d.dot(r);
262    let e = seg_d.dot(r);
263
264    let denom = c - b * b;
265    let t_seg = if denom.abs() > 1e-7 {
266        // a * c - b * b  where a = ray_d.dot(ray_d) = 1.0
267        ((e - b * d) / denom).clamp(0.0, 1.0)
268    } else {
269        0.0
270    };
271
272    let seg_pt = seg_a + seg_d * t_seg;
273    let dist = ray_point_dist(ray_o, ray_d, seg_pt);
274    (seg_pt, dist)
275}