viewport_lib/interaction/widgets/
spline.rs1use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius, ray_point_dist};
4use crate::interaction::clip_plane::ray_plane_intersection;
5use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
6
7pub struct SplineWidget {
12 pub points: Vec<glam::Vec3>,
14 pub colour: [f32; 4],
16 pub line_width: f32,
18 pub handle_colour: [f32; 4],
20 pub resolution: u32,
22 hovered_point: Option<usize>,
23 active_point: Option<usize>,
24 drag_plane_normal: glam::Vec3,
25 drag_plane_d: f32,
26}
27
28impl SplineWidget {
29 pub fn new(points: Vec<glam::Vec3>) -> Self {
31 Self {
32 points,
33 colour: [0.4, 0.8, 1.0, 1.0],
34 line_width: 2.0,
35 handle_colour: [1.0, 0.8, 0.2, 1.0],
36 resolution: 16,
37 hovered_point: None,
38 active_point: None,
39 drag_plane_normal: glam::Vec3::Y,
40 drag_plane_d: 0.0,
41 }
42 }
43
44 pub fn hovered_point(&self) -> Option<usize> {
46 self.hovered_point
47 }
48
49 pub fn is_active(&self) -> bool {
51 self.active_point.is_some()
52 }
53
54 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
56 let (ray_origin, ray_dir) = ctx_ray(ctx);
57 let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
59 let hit_radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 12.0);
60
61 if !ctx.dragging {
62 self.hovered_point = None;
63 let mut best_dist = hit_radius;
64 for (i, &pt) in self.points.iter().enumerate() {
65 let d = ray_point_dist(ray_origin, ray_dir, pt);
66 if d < best_dist {
67 best_dist = d;
68 self.hovered_point = Some(i);
69 }
70 }
71 }
72
73 if ctx.drag_started {
74 if let Some(idx) = self.hovered_point {
75 self.active_point = Some(idx);
76 self.drag_plane_normal = -glam::Vec3::from(ctx.camera.forward);
77 let pt = self.points[idx];
78 self.drag_plane_d = -self.drag_plane_normal.dot(pt);
79 }
80 }
81
82 if ctx.dragging {
83 if let Some(idx) = self.active_point {
84 if let Some(hit) = ray_plane_intersection(
85 ray_origin,
86 ray_dir,
87 self.drag_plane_normal,
88 self.drag_plane_d,
89 ) {
90 self.points[idx] = hit;
91 return WidgetResult::Updated;
92 }
93 }
94 }
95
96 if ctx.released {
97 self.active_point = None;
98 }
99
100 WidgetResult::None
101 }
102
103 pub fn polyline_item(&self, id: u64) -> PolylineItem {
105 let sampled = self.sampled_positions();
106 let n = sampled.len() as u32;
107 PolylineItem {
108 positions: sampled,
109 strip_lengths: if n > 0 { vec![n] } else { vec![] },
110 default_colour: self.colour,
111 line_width: self.line_width,
112 id,
113 ..PolylineItem::default()
114 }
115 }
116
117 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
122 let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
123 let radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 8.0);
124 GlyphItem {
125 positions: self.points.iter().map(|p| p.to_array()).collect(),
126 glyph_type: GlyphType::Sphere,
127 scale: radius,
128 use_default_colour: true,
129 default_colour: self.handle_colour,
130 id: id_base,
131 ..GlyphItem::default()
132 }
133 }
134
135 pub fn sampled_positions(&self) -> Vec<[f32; 3]> {
137 let n = self.points.len();
138 if n == 0 {
139 return Vec::new();
140 }
141 if n == 1 {
142 return vec![self.points[0].to_array()];
143 }
144 let res = self.resolution.max(1) as usize;
145 let mut out: Vec<[f32; 3]> = Vec::with_capacity((n - 1) * res + 1);
146 for seg in 0..(n - 1) {
147 let p0 = self.points[if seg > 0 { seg - 1 } else { seg }];
148 let p1 = self.points[seg];
149 let p2 = self.points[seg + 1];
150 let p3 = self.points[if seg + 2 < n { seg + 2 } else { seg + 1 }];
151 for s in 0..res {
152 let t = s as f32 / res as f32;
153 out.push(catmull_rom(p0, p1, p2, p3, t).to_array());
154 }
155 }
156 out.push(self.points[n - 1].to_array());
157 out
158 }
159}
160
161fn catmull_rom(
162 p0: glam::Vec3,
163 p1: glam::Vec3,
164 p2: glam::Vec3,
165 p3: glam::Vec3,
166 t: f32,
167) -> glam::Vec3 {
168 let t2 = t * t;
169 let t3 = t2 * t;
170 0.5 * ((-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
171 + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
172 + (-p0 + p2) * t
173 + 2.0 * p1)
174}