use crate::interaction::clip_plane::ray_plane_intersection;
use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius, ray_point_dist};
pub struct SplineWidget {
pub points: Vec<glam::Vec3>,
pub color: [f32; 4],
pub line_width: f32,
pub handle_color: [f32; 4],
pub resolution: u32,
hovered_point: Option<usize>,
active_point: Option<usize>,
drag_plane_normal: glam::Vec3,
drag_plane_d: f32,
}
impl SplineWidget {
pub fn new(points: Vec<glam::Vec3>) -> Self {
Self {
points,
color: [0.4, 0.8, 1.0, 1.0],
line_width: 2.0,
handle_color: [1.0, 0.8, 0.2, 1.0],
resolution: 16,
hovered_point: None,
active_point: None,
drag_plane_normal: glam::Vec3::Y,
drag_plane_d: 0.0,
}
}
pub fn hovered_point(&self) -> Option<usize> {
self.hovered_point
}
pub fn is_active(&self) -> bool {
self.active_point.is_some()
}
pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
let (ray_origin, ray_dir) = ctx_ray(ctx);
let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
let hit_radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 12.0);
if !ctx.dragging {
self.hovered_point = None;
let mut best_dist = hit_radius;
for (i, &pt) in self.points.iter().enumerate() {
let d = ray_point_dist(ray_origin, ray_dir, pt);
if d < best_dist {
best_dist = d;
self.hovered_point = Some(i);
}
}
}
if ctx.drag_started {
if let Some(idx) = self.hovered_point {
self.active_point = Some(idx);
self.drag_plane_normal = -glam::Vec3::from(ctx.camera.forward);
let pt = self.points[idx];
self.drag_plane_d = -self.drag_plane_normal.dot(pt);
}
}
if ctx.dragging {
if let Some(idx) = self.active_point {
if let Some(hit) = ray_plane_intersection(
ray_origin,
ray_dir,
self.drag_plane_normal,
self.drag_plane_d,
) {
self.points[idx] = hit;
return WidgetResult::Updated;
}
}
}
if ctx.released {
self.active_point = None;
}
WidgetResult::None
}
pub fn polyline_item(&self, id: u64) -> PolylineItem {
let sampled = self.sampled_positions();
let n = sampled.len() as u32;
PolylineItem {
positions: sampled,
strip_lengths: if n > 0 { vec![n] } else { vec![] },
default_color: self.color,
line_width: self.line_width,
id,
..PolylineItem::default()
}
}
pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
let ref_pos = self.points.first().copied().unwrap_or(glam::Vec3::ZERO);
let radius = handle_world_radius(ref_pos, &ctx.camera, ctx.viewport_size.y, 8.0);
GlyphItem {
positions: self.points.iter().map(|p| p.to_array()).collect(),
glyph_type: GlyphType::Sphere,
scale: radius,
use_default_color: true,
default_color: self.handle_color,
id: id_base,
..GlyphItem::default()
}
}
pub fn sampled_positions(&self) -> Vec<[f32; 3]> {
let n = self.points.len();
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![self.points[0].to_array()];
}
let res = self.resolution.max(1) as usize;
let mut out: Vec<[f32; 3]> = Vec::with_capacity((n - 1) * res + 1);
for seg in 0..(n - 1) {
let p0 = self.points[if seg > 0 { seg - 1 } else { seg }];
let p1 = self.points[seg];
let p2 = self.points[seg + 1];
let p3 = self.points[if seg + 2 < n { seg + 2 } else { seg + 1 }];
for s in 0..res {
let t = s as f32 / res as f32;
out.push(catmull_rom(p0, p1, p2, p3, t).to_array());
}
}
out.push(self.points[n - 1].to_array());
out
}
}
fn catmull_rom(p0: glam::Vec3, p1: glam::Vec3, p2: glam::Vec3, p3: glam::Vec3, t: f32) -> glam::Vec3 {
let t2 = t * t;
let t3 = t2 * t;
0.5 * (
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
+ (-p0 + p2) * t
+ 2.0 * p1
)
}