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 colour: [f32; 4],
33 pub line_width: f32,
35 pub handle_colour: [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 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 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 {
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 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 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 if ctx.double_clicked {
125 if let Some(idx) = self.hovered_point {
126 if self.points.len() > 2 {
128 self.points.remove(idx);
129 self.hovered_point = None;
130 return WidgetResult::Updated;
131 }
132 } else {
133 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 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 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 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 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 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
250fn 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 ((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}