maker_panel/
lib.rs

1extern crate conv;
2extern crate geo_booleanop;
3extern crate gerber_types;
4
5use geo::{Coordinate, MultiPolygon};
6use geo_booleanop::boolean::BooleanOp;
7use usvg::NodeExt;
8
9pub mod features;
10use features::{Feature, InnerAtom};
11
12mod drill;
13mod gerber;
14mod parser;
15#[cfg(feature = "tessellate")]
16mod tessellate;
17#[cfg(feature = "tessellate")]
18pub use tessellate::{Point as TPoint, TessellationError, VertexBuffers};
19#[cfg(feature = "text")]
20mod text;
21
22pub use parser::Err as SpecErr;
23
24/// Alignment of multiple elements in an array.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Align {
27    Start,
28    Center,
29    End,
30}
31
32/// PCB layers.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Layer {
35    FrontCopper,
36    FrontMask,
37    FrontLegend,
38    BackCopper,
39    BackMask,
40    BackLegend,
41    FabricationInstructions,
42}
43
44impl Layer {
45    fn color(&self) -> usvg::Color {
46        match self {
47            Layer::FrontCopper => usvg::Color::new(0x84, 0, 0),
48            Layer::FrontMask => usvg::Color::new(0x84, 0, 0x84),
49            Layer::FrontLegend => usvg::Color::new(0, 0xce, 0xde),
50            Layer::BackCopper => usvg::Color::new(0, 0x84, 0),
51            Layer::BackMask => usvg::Color::new(0x84, 0, 0x84),
52            Layer::BackLegend => usvg::Color::new(0x4, 0, 0x84),
53            Layer::FabricationInstructions => usvg::Color::new(0x66, 0x66, 0x66),
54        }
55    }
56
57    pub fn to_string(&self) -> String {
58        match self {
59            Layer::FrontCopper => String::from("FrontCopper"),
60            Layer::FrontMask => String::from("FrontMask"),
61            Layer::FrontLegend => String::from("FrontLegend"),
62            Layer::BackCopper => String::from("BackCopper"),
63            Layer::BackMask => String::from("BackMask"),
64            Layer::BackLegend => String::from("BackLegend"),
65            Layer::FabricationInstructions => String::from("FabricationInstructions"),
66        }
67    }
68}
69
70/// The direction in which repetitions occur.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum Direction {
73    Left,
74    Right,
75    Down,
76    Up,
77}
78
79impl std::fmt::Display for Direction {
80    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
81        match self {
82            Direction::Left => write!(f, "left"),
83            Direction::Right => write!(f, "right"),
84            Direction::Down => write!(f, "down"),
85            Direction::Up => write!(f, "up"),
86        }
87    }
88}
89
90impl Direction {
91    pub fn offset(&self, bounds: geo::Rect<f64>) -> (f64, f64) {
92        match self {
93            Direction::Left => (-bounds.width(), 0.0),
94            Direction::Right => (bounds.width(), 0.0),
95            Direction::Down => (0.0, bounds.height()),
96            Direction::Up => (0.0, -bounds.height()),
97        }
98    }
99}
100
101/// Failure modes when constructing or serializing geometry.
102#[derive(Debug)]
103pub enum Err {
104    NoFeatures,
105    NoBounds,
106    BadEdgeGeometry(String),
107    InternalGerberFailure,
108    #[cfg(feature = "tessellate")]
109    TessellationError(TessellationError),
110}
111
112/// Combines features into single geometry.
113pub struct Panel<'a> {
114    pub features: Vec<Box<dyn Feature + 'a>>,
115    convex_hull: bool,
116    grid_separation: Option<isize>,
117}
118
119impl<'a> Panel<'a> {
120    /// Constructs a [`Panel`].
121    pub fn new() -> Self {
122        let features = Vec::new();
123        let convex_hull = false;
124        let grid_separation = None;
125        Self {
126            features,
127            convex_hull,
128            grid_separation,
129        }
130    }
131
132    /// Constructs a [`Panel`], pre-sized to hold the given
133    /// number of [`features::Feature`] objects before need an allocation.
134    pub fn with_capacity(sz: usize) -> Self {
135        let features = Vec::with_capacity(sz);
136        let convex_hull = false;
137        let grid_separation = None;
138        Self {
139            features,
140            convex_hull,
141            grid_separation,
142        }
143    }
144
145    /// Enables or disables a convex hull transform on the computed edge geometry.
146    pub fn convex_hull(&mut self, convex_hull: bool) {
147        self.convex_hull = convex_hull;
148    }
149
150    /// Sets the grid separation that should be rendered on the SVG.
151    pub fn set_grid_separation(&mut self, grid_separation: Option<isize>) {
152        self.grid_separation = grid_separation;
153    }
154
155    /// Adds a feature to the panel.
156    pub fn push<F: Feature + 'a>(&mut self, f: F) {
157        self.features.push(Box::new(f));
158    }
159
160    /// Adds the feature described by the given spec to the panel.
161    pub fn push_spec(&mut self, spec_str: &str) -> Result<(), SpecErr> {
162        self.features.append(&mut parser::build(spec_str)?);
163        Ok(())
164    }
165
166    /// Computes the outer geometry of the panel.
167    pub fn edge_geometry(&self) -> Option<MultiPolygon<f64>> {
168        let mut edge = self
169            .features
170            .iter()
171            .map(|f| f.edge_union())
172            .fold(None, |mut acc, g| {
173                if let Some(poly) = g {
174                    if let Some(current) = acc {
175                        acc = Some(poly.union(&current));
176                    } else {
177                        acc = Some(poly);
178                    }
179                };
180                acc
181            });
182
183        edge = match (&edge, self.convex_hull) {
184            (Some(edges), true) => {
185                use geo::algorithm::convex_hull;
186                let mut points = edges
187                    .iter()
188                    .map(|p| p.exterior().points_iter().collect::<Vec<_>>())
189                    .flatten()
190                    .map(|p| p.into())
191                    .collect::<Vec<Coordinate<_>>>();
192
193                let poly = geo::Polygon::new(
194                    convex_hull::graham::graham_hull(points.as_mut_slice(), true),
195                    edges
196                        .iter()
197                        .map(|p| p.interiors())
198                        .flatten()
199                        .map(|p| p.clone())
200                        .collect::<Vec<_>>(),
201                );
202
203                Some((vec![poly]).into())
204            }
205            _ => edge,
206        };
207
208        for f in &self.features {
209            if let Some(sub) = f.edge_subtract() {
210                edge = match edge {
211                    Some(e) => Some(e.difference(&sub)),
212                    None => None,
213                };
214            }
215        }
216
217        edge
218    }
219
220    fn edge_poly(&self) -> Result<geo::Polygon<f64>, Err> {
221        match self.edge_geometry() {
222            Some(edges) => {
223                let mut polys = edges.into_iter();
224                match polys.len() {
225                    0 => Err(Err::NoFeatures),
226                    1 => Ok(polys.next().unwrap()),
227                    _ => Err(Err::BadEdgeGeometry(
228                        "multiple polygons provided for edge geometry".to_string(),
229                    )),
230                }
231            }
232            None => Err(Err::NoFeatures),
233        }
234    }
235
236    /// Computes the inner geometry of the panel.
237    pub fn interior_geometry(&self) -> Vec<InnerAtom> {
238        self.features
239            .iter()
240            .map(|f| f.interior())
241            .flatten()
242            .collect()
243    }
244
245    /// Serializes a gerber file describing the PCB profile to the provided writer.
246    pub fn serialize_gerber_edges<W: std::io::Write>(&self, w: &mut W) -> Result<(), Err> {
247        let edges = self.edge_poly()?;
248        let commands = gerber::serialize_edge(edges).map_err(|_| Err::InternalGerberFailure)?;
249        use gerber_types::GerberCode;
250        commands
251            .serialize(w)
252            .map_err(|_| Err::InternalGerberFailure)
253    }
254
255    /// Serializes a gerber file describing the layer (copper or soldermask) to
256    /// to the provided writer.
257    pub fn serialize_gerber_layer<W: std::io::Write>(
258        &self,
259        layer: Layer,
260        w: &mut W,
261    ) -> Result<(), Err> {
262        use geo::bounding_rect::BoundingRect;
263        let edges = self.edge_poly()?;
264        let bounds = edges.bounding_rect().unwrap();
265
266        let commands = gerber::serialize_layer(layer, self.interior_geometry(), bounds)
267            .map_err(|_| Err::InternalGerberFailure)?;
268        use gerber_types::GerberCode;
269        commands
270            .serialize(w)
271            .map_err(|_| Err::InternalGerberFailure)
272    }
273
274    /// Serializes a drill file describing drill hits to the provided writer.
275    pub fn serialize_drill<W: std::io::Write>(
276        &self,
277        w: &mut W,
278        want_plated: bool,
279    ) -> Result<(), std::io::Error> {
280        drill::serialize(&self.interior_geometry(), w, want_plated)
281    }
282
283    /// Computes the 2d tessellation of the panel.
284    #[cfg(feature = "tessellate")]
285    pub fn tessellate_2d(&self) -> Result<VertexBuffers<TPoint, u16>, Err> {
286        Ok(
287            tessellate::tessellate_2d(self.edge_poly()?, self.interior_geometry())
288                .map_err(|e| Err::TessellationError(e))?,
289        )
290    }
291
292    /// Computes the 3d tessellation of the panel.
293    #[cfg(feature = "tessellate")]
294    pub fn tessellate_3d(&self) -> Result<(Vec<[f64; 3]>, Vec<u16>), Err> {
295        Ok(tessellate::tessellate_3d(self.tessellate_2d()?))
296    }
297
298    /// Expands the bounds of the drawing area to give space to any
299    /// mechanical / fabrication markings.
300    fn expanded_bounds(&self, bounds: geo::Rect<f64>) -> geo::Rect<f64> {
301        let ig = self.interior_geometry();
302        let has_h_scores = ig.iter().any(|g| matches!(g, InnerAtom::VScoreH(_)));
303        let has_v_scores = ig.iter().any(|g| matches!(g, InnerAtom::VScoreV(_)));
304
305        match (has_h_scores, has_v_scores) {
306            (true, true) => geo::Rect::<f64>::new(
307                bounds.min() - [10., 15.].into(),
308                bounds.max() + [65., 65.].into(),
309            ),
310            (true, false) => geo::Rect::<f64>::new(
311                bounds.min() - [10., 5.].into(),
312                bounds.max() + [65., 5.].into(),
313            ),
314            (false, true) => geo::Rect::<f64>::new(
315                bounds.min() - [5., 15.].into(),
316                bounds.max() + [5., 65.].into(),
317            ),
318            _ => bounds,
319        }
320    }
321
322    /// Indicates if the panel has fabrication instructions, such as
323    /// V-score lines.
324    pub fn has_fab_markings(&self) -> bool {
325        let ig = self.interior_geometry();
326        let has_h_scores = ig.iter().any(|g| matches!(g, InnerAtom::VScoreH(_)));
327        let has_v_scores = ig.iter().any(|g| matches!(g, InnerAtom::VScoreV(_)));
328
329        has_h_scores || has_v_scores
330    }
331
332    /// Produces an SVG tree rendering the panel.
333    pub fn make_svg(&self) -> Result<usvg::Tree, Err> {
334        let edges = self.edge_poly()?;
335        use geo::bounding_rect::BoundingRect;
336        let bounds = edges.bounding_rect().unwrap();
337        let img_bounds = self.expanded_bounds(bounds);
338
339        let size = match usvg::Size::new(img_bounds.width(), img_bounds.height()) {
340            Some(sz) => sz,
341            None => {
342                return Err(Err::NoBounds);
343            }
344        };
345        let rtree = usvg::Tree::create(usvg::Svg {
346            size,
347            view_box: usvg::ViewBox {
348                rect: size.to_rect(0.0, 0.0),
349                aspect: usvg::AspectRatio::default(),
350            },
351        });
352
353        let mut path = usvg::PathData::new();
354        let mut has_moved = false;
355        for point in edges.exterior().points_iter() {
356            if !has_moved {
357                has_moved = true;
358                path.push_move_to(point.x(), point.y());
359            } else {
360                path.push_line_to(point.x(), point.y());
361            }
362        }
363        path.push_close_path();
364        rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
365            stroke: Some(usvg::Stroke {
366                paint: usvg::Paint::Color(usvg::Color::new(0, 0, 0)),
367                width: usvg::StrokeWidth::new(0.1),
368                ..usvg::Stroke::default()
369            }),
370            data: std::rc::Rc::new(path),
371            ..usvg::Path::default()
372        }));
373
374        for inners in edges.interiors() {
375            let mut path = usvg::PathData::new();
376            let mut has_moved = false;
377            for point in inners.points_iter() {
378                if !has_moved {
379                    has_moved = true;
380                    path.push_move_to(point.x(), point.y());
381                } else {
382                    path.push_line_to(point.x(), point.y());
383                }
384            }
385            path.push_close_path();
386            rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
387                stroke: Some(usvg::Stroke {
388                    paint: usvg::Paint::Color(usvg::Color::new(0, 0, 0)),
389                    width: usvg::StrokeWidth::new(0.1),
390                    ..usvg::Stroke::default()
391                }),
392                data: std::rc::Rc::new(path),
393                ..usvg::Path::default()
394            }));
395        }
396
397        for inner in self.interior_geometry() {
398            match inner {
399                InnerAtom::Circle { center, radius, .. } => {
400                    let p = circle(center, radius);
401                    rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
402                        stroke: inner.stroke(),
403                        fill: inner.fill(),
404                        data: std::rc::Rc::new(p),
405                        ..usvg::Path::default()
406                    }));
407                }
408                InnerAtom::Rect {
409                    rect: rect_pos,
410                    layer: _,
411                } => {
412                    let p = rect(rect_pos);
413                    rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
414                        stroke: inner.stroke(),
415                        fill: inner.fill(),
416                        data: std::rc::Rc::new(p),
417                        ..usvg::Path::default()
418                    }));
419                }
420                InnerAtom::Drill { center, radius, .. } => {
421                    let p = circle(center, radius);
422                    rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
423                        stroke: inner.stroke(),
424                        fill: inner.fill(),
425                        data: std::rc::Rc::new(p),
426                        ..usvg::Path::default()
427                    }));
428                }
429
430                InnerAtom::VScoreH(y) => {
431                    let mut p = usvg::PathData::with_capacity(2);
432                    p.push_move_to(bounds.min().x - 4., y);
433                    p.push_line_to(bounds.max().x + 4., y);
434                    rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
435                        stroke: inner.stroke(),
436                        fill: inner.fill(),
437                        data: std::rc::Rc::new(p),
438                        ..usvg::Path::default()
439                    }));
440
441                    #[cfg(feature = "text")]
442                    rtree
443                        .root()
444                        .append_kind(usvg::NodeKind::Image(text::blit_text_span(
445                            bounds.max().x,
446                            y,
447                            "v-score".into(),
448                        )));
449                }
450                InnerAtom::VScoreV(x) => {
451                    let mut p = usvg::PathData::with_capacity(2);
452                    p.push_move_to(x, bounds.min().y - 4.);
453                    p.push_line_to(x, bounds.max().y + 4.);
454                    rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
455                        stroke: inner.stroke(),
456                        fill: inner.fill(),
457                        data: std::rc::Rc::new(p),
458                        ..usvg::Path::default()
459                    }));
460                }
461            }
462        }
463
464        // for the grid
465        if let Some(sep) = self.grid_separation {
466            let lower = ((bounds.min().x.floor() as isize) / sep) * sep;
467            let upper = ((bounds.max().x.ceil() as isize) / sep) * sep;
468            let mut curs: isize = lower;
469            while curs <= upper {
470                let mut p = usvg::PathData::with_capacity(2);
471                p.push_move_to(curs as f64, bounds.min().y);
472                p.push_line_to(curs as f64, bounds.max().y);
473                rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
474                    stroke: Some(usvg::Stroke {
475                        paint: usvg::Paint::Color(usvg::Color::new(0, 0, 0)),
476                        width: usvg::StrokeWidth::new(0.1),
477                        dasharray: Some(vec![0.25, 0.75]),
478                        linejoin: usvg::LineJoin::Round,
479                        ..usvg::Stroke::default()
480                    }),
481                    data: std::rc::Rc::new(p),
482                    ..usvg::Path::default()
483                }));
484
485                #[cfg(feature = "text")]
486                rtree
487                    .root()
488                    .append_kind(usvg::NodeKind::Image(text::blit_text_span(
489                        curs as f64 + 0.8,
490                        bounds.min().y + 0.5,
491                        &curs.to_string(),
492                    )));
493
494                curs += sep;
495            }
496
497            let lower = ((bounds.min().y.floor() as isize) / sep) * sep;
498            let upper = ((bounds.max().y.ceil() as isize) / sep) * sep;
499            let mut curs: isize = lower;
500            while curs <= upper {
501                let mut p = usvg::PathData::with_capacity(2);
502                p.push_move_to(bounds.min().x, curs as f64);
503                p.push_line_to(bounds.max().x, curs as f64);
504                rtree.root().append_kind(usvg::NodeKind::Path(usvg::Path {
505                    stroke: Some(usvg::Stroke {
506                        paint: usvg::Paint::Color(usvg::Color::new(0, 0, 0)),
507                        width: usvg::StrokeWidth::new(0.1),
508                        dasharray: Some(vec![0.25, 0.75]),
509                        linejoin: usvg::LineJoin::Round,
510                        ..usvg::Stroke::default()
511                    }),
512                    data: std::rc::Rc::new(p),
513                    ..usvg::Path::default()
514                }));
515
516                #[cfg(feature = "text")]
517                rtree
518                    .root()
519                    .append_kind(usvg::NodeKind::Image(text::blit_text_span(
520                        bounds.min().x + 0.5,
521                        curs as f64 + 0.8,
522                        &curs.to_string(),
523                    )));
524
525                curs += sep;
526            }
527        }
528
529        Ok(rtree)
530    }
531}
532
533fn circle(center: Coordinate<f64>, radius: f64) -> usvg::PathData {
534    let mut p = usvg::PathData::with_capacity(6);
535    p.push_move_to(center.x + radius, center.y);
536    p.push_arc_to(
537        radius,
538        radius,
539        0.0,
540        false,
541        true,
542        center.x,
543        center.y + radius,
544    );
545    p.push_arc_to(
546        radius,
547        radius,
548        0.0,
549        false,
550        true,
551        center.x - radius,
552        center.y,
553    );
554    p.push_arc_to(
555        radius,
556        radius,
557        0.0,
558        false,
559        true,
560        center.x,
561        center.y - radius,
562    );
563    p.push_arc_to(
564        radius,
565        radius,
566        0.0,
567        false,
568        true,
569        center.x + radius,
570        center.y,
571    );
572    p.push_close_path();
573    p
574}
575
576fn rect(rect: geo::Rect<f64>) -> usvg::PathData {
577    let mut p = usvg::PathData::with_capacity(5);
578    p.push_move_to(rect.min().x, rect.min().y);
579    p.push_line_to(rect.max().x, rect.min().y);
580    p.push_line_to(rect.max().x, rect.max().y);
581    p.push_line_to(rect.min().x, rect.max().y);
582    p.push_line_to(rect.min().x, rect.min().y);
583    p.push_close_path();
584    p
585}
586
587impl std::fmt::Display for Panel<'_> {
588    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
589        write!(f, "panel(")?;
590        for feature in &self.features {
591            feature.fmt(f)?;
592            write!(f, " ")?;
593        }
594        write!(f, ")")
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn test_overlapping_rects() {
604        let mut panel = Panel::new();
605        panel.push_spec("R<@(-2.5, -2.5), 5>(h3)").unwrap();
606        panel.push(features::Rect::new([-0., -1.].into(), [5., 3.].into()));
607
608        assert_eq!(
609            panel.edge_geometry().unwrap(),
610            geo::MultiPolygon(vec![geo::Polygon::new(
611                geo::LineString(vec![
612                    geo::Coordinate { x: -5.0, y: -5.0 },
613                    geo::Coordinate { x: 0.0, y: -5.0 },
614                    geo::Coordinate { x: 0.0, y: -1.0 },
615                    geo::Coordinate { x: 5.0, y: -1.0 },
616                    geo::Coordinate { x: 5.0, y: 3.0 },
617                    geo::Coordinate { x: -0.0, y: 3.0 },
618                    geo::Coordinate { x: 0.0, y: 0.0 },
619                    geo::Coordinate { x: -5.0, y: 0.0 },
620                    geo::Coordinate { x: -5.0, y: -5.0 }
621                ]),
622                vec![],
623            )]),
624        );
625    }
626
627    #[test]
628    fn test_rect_inner() {
629        let mut panel = Panel::new();
630        panel.push_spec("R<@(2.5, -2.5), 5>(h3)").unwrap();
631
632        // eprintln!("{:?}", panel.interior_geometry());
633        for i in 0..5 {
634            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x > 2.49);
635            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x < 2.51);
636            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y < -2.49);
637            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y > -2.51);
638        }
639    }
640
641    #[test]
642    fn test_array_inner() {
643        let mut panel = Panel::new();
644        panel.push_spec("[5]R<5>(h3)").unwrap();
645        assert_eq!(panel.interior_geometry().len(), 25);
646
647        use geo::bounding_rect::BoundingRect;
648        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
649        assert!(bounds.width() > 24.99 && bounds.width() < 25.01);
650        assert!(bounds.height() > 4.99 && bounds.height() < 5.01);
651    }
652
653    #[test]
654    fn test_column_down() {
655        let mut panel = Panel::new();
656        panel
657            .push_spec("column left { R<5,5>(h) R<3>(h) } ")
658            .unwrap();
659
660        use geo::bounding_rect::BoundingRect;
661        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
662        eprintln!("{:?}\n\n{:?}", panel.features, bounds);
663        assert!(bounds.width() > 4.99 && bounds.width() < 5.01);
664        assert!(bounds.height() > 7.99 && bounds.height() < 8.01);
665    }
666
667    #[test]
668    fn test_circ_inner() {
669        let mut panel = Panel::new();
670        panel.push_spec("C<5>(h2)").unwrap();
671
672        for i in 0..5 {
673            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x > -0.01);
674            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x < 0.01);
675            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y < 0.01);
676            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y > -0.01);
677        }
678
679        let mut panel = Panel::new();
680        panel.push_spec("C<@(1, 1), 1>(h2)").unwrap();
681        // eprintln!("{:?}", panel.interior_geometry());
682        for i in 0..5 {
683            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x > 0.99);
684            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x < 1.01);
685            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y < 1.01);
686            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y > 0.99);
687        }
688    }
689
690    #[test]
691    fn test_atpos_xends() {
692        let mut panel = Panel::new();
693        panel.push(features::AtPos::x_ends(
694            features::Rect::with_center([4., 2.].into(), 2., 3.),
695            Some(features::Circle::wrap_with_radius(
696                features::ScrewHole::with_diameter(1.),
697                2.,
698            )),
699            Some(features::Circle::wrap_with_radius(
700                features::ScrewHole::with_diameter(1.),
701                2.,
702            )),
703        ));
704
705        for i in 0..5 {
706            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x < 3.01);
707            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x > 2.99);
708            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y < 2.01);
709            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y > -2.01);
710        }
711        for i in 5..10 {
712            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x < 5.01);
713            assert!(panel.interior_geometry()[i].bounds().unwrap().center().x > 4.99);
714            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y < 2.01);
715            assert!(panel.interior_geometry()[i].bounds().unwrap().center().y > -2.01);
716        }
717    }
718
719    #[test]
720    fn test_atpos_angle() {
721        let mut r = features::AtPos::<features::Rect, features::Rect>::new(
722            features::Rect::with_center([4., 2.].into(), 1., 1.),
723        );
724        r.push(
725            features::Rect::with_center([0., 0.].into(), 1., 1.),
726            features::Positioning::Angle {
727                degrees: 45.,
728                amount: 3.,
729            },
730        );
731        let ig = r.edge_union().unwrap();
732        use geo::prelude::Contains;
733        assert!(ig.contains(&geo::Coordinate::from([5.8, 3.8])));
734    }
735
736    #[test]
737    fn test_atpos_corner() {
738        let mut r = features::AtPos::<features::Rect, features::Rect>::new(
739            features::Rect::with_center([2., 2.].into(), 4., 4.),
740        );
741        r.push(
742            features::Rect::with_center([0., 0.].into(), 0.6, 0.6),
743            features::Positioning::Corner {
744                side: Direction::Left,
745                align: Align::End,
746                opposite: false,
747            },
748        );
749        let ig = r.edge_union().unwrap();
750        use geo::prelude::Contains;
751        assert!(ig.contains(&geo::Coordinate::from([-0.5, 0.5])));
752
753        let mut r = features::AtPos::<features::Rect, features::Rect>::new(
754            features::Rect::with_center([2., 2.].into(), 4., 4.),
755        );
756        r.push(
757            features::Rect::with_center([0., 0.].into(), 0.6, 0.6),
758            features::Positioning::Corner {
759                side: Direction::Up,
760                align: Align::End,
761                opposite: true,
762            },
763        );
764        let ig = r.edge_union().unwrap();
765        // eprintln!("{:?}", ig);
766        assert!(ig.contains(&geo::Coordinate::from([3.9, -0.5])));
767    }
768
769    #[test]
770    fn test_cel_basic() {
771        let mut panel = Panel::new();
772        panel.push_spec("let ye = !{2};\nR<$ye>").unwrap();
773
774        use geo::bounding_rect::BoundingRect;
775        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
776        // eprintln!("{:?}\n\n{:?}", panel.features, bounds);
777        assert!(bounds.width() > 1.99 && bounds.width() < 2.01);
778        assert!(bounds.height() > 1.99 && bounds.height() < 2.01);
779
780        let mut panel = Panel::new();
781        panel.push_spec("let ye = !{2.0};\nR<$ye>").unwrap();
782
783        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
784        // eprintln!("{:?}\n\n{:?}", panel.features, bounds);
785        assert!(bounds.width() > 1.99 && bounds.width() < 2.01);
786        assert!(bounds.height() > 1.99 && bounds.height() < 2.01);
787    }
788
789    #[test]
790    fn test_cel_expr() {
791        let mut panel = Panel::new();
792        panel
793            .push_spec("let ye = !{2 + 2.0};\nR<!{2}, $ye>")
794            .unwrap();
795
796        use geo::bounding_rect::BoundingRect;
797        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
798        // eprintln!("{:?}\n\n{:?}", panel.features, bounds);
799        assert!(bounds.width() > 1.99 && bounds.width() < 2.01);
800        assert!(bounds.height() > 3.99 && bounds.height() < 4.01);
801    }
802
803    #[test]
804    fn test_cel_wrap() {
805        let mut panel = Panel::new();
806        panel
807            .push_spec("let ye = !{2 + 1.0};\nwrap(R<!{2}>) with { left align exterior => R<!{ye + 1}>,\n }")
808            .unwrap();
809
810        use geo::bounding_rect::BoundingRect;
811        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
812        // eprintln!("{:?}\n\n{:?}", panel.features, bounds);
813        assert!(bounds.width() > 5.99 && bounds.width() < 6.01);
814        assert!(bounds.height() > 3.99 && bounds.height() < 4.01);
815    }
816
817    #[test]
818    fn test_rotate() {
819        let mut panel = Panel::new();
820        panel.push_spec("rotate(90) { C<2.5> }").unwrap();
821
822        use geo::bounding_rect::BoundingRect;
823        let bounds = panel.edge_geometry().unwrap().bounding_rect().unwrap();
824        eprintln!("{:?}\n\n{:?}", panel.features, bounds);
825        assert!(bounds.width() > 4.99 && bounds.width() < 5.0001);
826        assert!(bounds.height() > 4.99 && bounds.height() < 5.0001);
827    }
828}