use crate::interaction::clip_plane::ray_plane_intersection;
use crate::renderer::{ClipObject, ClipShape, GlyphItem, GlyphType, PolylineItem};
use parry3d::math::{Pose, Vector};
use parry3d::query::{Ray, RayCast};
use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum SphereHandle {
Center,
Radius,
}
pub struct SphereWidget {
pub center: glam::Vec3,
pub radius: f32,
pub color: [f32; 4],
pub handle_color: [f32; 4],
hovered_handle: Option<SphereHandle>,
active_handle: Option<SphereHandle>,
drag_plane_normal: glam::Vec3,
drag_plane_d: f32,
drag_anchor_world: glam::Vec3,
drag_anchor_radius: f32,
}
impl SphereWidget {
pub fn new(center: glam::Vec3, radius: f32) -> Self {
Self {
center,
radius: radius.max(0.01),
color: [0.3, 0.6, 1.0, 0.25],
handle_color: [0.0; 4],
hovered_handle: None,
active_handle: None,
drag_plane_normal: glam::Vec3::Z,
drag_plane_d: 0.0,
drag_anchor_world: glam::Vec3::ZERO,
drag_anchor_radius: 0.0,
}
}
pub fn is_active(&self) -> bool {
self.active_handle.is_some()
}
pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
let (ro, rd) = ctx_ray(ctx);
let mut updated = false;
if self.active_handle.is_none() {
let hit = self.hit_test(ro, rd, ctx);
if hit.is_some() || !ctx.drag_started {
self.hovered_handle = hit;
}
}
if ctx.drag_started {
if let Some(handle) = self.hovered_handle {
let anchor = match handle {
SphereHandle::Center => self.center,
SphereHandle::Radius => self.radius_handle_pos(),
};
let fwd = glam::Vec3::from(ctx.camera.forward);
let n = -fwd;
self.drag_plane_normal = n;
self.drag_plane_d = -n.dot(anchor);
self.drag_anchor_world = anchor;
self.drag_anchor_radius = self.radius;
self.active_handle = Some(handle);
}
}
if let Some(handle) = self.active_handle {
if ctx.released || (!ctx.dragging && !ctx.drag_started) {
self.active_handle = None;
self.hovered_handle = None;
} else if let Some(hit) =
ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
{
match handle {
SphereHandle::Center => {
let delta = hit - self.drag_anchor_world;
let new_center = self.center + delta;
if (new_center - self.center).length_squared() > 1e-10 {
self.center = new_center;
self.drag_anchor_world = hit;
updated = true;
}
}
SphereHandle::Radius => {
let new_r = (hit - self.center).length().max(0.01);
if (new_r - self.radius).abs() > 1e-5 {
self.radius = new_r;
updated = true;
}
}
}
}
}
if updated { WidgetResult::Updated } else { WidgetResult::None }
}
pub fn clip_object(&self) -> ClipObject {
let edge = [self.color[0], self.color[1], self.color[2], 1.0];
ClipObject {
shape: ClipShape::Sphere {
center: self.center.to_array(),
radius: self.radius,
},
color: Some(self.color),
edge_color: Some(edge),
clip_geometry: false,
enabled: true,
hovered: self.hovered_handle.is_some(),
active: self.active_handle.is_some(),
..ClipObject::default()
}
}
pub fn wireframe_item(&self, id: u64) -> PolylineItem {
const STEPS: usize = 64;
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(STEPS * 3 + 3);
let mut strip_lengths: Vec<u32> = Vec::with_capacity(3);
let c = self.center;
let r = self.radius;
for ring in 0..3_usize {
for i in 0..=STEPS {
let a = i as f32 * std::f32::consts::TAU / STEPS as f32;
let (s, co) = a.sin_cos();
let p = match ring {
0 => glam::Vec3::new(co * r, s * r, 0.0),
1 => glam::Vec3::new(co * r, 0.0, s * r),
_ => glam::Vec3::new(0.0, co * r, s * r),
};
positions.push((c + p).to_array());
}
strip_lengths.push((STEPS + 1) as u32);
}
let line_color = [self.color[0], self.color[1], self.color[2], 1.0];
PolylineItem {
positions,
strip_lengths,
default_color: line_color,
line_width: 1.5,
id,
..PolylineItem::default()
}
}
pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
let rp = self.radius_handle_pos();
let r_center = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
let r_rh = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
let sc = if self.hovered_handle == Some(SphereHandle::Center)
|| self.active_handle == Some(SphereHandle::Center)
{
1.0_f32
} else {
0.2
};
let sr = if self.hovered_handle == Some(SphereHandle::Radius)
|| self.active_handle == Some(SphereHandle::Radius)
{
1.0_f32
} else {
0.2
};
GlyphItem {
positions: vec![self.center.to_array(), rp.to_array()],
vectors: vec![[r_center, 0.0, 0.0], [r_rh, 0.0, 0.0]],
scale: 1.0,
scale_by_magnitude: true,
scalars: vec![sc, sr],
scalar_range: Some((0.0, 1.0)),
glyph_type: GlyphType::Sphere,
id: id_base,
default_color: self.handle_color,
use_default_color: self.handle_color[3] > 0.0,
..GlyphItem::default()
}
}
fn radius_handle_pos(&self) -> glam::Vec3 {
self.center + glam::Vec3::X * self.radius
}
fn hit_test(
&self,
ray_origin: glam::Vec3,
ray_dir: glam::Vec3,
ctx: &WidgetContext,
) -> Option<SphereHandle> {
let ray = Ray::new(
Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
);
let rp = self.radius_handle_pos();
let rh_r = handle_world_radius(rp, &ctx.camera, ctx.viewport_size.y, 8.0);
let ch_r = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
let rh_ball = parry3d::shape::Ball::new(rh_r);
let ch_ball = parry3d::shape::Ball::new(ch_r);
let rh_pose = Pose::from_parts(
[rp.x, rp.y, rp.z].into(),
glam::Quat::IDENTITY,
);
let ch_pose = Pose::from_parts(
[self.center.x, self.center.y, self.center.z].into(),
glam::Quat::IDENTITY,
);
let t_rh = rh_ball
.cast_ray(&rh_pose, &ray, f32::MAX, true)
.map(|i| i);
let t_ch = ch_ball
.cast_ray(&ch_pose, &ray, f32::MAX, true)
.map(|i| i);
match (t_ch, t_rh) {
(Some(tc), Some(tr)) => {
Some(if tc <= tr { SphereHandle::Center } else { SphereHandle::Radius })
}
(Some(_), None) => Some(SphereHandle::Center),
(None, Some(_)) => Some(SphereHandle::Radius),
(None, None) => None,
}
}
}