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