viewport_lib/interaction/widgets/
polyline_widget.rs1use 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
8pub struct PolylineWidget {
29 pub points: Vec<glam::Vec3>,
31 pub color: [f32; 4],
33 pub line_width: f32,
35 pub handle_color: [f32; 4],
37 pub hovered_point: Option<usize>,
39 pub active_point: Option<usize>,
41
42 drag_plane_normal: glam::Vec3,
43 drag_plane_d: f32,
44}
45
46impl PolylineWidget {
47 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 pub fn is_active(&self) -> bool {
70 self.active_point.is_some()
71 }
72
73 pub fn add_point(&mut self, pos: glam::Vec3) {
75 self.points.push(pos);
76 }
77
78 pub fn remove_point(&mut self, index: usize) {
80 if self.points.len() > 2 && index < self.points.len() {
81 self.points.remove(index);
82 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 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 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 if ctx.double_clicked {
121 if let Some(idx) = self.hovered_point {
122 if self.points.len() > 2 {
124 self.points.remove(idx);
125 self.hovered_point = None;
126 return WidgetResult::Updated;
127 }
128 } else {
129 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 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 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 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 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 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
246fn 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 ((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}