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
40 hovered_handle: Option<SphereHandle>,
41 active_handle: Option<SphereHandle>,
42 drag_plane_normal: glam::Vec3,
43 drag_plane_d: f32,
44 drag_anchor_world: glam::Vec3,
45 drag_anchor_radius: f32,
46}
47
48impl SphereWidget {
49 pub fn new(center: glam::Vec3, radius: f32) -> Self {
51 Self {
52 center,
53 radius: radius.max(0.01),
54 color: [0.3, 0.6, 1.0, 0.25],
55 hovered_handle: None,
56 active_handle: None,
57 drag_plane_normal: glam::Vec3::Z,
58 drag_plane_d: 0.0,
59 drag_anchor_world: glam::Vec3::ZERO,
60 drag_anchor_radius: 0.0,
61 }
62 }
63
64 pub fn is_active(&self) -> bool {
66 self.active_handle.is_some()
67 }
68
69 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
71 let (ro, rd) = ctx_ray(ctx);
72 let mut updated = false;
73
74 if self.active_handle.is_none() {
75 self.hovered_handle = self.hit_test(ro, rd, ctx);
76 }
77
78 if ctx.drag_started {
79 if let Some(handle) = self.hovered_handle {
80 let anchor = match handle {
81 SphereHandle::Center => self.center,
82 SphereHandle::Radius => self.radius_handle_pos(),
83 };
84 let fwd = glam::Vec3::from(ctx.camera.forward);
85 let n = -fwd;
86 self.drag_plane_normal = n;
87 self.drag_plane_d = -n.dot(anchor);
88 self.drag_anchor_world = anchor;
89 self.drag_anchor_radius = self.radius;
90 self.active_handle = Some(handle);
91 }
92 }
93
94 if let Some(handle) = self.active_handle {
95 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
96 self.active_handle = None;
97 self.hovered_handle = None;
98 } else if let Some(hit) =
99 ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
100 {
101 match handle {
102 SphereHandle::Center => {
103 let delta = hit - self.drag_anchor_world;
104 let new_center = self.center + delta;
105 if (new_center - self.center).length_squared() > 1e-10 {
106 self.center = new_center;
107 self.drag_anchor_world = hit;
108 updated = true;
109 }
110 }
111 SphereHandle::Radius => {
112 let new_r = (hit - self.center).length().max(0.01);
113 if (new_r - self.radius).abs() > 1e-5 {
114 self.radius = new_r;
115 updated = true;
116 }
117 }
118 }
119 }
120 }
121
122 if updated { WidgetResult::Updated } else { WidgetResult::None }
123 }
124
125 pub fn clip_object(&self) -> ClipObject {
130 let edge = [self.color[0], self.color[1], self.color[2], 1.0];
131 ClipObject {
132 shape: ClipShape::Sphere {
133 center: self.center.to_array(),
134 radius: self.radius,
135 },
136 color: Some(self.color),
137 edge_color: Some(edge),
138 clip_geometry: false,
139 enabled: true,
140 hovered: self.hovered_handle.is_some(),
141 active: self.active_handle.is_some(),
142 ..ClipObject::default()
143 }
144 }
145
146 pub fn wireframe_item(&self, id: u64) -> PolylineItem {
151 const STEPS: usize = 64;
152 let mut positions: Vec<[f32; 3]> = Vec::with_capacity(STEPS * 3 + 3);
153 let mut strip_lengths: Vec<u32> = Vec::with_capacity(3);
154 let c = self.center;
155 let r = self.radius;
156
157 for ring in 0..3_usize {
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 let p = match ring {
162 0 => glam::Vec3::new(co * r, s * r, 0.0),
163 1 => glam::Vec3::new(co * r, 0.0, s * r),
164 _ => glam::Vec3::new(0.0, co * r, s * r),
165 };
166 positions.push((c + p).to_array());
167 }
168 strip_lengths.push((STEPS + 1) as u32);
169 }
170
171 let line_color = [self.color[0], self.color[1], self.color[2], 1.0];
172 PolylineItem {
173 positions,
174 strip_lengths,
175 default_color: line_color,
176 line_width: 1.5,
177 id,
178 ..PolylineItem::default()
179 }
180 }
181
182 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
187 let rp = self.radius_handle_pos();
188 let r_center = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
189 let r_rh = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
190
191 let sc = if self.hovered_handle == Some(SphereHandle::Center)
192 || self.active_handle == Some(SphereHandle::Center)
193 {
194 1.0_f32
195 } else {
196 0.2
197 };
198 let sr = if self.hovered_handle == Some(SphereHandle::Radius)
199 || self.active_handle == Some(SphereHandle::Radius)
200 {
201 1.0_f32
202 } else {
203 0.2
204 };
205
206 GlyphItem {
207 positions: vec![self.center.to_array(), rp.to_array()],
208 vectors: vec![[r_center, 0.0, 0.0], [r_rh, 0.0, 0.0]],
209 scale: 1.0,
210 scale_by_magnitude: true,
211 scalars: vec![sc, sr],
212 scalar_range: Some((0.0, 1.0)),
213 glyph_type: GlyphType::Sphere,
214 id: id_base,
215 ..GlyphItem::default()
216 }
217 }
218
219 fn radius_handle_pos(&self) -> glam::Vec3 {
224 self.center + glam::Vec3::X * self.radius
225 }
226
227 fn hit_test(
228 &self,
229 ray_origin: glam::Vec3,
230 ray_dir: glam::Vec3,
231 ctx: &WidgetContext,
232 ) -> Option<SphereHandle> {
233 let ray = Ray::new(
234 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
235 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
236 );
237
238 let rp = self.radius_handle_pos();
239 let rh_r = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 12.0);
240 let ch_r = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 12.0);
241
242 let rh_ball = parry3d::shape::Ball::new(rh_r);
243 let ch_ball = parry3d::shape::Ball::new(ch_r);
244
245 let rh_pose = Pose::from_parts(
246 [rp.x, rp.y, rp.z].into(),
247 glam::Quat::IDENTITY,
248 );
249 let ch_pose = Pose::from_parts(
250 [self.center.x, self.center.y, self.center.z].into(),
251 glam::Quat::IDENTITY,
252 );
253
254 let t_rh = rh_ball
255 .cast_ray(&rh_pose, &ray, f32::MAX, true)
256 .map(|i| i);
257 let t_ch = ch_ball
258 .cast_ray(&ch_pose, &ray, f32::MAX, true)
259 .map(|i| i);
260
261 match (t_ch, t_rh) {
262 (Some(tc), Some(tr)) => {
263 Some(if tc <= tr { SphereHandle::Center } else { SphereHandle::Radius })
264 }
265 (Some(_), None) => Some(SphereHandle::Center),
266 (None, Some(_)) => Some(SphereHandle::Radius),
267 (None, None) => None,
268 }
269 }
270}