viewport_lib/interaction/widgets/
disk.rs1use crate::interaction::clip_plane::ray_plane_intersection;
4use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
5use parry3d::math::{Pose, Vector};
6use parry3d::query::{Ray, RayCast};
7
8use super::{WidgetContext, WidgetResult, any_perpendicular, any_perpendicular_pair, ctx_ray, handle_world_radius, ray_point_dist};
9
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
11enum DiskHandle {
12 Center,
13 NormalTip,
14 Radius,
15}
16
17pub struct DiskWidget {
33 pub center: glam::Vec3,
35 pub normal: glam::Vec3,
37 pub radius: f32,
39 pub color: [f32; 4],
41 pub handle_color: [f32; 4],
43 pub normal_display_length: f32,
45
46 hovered_handle: Option<DiskHandle>,
47 active_handle: Option<DiskHandle>,
48 drag_plane_normal: glam::Vec3,
49 drag_plane_d: f32,
50 drag_anchor: glam::Vec3,
51}
52
53impl DiskWidget {
54 pub fn new(center: glam::Vec3, normal: glam::Vec3, radius: f32) -> Self {
58 let len = normal.length();
59 let normal = if len > 1e-6 { normal / len } else { glam::Vec3::Z };
60 Self {
61 center,
62 normal,
63 radius: radius.max(0.01),
64 color: [0.9, 0.6, 0.1, 1.0],
65 handle_color: [0.0; 4],
66 normal_display_length: 2.0,
67 hovered_handle: None,
68 active_handle: None,
69 drag_plane_normal: glam::Vec3::Z,
70 drag_plane_d: 0.0,
71 drag_anchor: glam::Vec3::ZERO,
72 }
73 }
74
75 pub fn is_active(&self) -> bool {
77 self.active_handle.is_some()
78 }
79
80 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
82 let (ro, rd) = ctx_ray(ctx);
83 let mut updated = false;
84
85 if self.active_handle.is_none() {
86 let hit = self.hit_test(ro, rd, ctx);
87 if hit.is_some() || !ctx.drag_started {
88 self.hovered_handle = hit;
89 }
90 }
91
92 if ctx.drag_started {
93 if let Some(handle) = self.hovered_handle {
94 let anchor = match handle {
95 DiskHandle::Center => self.center,
96 DiskHandle::NormalTip => self.normal_tip_pos(),
97 DiskHandle::Radius => self.radius_handle_pos(),
98 };
99 let n = -glam::Vec3::from(ctx.camera.forward);
100 self.drag_plane_normal = n;
101 self.drag_plane_d = -n.dot(anchor);
102 self.drag_anchor = anchor;
103 self.active_handle = Some(handle);
104 }
105 }
106
107 if let Some(handle) = self.active_handle {
108 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
109 self.active_handle = None;
110 self.hovered_handle = None;
111 } else if let Some(hit) =
112 ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
113 {
114 match handle {
115 DiskHandle::Center => {
116 let delta = hit - self.drag_anchor;
117 if delta.length_squared() > 1e-10 {
118 self.center += delta;
119 self.drag_anchor = hit;
120 updated = true;
121 }
122 }
123 DiskHandle::NormalTip => {
124 let dir = hit - self.center;
125 let len = dir.length();
126 if len > 1e-3 {
127 let new_normal = dir / len;
128 if (new_normal - self.normal).length_squared() > 1e-8 {
129 self.normal = new_normal;
130 updated = true;
131 }
132 }
133 }
134 DiskHandle::Radius => {
135 let new_r = (hit - self.center).length().max(0.01);
136 if (new_r - self.radius).abs() > 1e-5 {
137 self.radius = new_r;
138 updated = true;
139 }
140 }
141 }
142 }
143 }
144
145 if updated { WidgetResult::Updated } else { WidgetResult::None }
146 }
147
148 pub fn wireframe_item(&self, id: u64) -> PolylineItem {
152 const STEPS: usize = 32;
153 let (u, v) = any_perpendicular_pair(self.normal);
154 let c = self.center;
155 let r = self.radius;
156
157 let mut positions: Vec<[f32; 3]> = Vec::with_capacity(STEPS + 1 + 2);
158 for i in 0..=STEPS {
159 let a = i as f32 * std::f32::consts::TAU / STEPS as f32;
160 let (s, co) = a.sin_cos();
161 positions.push((c + u * (co * r) + v * (s * r)).to_array());
162 }
163 positions.push(c.to_array());
164 positions.push(self.normal_tip_pos().to_array());
165
166 PolylineItem {
167 positions,
168 strip_lengths: vec![(STEPS + 1) as u32, 2],
169 default_color: self.color,
170 line_width: 1.5,
171 id,
172 ..PolylineItem::default()
173 }
174 }
175
176 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
180 let tip = self.normal_tip_pos();
181 let rh = self.radius_handle_pos();
182
183 let rc = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
184 let rt = handle_world_radius(tip, &ctx.camera, ctx.viewport_size.y, 8.0);
185 let rr = handle_world_radius(rh, &ctx.camera, ctx.viewport_size.y, 8.0);
186
187 let scalar = |h: DiskHandle| {
188 if self.hovered_handle == Some(h) || self.active_handle == Some(h) { 1.0_f32 } else { 0.2 }
189 };
190
191 GlyphItem {
192 positions: vec![self.center.to_array(), tip.to_array(), rh.to_array()],
193 vectors: vec![[rc, 0.0, 0.0], [rt, 0.0, 0.0], [rr, 0.0, 0.0]],
194 scale: 1.0,
195 scale_by_magnitude: true,
196 scalars: vec![
197 scalar(DiskHandle::Center),
198 scalar(DiskHandle::NormalTip),
199 scalar(DiskHandle::Radius),
200 ],
201 scalar_range: Some((0.0, 1.0)),
202 glyph_type: GlyphType::Sphere,
203 id: id_base,
204 default_color: self.handle_color,
205 use_default_color: self.handle_color[3] > 0.0,
206 ..GlyphItem::default()
207 }
208 }
209
210 fn normal_tip_pos(&self) -> glam::Vec3 {
215 self.center + self.normal * self.normal_display_length
216 }
217
218 fn radius_handle_pos(&self) -> glam::Vec3 {
219 self.center + any_perpendicular(self.normal) * self.radius
221 }
222
223 fn hit_test(
224 &self,
225 ray_origin: glam::Vec3,
226 ray_dir: glam::Vec3,
227 ctx: &WidgetContext,
228 ) -> Option<DiskHandle> {
229 let tip = self.normal_tip_pos();
230 let rh = self.radius_handle_pos();
231
232 let ray = Ray::new(
233 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
234 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
235 );
236
237 let rc = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
238 let rt = handle_world_radius(tip, &ctx.camera, ctx.viewport_size.y, 8.0);
239 let rr = handle_world_radius(rh, &ctx.camera, ctx.viewport_size.y, 8.0);
240
241 let handles = [
242 (DiskHandle::Center, self.center, rc),
243 (DiskHandle::NormalTip, tip, rt),
244 (DiskHandle::Radius, rh, rr),
245 ];
246
247 let mut best: Option<(f32, DiskHandle)> = None;
248 for (handle, pos, radius) in handles {
249 let d = ray_point_dist(ray_origin, ray_dir, pos);
250 if d < radius {
251 let ball = parry3d::shape::Ball::new(radius);
252 let pose = Pose::from_parts([pos.x, pos.y, pos.z].into(), glam::Quat::IDENTITY);
253 if let Some(t) = ball.cast_ray(&pose, &ray, f32::MAX, true) {
254 if best.is_none() || t < best.unwrap().0 {
255 best = Some((t, handle));
256 }
257 }
258 }
259 }
260
261 best.map(|(_, h)| h)
262 }
263}