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#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Align {
27 Start,
28 Center,
29 End,
30}
31
32#[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#[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#[derive(Debug)]
103pub enum Err {
104 NoFeatures,
105 NoBounds,
106 BadEdgeGeometry(String),
107 InternalGerberFailure,
108 #[cfg(feature = "tessellate")]
109 TessellationError(TessellationError),
110}
111
112pub 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 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 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 pub fn convex_hull(&mut self, convex_hull: bool) {
147 self.convex_hull = convex_hull;
148 }
149
150 pub fn set_grid_separation(&mut self, grid_separation: Option<isize>) {
152 self.grid_separation = grid_separation;
153 }
154
155 pub fn push<F: Feature + 'a>(&mut self, f: F) {
157 self.features.push(Box::new(f));
158 }
159
160 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 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(¤t));
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 pub fn interior_geometry(&self) -> Vec<InnerAtom> {
238 self.features
239 .iter()
240 .map(|f| f.interior())
241 .flatten()
242 .collect()
243 }
244
245 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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}