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 PolylineWidget {
pub points: Vec<glam::Vec3>,
pub color: [f32; 4],
pub line_width: f32,
pub handle_color: [f32; 4],
pub hovered_point: Option<usize>,
pub active_point: Option<usize>,
drag_plane_normal: glam::Vec3,
drag_plane_d: f32,
}
impl PolylineWidget {
pub fn new(mut points: Vec<glam::Vec3>) -> Self {
if points.is_empty() {
points.push(glam::Vec3::ZERO);
}
if points.len() < 2 {
points.push(points[0] + glam::Vec3::X);
}
Self {
points,
color: [0.9, 0.5, 0.1, 1.0],
line_width: 2.0,
handle_color: [0.0; 4],
hovered_point: None,
active_point: None,
drag_plane_normal: glam::Vec3::Y,
drag_plane_d: 0.0,
}
}
pub fn is_active(&self) -> bool {
self.active_point.is_some()
}
pub fn add_point(&mut self, pos: glam::Vec3) {
self.points.push(pos);
}
pub fn remove_point(&mut self, index: usize) {
if self.points.len() > 2 && index < self.points.len() {
self.points.remove(index);
if self.hovered_point == Some(index) {
self.hovered_point = None;
} else if let Some(h) = self.hovered_point {
if h > index { self.hovered_point = Some(h - 1); }
}
if self.active_point == Some(index) {
self.active_point = None;
} else if let Some(a) = self.active_point {
if a > index { self.active_point = Some(a - 1); }
}
}
}
pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
let (ro, rd) = 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(ro, rd, pt);
if d < best_dist {
best_dist = d;
self.hovered_point = Some(i);
}
}
}
if ctx.double_clicked {
if let Some(idx) = self.hovered_point {
if self.points.len() > 2 {
self.points.remove(idx);
self.hovered_point = None;
return WidgetResult::Updated;
}
} else {
let seg_threshold = hit_radius * 2.0;
if let Some((seg_idx, insert_pos)) = self.closest_segment(ro, rd, seg_threshold) {
self.points.insert(seg_idx + 1, insert_pos);
self.hovered_point = Some(seg_idx + 1);
return WidgetResult::Updated;
}
}
}
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);
self.drag_plane_d = -self.drag_plane_normal.dot(self.points[idx]);
}
}
if ctx.dragging {
if let Some(idx) = self.active_point {
if let Some(hit) =
ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
{
if (hit - self.points[idx]).length_squared() > 1e-10 {
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 n = self.points.len() as u32;
PolylineItem {
positions: self.points.iter().map(|p| p.to_array()).collect(),
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 mut positions = Vec::with_capacity(self.points.len());
let mut vectors = Vec::with_capacity(self.points.len());
let mut scalars = Vec::with_capacity(self.points.len());
for (i, pt) in self.points.iter().enumerate() {
let r = handle_world_radius(*pt, &ctx.camera, ctx.viewport_size.y, 9.0);
let s = if self.hovered_point == Some(i) || self.active_point == Some(i) {
1.0_f32
} else {
0.2
};
positions.push(pt.to_array());
vectors.push([r, 0.0, 0.0]);
scalars.push(s);
}
GlyphItem {
positions,
vectors,
scale: 1.0,
scale_by_magnitude: true,
scalars,
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 closest_segment(
&self,
ray_origin: glam::Vec3,
ray_dir: glam::Vec3,
threshold: f32,
) -> Option<(usize, glam::Vec3)> {
let mut best: Option<(f32, usize, glam::Vec3)> = None;
for i in 0..self.points.len().saturating_sub(1) {
let a = self.points[i];
let b = self.points[i + 1];
let (pt, dist) = closest_point_on_segment_to_ray(ray_origin, ray_dir, a, b);
if dist < threshold {
if best.is_none() || dist < best.unwrap().0 {
best = Some((dist, i, pt));
}
}
}
best.map(|(_, i, pt)| (i, pt))
}
}
fn closest_point_on_segment_to_ray(
ray_o: glam::Vec3,
ray_d: glam::Vec3,
seg_a: glam::Vec3,
seg_b: glam::Vec3,
) -> (glam::Vec3, f32) {
let seg_d = seg_b - seg_a;
let r = ray_o - seg_a;
let b = ray_d.dot(seg_d);
let c = seg_d.dot(seg_d);
let d = ray_d.dot(r);
let e = seg_d.dot(r);
let denom = c - b * b;
let t_seg = if denom.abs() > 1e-7 {
((e - b * d) / denom).clamp(0.0, 1.0)
} else {
0.0
};
let seg_pt = seg_a + seg_d * t_seg;
let dist = ray_point_dist(ray_o, ray_d, seg_pt);
(seg_pt, dist)
}