Skip to main content

rpic_core/
geom.rs

1//! Geometry primitives. pic works in a Cartesian plane with y pointing up;
2//! internal units are pic "inches" (scaled to device units by each backend).
3
4use std::ops::{Add, Div, Mul, Sub};
5
6/// A point / 2-vector in pic coordinate space (inches, y-up).
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub struct Point {
9    pub x: f64,
10    pub y: f64,
11}
12
13impl Point {
14    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
15
16    pub fn new(x: f64, y: f64) -> Self {
17        Point { x, y }
18    }
19
20    /// Euclidean distance to another point.
21    pub fn dist(self, other: Point) -> f64 {
22        (self - other).len()
23    }
24
25    /// Length (magnitude) of this point treated as a vector.
26    pub fn len(self) -> f64 {
27        self.x.hypot(self.y)
28    }
29
30    /// Linear interpolation: `self` at t=0, `other` at t=1.
31    pub fn lerp(self, other: Point, t: f64) -> Point {
32        self + (other - self) * t
33    }
34
35    /// Rotate around the origin by `angle` radians (counter-clockwise).
36    pub fn rotate(self, angle: f64) -> Point {
37        let (s, c) = angle.sin_cos();
38        Point::new(self.x * c - self.y * s, self.x * s + self.y * c)
39    }
40}
41
42impl Add for Point {
43    type Output = Point;
44    fn add(self, o: Point) -> Point {
45        Point::new(self.x + o.x, self.y + o.y)
46    }
47}
48impl Sub for Point {
49    type Output = Point;
50    fn sub(self, o: Point) -> Point {
51        Point::new(self.x - o.x, self.y - o.y)
52    }
53}
54impl Mul<f64> for Point {
55    type Output = Point;
56    fn mul(self, k: f64) -> Point {
57        Point::new(self.x * k, self.y * k)
58    }
59}
60impl Div<f64> for Point {
61    type Output = Point;
62    fn div(self, k: f64) -> Point {
63        Point::new(self.x / k, self.y / k)
64    }
65}
66
67/// An axis-aligned bounding box. Empty until the first point is added.
68#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct Bbox {
70    pub min: Point,
71    pub max: Point,
72    empty: bool,
73}
74
75impl Default for Bbox {
76    fn default() -> Self {
77        Bbox::new()
78    }
79}
80
81impl Bbox {
82    /// Add the cover of an axis-aligned rect after rotating it `deg` (CCW,
83    /// model space) about `pivot`. The pivot must match where the renderer
84    /// actually rotates the ink — for a label that is the text anchor, not the
85    /// rect centre (a justified label's anchor is its left/right edge), so the
86    /// four corners are rotated exactly rather than assuming a centred spin.
87    pub(crate) fn add_rect_rotated_about(
88        &mut self,
89        min: Point,
90        max: Point,
91        pivot: Point,
92        deg: f64,
93    ) {
94        let (sin, cos) = deg.to_radians().sin_cos();
95        for (cx, cy) in [
96            (min.x, min.y),
97            (max.x, min.y),
98            (min.x, max.y),
99            (max.x, max.y),
100        ] {
101            let (dx, dy) = (cx - pivot.x, cy - pivot.y);
102            self.add(Point::new(
103                pivot.x + dx * cos - dy * sin,
104                pivot.y + dx * sin + dy * cos,
105            ));
106        }
107    }
108
109    pub fn new() -> Self {
110        Bbox {
111            min: Point::ZERO,
112            max: Point::ZERO,
113            empty: true,
114        }
115    }
116
117    pub fn is_empty(&self) -> bool {
118        self.empty
119    }
120
121    /// Extend the box to include a point.
122    pub fn add(&mut self, p: Point) {
123        if !p.x.is_finite() || !p.y.is_finite() {
124            return;
125        }
126        if self.empty {
127            self.min = p;
128            self.max = p;
129            self.empty = false;
130        } else {
131            self.min.x = self.min.x.min(p.x);
132            self.min.y = self.min.y.min(p.y);
133            self.max.x = self.max.x.max(p.x);
134            self.max.y = self.max.y.max(p.y);
135        }
136    }
137
138    /// Merge another bounding box into this one.
139    pub fn union(&mut self, other: &Bbox) {
140        if !other.empty {
141            self.add(other.min);
142            self.add(other.max);
143        }
144    }
145
146    pub fn width(&self) -> f64 {
147        self.max.x - self.min.x
148    }
149    pub fn height(&self) -> f64 {
150        self.max.y - self.min.y
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn point_ops() {
160        let a = Point::new(1.0, 2.0);
161        let b = Point::new(3.0, 4.0);
162        assert_eq!(a + b, Point::new(4.0, 6.0));
163        assert_eq!(b - a, Point::new(2.0, 2.0));
164        assert_eq!(a * 2.0, Point::new(2.0, 4.0));
165        assert_eq!(a.lerp(b, 0.5), Point::new(2.0, 3.0));
166        assert!((Point::new(3.0, 4.0).len() - 5.0).abs() < 1e-12);
167    }
168
169    #[test]
170    fn bbox_grows() {
171        let mut bb = Bbox::new();
172        assert!(bb.is_empty());
173        bb.add(Point::new(1.0, 1.0));
174        bb.add(Point::new(-1.0, 3.0));
175        assert_eq!(bb.min, Point::new(-1.0, 1.0));
176        assert_eq!(bb.max, Point::new(1.0, 3.0));
177        assert_eq!(bb.width(), 2.0);
178        assert_eq!(bb.height(), 2.0);
179    }
180}