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