gerber_types/
macros.rs

1//! Aperture Macros.
2
3use std::convert::From;
4use std::io::Write;
5
6use crate::errors::{GerberError, GerberResult};
7use crate::traits::PartialGerberCode;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct ApertureMacro {
11    pub name: String,
12    pub content: Vec<MacroContent>,
13}
14
15impl ApertureMacro {
16    pub fn new<S: Into<String>>(name: S) -> Self {
17        ApertureMacro {
18            name: name.into(),
19            content: Vec::new(),
20        }
21    }
22
23    pub fn add_content<C>(mut self, c: C) -> Self
24    where
25        C: Into<MacroContent>,
26    {
27        self.content.push(c.into());
28        self
29    }
30
31    pub fn add_content_mut<C>(&mut self, c: C)
32    where
33        C: Into<MacroContent>,
34    {
35        self.content.push(c.into());
36    }
37}
38
39impl<W: Write> PartialGerberCode<W> for ApertureMacro {
40    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
41        if self.content.is_empty() {
42            return Err(GerberError::MissingDataError(
43                "There must be at least 1 content element in an aperture macro".into(),
44            ));
45        }
46        writeln!(writer, "AM{}*", self.name)?;
47        let mut first = true;
48        for content in &self.content {
49            if first {
50                first = false;
51            } else {
52                writeln!(writer)?;
53            }
54            content.serialize_partial(writer)?;
55        }
56        Ok(())
57    }
58}
59
60#[derive(Debug, Clone, PartialEq)]
61/// A macro decimal can either be an f64 or a variable placeholder.
62pub enum MacroDecimal {
63    /// A decimal value.
64    Value(f64),
65    /// A variable placeholder.
66    Variable(u32),
67    /// An expression placeholder, e.g. "$3+($4/2)" or "$1x4".
68    ///
69    /// Gerber spec (4.5.4.2 Arithmetic Expressions) says:
70    /// "The standard arithmetic precedence rules apply". i.e. *not* Rust/C precedence rules.
71    /// Allowed elements: "integer and decimal constants, other variables, arithmetic operators and the brackets '(' and ')'"
72    /// Allowed operators: '+', '-', 'x' (lower case), '/'.
73    Expression(String),
74}
75
76impl MacroDecimal {
77    fn is_negative(&self) -> bool {
78        match *self {
79            MacroDecimal::Value(v) => v < 0.0,
80            MacroDecimal::Variable(_) => false,
81            MacroDecimal::Expression(_) => false,
82        }
83    }
84}
85
86impl From<f32> for MacroDecimal {
87    fn from(val: f32) -> Self {
88        MacroDecimal::Value(val as f64)
89    }
90}
91
92impl From<f64> for MacroDecimal {
93    fn from(val: f64) -> Self {
94        MacroDecimal::Value(val)
95    }
96}
97
98impl<W: Write> PartialGerberCode<W> for MacroDecimal {
99    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
100        match *self {
101            MacroDecimal::Value(ref v) => write!(writer, "{}", v)?,
102            MacroDecimal::Variable(ref v) => write!(writer, "${}", v)?,
103            MacroDecimal::Expression(ref v) => write!(writer, "{}", v)?,
104        };
105        Ok(())
106    }
107}
108
109/// The gerber specification (2021.02 - 2024.05) 3.4 Data Types does not define a boolean
110/// However, there are various places where they are used in macros whey there are defined
111/// as having a 0 or 1 value.  Such as the 'exposure' flag.
112#[derive(Debug, Clone, PartialEq)]
113pub enum MacroBoolean {
114    Value(bool),
115    Variable(u32),
116    Expression(String),
117}
118
119impl<W: Write> PartialGerberCode<W> for MacroBoolean {
120    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
121        match *self {
122            MacroBoolean::Value(ref v) => write!(writer, "{}", *v as u8)?,
123            MacroBoolean::Variable(ref v) => write!(writer, "${}", v)?,
124            MacroBoolean::Expression(ref v) => write!(writer, "{}", v)?,
125        };
126        Ok(())
127    }
128}
129
130impl From<MacroDecimal> for MacroBoolean {
131    fn from(value: MacroDecimal) -> Self {
132        match value {
133            MacroDecimal::Value(decimal) => Self::Value(decimal == 1.0),
134            MacroDecimal::Variable(variable) => Self::Variable(variable),
135            MacroDecimal::Expression(expressions) => Self::Expression(expressions),
136        }
137    }
138}
139
140#[test]
141fn test_macro_boolean_from_decimal() {
142    assert_eq!(
143        MacroBoolean::from(MacroDecimal::Value(1.0)),
144        MacroBoolean::Value(true)
145    );
146    assert_eq!(
147        MacroBoolean::from(MacroDecimal::Value(0.0)),
148        MacroBoolean::Value(false)
149    );
150    assert_eq!(
151        MacroBoolean::from(MacroDecimal::Variable(42)),
152        MacroBoolean::Variable(42)
153    );
154    assert_eq!(
155        MacroBoolean::from(MacroDecimal::Expression("$1x$2".to_string())),
156        MacroBoolean::Expression("$1x$2".to_string())
157    );
158}
159
160/// Gerber specification (2021.02 - 2024.05) 3.4.1 Integers
161/// "Integers must fit in a 32-bit signed integer"
162#[derive(Debug, Clone, PartialEq)]
163pub enum MacroInteger {
164    Value(u32),
165    Variable(u32),
166    Expression(String),
167}
168
169impl<W: Write> PartialGerberCode<W> for MacroInteger {
170    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
171        match *self {
172            MacroInteger::Value(ref v) => write!(writer, "{}", v)?,
173            MacroInteger::Variable(ref v) => write!(writer, "${}", v)?,
174            MacroInteger::Expression(ref v) => write!(writer, "{}", v)?,
175        };
176        Ok(())
177    }
178}
179
180/// Gerber specification (2021.02 - 2024.05) 4.5.1.1 Primitives / Overview
181/// "Except for the comment all the parameters can be a decimal, integer, macro variables or an arithmetic expression"
182///
183/// However, AFAICT (DC), it doesn't make sense for a the Vertices of the Outline to be a macro variable or expression.
184/// Only a literal makes sense since the amount of co-ordinate arguments for the macro cannot change after it is defined
185/// *and* the last point of an outline has to be the same as the first point.
186///
187/// FUTURE report this specification inconsistency with `UCamco` and provide a support reference or link here.
188///
189/// Fortunately for us the [`OutlinePrimitive`] doesn't need to store the amount of vertices since it uses a [`Vec`] to store the points.
190///
191#[derive(Debug, Clone, PartialEq)]
192pub enum MacroContent {
193    // Primitives
194    Circle(CirclePrimitive),
195    VectorLine(VectorLinePrimitive),
196    CenterLine(CenterLinePrimitive),
197    Outline(OutlinePrimitive),
198    Polygon(PolygonPrimitive),
199    Moire(MoirePrimitive),
200    Thermal(ThermalPrimitive),
201
202    // Variables
203    VariableDefinition(VariableDefinition),
204
205    // Comment
206    Comment(String),
207}
208
209impl<W: Write> PartialGerberCode<W> for MacroContent {
210    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
211        match *self {
212            MacroContent::Circle(ref c) => c.serialize_partial(writer)?,
213            MacroContent::VectorLine(ref vl) => vl.serialize_partial(writer)?,
214            MacroContent::CenterLine(ref cl) => cl.serialize_partial(writer)?,
215            MacroContent::Outline(ref o) => o.serialize_partial(writer)?,
216            MacroContent::Polygon(ref p) => p.serialize_partial(writer)?,
217            MacroContent::Moire(ref m) => m.serialize_partial(writer)?,
218            MacroContent::Thermal(ref t) => t.serialize_partial(writer)?,
219            MacroContent::Comment(ref s) => write!(writer, "0 {}*", &s)?,
220            MacroContent::VariableDefinition(ref v) => v.serialize_partial(writer)?,
221        };
222        Ok(())
223    }
224}
225
226macro_rules! impl_into {
227    ($target:ty, $from:ty, $choice:expr) => {
228        impl From<$from> for $target {
229            fn from(val: $from) -> $target {
230                $choice(val)
231            }
232        }
233    };
234}
235
236impl_into!(MacroContent, CirclePrimitive, MacroContent::Circle);
237impl_into!(MacroContent, VectorLinePrimitive, MacroContent::VectorLine);
238impl_into!(MacroContent, CenterLinePrimitive, MacroContent::CenterLine);
239impl_into!(MacroContent, OutlinePrimitive, MacroContent::Outline);
240impl_into!(MacroContent, PolygonPrimitive, MacroContent::Polygon);
241impl_into!(MacroContent, MoirePrimitive, MacroContent::Moire);
242impl_into!(MacroContent, ThermalPrimitive, MacroContent::Thermal);
243impl_into!(
244    MacroContent,
245    VariableDefinition,
246    MacroContent::VariableDefinition
247);
248
249impl<T: Into<String>> From<T> for MacroContent {
250    fn from(val: T) -> Self {
251        MacroContent::Comment(val.into())
252    }
253}
254
255#[derive(Debug, Clone, PartialEq)]
256pub struct CirclePrimitive {
257    /// Exposure off/on
258    pub exposure: MacroBoolean,
259
260    /// Diameter, a decimal >= 0
261    pub diameter: MacroDecimal,
262
263    /// X and Y coordinates of center position, decimals
264    pub center: (MacroDecimal, MacroDecimal),
265
266    /// Rotation angle.
267    ///
268    /// The rotation angle is specified by a decimal, in degrees. The primitive
269    /// is rotated around the origin of the macro definition, i.e. the (0, 0)
270    /// point of macro coordinates.
271    ///
272    /// The rotation modifier is optional. The default is no rotation. (We
273    /// recommend always to set the angle explicitly.
274    pub angle: Option<MacroDecimal>,
275}
276
277impl CirclePrimitive {
278    pub fn new(diameter: MacroDecimal) -> Self {
279        CirclePrimitive {
280            exposure: MacroBoolean::Value(true),
281            diameter,
282            center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)),
283            angle: None,
284        }
285    }
286
287    pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self {
288        self.center = center;
289        self
290    }
291
292    pub fn with_exposure(mut self, exposure: MacroBoolean) -> Self {
293        self.exposure = exposure;
294        self
295    }
296
297    #[deprecated(since = "0.4.0", note = "Use `with_exposure` instead")]
298    #[allow(unused_mut)]
299    pub fn exposure_on(mut self, exposure: bool) -> Self {
300        self.with_exposure(MacroBoolean::Value(exposure))
301    }
302
303    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
304        self.angle = Some(angle);
305        self
306    }
307}
308
309impl<W: Write> PartialGerberCode<W> for CirclePrimitive {
310    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
311        write!(writer, "1,")?;
312        self.exposure.serialize_partial(writer)?;
313        write!(writer, ",")?;
314        self.diameter.serialize_partial(writer)?;
315        write!(writer, ",")?;
316        self.center.0.serialize_partial(writer)?;
317        write!(writer, ",")?;
318        self.center.1.serialize_partial(writer)?;
319        if let Some(ref a) = self.angle {
320            write!(writer, ",")?;
321            a.serialize_partial(writer)?;
322        }
323        write!(writer, "*")?;
324        Ok(())
325    }
326}
327
328#[derive(Debug, Clone, PartialEq)]
329pub struct VectorLinePrimitive {
330    /// Exposure off/on
331    pub exposure: MacroBoolean,
332
333    /// Line width, a decimal >= 0
334    pub width: MacroDecimal,
335
336    /// X and Y coordinates of start point, decimals
337    pub start: (MacroDecimal, MacroDecimal),
338
339    /// X and Y coordinates of end point, decimals
340    pub end: (MacroDecimal, MacroDecimal),
341
342    /// Rotation angle of the vector line primitive
343    ///
344    /// The rotation angle is specified by a decimal, in degrees. The primitive
345    /// is rotated around the origin of the macro definition, i.e. the (0, 0)
346    /// point of macro coordinates.
347    pub angle: MacroDecimal,
348}
349
350impl VectorLinePrimitive {
351    pub fn new(start: (MacroDecimal, MacroDecimal), end: (MacroDecimal, MacroDecimal)) -> Self {
352        VectorLinePrimitive {
353            exposure: MacroBoolean::Value(true),
354            width: MacroDecimal::Value(0.0),
355            start,
356            end,
357            angle: MacroDecimal::Value(0.0),
358        }
359    }
360
361    pub fn with_exposure(mut self, exposure: MacroBoolean) -> Self {
362        self.exposure = exposure;
363        self
364    }
365
366    #[deprecated(since = "0.4.0", note = "Use `with_exposure` instead")]
367    #[allow(unused_mut)]
368    pub fn exposure_on(mut self, exposure: bool) -> Self {
369        self.with_exposure(MacroBoolean::Value(exposure))
370    }
371
372    pub fn with_width(mut self, width: MacroDecimal) -> Self {
373        self.width = width;
374        self
375    }
376
377    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
378        self.angle = angle;
379        self
380    }
381}
382
383impl<W: Write> PartialGerberCode<W> for VectorLinePrimitive {
384    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
385        write!(writer, "20,")?;
386        self.exposure.serialize_partial(writer)?;
387        write!(writer, ",")?;
388        self.width.serialize_partial(writer)?;
389        write!(writer, ",")?;
390        self.start.0.serialize_partial(writer)?;
391        write!(writer, ",")?;
392        self.start.1.serialize_partial(writer)?;
393        write!(writer, ",")?;
394        self.end.0.serialize_partial(writer)?;
395        write!(writer, ",")?;
396        self.end.1.serialize_partial(writer)?;
397        write!(writer, ",")?;
398        self.angle.serialize_partial(writer)?;
399        write!(writer, "*")?;
400        Ok(())
401    }
402}
403
404#[derive(Debug, Clone, PartialEq)]
405pub struct CenterLinePrimitive {
406    /// Exposure off/on (0/1)
407    pub exposure: MacroBoolean,
408
409    /// Rectangle dimensions (width/height)
410    pub dimensions: (MacroDecimal, MacroDecimal),
411
412    /// X and Y coordinates of center point, decimals
413    pub center: (MacroDecimal, MacroDecimal),
414
415    /// Rotation angle
416    ///
417    /// The rotation angle is specified by a decimal, in degrees. The primitive
418    /// is rotated around the origin of the macro definition, i.e. (0, 0) point
419    /// of macro coordinates.
420    pub angle: MacroDecimal,
421}
422
423impl CenterLinePrimitive {
424    pub fn new(dimensions: (MacroDecimal, MacroDecimal)) -> Self {
425        CenterLinePrimitive {
426            exposure: MacroBoolean::Value(true),
427            dimensions,
428            center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)),
429            angle: MacroDecimal::Value(0.0),
430        }
431    }
432
433    pub fn with_exposure(mut self, exposure: MacroBoolean) -> Self {
434        self.exposure = exposure;
435        self
436    }
437
438    #[deprecated(since = "0.4.0", note = "Use `with_exposure` instead")]
439    #[allow(unused_mut)]
440    pub fn exposure_on(mut self, exposure: bool) -> Self {
441        self.with_exposure(MacroBoolean::Value(exposure))
442    }
443
444    pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self {
445        self.center = center;
446        self
447    }
448
449    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
450        self.angle = angle;
451        self
452    }
453}
454
455impl<W: Write> PartialGerberCode<W> for CenterLinePrimitive {
456    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
457        write!(writer, "21,")?;
458        self.exposure.serialize_partial(writer)?;
459        write!(writer, ",")?;
460        self.dimensions.0.serialize_partial(writer)?;
461        write!(writer, ",")?;
462        self.dimensions.1.serialize_partial(writer)?;
463        write!(writer, ",")?;
464        self.center.0.serialize_partial(writer)?;
465        write!(writer, ",")?;
466        self.center.1.serialize_partial(writer)?;
467        write!(writer, ",")?;
468        self.angle.serialize_partial(writer)?;
469        write!(writer, "*")?;
470        Ok(())
471    }
472}
473
474#[derive(Debug, Clone, PartialEq)]
475pub struct OutlinePrimitive {
476    /// Exposure off/on (0/1)
477    pub exposure: MacroBoolean,
478
479    /// Vector of coordinate pairs.
480    ///
481    /// The last coordinate pair must be equal to the first coordinate pair!
482    pub points: Vec<(MacroDecimal, MacroDecimal)>,
483
484    /// Rotation angle of the outline primitive
485    ///
486    /// The rotation angle is specified by a decimal, in degrees. The primitive
487    /// is rotated around the origin of the macro definition, i.e. the (0, 0)
488    /// point of macro coordinates.
489    pub angle: MacroDecimal,
490}
491
492impl OutlinePrimitive {
493    pub fn new() -> Self {
494        OutlinePrimitive {
495            exposure: MacroBoolean::Value(true),
496            points: Vec::new(),
497            angle: MacroDecimal::Value(0.0),
498        }
499    }
500
501    pub fn from_points(points: Vec<(MacroDecimal, MacroDecimal)>) -> Self {
502        let mut outline_prim = Self::new();
503        outline_prim.points = points;
504        outline_prim
505    }
506
507    pub fn add_point(mut self, point: (MacroDecimal, MacroDecimal)) -> Self {
508        self.points.push(point);
509        self
510    }
511
512    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
513        self.angle = angle;
514        self
515    }
516}
517
518impl<W: Write> PartialGerberCode<W> for OutlinePrimitive {
519    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
520        // Points invariants
521        if self.points.len() < 2 {
522            return Err(GerberError::MissingDataError(
523                "There must be at least 1 subsequent point in an outline".into(),
524            ));
525        }
526        if self.points.len() > 5001 {
527            return Err(GerberError::RangeError(
528                "The maximum number of subsequent points in an outline is 5000".into(),
529            ));
530        }
531        if self.points[0] != self.points[self.points.len() - 1] {
532            return Err(GerberError::RangeError(
533                "The last point must be equal to the first point".into(),
534            ));
535        }
536
537        write!(writer, "4,")?;
538        self.exposure.serialize_partial(writer)?;
539        writeln!(writer, ",{},", self.points.len() - 1)?;
540
541        for (ref x, ref y) in &self.points {
542            x.serialize_partial(writer)?;
543            write!(writer, ",")?;
544            y.serialize_partial(writer)?;
545            writeln!(writer, ",")?;
546        }
547        self.angle.serialize_partial(writer)?;
548        write!(writer, "*")?;
549        Ok(())
550    }
551}
552
553#[derive(Debug, Clone, PartialEq)]
554/// A polygon primitive is a regular polygon defined by the number of vertices,
555/// the center point and the diameter of the circumscribed circle.
556pub struct PolygonPrimitive {
557    /// Exposure off/on (0/1)
558    pub exposure: MacroBoolean,
559
560    /// Number of vertices n, 3 <= n <= 12
561    pub vertices: MacroInteger,
562
563    /// X and Y coordinates of center point, decimals
564    pub center: (MacroDecimal, MacroDecimal),
565
566    /// Diameter of the circumscribed circle, a decimal >= 0
567    pub diameter: MacroDecimal,
568
569    /// Rotation angle of the polygon primitive
570    ///
571    /// The rotation angle is specified by a decimal, in degrees. The primitive
572    /// is rotated around the origin of the macro definition, i.e. the (0, 0)
573    /// point of macro coordinates. The first vertex is on the positive X-axis
574    /// through the center point when the rotation angle is zero.
575    ///
576    /// Note: Rotation is only allowed if the primitive center point coincides
577    /// with the origin of the macro definition.
578    pub angle: MacroDecimal,
579}
580
581impl PolygonPrimitive {
582    pub fn new(vertices: MacroInteger) -> Self {
583        PolygonPrimitive {
584            exposure: MacroBoolean::Value(true),
585            vertices,
586            center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)),
587            diameter: MacroDecimal::Value(0.0),
588            angle: MacroDecimal::Value(0.0),
589        }
590    }
591
592    pub fn with_exposure(mut self, exposure: MacroBoolean) -> Self {
593        self.exposure = exposure;
594        self
595    }
596
597    #[deprecated(since = "0.4.0", note = "Use `with_exposure` instead")]
598    #[allow(unused_mut)]
599    pub fn exposure_on(mut self, exposure: bool) -> Self {
600        self.with_exposure(MacroBoolean::Value(exposure))
601    }
602
603    pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self {
604        self.center = center;
605        self
606    }
607
608    pub fn with_diameter(mut self, diameter: MacroDecimal) -> Self {
609        self.diameter = diameter;
610        self
611    }
612
613    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
614        self.angle = angle;
615        self
616    }
617}
618
619impl<W: Write> PartialGerberCode<W> for PolygonPrimitive {
620    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
621        // Vertice count invariants
622        if matches!(self.vertices, MacroInteger::Value(value) if value < 3) {
623            return Err(GerberError::MissingDataError(
624                "There must be at least 3 vertices in a polygon".into(),
625            ));
626        }
627        if matches!(self.vertices, MacroInteger::Value(value) if value > 12) {
628            return Err(GerberError::RangeError(
629                "The maximum number of vertices in a polygon is 12".into(),
630            ));
631        }
632        if self.diameter.is_negative() {
633            return Err(GerberError::RangeError(
634                "The diameter must not be negative".into(),
635            ));
636        }
637        write!(writer, "5,")?;
638        self.exposure.serialize_partial(writer)?;
639        write!(writer, ",")?;
640        self.vertices.serialize_partial(writer)?;
641        write!(writer, ",")?;
642        self.center.0.serialize_partial(writer)?;
643        write!(writer, ",")?;
644        self.center.1.serialize_partial(writer)?;
645        write!(writer, ",")?;
646        self.diameter.serialize_partial(writer)?;
647        write!(writer, ",")?;
648        self.angle.serialize_partial(writer)?;
649        write!(writer, "*")?;
650        Ok(())
651    }
652}
653
654/// The moiré primitive is a cross hair centered on concentric rings (annuli).
655/// Exposure is always on.
656#[derive(Debug, Clone, PartialEq)]
657pub struct MoirePrimitive {
658    /// X and Y coordinates of center point, decimals
659    pub center: (MacroDecimal, MacroDecimal),
660
661    /// Outer diameter of outer concentric ring, a decimal >= 0
662    pub diameter: MacroDecimal,
663
664    /// Ring thickness, a decimal >= 0
665    pub ring_thickness: MacroDecimal,
666
667    /// Gap between rings, a decimal >= 0
668    pub gap: MacroDecimal,
669
670    /// Maximum number of rings
671    pub max_rings: u32,
672
673    /// Cross hair thickness, a decimal >= 0
674    pub cross_hair_thickness: MacroDecimal,
675
676    /// Cross hair length, a decimal >= 0
677    pub cross_hair_length: MacroDecimal,
678
679    /// Rotation angle of the moiré primitive
680    ///
681    /// The rotation angle is specified by a decimal, in degrees. The primitive
682    /// is rotated around the origin of the macro definition, i.e. the (0, 0)
683    /// point of macro coordinates.
684    ///
685    /// Note: Rotation is only allowed if the primitive center point coincides
686    /// with the origin of the macro definition.
687    pub angle: MacroDecimal,
688}
689
690impl MoirePrimitive {
691    pub fn new() -> Self {
692        MoirePrimitive {
693            center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)),
694            diameter: MacroDecimal::Value(0.0),
695            ring_thickness: MacroDecimal::Value(0.0),
696            gap: MacroDecimal::Value(0.0),
697            max_rings: 1,
698            cross_hair_thickness: MacroDecimal::Value(0.0),
699            cross_hair_length: MacroDecimal::Value(0.0),
700            angle: MacroDecimal::Value(0.0),
701        }
702    }
703
704    pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self {
705        self.center = center;
706        self
707    }
708
709    pub fn with_diameter(mut self, diameter: MacroDecimal) -> Self {
710        self.diameter = diameter;
711        self
712    }
713
714    pub fn with_rings_max(mut self, max_rings: u32) -> Self {
715        self.max_rings = max_rings;
716        self
717    }
718
719    pub fn with_ring_thickness(mut self, thickness: MacroDecimal) -> Self {
720        self.ring_thickness = thickness;
721        self
722    }
723
724    pub fn with_gap(mut self, gap: MacroDecimal) -> Self {
725        self.gap = gap;
726        self
727    }
728
729    pub fn with_cross_thickness(mut self, thickness: MacroDecimal) -> Self {
730        self.cross_hair_thickness = thickness;
731        self
732    }
733
734    pub fn with_cross_length(mut self, length: MacroDecimal) -> Self {
735        self.cross_hair_length = length;
736        self
737    }
738
739    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
740        self.angle = angle;
741        self
742    }
743}
744
745impl<W: Write> PartialGerberCode<W> for MoirePrimitive {
746    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
747        // Decimal invariants
748        if self.diameter.is_negative() {
749            return Err(GerberError::RangeError(
750                "Outer diameter of a moiré may not be negative".into(),
751            ));
752        }
753        if self.ring_thickness.is_negative() {
754            return Err(GerberError::RangeError(
755                "Ring thickness of a moiré may not be negative".into(),
756            ));
757        }
758        if self.gap.is_negative() {
759            return Err(GerberError::RangeError(
760                "Gap of a moiré may not be negative".into(),
761            ));
762        }
763        if self.cross_hair_thickness.is_negative() {
764            return Err(GerberError::RangeError(
765                "Cross hair thickness of a moiré may not be negative".into(),
766            ));
767        }
768        if self.cross_hair_length.is_negative() {
769            return Err(GerberError::RangeError(
770                "Cross hair length of a moiré may not be negative".into(),
771            ));
772        }
773        write!(writer, "6,")?;
774        self.center.0.serialize_partial(writer)?;
775        write!(writer, ",")?;
776        self.center.1.serialize_partial(writer)?;
777        write!(writer, ",")?;
778        self.diameter.serialize_partial(writer)?;
779        write!(writer, ",")?;
780        self.ring_thickness.serialize_partial(writer)?;
781        write!(writer, ",")?;
782        self.gap.serialize_partial(writer)?;
783        write!(writer, ",{},", self.max_rings)?;
784        self.cross_hair_thickness.serialize_partial(writer)?;
785        write!(writer, ",")?;
786        self.cross_hair_length.serialize_partial(writer)?;
787        write!(writer, ",")?;
788        self.angle.serialize_partial(writer)?;
789        write!(writer, "*")?;
790        Ok(())
791    }
792}
793
794/// The thermal primitive is a ring (annulus) interrupted by four gaps.
795/// Exposure is always on.
796#[derive(Debug, Clone, PartialEq)]
797pub struct ThermalPrimitive {
798    /// X and Y coordinates of center point, decimals
799    pub center: (MacroDecimal, MacroDecimal),
800
801    /// Outer diameter, a decimal > inner diameter
802    pub outer_diameter: MacroDecimal,
803
804    /// Inner diameter, a decimal >= 0
805    pub inner_diameter: MacroDecimal,
806
807    /// Gap thickness, a decimal < (outer diameter) / sqrt(2)
808    pub gap: MacroDecimal,
809
810    /// Rotation angle of the thermal primitive
811    ///
812    /// The rotation angle is specified by a decimal, in degrees. The primitive
813    /// is rotated around the origin of the macro definition, i.e. the (0, 0)
814    /// point of macro coordinates. The gaps are on the X and Y axes through
815    /// the center when the rotation angle is zero
816    ///
817    /// Note: Rotation is only allowed if the primitive center point coincides
818    /// with the origin of the macro definition.
819    pub angle: MacroDecimal,
820}
821
822impl ThermalPrimitive {
823    pub fn new(inner: MacroDecimal, outer: MacroDecimal, gap: MacroDecimal) -> Self {
824        ThermalPrimitive {
825            center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)),
826            outer_diameter: outer,
827            inner_diameter: inner,
828            gap,
829            angle: MacroDecimal::Value(0.0),
830        }
831    }
832
833    pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self {
834        self.center = center;
835        self
836    }
837
838    pub fn with_angle(mut self, angle: MacroDecimal) -> Self {
839        self.angle = angle;
840        self
841    }
842}
843
844impl<W: Write> PartialGerberCode<W> for ThermalPrimitive {
845    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
846        // Decimal invariants
847        if self.inner_diameter.is_negative() {
848            return Err(GerberError::RangeError(
849                "Inner diameter of a thermal may not be negative".into(),
850            ));
851        }
852        write!(writer, "7,")?;
853        self.center.0.serialize_partial(writer)?;
854        write!(writer, ",")?;
855        self.center.1.serialize_partial(writer)?;
856        write!(writer, ",")?;
857        self.outer_diameter.serialize_partial(writer)?;
858        write!(writer, ",")?;
859        self.inner_diameter.serialize_partial(writer)?;
860        write!(writer, ",")?;
861        self.gap.serialize_partial(writer)?;
862        write!(writer, ",")?;
863        self.angle.serialize_partial(writer)?;
864        write!(writer, "*")?;
865        Ok(())
866    }
867}
868
869#[derive(Debug, Clone, PartialEq, Eq)]
870pub struct VariableDefinition {
871    pub number: u32,
872    pub expression: String,
873}
874
875impl VariableDefinition {
876    pub fn new(number: u32, expr: &str) -> Self {
877        VariableDefinition {
878            number,
879            expression: expr.into(),
880        }
881    }
882}
883
884impl<W: Write> PartialGerberCode<W> for VariableDefinition {
885    fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> {
886        write!(writer, "${}={}*", self.number, self.expression)?;
887        Ok(())
888    }
889}
890
891#[cfg(test)]
892mod test {
893    use std::io::BufWriter;
894
895    use crate::traits::PartialGerberCode;
896
897    use super::MacroDecimal::{Expression, Value, Variable};
898    use super::*;
899
900    macro_rules! assert_partial_code {
901        ($obj:expr, $expected:expr) => {
902            let mut buf = BufWriter::new(Vec::new());
903            $obj.serialize_partial(&mut buf)
904                .expect("Could not generate Gerber code");
905            let bytes = buf.into_inner().unwrap();
906            let code = String::from_utf8(bytes).unwrap();
907            assert_eq!(&code, $expected);
908        };
909    }
910
911    #[test]
912    fn test_circle_primitive_codegen() {
913        let with_angle = CirclePrimitive {
914            exposure: MacroBoolean::Value(true),
915            diameter: Value(1.5),
916            center: (Value(0.), Value(0.)),
917            angle: Some(Value(0.)),
918        };
919        assert_partial_code!(with_angle, "1,1,1.5,0,0,0*");
920        let no_angle = CirclePrimitive {
921            exposure: MacroBoolean::Value(false),
922            diameter: Value(99.9),
923            center: (Value(1.1), Value(2.2)),
924            angle: None,
925        };
926        assert_partial_code!(no_angle, "1,0,99.9,1.1,2.2*");
927    }
928
929    #[test]
930    fn test_vector_line_primitive_codegen() {
931        let line = VectorLinePrimitive {
932            exposure: MacroBoolean::Value(true),
933            width: Value(0.9),
934            start: (Value(0.), Value(0.45)),
935            end: (Value(12.), Value(0.45)),
936            angle: Value(0.),
937        };
938        assert_partial_code!(line, "20,1,0.9,0,0.45,12,0.45,0*");
939    }
940
941    #[test]
942    fn test_center_line_primitive_codegen() {
943        let line = CenterLinePrimitive {
944            exposure: MacroBoolean::Value(true),
945            dimensions: (Value(6.8), Value(1.2)),
946            center: (Value(3.4), Value(0.6)),
947            angle: Value(30.0),
948        };
949        assert_partial_code!(line, "21,1,6.8,1.2,3.4,0.6,30*");
950    }
951
952    #[test]
953    fn test_outline_primitive_codegen() {
954        let line = OutlinePrimitive {
955            exposure: MacroBoolean::Value(true),
956            points: vec![
957                (Value(0.1), Value(0.1)),
958                (Value(0.5), Value(0.1)),
959                (Value(0.5), Value(0.5)),
960                (Value(0.1), Value(0.5)),
961                (Value(0.1), Value(0.1)),
962            ],
963            angle: Value(0.0),
964        };
965        assert_partial_code!(
966            line,
967            "4,1,4,\n0.1,0.1,\n0.5,0.1,\n0.5,0.5,\n0.1,0.5,\n0.1,0.1,\n0*"
968        );
969    }
970
971    #[test]
972    fn test_polygon_primitive_codegen() {
973        let line = PolygonPrimitive {
974            exposure: MacroBoolean::Value(true),
975            vertices: MacroInteger::Value(8),
976            center: (Value(1.5), Value(2.0)),
977            diameter: Value(8.0),
978            angle: Value(0.0),
979        };
980        assert_partial_code!(line, "5,1,8,1.5,2,8,0*");
981    }
982
983    #[test]
984    fn test_moire_primitive_codegen() {
985        let line = MoirePrimitive {
986            center: (Value(0.0), Value(0.0)),
987            diameter: Value(5.0),
988            ring_thickness: Value(0.5),
989            gap: Value(0.5),
990            max_rings: 2,
991            cross_hair_thickness: Value(0.1),
992            cross_hair_length: Value(6.0),
993            angle: Value(0.0),
994        };
995        assert_partial_code!(line, "6,0,0,5,0.5,0.5,2,0.1,6,0*");
996    }
997
998    #[test]
999    fn test_thermal_primitive_codegen() {
1000        let line = ThermalPrimitive {
1001            center: (Value(0.0), Value(0.0)),
1002            outer_diameter: Value(8.0),
1003            inner_diameter: Value(6.5),
1004            gap: Value(1.0),
1005            angle: Value(45.0),
1006        };
1007        assert_partial_code!(line, "7,0,0,8,6.5,1,45*");
1008    }
1009
1010    #[test]
1011    fn test_aperture_macro_codegen() {
1012        let am = ApertureMacro::new("CRAZY")
1013            .add_content(MacroContent::Thermal(ThermalPrimitive {
1014                center: (Value(0.0), Value(0.0)),
1015                outer_diameter: Value(0.08),
1016                inner_diameter: Value(0.055),
1017                gap: Value(0.0125),
1018                angle: Value(45.0),
1019            }))
1020            .add_content(MacroContent::Moire(MoirePrimitive {
1021                center: (Value(0.0), Value(0.0)),
1022                diameter: Value(0.125),
1023                ring_thickness: Value(0.01),
1024                gap: Value(0.01),
1025                max_rings: 3,
1026                cross_hair_thickness: Value(0.003),
1027                cross_hair_length: Value(0.150),
1028                angle: Value(0.0),
1029            }));
1030        assert_partial_code!(
1031            am,
1032            "AMCRAZY*\n7,0,0,0.08,0.055,0.0125,45*\n6,0,0,0.125,0.01,0.01,3,0.003,0.15,0*"
1033        );
1034    }
1035
1036    /// This test should use at least one each of the enum variants in [`MacroDecimal`]
1037    #[test]
1038    fn test_codegen_with_variable() {
1039        let line = VectorLinePrimitive {
1040            exposure: MacroBoolean::Value(true),
1041            width: Variable(0),
1042            start: (Variable(1), 0.45.into()),
1043            end: (Value(12.), Expression("$2x4".to_string())),
1044            angle: Variable(3),
1045        };
1046        assert_partial_code!(line, "20,1,$0,$1,0.45,12,$2x4,$3*");
1047    }
1048
1049    #[test]
1050    fn test_macro_decimal_into() {
1051        let a = Value(1.0);
1052        let b: MacroDecimal = 1.0.into();
1053        assert_eq!(a, b);
1054        let c = Variable(1);
1055        let d = Variable(1);
1056        assert_eq!(c, d);
1057    }
1058
1059    #[test]
1060    fn test_comment_codegen() {
1061        let comment = MacroContent::Comment("hello world".to_string());
1062        assert_partial_code!(comment, "0 hello world*");
1063    }
1064
1065    #[test]
1066    fn test_variable_definition_codegen() {
1067        let var = VariableDefinition {
1068            number: 17,
1069            expression: "$40+2".to_string(),
1070        };
1071        assert_partial_code!(var, "$17=$40+2*");
1072    }
1073
1074    #[test]
1075    fn test_macrocontent_from_into() {
1076        let a = MacroContent::Comment("hello".into());
1077        let b: MacroContent = "hello".to_string().into();
1078        let c: MacroContent = "hello".into();
1079        assert_eq!(a, b);
1080        assert_eq!(b, c);
1081    }
1082
1083    #[test]
1084    fn test_circle_primitive_new() {
1085        let c1 = CirclePrimitive::new(Value(3.0)).centered_at((Value(5.0), Value(0.0)));
1086        let c2 = CirclePrimitive {
1087            exposure: MacroBoolean::Value(true),
1088            diameter: Value(3.0),
1089            center: (Value(5.0), Value(0.0)),
1090            angle: None,
1091        };
1092        assert_eq!(c1, c2);
1093    }
1094
1095    #[test]
1096    fn test_vectorline_primitive_new() {
1097        let vl1 = VectorLinePrimitive::new((Value(0.0), Value(5.3)), (Value(3.9), Value(8.5)))
1098            .with_angle(Value(38.0));
1099        let vl2 = VectorLinePrimitive {
1100            exposure: MacroBoolean::Value(true),
1101            width: Value(0.0),
1102            start: (Value(0.0), Value(5.3)),
1103            end: (Value(3.9), Value(8.5)),
1104            angle: Value(38.0),
1105        };
1106        assert_eq!(vl1, vl2);
1107    }
1108
1109    #[test]
1110    fn test_centerline_primitive_new() {
1111        let cl1 = CenterLinePrimitive::new((Value(3.0), Value(4.5)))
1112            .with_exposure(MacroBoolean::Value(false));
1113        let cl2 = CenterLinePrimitive {
1114            exposure: MacroBoolean::Value(false),
1115            dimensions: (Value(3.0), Value(4.5)),
1116            center: (Value(0.0), Value(0.0)),
1117            angle: Value(0.0),
1118        };
1119        assert_eq!(cl1, cl2);
1120    }
1121
1122    #[test]
1123    fn test_outline_primitive_new() {
1124        let op1 = OutlinePrimitive::new()
1125            .add_point((Value(0.0), Value(0.0)))
1126            .add_point((Value(2.0), Value(2.0)))
1127            .add_point((Value(-2.0), Value(-2.0)))
1128            .add_point((Value(0.0), Value(0.0)));
1129
1130        let pts = vec![
1131            (Value(0.0), Value(0.0)),
1132            (Value(2.0), Value(2.0)),
1133            (Value(-2.0), Value(-2.0)),
1134            (Value(0.0), Value(0.0)),
1135        ];
1136
1137        let op2 = OutlinePrimitive {
1138            exposure: MacroBoolean::Value(true),
1139            points: pts,
1140            angle: Value(0.0),
1141        };
1142        assert_eq!(op1, op2);
1143    }
1144
1145    #[test]
1146    fn test_polygon_primitive_new() {
1147        let pp1 = PolygonPrimitive::new(MacroInteger::Value(5))
1148            .with_angle(Value(98.0))
1149            .with_diameter(Value(5.3))
1150            .centered_at((Value(1.0), Value(1.0)));
1151        let pp2 = PolygonPrimitive {
1152            exposure: MacroBoolean::Value(true),
1153            vertices: MacroInteger::Value(5),
1154            angle: Value(98.0),
1155            diameter: Value(5.3),
1156            center: (Value(1.0), Value(1.0)),
1157        };
1158        assert_eq!(pp1, pp2);
1159    }
1160
1161    #[test]
1162    fn test_moire_primitive_new() {
1163        let mp1 = MoirePrimitive::new()
1164            .with_diameter(Value(3.0))
1165            .with_ring_thickness(Value(0.05))
1166            .with_cross_thickness(Value(0.01))
1167            .with_cross_length(Value(0.5))
1168            .with_rings_max(3);
1169        let mp2 = MoirePrimitive {
1170            center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)),
1171            diameter: MacroDecimal::Value(3.0),
1172            ring_thickness: MacroDecimal::Value(0.05),
1173            gap: MacroDecimal::Value(0.0),
1174            max_rings: 3,
1175            cross_hair_thickness: MacroDecimal::Value(0.01),
1176            cross_hair_length: MacroDecimal::Value(0.5),
1177            angle: MacroDecimal::Value(0.0),
1178        };
1179        assert_eq!(mp1, mp2);
1180    }
1181
1182    #[test]
1183    fn test_thermal_primitive_new() {
1184        let tp1 = ThermalPrimitive::new(Value(1.0), Value(2.0), Value(1.5)).with_angle(Value(87.3));
1185        let tp2 = ThermalPrimitive {
1186            inner_diameter: Value(1.0),
1187            outer_diameter: Value(2.0),
1188            gap: Value(1.5),
1189            angle: Value(87.3),
1190            center: (Value(0.0), Value(0.0)),
1191        };
1192        assert_eq!(tp1, tp2);
1193    }
1194
1195    #[test]
1196    fn test_variabledefinition_new() {
1197        let vd1 = VariableDefinition::new(3, "Test!");
1198        let vd2 = VariableDefinition {
1199            number: 3,
1200            expression: "Test!".into(),
1201        };
1202        assert_eq!(vd1, vd2);
1203    }
1204}