Skip to main content

ply_engine/
math.rs

1#[derive(Debug, Clone, Copy, PartialEq, Default)]
2#[repr(C)]
3pub struct Vector2 {
4    pub x: f32,
5    pub y: f32,
6}
7
8impl Vector2 {
9    pub fn new(x: f32, y: f32) -> Self {
10        Self { x, y }
11    }
12}
13
14impl From<(f32, f32)> for Vector2 {
15    fn from(value: (f32, f32)) -> Self {
16        Self::new(value.0, value.1)
17    }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Default)]
21#[repr(C)]
22pub struct Dimensions {
23    pub width: f32,
24    pub height: f32,
25}
26
27impl Dimensions {
28    pub fn new(width: f32, height: f32) -> Self {
29        Self { width, height }
30    }
31}
32
33impl From<(f32, f32)> for Dimensions {
34    fn from(value: (f32, f32)) -> Self {
35        Self::new(value.0, value.1)
36    }
37}
38
39/// An axis-aligned rectangle defined by its top-left position and dimensions.
40#[derive(Debug, Clone, Copy, PartialEq, Default)]
41#[repr(C)]
42pub struct BoundingBox {
43    pub x: f32,
44    pub y: f32,
45    pub width: f32,
46    pub height: f32,
47}
48
49impl BoundingBox {
50    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
51        Self {
52            x,
53            y,
54            width,
55            height,
56        }
57    }
58}
59
60/// Classifies a rotation angle into common fast-path categories.
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub enum AngleType {
63    /// 0° (or 360°) — no rotation needed.
64    Zero,
65    /// 90° clockwise.
66    Right90,
67    /// 180°.
68    Straight180,
69    /// 270° clockwise (= 90° counter-clockwise).
70    Right270,
71    /// An angle that doesn't match any fast-path.
72    Arbitrary(f32),
73}
74
75/// Classifies a rotation in radians into an [`AngleType`].
76/// Normalises to `[0, 2π)` first, then checks within `EPS` of each cardinal.
77pub fn classify_angle(radians: f32) -> AngleType {
78    let normalized = radians.rem_euclid(std::f32::consts::TAU);
79    const EPS: f32 = 0.001;
80    if normalized < EPS || (std::f32::consts::TAU - normalized) < EPS {
81        AngleType::Zero
82    } else if (normalized - std::f32::consts::FRAC_PI_2).abs() < EPS {
83        AngleType::Right90
84    } else if (normalized - std::f32::consts::PI).abs() < EPS {
85        AngleType::Straight180
86    } else if (normalized - 3.0 * std::f32::consts::FRAC_PI_2).abs() < EPS {
87        AngleType::Right270
88    } else {
89        AngleType::Arbitrary(normalized)
90    }
91}
92
93use crate::layout::CornerRadius;
94
95/// Computes the axis-aligned bounding box of a rounded rectangle after rotation.
96///
97/// Uses the Minkowski-sum approach for equal corner radii:
98///   `AABB_w = |(w-2r)·cosθ| + |(h-2r)·sinθ| + 2r`
99///   `AABB_h = |(w-2r)·sinθ| + |(h-2r)·cosθ| + 2r`
100///
101/// For non-uniform radii, uses the maximum radius as a conservative approximation.
102/// Returns `(effective_width, effective_height)`.
103pub fn compute_rotated_aabb(
104    width: f32,
105    height: f32,
106    corner_radius: &CornerRadius,
107    rotation_radians: f32,
108) -> (f32, f32) {
109    let angle = classify_angle(rotation_radians);
110    match angle {
111        AngleType::Zero => (width, height),
112        AngleType::Straight180 => (width, height),
113        AngleType::Right90 | AngleType::Right270 => (height, width),
114        AngleType::Arbitrary(theta) => {
115            let r = corner_radius
116                .top_left
117                .max(corner_radius.top_right)
118                .max(corner_radius.bottom_left)
119                .max(corner_radius.bottom_right)
120                .min(width / 2.0)
121                .min(height / 2.0);
122
123            let cos_t = theta.cos().abs();
124            let sin_t = theta.sin().abs();
125            let inner_w = (width - 2.0 * r).max(0.0);
126            let inner_h = (height - 2.0 * r).max(0.0);
127
128            let eff_w = inner_w * cos_t + inner_h * sin_t + 2.0 * r;
129            let eff_h = inner_w * sin_t + inner_h * cos_t + 2.0 * r;
130            (eff_w, eff_h)
131        }
132    }
133}