typst_library/layout/
angle.rs

1use std::f64::consts::PI;
2use std::fmt::{self, Debug, Formatter};
3use std::iter::Sum;
4use std::ops::{Add, Div, Mul, Neg};
5
6use ecow::EcoString;
7use typst_utils::{Numeric, Scalar};
8
9use crate::foundations::{func, repr, scope, ty, Repr};
10
11/// An angle describing a rotation.
12///
13/// Typst supports the following angular units:
14///
15/// - Degrees: `{180deg}`
16/// - Radians: `{3.14rad}`
17///
18/// # Example
19/// ```example
20/// #rotate(10deg)[Hello there!]
21/// ```
22#[ty(scope, cast)]
23#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
24pub struct Angle(Scalar);
25
26impl Angle {
27    /// The zero angle.
28    pub const fn zero() -> Self {
29        Self(Scalar::ZERO)
30    }
31
32    /// Create an angle from a number of raw units.
33    pub const fn raw(raw: f64) -> Self {
34        Self(Scalar::new(raw))
35    }
36
37    /// Create an angle from a value in a unit.
38    pub fn with_unit(val: f64, unit: AngleUnit) -> Self {
39        Self(Scalar::new(val * unit.raw_scale()))
40    }
41
42    /// Create an angle from a number of radians.
43    pub fn rad(rad: f64) -> Self {
44        Self::with_unit(rad, AngleUnit::Rad)
45    }
46
47    /// Create an angle from a number of degrees.
48    pub fn deg(deg: f64) -> Self {
49        Self::with_unit(deg, AngleUnit::Deg)
50    }
51
52    /// Get the value of this angle in raw units.
53    pub const fn to_raw(self) -> f64 {
54        (self.0).get()
55    }
56
57    /// Get the value of this angle in a unit.
58    pub fn to_unit(self, unit: AngleUnit) -> f64 {
59        self.to_raw() / unit.raw_scale()
60    }
61
62    /// The absolute value of the this angle.
63    pub fn abs(self) -> Self {
64        Self::raw(self.to_raw().abs())
65    }
66
67    /// Get the sine of this angle in radians.
68    pub fn sin(self) -> f64 {
69        self.to_rad().sin()
70    }
71
72    /// Get the cosine of this angle in radians.
73    pub fn cos(self) -> f64 {
74        self.to_rad().cos()
75    }
76
77    /// Get the tangent of this angle in radians.
78    pub fn tan(self) -> f64 {
79        self.to_rad().tan()
80    }
81
82    /// Get the quadrant of the Cartesian plane that this angle lies in.
83    ///
84    /// The angle is automatically normalized to the range `0deg..=360deg`.
85    ///
86    /// The quadrants are defined as follows:
87    /// - First: `0deg..=90deg` (top-right)
88    /// - Second: `90deg..=180deg` (top-left)
89    /// - Third: `180deg..=270deg` (bottom-left)
90    /// - Fourth: `270deg..=360deg` (bottom-right)
91    pub fn quadrant(self) -> Quadrant {
92        let angle = self.to_deg().rem_euclid(360.0);
93        if angle <= 90.0 {
94            Quadrant::First
95        } else if angle <= 180.0 {
96            Quadrant::Second
97        } else if angle <= 270.0 {
98            Quadrant::Third
99        } else {
100            Quadrant::Fourth
101        }
102    }
103}
104
105#[scope]
106impl Angle {
107    /// Converts this angle to radians.
108    #[func(name = "rad", title = "Radians")]
109    pub fn to_rad(self) -> f64 {
110        self.to_unit(AngleUnit::Rad)
111    }
112
113    /// Converts this angle to degrees.
114    #[func(name = "deg", title = "Degrees")]
115    pub fn to_deg(self) -> f64 {
116        self.to_unit(AngleUnit::Deg)
117    }
118}
119
120impl Numeric for Angle {
121    fn zero() -> Self {
122        Self::zero()
123    }
124
125    fn is_finite(self) -> bool {
126        self.0.is_finite()
127    }
128}
129
130impl Debug for Angle {
131    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
132        write!(f, "{:?}deg", self.to_deg())
133    }
134}
135
136impl Repr for Angle {
137    fn repr(&self) -> EcoString {
138        repr::format_float_with_unit(self.to_deg(), "deg")
139    }
140}
141
142impl Neg for Angle {
143    type Output = Self;
144
145    fn neg(self) -> Self {
146        Self(-self.0)
147    }
148}
149
150impl Add for Angle {
151    type Output = Self;
152
153    fn add(self, other: Self) -> Self {
154        Self(self.0 + other.0)
155    }
156}
157
158typst_utils::sub_impl!(Angle - Angle -> Angle);
159
160impl Mul<f64> for Angle {
161    type Output = Self;
162
163    fn mul(self, other: f64) -> Self {
164        Self(self.0 * other)
165    }
166}
167
168impl Mul<Angle> for f64 {
169    type Output = Angle;
170
171    fn mul(self, other: Angle) -> Angle {
172        other * self
173    }
174}
175
176impl Div for Angle {
177    type Output = f64;
178
179    fn div(self, other: Self) -> f64 {
180        self.to_raw() / other.to_raw()
181    }
182}
183
184impl Div<f64> for Angle {
185    type Output = Self;
186
187    fn div(self, other: f64) -> Self {
188        Self(self.0 / other)
189    }
190}
191
192typst_utils::assign_impl!(Angle += Angle);
193typst_utils::assign_impl!(Angle -= Angle);
194typst_utils::assign_impl!(Angle *= f64);
195typst_utils::assign_impl!(Angle /= f64);
196
197impl Sum for Angle {
198    fn sum<I: Iterator<Item = Angle>>(iter: I) -> Self {
199        Self(iter.map(|s| s.0).sum())
200    }
201}
202
203/// Different units of angular measurement.
204#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
205pub enum AngleUnit {
206    /// Radians.
207    Rad,
208    /// Degrees.
209    Deg,
210}
211
212impl AngleUnit {
213    /// How many raw units correspond to a value of `1.0` in this unit.
214    fn raw_scale(self) -> f64 {
215        match self {
216            Self::Rad => 1.0,
217            Self::Deg => PI / 180.0,
218        }
219    }
220}
221
222/// A quadrant of the Cartesian plane.
223#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
224pub enum Quadrant {
225    /// The first quadrant, containing positive x and y values.
226    First,
227    /// The second quadrant, containing negative x and positive y values.
228    Second,
229    /// The third quadrant, containing negative x and y values.
230    Third,
231    /// The fourth quadrant, containing positive x and negative y values.
232    Fourth,
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_angle_unit_conversion() {
241        assert!((Angle::rad(2.0 * PI).to_deg() - 360.0) < 1e-4);
242        assert!((Angle::deg(45.0).to_rad() - std::f64::consts::FRAC_PI_4) < 1e-4);
243    }
244}