viewport_lib/interaction/widgets/
sphere.rs1use crate::interaction::clip_plane::ray_plane_intersection;
4use crate::renderer::{ClipObject, ClipShape, GlyphItem, GlyphType, PolylineItem};
5use parry3d::math::{Pose, Vector};
6use parry3d::query::{Ray, RayCast};
7
8use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius};
9
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
11enum SphereHandle {
12 Center,
13 Radius,
14}
15
16pub struct SphereWidget {
33 pub center: glam::Vec3,
35 pub radius: f32,
37 pub colour: [f32; 4],
39 pub handle_colour: [f32; 4],
41
42 hovered_handle: Option<SphereHandle>,
43 active_handle: Option<SphereHandle>,
44 drag_plane_normal: glam::Vec3,
45 drag_plane_d: f32,
46 drag_anchor_world: glam::Vec3,
47 drag_anchor_radius: f32,
48}
49
50impl SphereWidget {
51 pub fn new(center: glam::Vec3, radius: f32) -> Self {
53 Self {
54 center,
55 radius: radius.max(0.01),
56 colour: [0.3, 0.6, 1.0, 0.25],
57 handle_colour: [0.0; 4],
58 hovered_handle: None,
59 active_handle: None,
60 drag_plane_normal: glam::Vec3::Z,
61 drag_plane_d: 0.0,
62 drag_anchor_world: glam::Vec3::ZERO,
63 drag_anchor_radius: 0.0,
64 }
65 }
66
67 pub fn is_active(&self) -> bool {
69 self.active_handle.is_some()
70 }
71
72 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
74 let (ro, rd) = ctx_ray(ctx);
75 let mut updated = false;
76
77 if self.active_handle.is_none() {
78 let hit = self.hit_test(ro, rd, ctx);
79 if hit.is_some() || !ctx.drag_started {
83 self.hovered_handle = hit;
84 }
85 }
86
87 if ctx.drag_started {
88 if let Some(handle) = self.hovered_handle {
89 let anchor = match handle {
90 SphereHandle::Center => self.center,
91 SphereHandle::Radius => self.radius_handle_pos(),
92 };
93 let fwd = glam::Vec3::from(ctx.camera.forward);
94 let n = -fwd;
95 self.drag_plane_normal = n;
96 self.drag_plane_d = -n.dot(anchor);
97 self.drag_anchor_world = anchor;
98 self.drag_anchor_radius = self.radius;
99 self.active_handle = Some(handle);
100 }
101 }
102
103 if let Some(handle) = self.active_handle {
104 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
105 self.active_handle = None;
106 self.hovered_handle = None;
107 } else if let Some(hit) =
108 ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
109 {
110 match handle {
111 SphereHandle::Center => {
112 let delta = hit - self.drag_anchor_world;
113 let new_center = self.center + delta;
114 if (new_center - self.center).length_squared() > 1e-10 {
115 self.center = new_center;
116 self.drag_anchor_world = hit;
117 updated = true;
118 }
119 }
120 SphereHandle::Radius => {
121 let new_r = (hit - self.center).length().max(0.01);
122 if (new_r - self.radius).abs() > 1e-5 {
123 self.radius = new_r;
124 updated = true;
125 }
126 }
127 }
128 }
129 }
130
131 if updated {
132 WidgetResult::Updated
133 } else {
134 WidgetResult::None
135 }
136 }
137
138 pub fn clip_object(&self) -> ClipObject {
143 let edge = [self.colour[0], self.colour[1], self.colour[2], 1.0];
144 ClipObject {
145 shape: ClipShape::Sphere {
146 center: self.center.to_array(),
147 radius: self.radius,
148 },
149 colour: Some(self.colour),
150 edge_colour: Some(edge),
151 clip_geometry: false,
152 enabled: true,
153 hovered: self.hovered_handle.is_some(),
154 active: self.active_handle.is_some(),
155 ..ClipObject::default()
156 }
157 }
158
159 pub fn wireframe_item(&self, id: u64) -> PolylineItem {
164 const STEPS: usize = 64;
165 let mut positions: Vec<[f32; 3]> = Vec::with_capacity(STEPS * 3 + 3);
166 let mut strip_lengths: Vec<u32> = Vec::with_capacity(3);
167 let c = self.center;
168 let r = self.radius;
169
170 for ring in 0..3_usize {
171 for i in 0..=STEPS {
172 let a = i as f32 * std::f32::consts::TAU / STEPS as f32;
173 let (s, co) = a.sin_cos();
174 let p = match ring {
175 0 => glam::Vec3::new(co * r, s * r, 0.0),
176 1 => glam::Vec3::new(co * r, 0.0, s * r),
177 _ => glam::Vec3::new(0.0, co * r, s * r),
178 };
179 positions.push((c + p).to_array());
180 }
181 strip_lengths.push((STEPS + 1) as u32);
182 }
183
184 let line_colour = [self.colour[0], self.colour[1], self.colour[2], 1.0];
185 PolylineItem {
186 positions,
187 strip_lengths,
188 default_colour: line_colour,
189 line_width: 1.5,
190 id,
191 ..PolylineItem::default()
192 }
193 }
194
195 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
200 let rp = self.radius_handle_pos();
201 let r_center = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
202 let r_rh = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
203
204 let sc = if self.hovered_handle == Some(SphereHandle::Center)
205 || self.active_handle == Some(SphereHandle::Center)
206 {
207 1.0_f32
208 } else {
209 0.2
210 };
211 let sr = if self.hovered_handle == Some(SphereHandle::Radius)
212 || self.active_handle == Some(SphereHandle::Radius)
213 {
214 1.0_f32
215 } else {
216 0.2
217 };
218
219 GlyphItem {
220 positions: vec![self.center.to_array(), rp.to_array()],
221 vectors: vec![[r_center, 0.0, 0.0], [r_rh, 0.0, 0.0]],
222 scale: 1.0,
223 scale_by_magnitude: true,
224 scalars: vec![sc, sr],
225 scalar_range: Some((0.0, 1.0)),
226 glyph_type: GlyphType::Sphere,
227 id: id_base,
228 default_colour: self.handle_colour,
229 use_default_colour: self.handle_colour[3] > 0.0,
230 ..GlyphItem::default()
231 }
232 }
233
234 fn radius_handle_pos(&self) -> glam::Vec3 {
239 self.center + glam::Vec3::X * self.radius
240 }
241
242 fn hit_test(
243 &self,
244 ray_origin: glam::Vec3,
245 ray_dir: glam::Vec3,
246 ctx: &WidgetContext,
247 ) -> Option<SphereHandle> {
248 let ray = Ray::new(
249 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
250 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
251 );
252
253 let rp = self.radius_handle_pos();
254 let rh_r = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
255 let ch_r = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
256
257 let rh_ball = parry3d::shape::Ball::new(rh_r);
258 let ch_ball = parry3d::shape::Ball::new(ch_r);
259
260 let rh_pose = Pose::from_parts([rp.x, rp.y, rp.z].into(), glam::Quat::IDENTITY);
261 let ch_pose = Pose::from_parts(
262 [self.center.x, self.center.y, self.center.z].into(),
263 glam::Quat::IDENTITY,
264 );
265
266 let t_rh = rh_ball.cast_ray(&rh_pose, &ray, f32::MAX, true).map(|i| i);
267 let t_ch = ch_ball.cast_ray(&ch_pose, &ray, f32::MAX, true).map(|i| i);
268
269 match (t_ch, t_rh) {
270 (Some(tc), Some(tr)) => Some(if tc <= tr {
271 SphereHandle::Center
272 } else {
273 SphereHandle::Radius
274 }),
275 (Some(_), None) => Some(SphereHandle::Center),
276 (None, Some(_)) => Some(SphereHandle::Radius),
277 (None, None) => None,
278 }
279 }
280}