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
182 settings: crate::scene::material::ItemSettings { pick_id: crate::renderer::PickId(id), ..Default::default() },
183 ..PolylineItem::default()
184 }
185 }
186
187 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
192 let mut positions = Vec::with_capacity(self.points.len());
193 let mut vectors = Vec::with_capacity(self.points.len());
194 let mut scalars = Vec::with_capacity(self.points.len());
195
196 for (i, pt) in self.points.iter().enumerate() {
197 let r = handle_world_radius(*pt, &ctx.camera, ctx.viewport_size.y, 9.0);
198 let s = if self.hovered_point == Some(i) || self.active_point == Some(i) {
199 1.0_f32
200 } else {
201 0.2
202 };
203 positions.push(pt.to_array());
204 vectors.push([r, 0.0, 0.0]);
205 scalars.push(s);
206 }
207 GlyphItem {
208 positions,
209 vectors,
210 scale: 1.0,
211 scale_by_magnitude: true,
212 scalars,
213 scalar_range: Some((0.0, 1.0)),
214 glyph_type: GlyphType::Sphere,
215
216 settings: crate::scene::material::ItemSettings { pick_id: crate::renderer::PickId(id_base), ..Default::default() },
217 default_colour: self.handle_colour,
218 use_default_colour: self.handle_colour[3] > 0.0,
219 ..GlyphItem::default()
220 }
221 }
222
223 fn closest_segment(
230 &self,
231 ray_origin: glam::Vec3,
232 ray_dir: glam::Vec3,
233 threshold: f32,
234 ) -> Option<(usize, glam::Vec3)> {
235 let mut best: Option<(f32, usize, glam::Vec3)> = None;
236
237 for i in 0..self.points.len().saturating_sub(1) {
238 let a = self.points[i];
239 let b = self.points[i + 1];
240 let (pt, dist) = closest_point_on_segment_to_ray(ray_origin, ray_dir, a, b);
241 if dist < threshold {
242 if best.is_none() || dist < best.unwrap().0 {
243 best = Some((dist, i, pt));
244 }
245 }
246 }
247
248 best.map(|(_, i, pt)| (i, pt))
249 }
250}
251
252fn closest_point_on_segment_to_ray(
254 ray_o: glam::Vec3,
255 ray_d: glam::Vec3,
256 seg_a: glam::Vec3,
257 seg_b: glam::Vec3,
258) -> (glam::Vec3, f32) {
259 let seg_d = seg_b - seg_a;
260 let r = ray_o - seg_a;
261 let b = ray_d.dot(seg_d);
262 let c = seg_d.dot(seg_d);
263 let d = ray_d.dot(r);
264 let e = seg_d.dot(r);
265
266 let denom = c - b * b;
267 let t_seg = if denom.abs() > 1e-7 {
268 ((e - b * d) / denom).clamp(0.0, 1.0)
270 } else {
271 0.0
272 };
273
274 let seg_pt = seg_a + seg_d * t_seg;
275 let dist = ray_point_dist(ray_o, ray_d, seg_pt);
276 (seg_pt, dist)
277}