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 color: [f32; 4],
39 pub handle_color: [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 color: [0.3, 0.6, 1.0, 0.25],
57 handle_color: [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 { WidgetResult::Updated } else { WidgetResult::None }
132 }
133
134 pub fn clip_object(&self) -> ClipObject {
139 let edge = [self.color[0], self.color[1], self.color[2], 1.0];
140 ClipObject {
141 shape: ClipShape::Sphere {
142 center: self.center.to_array(),
143 radius: self.radius,
144 },
145 color: Some(self.color),
146 edge_color: Some(edge),
147 clip_geometry: false,
148 enabled: true,
149 hovered: self.hovered_handle.is_some(),
150 active: self.active_handle.is_some(),
151 ..ClipObject::default()
152 }
153 }
154
155 pub fn wireframe_item(&self, id: u64) -> PolylineItem {
160 const STEPS: usize = 64;
161 let mut positions: Vec<[f32; 3]> = Vec::with_capacity(STEPS * 3 + 3);
162 let mut strip_lengths: Vec<u32> = Vec::with_capacity(3);
163 let c = self.center;
164 let r = self.radius;
165
166 for ring in 0..3_usize {
167 for i in 0..=STEPS {
168 let a = i as f32 * std::f32::consts::TAU / STEPS as f32;
169 let (s, co) = a.sin_cos();
170 let p = match ring {
171 0 => glam::Vec3::new(co * r, s * r, 0.0),
172 1 => glam::Vec3::new(co * r, 0.0, s * r),
173 _ => glam::Vec3::new(0.0, co * r, s * r),
174 };
175 positions.push((c + p).to_array());
176 }
177 strip_lengths.push((STEPS + 1) as u32);
178 }
179
180 let line_color = [self.color[0], self.color[1], self.color[2], 1.0];
181 PolylineItem {
182 positions,
183 strip_lengths,
184 default_color: line_color,
185 line_width: 1.5,
186 id,
187 ..PolylineItem::default()
188 }
189 }
190
191 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
196 let rp = self.radius_handle_pos();
197 let r_center = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
198 let r_rh = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
199
200 let sc = if self.hovered_handle == Some(SphereHandle::Center)
201 || self.active_handle == Some(SphereHandle::Center)
202 {
203 1.0_f32
204 } else {
205 0.2
206 };
207 let sr = if self.hovered_handle == Some(SphereHandle::Radius)
208 || self.active_handle == Some(SphereHandle::Radius)
209 {
210 1.0_f32
211 } else {
212 0.2
213 };
214
215 GlyphItem {
216 positions: vec![self.center.to_array(), rp.to_array()],
217 vectors: vec![[r_center, 0.0, 0.0], [r_rh, 0.0, 0.0]],
218 scale: 1.0,
219 scale_by_magnitude: true,
220 scalars: vec![sc, sr],
221 scalar_range: Some((0.0, 1.0)),
222 glyph_type: GlyphType::Sphere,
223 id: id_base,
224 default_color: self.handle_color,
225 use_default_color: self.handle_color[3] > 0.0,
226 ..GlyphItem::default()
227 }
228 }
229
230 fn radius_handle_pos(&self) -> glam::Vec3 {
235 self.center + glam::Vec3::X * self.radius
236 }
237
238 fn hit_test(
239 &self,
240 ray_origin: glam::Vec3,
241 ray_dir: glam::Vec3,
242 ctx: &WidgetContext,
243 ) -> Option<SphereHandle> {
244 let ray = Ray::new(
245 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
246 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
247 );
248
249 let rp = self.radius_handle_pos();
250 let rh_r = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
251 let ch_r = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
252
253 let rh_ball = parry3d::shape::Ball::new(rh_r);
254 let ch_ball = parry3d::shape::Ball::new(ch_r);
255
256 let rh_pose = Pose::from_parts(
257 [rp.x, rp.y, rp.z].into(),
258 glam::Quat::IDENTITY,
259 );
260 let ch_pose = Pose::from_parts(
261 [self.center.x, self.center.y, self.center.z].into(),
262 glam::Quat::IDENTITY,
263 );
264
265 let t_rh = rh_ball
266 .cast_ray(&rh_pose, &ray, f32::MAX, true)
267 .map(|i| i);
268 let t_ch = ch_ball
269 .cast_ray(&ch_pose, &ray, f32::MAX, true)
270 .map(|i| i);
271
272 match (t_ch, t_rh) {
273 (Some(tc), Some(tr)) => {
274 Some(if tc <= tr { SphereHandle::Center } else { SphereHandle::Radius })
275 }
276 (Some(_), None) => Some(SphereHandle::Center),
277 (None, Some(_)) => Some(SphereHandle::Radius),
278 (None, None) => None,
279 }
280 }
281}