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 color for the line segments.
32    pub color: [f32; 4],
33    /// Line width in pixels.
34    pub line_width: f32,
35    /// RGBA color for the drag handles.
36    pub handle_color: [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            color: [0.9, 0.5, 0.1, 1.0],
59            line_width: 2.0,
60            handle_color: [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 { self.hovered_point = Some(h - 1); }
87            }
88            if self.active_point == Some(index) {
89                self.active_point = None;
90            } else if let Some(a) = self.active_point {
91                if a > index { self.active_point = Some(a - 1); }
92            }
93        }
94    }
95
96    /// Process input for this frame. Returns `Updated` if state changed.
97    ///
98    /// Double-click behavior (requires `ctx.double_clicked`):
99    /// - On a hovered control point: removes that point (minimum 2 enforced).
100    /// - On a line segment: inserts a new point at the projected position.
101    pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
102        let (ro, rd) = ctx_ray(ctx);
103        let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
104        let hit_radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 12.0);
105
106        // --- Hover detection ---
107        if !ctx.dragging {
108            self.hovered_point = None;
109            let mut best_dist = hit_radius;
110            for (i, &pt) in self.points.iter().enumerate() {
111                let d = ray_point_dist(ro, rd, pt);
112                if d < best_dist {
113                    best_dist = d;
114                    self.hovered_point = Some(i);
115                }
116            }
117        }
118
119        // --- Double-click: add or remove points ---
120        if ctx.double_clicked {
121            if let Some(idx) = self.hovered_point {
122                // Double-click on handle: remove that point.
123                if self.points.len() > 2 {
124                    self.points.remove(idx);
125                    self.hovered_point = None;
126                    return WidgetResult::Updated;
127                }
128            } else {
129                // Double-click on a segment: find the closest segment and insert a point.
130                let seg_threshold = hit_radius * 2.0;
131                if let Some((seg_idx, insert_pos)) = self.closest_segment(ro, rd, seg_threshold) {
132                    self.points.insert(seg_idx + 1, insert_pos);
133                    self.hovered_point = Some(seg_idx + 1);
134                    return WidgetResult::Updated;
135                }
136            }
137        }
138
139        // --- Drag start ---
140        if ctx.drag_started {
141            if let Some(idx) = self.hovered_point {
142                self.active_point = Some(idx);
143                self.drag_plane_normal = -glam::Vec3::from(ctx.camera.forward);
144                self.drag_plane_d = -self.drag_plane_normal.dot(self.points[idx]);
145            }
146        }
147
148        // --- Drag in progress ---
149        if ctx.dragging {
150            if let Some(idx) = self.active_point {
151                if let Some(hit) =
152                    ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
153                {
154                    if (hit - self.points[idx]).length_squared() > 1e-10 {
155                        self.points[idx] = hit;
156                        return WidgetResult::Updated;
157                    }
158                }
159            }
160        }
161
162        if ctx.released {
163            self.active_point = None;
164        }
165
166        WidgetResult::None
167    }
168
169    /// Build a `PolylineItem` through all control points.
170    pub fn polyline_item(&self, id: u64) -> PolylineItem {
171        let n = self.points.len() as u32;
172        PolylineItem {
173            positions: self.points.iter().map(|p| p.to_array()).collect(),
174            strip_lengths: if n > 0 { vec![n] } else { vec![] },
175            default_color: self.color,
176            line_width: self.line_width,
177            id,
178            ..PolylineItem::default()
179        }
180    }
181
182    /// Build a `GlyphItem` with sphere handles for each control point.
183    ///
184    /// Hovered and active handles appear brighter (scalar = 1.0 vs 0.2).
185    /// `id_base` is the pick ID for the first point; each subsequent point uses `id_base + i`.
186    pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
187        let mut positions = Vec::with_capacity(self.points.len());
188        let mut vectors = Vec::with_capacity(self.points.len());
189        let mut scalars = Vec::with_capacity(self.points.len());
190
191        for (i, pt) in self.points.iter().enumerate() {
192            let r = handle_world_radius(*pt, &ctx.camera, ctx.viewport_size.y, 9.0);
193            let s = if self.hovered_point == Some(i) || self.active_point == Some(i) {
194                1.0_f32
195            } else {
196                0.2
197            };
198            positions.push(pt.to_array());
199            vectors.push([r, 0.0, 0.0]);
200            scalars.push(s);
201        }
202        GlyphItem {
203            positions,
204            vectors,
205            scale: 1.0,
206            scale_by_magnitude: true,
207            scalars,
208            scalar_range: Some((0.0, 1.0)),
209            glyph_type: GlyphType::Sphere,
210            id: id_base,
211            default_color: self.handle_color,
212            use_default_color: self.handle_color[3] > 0.0,
213            ..GlyphItem::default()
214        }
215    }
216
217    // -----------------------------------------------------------------------
218    // Internal
219    // -----------------------------------------------------------------------
220
221    /// Find the closest segment to the ray, within `threshold` world units.
222    /// Returns `(segment_index, insertion_point_on_segment)` or `None`.
223    fn closest_segment(
224        &self,
225        ray_origin: glam::Vec3,
226        ray_dir: glam::Vec3,
227        threshold: f32,
228    ) -> Option<(usize, glam::Vec3)> {
229        let mut best: Option<(f32, usize, glam::Vec3)> = None;
230
231        for i in 0..self.points.len().saturating_sub(1) {
232            let a = self.points[i];
233            let b = self.points[i + 1];
234            let (pt, dist) = closest_point_on_segment_to_ray(ray_origin, ray_dir, a, b);
235            if dist < threshold {
236                if best.is_none() || dist < best.unwrap().0 {
237                    best = Some((dist, i, pt));
238                }
239            }
240        }
241
242        best.map(|(_, i, pt)| (i, pt))
243    }
244}
245
246/// Returns the closest point on segment [a, b] to the ray, plus the distance.
247fn closest_point_on_segment_to_ray(
248    ray_o: glam::Vec3,
249    ray_d: glam::Vec3,
250    seg_a: glam::Vec3,
251    seg_b: glam::Vec3,
252) -> (glam::Vec3, f32) {
253    let seg_d = seg_b - seg_a;
254    let r = ray_o - seg_a;
255    let b = ray_d.dot(seg_d);
256    let c = seg_d.dot(seg_d);
257    let d = ray_d.dot(r);
258    let e = seg_d.dot(r);
259
260    let denom = c - b * b;
261    let t_seg = if denom.abs() > 1e-7 {
262        // a * c - b * b  where a = ray_d.dot(ray_d) = 1.0
263        ((e - b * d) / denom).clamp(0.0, 1.0)
264    } else {
265        0.0
266    };
267
268    let seg_pt = seg_a + seg_d * t_seg;
269    let dist = ray_point_dist(ray_o, ray_d, seg_pt);
270    (seg_pt, dist)
271}