Skip to main content

bymsdfgen_core/geometry/
shape.rs

1//! A shape: a set of closed contours plus a Y-axis orientation flag.
2//! Port of `core/Shape.{h,cpp}`.
3
4use super::contour::Contour;
5use super::convergent::convergent_curve_ordering;
6use super::edge_segment::EdgeSegment;
7use crate::math::scalar::{mix, sign};
8use crate::math::{Vector2, dot};
9
10/// Matches `MSDFGEN_CORNER_DOT_EPSILON` in `Shape.h`.
11const CORNER_DOT_EPSILON: f64 = 0.000_001;
12/// Matches `DECONVERGE_OVERSHOOT` in `Shape.cpp`.
13const DECONVERGE_OVERSHOOT: f64 = 1.111_111_111_111_111_1;
14const LARGE_VALUE: f64 = 1e240;
15
16/// Axis-aligned bounds: left, bottom, right, top.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Bounds {
19    pub l: f64,
20    pub b: f64,
21    pub r: f64,
22    pub t: f64,
23}
24
25#[derive(Debug, Clone, Default)]
26pub struct Shape {
27    pub contours: Vec<Contour>,
28    /// When true, the shape's Y axis points downward (non-default orientation).
29    pub inverse_y_axis: bool,
30}
31
32fn deconverge_edge(edge: &mut EdgeSegment, param: i32, vector: Vector2) {
33    if matches!(edge, EdgeSegment::Quadratic(_)) {
34        *edge = edge.convert_to_cubic();
35    }
36    if let EdgeSegment::Cubic(p) = edge {
37        match param {
38            0 => p[1] = p[1] + (p[1] - p[0]).length() * vector,
39            1 => p[2] = p[2] + (p[2] - p[3]).length() * vector,
40            _ => {}
41        }
42    }
43}
44
45impl Shape {
46    pub fn new() -> Self {
47        Shape::default()
48    }
49
50    pub fn add_contour(&mut self, contour: Contour) {
51        self.contours.push(contour);
52    }
53
54    /// Push and return a mutable reference to a fresh contour.
55    pub fn add_contour_mut(&mut self) -> &mut Contour {
56        self.contours.push(Contour::new());
57        self.contours.last_mut().unwrap()
58    }
59
60    /// Every contour must be closed and consistent (each edge starts where the
61    /// previous ended). Port of `Shape::validate`.
62    pub fn validate(&self) -> bool {
63        for contour in &self.contours {
64            if let Some(last) = contour.segments.last() {
65                let mut corner = last.point(1.0);
66                for edge in &contour.segments {
67                    if edge.point(0.0) != corner {
68                        return false;
69                    }
70                    corner = edge.point(1.0);
71                }
72            }
73        }
74        true
75    }
76
77    /// Split single-edge contours into thirds and push apart convergent corners.
78    /// Port of `Shape::normalize`.
79    pub fn normalize(&mut self) {
80        for contour in &mut self.contours {
81            let n = contour.segments.len();
82            if n == 1 {
83                let parts = contour.segments[0].split_in_thirds();
84                let color = contour.colors[0];
85                contour.segments = parts.to_vec();
86                contour.colors = vec![color; 3];
87            } else if n >= 2 {
88                let mut prev_idx = n - 1;
89                for cur_idx in 0..n {
90                    let prev_dir = contour.segments[prev_idx].direction(1.0).normalize(false);
91                    let cur_dir = contour.segments[cur_idx].direction(0.0).normalize(false);
92                    if dot(prev_dir, cur_dir) < CORNER_DOT_EPSILON - 1.0 {
93                        let e = CORNER_DOT_EPSILON - 1.0;
94                        let factor = DECONVERGE_OVERSHOOT * (1.0 - e * e).sqrt() / e;
95                        let mut axis = factor * (cur_dir - prev_dir).normalize(false);
96                        if convergent_curve_ordering(
97                            &contour.segments[prev_idx],
98                            &contour.segments[cur_idx],
99                        ) < 0
100                        {
101                            axis = -axis;
102                        }
103                        deconverge_edge(&mut contour.segments[prev_idx], 1, axis.orthogonal(true));
104                        deconverge_edge(&mut contour.segments[cur_idx], 0, axis.orthogonal(false));
105                    }
106                    prev_idx = cur_idx;
107                }
108            }
109        }
110    }
111
112    /// Expand `(min, max)` over all contours.
113    pub fn bound(&self, min: &mut Vector2, max: &mut Vector2) {
114        for c in &self.contours {
115            c.bound(min, max);
116        }
117    }
118
119    /// Axis-aligned bounds, optionally expanded by a uniform `border`.
120    pub fn get_bounds(&self, border: f64) -> Bounds {
121        let mut min = Vector2::new(LARGE_VALUE, LARGE_VALUE);
122        let mut max = Vector2::new(-LARGE_VALUE, -LARGE_VALUE);
123        self.bound(&mut min, &mut max);
124        let mut bounds = Bounds {
125            l: min.x,
126            b: min.y,
127            r: max.x,
128            t: max.y,
129        };
130        if border > 0.0 {
131            bounds.l -= border;
132            bounds.b -= border;
133            bounds.r += border;
134            bounds.t += border;
135        }
136        bounds
137    }
138
139    pub fn edge_count(&self) -> usize {
140        self.contours.iter().map(|c| c.segments.len()).sum()
141    }
142
143    /// Ensure all contours wind consistently (outer CCW, holes CW). Port of
144    /// `Shape::orientContours`.
145    pub fn orient_contours(&mut self) {
146        #[derive(Clone, Copy)]
147        struct Inter {
148            x: f64,
149            direction: i32,
150            contour_index: usize,
151        }
152
153        let ratio = 0.5 * (5.0_f64.sqrt() - 1.0); // irrational, avoids hitting corners
154        let nc = self.contours.len();
155        let mut orientations = vec![0i32; nc];
156        let mut intersections: Vec<Inter> = Vec::new();
157        let mut buf = [(0.0, 0); 3];
158
159        for i in 0..nc {
160            if orientations[i] != 0 || self.contours[i].segments.is_empty() {
161                continue;
162            }
163            // Find a Y that crosses the contour.
164            let y0 = self.contours[i].segments[0].point(0.0).y;
165            let mut y1 = y0;
166            for edge in &self.contours[i].segments {
167                if y0 != y1 {
168                    break;
169                }
170                y1 = edge.point(1.0).y;
171            }
172            for edge in &self.contours[i].segments {
173                if y0 != y1 {
174                    break;
175                }
176                y1 = edge.point(ratio).y; // horizontal-line fallback
177            }
178            let y = mix(y0, y1, ratio);
179
180            for (j, contour) in self.contours.iter().enumerate() {
181                for edge in &contour.segments {
182                    let n = edge.scanline_intersections(y, &mut buf);
183                    for &(x, dy) in buf.iter().take(n) {
184                        intersections.push(Inter {
185                            x,
186                            direction: dy,
187                            contour_index: j,
188                        });
189                    }
190                }
191            }
192
193            if !intersections.is_empty() {
194                intersections
195                    .sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
196                // Disqualify coincident intersections.
197                for j in 1..intersections.len() {
198                    if intersections[j].x == intersections[j - 1].x {
199                        intersections[j].direction = 0;
200                        intersections[j - 1].direction = 0;
201                    }
202                }
203                for (j, inter) in intersections.iter().enumerate() {
204                    if inter.direction != 0 {
205                        orientations[inter.contour_index] +=
206                            2 * (((j & 1) as i32) ^ ((inter.direction > 0) as i32)) - 1;
207                    }
208                }
209                intersections.clear();
210            }
211        }
212
213        for (i, contour) in self.contours.iter_mut().enumerate() {
214            if orientations[i] < 0 {
215                contour.reverse();
216            }
217        }
218        // suppress unused warning of `sign` import in some build configs
219        let _ = sign;
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::geometry::edge_color::EdgeColor;
227
228    fn square() -> Shape {
229        let mut shape = Shape::new();
230        let c = shape.add_contour_mut();
231        let pts = [
232            Vector2::new(0.0, 0.0),
233            Vector2::new(10.0, 0.0),
234            Vector2::new(10.0, 10.0),
235            Vector2::new(0.0, 10.0),
236        ];
237        for i in 0..4 {
238            c.add_edge_colored(
239                EdgeSegment::line(pts[i], pts[(i + 1) % 4]),
240                EdgeColor::White,
241            );
242        }
243        shape
244    }
245
246    #[test]
247    fn validate_closed_square() {
248        assert!(square().validate());
249    }
250
251    #[test]
252    fn bounds_square() {
253        let b = square().get_bounds(0.0);
254        assert_eq!((b.l, b.b, b.r, b.t), (0.0, 0.0, 10.0, 10.0));
255    }
256
257    #[test]
258    fn winding_and_orient() {
259        let mut s = square();
260        // msdfgen's shoelace convention yields -1 for this CCW vertex order.
261        assert_eq!(s.contours[0].winding(), -1);
262        s.orient_contours();
263        // After orientation a lone outer contour stays a valid, non-degenerate loop.
264        assert!(s.contours[0].winding() != 0);
265    }
266
267    #[test]
268    fn single_edge_contour_splits() {
269        let mut s = Shape::new();
270        let c = s.add_contour_mut();
271        // a single cubic loop
272        c.add_edge(EdgeSegment::cubic(
273            Vector2::new(0.0, 0.0),
274            Vector2::new(2.0, 4.0),
275            Vector2::new(-2.0, 4.0),
276            Vector2::new(0.0, 0.0),
277        ));
278        s.normalize();
279        assert_eq!(s.contours[0].segments.len(), 3);
280    }
281}