Skip to main content

typst_library/layout/
angle.rs

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