maker_panel/features/
pos.rs

1use crate::{Align, Direction};
2use geo::{Coordinate, MultiPolygon};
3use std::fmt;
4
5/// How a feature should be positioned relative to an inner feature.
6#[derive(Debug, Clone)]
7pub enum Positioning {
8    Cardinal {
9        side: Direction,
10        centerline_adjustment: f64,
11        align: Align,
12    },
13    Corner {
14        side: Direction,
15        opposite: bool,
16        align: Align,
17    },
18    Angle {
19        degrees: f64,
20        amount: f64,
21    },
22}
23
24impl Positioning {
25    fn compute_translation(&self, bounds: geo::Rect<f64>, feature: geo::Rect<f64>) -> (f64, f64) {
26        match self {
27            Positioning::Cardinal {
28                side,
29                centerline_adjustment,
30                align: _,
31            } => match side {
32                Direction::Left => (
33                    bounds.min().x - self.compute_align_ref(feature),
34                    bounds.center().y - feature.center().y
35                        + (centerline_adjustment * bounds.height()),
36                ),
37                Direction::Right => (
38                    bounds.max().x - self.compute_align_ref(feature),
39                    bounds.center().y - feature.center().y
40                        + (centerline_adjustment * bounds.height()),
41                ),
42                Direction::Up => (
43                    bounds.center().x - feature.center().x
44                        + (centerline_adjustment * bounds.width()),
45                    bounds.min().y - self.compute_align_ref(feature),
46                ),
47                Direction::Down => (
48                    bounds.center().x - feature.center().x
49                        + (centerline_adjustment * bounds.width()),
50                    bounds.max().y - self.compute_align_ref(feature),
51                ),
52            },
53            Positioning::Corner {
54                side,
55                opposite,
56                align: _,
57            } => match side {
58                Direction::Left => (
59                    bounds.min().x - self.compute_align_ref(feature),
60                    match opposite {
61                        false => bounds.min().y - feature.min().y,
62                        true => bounds.max().y - feature.max().y,
63                    },
64                ),
65                Direction::Right => (
66                    bounds.max().x - self.compute_align_ref(feature),
67                    match opposite {
68                        false => bounds.min().y - feature.min().y,
69                        true => bounds.max().y - feature.max().y,
70                    },
71                ),
72                Direction::Up => (
73                    match opposite {
74                        false => bounds.min().x - feature.min().x,
75                        true => bounds.max().x - feature.max().x,
76                    },
77                    bounds.min().y - self.compute_align_ref(feature),
78                ),
79                Direction::Down => (
80                    match opposite {
81                        false => bounds.min().x - feature.min().x,
82                        true => bounds.max().x - feature.max().x,
83                    },
84                    bounds.max().y - self.compute_align_ref(feature),
85                ),
86            },
87            Positioning::Angle { degrees, amount } => {
88                let r = degrees * std::f64::consts::PI / 180.;
89                (
90                    bounds.center().x + (amount * r.cos()),
91                    bounds.center().y + (amount * r.sin()),
92                )
93            }
94        }
95    }
96
97    fn compute_align_ref(&self, feature: geo::Rect<f64>) -> f64 {
98        match self {
99            Positioning::Cardinal {
100                side,
101                align,
102                centerline_adjustment: _,
103            } => match side {
104                Direction::Left => match align {
105                    Align::Start => feature.min().x,
106                    Align::Center => feature.center().x,
107                    Align::End => feature.max().x,
108                },
109                Direction::Right => match align {
110                    Align::Start => feature.max().x,
111                    Align::Center => feature.center().x,
112                    Align::End => feature.min().x,
113                },
114                Direction::Up => match align {
115                    Align::Start => feature.min().y,
116                    Align::Center => feature.center().y,
117                    Align::End => feature.max().y,
118                },
119                Direction::Down => match align {
120                    Align::Start => feature.max().y,
121                    Align::Center => feature.center().y,
122                    Align::End => feature.min().y,
123                },
124            },
125            Positioning::Corner {
126                side,
127                align,
128                opposite: _,
129            } => match side {
130                Direction::Left => match align {
131                    Align::Start => feature.min().x,
132                    Align::Center => feature.center().x,
133                    Align::End => feature.max().x,
134                },
135                Direction::Right => match align {
136                    Align::Start => feature.max().x,
137                    Align::Center => feature.center().x,
138                    Align::End => feature.min().x,
139                },
140                Direction::Up => match align {
141                    Align::Start => feature.min().y,
142                    Align::Center => feature.center().y,
143                    Align::End => feature.max().y,
144                },
145                Direction::Down => match align {
146                    Align::Start => feature.max().y,
147                    Align::Center => feature.center().y,
148                    Align::End => feature.min().y,
149                },
150            },
151            Positioning::Angle { .. } => unreachable!(),
152        }
153    }
154}
155
156/// A wrapper around a feature that can position other features.
157#[derive(Debug, Clone)]
158pub struct AtPos<U = super::Unit, S = super::Unit> {
159    inner: U,
160    elements: Vec<(S, Positioning)>,
161}
162
163impl<U, S> AtPos<U, S>
164where
165    U: super::Feature + std::fmt::Debug + Clone,
166    S: super::Feature + std::fmt::Debug + Clone,
167{
168    /// Constructs a feature that positions the centeroid of other
169    /// features at the left & right points of the primary feature.
170    pub fn x_ends(primary: U, left: Option<S>, right: Option<S>) -> Self {
171        let mut elements = Vec::with_capacity(2);
172        if let Some(left) = left {
173            elements.push((
174                left,
175                Positioning::Cardinal {
176                    side: Direction::Left,
177                    centerline_adjustment: 0.0,
178                    align: Align::Center,
179                },
180            ));
181        }
182        if let Some(right) = right {
183            elements.push((
184                right,
185                Positioning::Cardinal {
186                    side: Direction::Right,
187                    centerline_adjustment: 0.0,
188                    align: Align::Center,
189                },
190            ));
191        }
192        Self {
193            elements,
194            inner: primary,
195        }
196    }
197
198    /// Wraps a feature so others can be positioned around it.
199    pub fn new(primary: U) -> Self {
200        let elements = Vec::with_capacity(4);
201        Self {
202            elements,
203            inner: primary,
204        }
205    }
206
207    /// Constructs a feature that positions the centeroid of the other
208    /// feature to the left of the primary feature.
209    pub fn left(primary: U, left: S) -> Self {
210        let mut elements = Vec::with_capacity(2);
211        elements.push((
212            left,
213            Positioning::Cardinal {
214                side: Direction::Left,
215                centerline_adjustment: 0.0,
216                align: Align::Center,
217            },
218        ));
219
220        Self {
221            elements,
222            inner: primary,
223        }
224    }
225
226    /// Constructs a feature that positions the centeroid of the other
227    /// feature to the right of the primary feature.
228    pub fn right(primary: U, right: S) -> Self {
229        let mut elements = Vec::with_capacity(2);
230        elements.push((
231            right,
232            Positioning::Cardinal {
233                side: Direction::Right,
234                centerline_adjustment: 0.0,
235                align: Align::Center,
236            },
237        ));
238
239        Self {
240            elements,
241            inner: primary,
242        }
243    }
244
245    /// Adds a feature to be positioned relative to the inner feature,
246    /// according to the provided positioning parameters.
247    pub fn push(&mut self, feature: S, pos: Positioning) {
248        self.elements.push((feature, pos));
249    }
250
251    fn feature_bounds(&self, feature: &S) -> Option<geo::Rect<f64>> {
252        let union_bounds = if let Some(geo) = feature.edge_union() {
253            Some(compute_bounds(geo.clone()))
254        } else {
255            None
256        };
257
258        let subtract_bounds = if let Some(geo) = feature.edge_subtract() {
259            Some(compute_bounds(geo.clone()))
260        } else {
261            None
262        };
263
264        match (union_bounds, subtract_bounds) {
265            (Some(b), None) => Some(b),
266            (None, Some(b)) => Some(b),
267            (Some(u), Some(s)) => {
268                use geo::bounding_rect::BoundingRect;
269                use geo_booleanop::boolean::BooleanOp;
270                Some(
271                    u.to_polygon()
272                        .union(&s.to_polygon())
273                        .bounding_rect()
274                        .unwrap(),
275                )
276            }
277            (None, None) => None,
278        }
279    }
280}
281
282fn compute_bounds(poly: MultiPolygon<f64>) -> geo::Rect<f64> {
283    use geo::bounding_rect::BoundingRect;
284    poly.bounding_rect().unwrap()
285}
286
287impl<U, S> fmt::Display for AtPos<U, S>
288where
289    U: super::Feature + std::fmt::Debug + Clone,
290    S: super::Feature + std::fmt::Debug + Clone,
291{
292    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
293        write!(f, "pos(U = {}, S = {:?})", self.inner, self.elements,)
294    }
295}
296
297impl<U, S> super::Feature for AtPos<U, S>
298where
299    U: super::Feature + std::fmt::Debug + Clone,
300    S: super::Feature + std::fmt::Debug + Clone,
301{
302    fn name(&self) -> &'static str {
303        "pos"
304    }
305
306    fn edge_union(&self) -> Option<MultiPolygon<f64>> {
307        use geo::algorithm::translate::Translate;
308        use geo_booleanop::boolean::BooleanOp;
309
310        let mut out = match self.inner.edge_union() {
311            Some(p) => p,
312            None => MultiPolygon(vec![]),
313        };
314        let bounds = compute_bounds(out.clone());
315
316        for (feature, position) in &self.elements {
317            if let Some(mut geo) = feature.edge_union() {
318                let t = position.compute_translation(bounds, self.feature_bounds(feature).unwrap());
319                geo.translate_inplace(t.0, t.1);
320                out = out.union(&geo)
321            }
322        }
323
324        if out.0.len() > 0 {
325            Some(out)
326        } else {
327            None
328        }
329    }
330
331    fn edge_subtract(&self) -> Option<MultiPolygon<f64>> {
332        let bounds = compute_bounds(match self.inner.edge_union() {
333            Some(p) => p,
334            None => MultiPolygon(vec![]),
335        });
336
337        let mut out = match self.inner.edge_subtract() {
338            Some(p) => p,
339            None => MultiPolygon(vec![]),
340        };
341
342        for (feature, position) in &self.elements {
343            if let Some(mut geo) = feature.edge_subtract() {
344                use geo::algorithm::translate::Translate;
345                use geo_booleanop::boolean::BooleanOp;
346                let t = position.compute_translation(bounds, self.feature_bounds(feature).unwrap());
347                geo.translate_inplace(t.0, t.1);
348                out = out.union(&geo)
349            }
350        }
351
352        if out.0.len() > 0 {
353            Some(out)
354        } else {
355            None
356        }
357    }
358
359    fn translate(&mut self, v: Coordinate<f64>) {
360        self.inner.translate(v);
361        // No need to move the others, we position them ourselves.
362    }
363
364    fn interior(&self) -> Vec<super::InnerAtom> {
365        let bounds = compute_bounds(match self.inner.edge_union() {
366            Some(p) => p,
367            None => MultiPolygon(vec![]),
368        });
369
370        self.inner
371            .interior()
372            .into_iter()
373            .chain(
374                self.elements
375                    .iter()
376                    .map(|(feature, position)| {
377                        if let Some(feature_bounds) = self.feature_bounds(feature) {
378                            let t = position.compute_translation(bounds, feature_bounds);
379                            let mut out = feature.interior();
380                            for a in out.iter_mut() {
381                                a.translate(t.0, t.1);
382                            }
383                            out
384                        } else {
385                            vec![]
386                        }
387                    })
388                    .flatten(),
389            )
390            .collect()
391    }
392}