Skip to main content

scenix_helpers/
arrow.rs

1use scenix_core::Color;
2use scenix_math::Vec3;
3
4use crate::{EPSILON, LineGeometry};
5
6/// Directional arrow helper.
7#[derive(Clone, Copy, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct ArrowHelper {
10    /// Arrow origin.
11    pub origin: Vec3,
12    /// Arrow direction. Zero falls back to negative Z.
13    pub direction: Vec3,
14    /// Total arrow length.
15    pub length: f32,
16    /// Line color.
17    pub color: Color,
18    /// Arrow head length.
19    pub head_length: f32,
20    /// Arrow head half-width.
21    pub head_width: f32,
22}
23
24impl ArrowHelper {
25    /// Creates an arrow helper.
26    #[inline]
27    pub fn new(origin: Vec3, direction: Vec3, length: f32, color: Color) -> Self {
28        let length = length.abs().max(EPSILON);
29        Self {
30            origin,
31            direction: normalize_direction(direction),
32            length,
33            color,
34            head_length: length * 0.2,
35            head_width: length * 0.08,
36        }
37    }
38
39    /// Returns this helper with explicit head dimensions.
40    #[inline]
41    pub fn head(mut self, length: f32, width: f32) -> Self {
42        self.head_length = length.abs().min(self.length).max(EPSILON);
43        self.head_width = width.abs().max(EPSILON);
44        self
45    }
46
47    /// Generates line-list geometry.
48    pub fn to_geometry(&self) -> LineGeometry {
49        let direction = normalize_direction(self.direction);
50        let length = self.length.abs().max(EPSILON);
51        let head_length = self.head_length.abs().min(length).max(EPSILON);
52        let head_width = self.head_width.abs().max(EPSILON);
53        let tip = self.origin + direction * length;
54        let shaft_end = tip - direction * head_length;
55        let basis = perpendicular_basis(direction);
56        let right = basis.0 * head_width;
57        let up = basis.1 * head_width;
58
59        let mut geometry = LineGeometry::new();
60        geometry.push_segment(self.origin, shaft_end, self.color);
61        geometry.push_segment(tip, shaft_end + right, self.color);
62        geometry.push_segment(tip, shaft_end - right, self.color);
63        geometry.push_segment(tip, shaft_end + up, self.color);
64        geometry.push_segment(tip, shaft_end - up, self.color);
65        geometry
66    }
67}
68
69impl Default for ArrowHelper {
70    #[inline]
71    fn default() -> Self {
72        Self::new(Vec3::ZERO, Vec3::NEG_Z, 1.0, Color::WHITE)
73    }
74}
75
76pub(crate) fn normalize_direction(direction: Vec3) -> Vec3 {
77    let direction = direction.normalize();
78    if direction.length_squared() <= EPSILON {
79        Vec3::NEG_Z
80    } else {
81        direction
82    }
83}
84
85pub(crate) fn perpendicular_basis(direction: Vec3) -> (Vec3, Vec3) {
86    let reference = if direction.y.abs() > 0.9 {
87        Vec3::X
88    } else {
89        Vec3::Y
90    };
91    let right = direction.cross(reference).normalize();
92    let up = right.cross(direction).normalize();
93    (right, up)
94}