Skip to main content

stet_fonts/
geometry.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Geometry types: affine transform matrices, path segments, and paths.
6
7/// Round to 10 decimal places to eliminate floating-point artifacts.
8#[inline]
9pub fn round10(v: f64) -> f64 {
10    (v * 1e10).round() / 1e10
11}
12
13/// Affine transformation matrix `[a, b, c, d, tx, ty]`.
14///
15/// Transforms point (x, y) to:
16///   x' = a*x + c*y + tx
17///   y' = b*x + d*y + ty
18#[derive(Clone, Copy, Debug)]
19pub struct Matrix {
20    pub a: f64,
21    pub b: f64,
22    pub c: f64,
23    pub d: f64,
24    pub tx: f64,
25    pub ty: f64,
26}
27
28impl Default for Matrix {
29    fn default() -> Self {
30        Self::identity()
31    }
32}
33
34impl Matrix {
35    /// Identity matrix.
36    pub fn identity() -> Self {
37        Self {
38            a: 1.0,
39            b: 0.0,
40            c: 0.0,
41            d: 1.0,
42            tx: 0.0,
43            ty: 0.0,
44        }
45    }
46
47    /// Create from 6 components.
48    pub fn new(a: f64, b: f64, c: f64, d: f64, tx: f64, ty: f64) -> Self {
49        Self { a, b, c, d, tx, ty }
50    }
51
52    /// Translation matrix.
53    pub fn translate(tx: f64, ty: f64) -> Self {
54        Self {
55            a: 1.0,
56            b: 0.0,
57            c: 0.0,
58            d: 1.0,
59            tx,
60            ty,
61        }
62    }
63
64    /// Scaling matrix.
65    pub fn scale(sx: f64, sy: f64) -> Self {
66        Self {
67            a: sx,
68            b: 0.0,
69            c: 0.0,
70            d: sy,
71            tx: 0.0,
72            ty: 0.0,
73        }
74    }
75
76    /// Rotation matrix (angle in degrees).
77    pub fn rotate(angle: f64) -> Self {
78        let rad = angle.to_radians();
79        let (sin, cos) = (rad.sin(), rad.cos());
80        Self {
81            a: round10(cos),
82            b: round10(sin),
83            c: round10(-sin),
84            d: round10(cos),
85            tx: 0.0,
86            ty: 0.0,
87        }
88    }
89
90    /// Column-vector multiply: self × other.
91    ///
92    /// Composes two transforms: the result applies `other` first, then `self`.
93    #[inline]
94    pub fn multiply(&self, other: &Matrix) -> Matrix {
95        Matrix {
96            a: round10(self.a * other.a + self.c * other.b),
97            b: round10(self.b * other.a + self.d * other.b),
98            c: round10(self.a * other.c + self.c * other.d),
99            d: round10(self.b * other.c + self.d * other.d),
100            tx: round10(self.a * other.tx + self.c * other.ty + self.tx),
101            ty: round10(self.b * other.tx + self.d * other.ty + self.ty),
102        }
103    }
104
105    /// PostScript `concat`: CTM = other × CTM (row-vector convention).
106    ///
107    /// Uses column-vector multiply internally: `self.multiply(other)`.
108    #[inline]
109    pub fn concat(&self, other: &Matrix) -> Matrix {
110        self.multiply(other)
111    }
112
113    /// Transform a point.
114    #[inline]
115    pub fn transform_point(&self, x: f64, y: f64) -> (f64, f64) {
116        (
117            round10(self.a * x + self.c * y + self.tx),
118            round10(self.b * x + self.d * y + self.ty),
119        )
120    }
121
122    /// Transform a delta (no translation).
123    #[inline]
124    pub fn transform_delta(&self, dx: f64, dy: f64) -> (f64, f64) {
125        (
126            round10(self.a * dx + self.c * dy),
127            round10(self.b * dx + self.d * dy),
128        )
129    }
130
131    /// Determinant.
132    pub fn determinant(&self) -> f64 {
133        self.a * self.d - self.b * self.c
134    }
135
136    /// Inverse matrix, or None if singular.
137    pub fn invert(&self) -> Option<Matrix> {
138        let det = self.determinant();
139        if det.abs() < 1e-20 {
140            return None;
141        }
142        let inv_det = 1.0 / det;
143        Some(Matrix {
144            a: round10(self.d * inv_det),
145            b: round10(-self.b * inv_det),
146            c: round10(-self.c * inv_det),
147            d: round10(self.a * inv_det),
148            tx: round10((self.c * self.ty - self.d * self.tx) * inv_det),
149            ty: round10((self.b * self.tx - self.a * self.ty) * inv_det),
150        })
151    }
152
153    /// Convert to [a, b, c, d, tx, ty] array.
154    pub fn to_array(&self) -> [f64; 6] {
155        [self.a, self.b, self.c, self.d, self.tx, self.ty]
156    }
157}
158
159/// A segment in a device-space path.
160#[derive(Clone, Debug)]
161pub enum PathSegment {
162    MoveTo(f64, f64),
163    LineTo(f64, f64),
164    CurveTo {
165        x1: f64,
166        y1: f64,
167        x2: f64,
168        y2: f64,
169        x3: f64,
170        y3: f64,
171    },
172    ClosePath,
173}
174
175/// A path in device space, composed of path segments.
176#[derive(Clone, Debug)]
177pub struct PsPath {
178    pub segments: Vec<PathSegment>,
179}
180
181impl PsPath {
182    /// Create an empty path.
183    pub fn new() -> Self {
184        Self {
185            segments: Vec::new(),
186        }
187    }
188
189    /// Returns true if the path has no segments.
190    pub fn is_empty(&self) -> bool {
191        self.segments.is_empty()
192    }
193
194    /// Remove all segments.
195    pub fn clear(&mut self) {
196        self.segments.clear();
197    }
198
199    /// Transform all points through a matrix, returning a new path.
200    pub fn transform(&self, m: &Matrix) -> PsPath {
201        let segments = self
202            .segments
203            .iter()
204            .map(|seg| match *seg {
205                PathSegment::MoveTo(x, y) => {
206                    let (tx, ty) = m.transform_point(x, y);
207                    PathSegment::MoveTo(tx, ty)
208                }
209                PathSegment::LineTo(x, y) => {
210                    let (tx, ty) = m.transform_point(x, y);
211                    PathSegment::LineTo(tx, ty)
212                }
213                PathSegment::CurveTo {
214                    x1,
215                    y1,
216                    x2,
217                    y2,
218                    x3,
219                    y3,
220                } => {
221                    let (tx1, ty1) = m.transform_point(x1, y1);
222                    let (tx2, ty2) = m.transform_point(x2, y2);
223                    let (tx3, ty3) = m.transform_point(x3, y3);
224                    PathSegment::CurveTo {
225                        x1: tx1,
226                        y1: ty1,
227                        x2: tx2,
228                        y2: ty2,
229                        x3: tx3,
230                        y3: ty3,
231                    }
232                }
233                PathSegment::ClosePath => PathSegment::ClosePath,
234            })
235            .collect();
236        PsPath { segments }
237    }
238}
239
240impl Default for PsPath {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_matrix_identity() {
252        let m = Matrix::identity();
253        let (x, y) = m.transform_point(3.0, 4.0);
254        assert!((x - 3.0).abs() < 1e-10);
255        assert!((y - 4.0).abs() < 1e-10);
256    }
257
258    #[test]
259    fn test_matrix_translate() {
260        let m = Matrix::translate(10.0, 20.0);
261        let (x, y) = m.transform_point(3.0, 4.0);
262        assert!((x - 13.0).abs() < 1e-10);
263        assert!((y - 24.0).abs() < 1e-10);
264    }
265
266    #[test]
267    fn test_matrix_scale() {
268        let m = Matrix::scale(2.0, 3.0);
269        let (x, y) = m.transform_point(5.0, 7.0);
270        assert!((x - 10.0).abs() < 1e-10);
271        assert!((y - 21.0).abs() < 1e-10);
272    }
273
274    #[test]
275    fn test_matrix_rotate_90() {
276        let m = Matrix::rotate(90.0);
277        let (x, y) = m.transform_point(1.0, 0.0);
278        assert!(x.abs() < 1e-10);
279        assert!((y - 1.0).abs() < 1e-10);
280    }
281
282    #[test]
283    fn test_matrix_multiply() {
284        let t = Matrix::translate(10.0, 0.0);
285        let s = Matrix::scale(2.0, 2.0);
286        let m = t.multiply(&s);
287        let (x, y) = m.transform_point(5.0, 3.0);
288        assert!((x - 20.0).abs() < 1e-10);
289        assert!((y - 6.0).abs() < 1e-10);
290    }
291
292    #[test]
293    fn test_matrix_concat() {
294        let ctm = Matrix::identity();
295        let t = Matrix::translate(10.0, 20.0);
296        let result = ctm.concat(&t);
297        let (x, y) = result.transform_point(0.0, 0.0);
298        assert!((x - 10.0).abs() < 1e-10);
299        assert!((y - 20.0).abs() < 1e-10);
300    }
301
302    #[test]
303    fn test_matrix_invert() {
304        let m = Matrix::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
305        let inv = m.invert().unwrap();
306        let (x, y) = m.transform_point(5.0, 7.0);
307        let (x2, y2) = inv.transform_point(x, y);
308        assert!((x2 - 5.0).abs() < 1e-8);
309        assert!((y2 - 7.0).abs() < 1e-8);
310    }
311
312    #[test]
313    fn test_matrix_invert_singular() {
314        let m = Matrix::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
315        assert!(m.invert().is_none());
316    }
317
318    #[test]
319    fn test_matrix_transform_delta() {
320        let m = Matrix::translate(100.0, 200.0);
321        let (dx, dy) = m.transform_delta(5.0, 3.0);
322        assert!((dx - 5.0).abs() < 1e-10);
323        assert!((dy - 3.0).abs() < 1e-10);
324    }
325}