viewport_lib/interaction/widgets/
sphere.rs1use crate::geometry::intersect::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 let bases = [
172 (glam::Vec3::X, glam::Vec3::Y),
173 (glam::Vec3::X, glam::Vec3::Z),
174 (glam::Vec3::Y, glam::Vec3::Z),
175 ];
176 for (u, v) in bases {
177 crate::geometry::polyline::push_circle_loop(&mut positions, c, u, v, r, STEPS);
178 strip_lengths.push((STEPS + 1) as u32);
179 }
180
181 let line_colour = [self.colour[0], self.colour[1], self.colour[2], 1.0];
182 PolylineItem {
183 positions,
184 strip_lengths,
185 default_colour: line_colour,
186 line_width: 1.5,
187
188 settings: crate::scene::material::ItemSettings {
189 pick_id: crate::renderer::PickId(id),
190 ..Default::default()
191 },
192 ..PolylineItem::default()
193 }
194 }
195
196 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
201 let rp = self.radius_handle_pos();
202 let r_center = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
203 let r_rh = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
204
205 let sc = if self.hovered_handle == Some(SphereHandle::Center)
206 || self.active_handle == Some(SphereHandle::Center)
207 {
208 1.0_f32
209 } else {
210 0.2
211 };
212 let sr = if self.hovered_handle == Some(SphereHandle::Radius)
213 || self.active_handle == Some(SphereHandle::Radius)
214 {
215 1.0_f32
216 } else {
217 0.2
218 };
219
220 GlyphItem {
221 positions: vec![self.center.to_array(), rp.to_array()],
222 vectors: vec![[r_center, 0.0, 0.0], [r_rh, 0.0, 0.0]],
223 scale: 1.0,
224 scale_by_magnitude: true,
225 scalars: vec![sc, sr],
226 scalar_range: Some((0.0, 1.0)),
227 glyph_type: GlyphType::Sphere,
228
229 settings: crate::scene::material::ItemSettings {
230 pick_id: crate::renderer::PickId(id_base),
231 ..Default::default()
232 },
233 default_colour: self.handle_colour,
234 use_default_colour: self.handle_colour[3] > 0.0,
235 ..GlyphItem::default()
236 }
237 }
238
239 fn radius_handle_pos(&self) -> glam::Vec3 {
244 self.center + glam::Vec3::X * self.radius
245 }
246
247 fn hit_test(
248 &self,
249 ray_origin: glam::Vec3,
250 ray_dir: glam::Vec3,
251 ctx: &WidgetContext,
252 ) -> Option<SphereHandle> {
253 let ray = Ray::new(
254 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
255 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
256 );
257
258 let rp = self.radius_handle_pos();
259 let rh_r = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
260 let ch_r = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
261
262 let rh_ball = parry3d::shape::Ball::new(rh_r);
263 let ch_ball = parry3d::shape::Ball::new(ch_r);
264
265 let rh_pose = Pose::from_parts([rp.x, rp.y, rp.z].into(), glam::Quat::IDENTITY);
266 let ch_pose = Pose::from_parts(
267 [self.center.x, self.center.y, self.center.z].into(),
268 glam::Quat::IDENTITY,
269 );
270
271 let t_rh = rh_ball.cast_ray(&rh_pose, &ray, f32::MAX, true).map(|i| i);
272 let t_ch = ch_ball.cast_ray(&ch_pose, &ray, f32::MAX, true).map(|i| i);
273
274 match (t_ch, t_rh) {
275 (Some(tc), Some(tr)) => Some(if tc <= tr {
276 SphereHandle::Center
277 } else {
278 SphereHandle::Radius
279 }),
280 (Some(_), None) => Some(SphereHandle::Center),
281 (None, Some(_)) => Some(SphereHandle::Radius),
282 (None, None) => None,
283 }
284 }
285}