maker_panel/features/
array.rs

1use geo::{Coordinate, MultiPolygon};
2use std::fmt;
3
4/// A feature which aligns a sequence of features vertically.
5#[derive(Debug, Clone)]
6pub struct Column<U = super::Unit> {
7    array: Vec<U>,
8    align: crate::Align,
9    bbox: bool,
10}
11
12impl<U: super::Feature + fmt::Debug + Clone> Column<U> {
13    /// Lays out the given features in an array going downwards, with
14    /// their leftmost elements aligned.
15    pub fn align_left(array: Vec<U>) -> Self {
16        Self::new(array, crate::Align::Start)
17    }
18
19    /// Lays out the given features in an array going downwards, with
20    /// their rightmost elements aligned.``
21    pub fn align_right(array: Vec<U>) -> Self {
22        Self::new(array, crate::Align::End)
23    }
24
25    /// Lays out the given features in an array going downwards, with
26    /// each element aligned to the center.
27    pub fn align_center(array: Vec<U>) -> Self {
28        Self::new(array, crate::Align::Center)
29    }
30
31    fn new(mut array: Vec<U>, align: crate::Align) -> Self {
32        // Position any containing geometry to exist entirely in positive
33        // (x>=0, y>=0) coordinate space.
34        for e in array.iter_mut() {
35            if let Some(b) = e.edge_union() {
36                use geo::bounding_rect::BoundingRect;
37                let v = b.bounding_rect().unwrap().min();
38                e.translate(-v);
39            }
40        }
41
42        Self {
43            align,
44            array,
45            bbox: true,
46        }
47    }
48
49    fn all_bounds(&self) -> Vec<geo::Rect<f64>> {
50        self.array
51            .iter()
52            .map(|f| {
53                let add_b = match f.edge_union() {
54                    Some(edge) => {
55                        use geo::bounding_rect::BoundingRect;
56                        edge.bounding_rect()
57                    }
58                    None => None,
59                };
60                let sub_b = match f.edge_subtract() {
61                    Some(edge) => {
62                        use geo::bounding_rect::BoundingRect;
63                        edge.bounding_rect()
64                    }
65                    None => None,
66                };
67
68                match (add_b, sub_b) {
69                    (Some(b), None) => b,
70                    (None, Some(b)) => b,
71                    (Some(u), Some(s)) => {
72                        use geo::bounding_rect::BoundingRect;
73                        use geo_booleanop::boolean::BooleanOp;
74                        u.to_polygon()
75                            .union(&s.to_polygon())
76                            .bounding_rect()
77                            .unwrap()
78                    }
79                    (None, None) => geo::Rect::new(
80                        Coordinate::<f64> { x: 0., y: 0. },
81                        Coordinate::<f64> { x: 0., y: 0. },
82                    ),
83                }
84            })
85            .collect()
86    }
87
88    fn largest(&self) -> geo::Rect<f64> {
89        self.all_bounds()
90            .into_iter()
91            .max_by(|x, y| x.width().partial_cmp(&y.width()).unwrap())
92            .unwrap()
93    }
94
95    fn translations<'a>(
96        &'a self,
97        largest: geo::Rect<f64>,
98    ) -> Box<dyn Iterator<Item = Option<(f64, f64)>> + 'a> {
99        Box::new(
100            self.all_bounds()
101                .iter()
102                .scan(0f64, |y_off, b| {
103                    let out = Some((b, *y_off));
104                    *y_off = *y_off + b.height();
105                    out
106                })
107                .map(move |(bounds, y_off)| {
108                    Some(match self.align {
109                        crate::Align::Start => (largest.min().x - bounds.min().x, y_off),
110                        crate::Align::End => (largest.max().x - bounds.max().x, y_off),
111                        crate::Align::Center => (largest.center().x - bounds.center().x, y_off),
112                    })
113                })
114                .collect::<Vec<_>>()
115                .into_iter(),
116        )
117    }
118}
119
120impl<U: super::Feature + fmt::Debug> fmt::Display for Column<U> {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        write!(f, "Column(align = {:?}, {:?})", self.align, self.array)
123    }
124}
125
126impl<U: super::Feature + fmt::Debug + Clone> super::Feature for Column<U> {
127    fn name(&self) -> &'static str {
128        "Column"
129    }
130
131    fn edge_union(&self) -> Option<MultiPolygon<f64>> {
132        let out = self
133            .array
134            .iter()
135            .map(|f| match f.edge_union() {
136                Some(edge_geo) => Some(edge_geo.clone()),
137                None => None,
138            })
139            .zip(self.translations(self.largest()).into_iter())
140            .filter(|(f, t)| f.is_some() && t.is_some())
141            .map(|(f, t)| (f.unwrap(), t.unwrap()))
142            .fold(None, |mut acc, (g, (tx, ty))| {
143                use geo::translate::Translate;
144                use geo_booleanop::boolean::BooleanOp;
145                if let Some(current) = acc {
146                    acc = Some(g.translate(tx, ty).union(&current));
147                } else {
148                    acc = Some(g.translate(tx, ty));
149                };
150                acc
151            });
152
153        // If we are in bbox mode, all we need to do is compute the bounding
154        // box and use that as our outer geometry.
155        if self.bbox {
156            match out {
157                None => None,
158                Some(poly) => {
159                    use geo::bounding_rect::BoundingRect;
160                    Some(poly.bounding_rect().unwrap().to_polygon().into())
161                }
162            }
163        } else {
164            out
165        }
166    }
167
168    fn edge_subtract(&self) -> Option<MultiPolygon<f64>> {
169        let out = self
170            .array
171            .iter()
172            .map(|f| match f.edge_subtract() {
173                Some(edge_geo) => Some(edge_geo.clone()),
174                None => None,
175            })
176            .zip(self.translations(self.largest()).into_iter())
177            .filter(|(f, t)| f.is_some() && t.is_some())
178            .map(|(f, t)| (f.unwrap(), t.unwrap()))
179            .fold(None, |mut acc, (g, (tx, ty))| {
180                use geo::translate::Translate;
181                use geo_booleanop::boolean::BooleanOp;
182                if let Some(current) = acc {
183                    acc = Some(g.translate(tx, ty).union(&current));
184                } else {
185                    acc = Some(g.translate(tx, ty));
186                };
187                acc
188            });
189
190        out
191    }
192
193    fn translate(&mut self, v: Coordinate<f64>) {
194        for e in self.array.iter_mut() {
195            e.translate(v);
196        }
197    }
198
199    fn interior(&self) -> Vec<super::InnerAtom> {
200        let largest = self.largest();
201
202        self.array
203            .iter()
204            .map(|f| f.interior())
205            .zip(self.translations(largest).into_iter())
206            .map(|(f, t)| {
207                let (tx, ty) = match t {
208                    Some((tx, ty)) => (tx, ty),
209                    None => (0., 0.),
210                };
211
212                f.into_iter().map(move |mut a| {
213                    a.translate(tx, ty);
214                    a
215                })
216            })
217            .flatten()
218            .collect()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::features::Rect;
226    use test_case::test_case;
227
228    #[test]
229    fn bounds() {
230        let a = Column::align_left(vec![
231            Rect::with_center([0., 0.].into(), 2., 3.),
232            Rect::with_center([0., 0.].into(), 3., 2.),
233        ]);
234
235        assert_eq!(
236            a.all_bounds(),
237            vec![
238                geo::Rect::new::<geo::Coordinate<_>>([0., 0.].into(), [2., 3.].into()),
239                geo::Rect::new::<geo::Coordinate<_>>([0., 0.].into(), [3., 2.].into()),
240            ]
241        );
242    }
243
244    #[test]
245    fn largest() {
246        let a = Column::align_left(vec![
247            Rect::with_center([0., 0.].into(), 2., 3.),
248            Rect::with_center([0., 0.].into(), 3., 2.),
249        ]);
250
251        assert_eq!(
252            a.largest(),
253            geo::Rect::new::<geo::Coordinate<_>>([0., 0.].into(), [3., 2.].into(),),
254        );
255    }
256
257    #[test_case(
258        vec![
259            Rect::with_center([0., 0.].into(), 4., 2.),
260            Rect::with_center([0., 0.].into(), 2., 2.),
261        ], vec![
262            Some((0., 0.)),
263            Some((0., 2.)),
264        ] ; "origin centered"
265    )
266    ]
267    fn translations_left(inners: Vec<Rect>, want: Vec<Option<(f64, f64)>>) {
268        let a = Column::align_left(inners);
269        assert_eq!(a.translations(a.largest()).collect::<Vec<_>>(), want,);
270    }
271
272    #[test_case( vec![
273        Rect::with_center([0., 0.].into(), 2., 2.),
274        Rect::with_center([0., 0.].into(), 4., 2.),
275    ], vec![
276        Some((1., 0.)),
277        Some((0., 2.)),
278    ] ; "origin centered")]
279    #[test_case( vec![
280        Rect::new([0., 0.].into(), [2., 3.].into()),
281        Rect::new([0., 0.].into(), [2., 2.].into()),
282    ], vec![
283        Some((0., 0.)),
284        Some((0., 3.)),
285    ] ; "origin tl zeroed")]
286    fn translations_center(inners: Vec<Rect>, want: Vec<Option<(f64, f64)>>) {
287        let a = Column::align_center(inners);
288        assert_eq!(a.translations(a.largest()).collect::<Vec<_>>(), want,);
289    }
290}