Skip to main content

lib_gerber_edit/
lib.rs

1//! # lib_gerber_edit
2//!
3//! Manipulation library for RS-274X (Extended Gerber) and Excellon drill files,
4//! built on top of [`gerber_parser`].
5//!
6//! All lengths exposed through the public API are in **millimetres**.
7//!
8//! ## Key types
9//!
10//! | Type | Description |
11//! |------|-------------|
12//! | [`Board`](board::Board) | Collection of mixed Gerber/Excellon layers |
13//! | [`GerberLayerData`](gerber::GerberLayerData) | Single RS-274X layer |
14//! | [`AsciiText`](gerber_ascii::AsciiText) | Renders vector text into a Gerber layer |
15//! | [`LayerType`](layer::LayerType) | Identifies each layer (copper, silkscreen, drill …) |
16//!
17//! ## Traits
18//!
19//! | Trait | Description |
20//! |-------|-------------|
21//! | [`LayerCorners`] | Bounding box (`get_corners`) and size (`get_size`) |
22//! | [`LayerTransform`] | Translate by a [`Pos`] offset |
23//! | [`LayerScale`] | Scale by independent X/Y factors |
24//! | [`LayerRotate`] | Rotate by 90-degree steps about the centroid |
25//! | [`LayerMerge`] | Merge two layers or boards of the same type |
26//! | [`LayerStepAndRepeat`] | Tile a pattern across a grid |
27//!
28//! ## Quick start
29//!
30//! ```no_run
31//! use lib_gerber_edit::board::Board;
32//! use lib_gerber_edit::{LayerTransform, Pos};
33//! use std::path::Path;
34//!
35//! // Load every recognised layer from a directory.
36//! let result = Board::from_folder(Path::new("gerbers/")).unwrap();
37//! for (name, err) in &result.errors {
38//!     eprintln!("Warning: failed to load '{}': {}", name, err);
39//! }
40//! let mut board = result.board;
41//!
42//! // Shift the whole board by (10 mm, 5 mm).
43//! board.transform(&Pos { x: 10.0, y: 5.0 });
44//!
45//! // Write back to a different directory.
46//! board.write_to_folder(Path::new("output/")).unwrap();
47//! ```
48//!
49//! ### Rendering text
50//!
51//! ```no_run
52//! use lib_gerber_edit::gerber_ascii::{AsciiText, HAlign, VAlign};
53//! use lib_gerber_edit::layer::LayerType;
54//!
55//! let fmt = AsciiText::new(3.0)           // 3 mm character height
56//!     .h_align(HAlign::Center)
57//!     .v_align(VAlign::Middle);
58//!
59//! // Same format object — render two different strings.
60//! let rev1 = fmt.build("Rev 1.0", LayerType::SilkScreenTop);
61//! let rev2 = fmt.build("Rev 2.0", LayerType::SilkScreenTop);
62//! ```
63
64pub mod board;
65pub mod error;
66pub mod excellon_format;
67pub mod gerber;
68pub mod gerber_ascii;
69pub mod layer;
70pub mod unit_able;
71
72pub use gerber_parser;
73pub use gerber_parser::gerber_types;
74
75use crate::layer::{LayerData, LayerType};
76use derive_more::Display;
77
78#[cfg(feature = "serde")]
79use serde::{Deserialize, Serialize};
80
81pub type Result<T> = std::result::Result<T, error::Error>;
82
83/// Bounding-box queries.
84///
85/// All coordinates are in millimetres.
86pub trait LayerCorners {
87    /// Returns the axis-aligned bounding box as `(min, max)`.
88    ///
89    /// Tool widths (aperture sizes) are taken into account, so the returned
90    /// rectangle is the true ink boundary of the layer.
91    fn get_corners(&self) -> (Pos, Pos);
92
93    /// Returns the bounding-box dimensions derived from [`get_corners`](Self::get_corners).
94    fn get_size(&self) -> Size {
95        let (min, max) = self.get_corners();
96        let width = max.x - min.x;
97        let height = max.y - min.y;
98        Size { width, height }
99    }
100}
101
102/// Rigid translation.
103pub trait LayerTransform {
104    /// Shifts all coordinates by `transform` (mm).
105    fn transform(&mut self, transform: &Pos);
106}
107
108/// Uniform or anisotropic scaling.
109pub trait LayerScale {
110    /// Multiplies every X coordinate by `x` and every Y coordinate by `y`.
111    fn scale(&mut self, x: f64, y: f64);
112}
113
114/// Layer concatenation.
115pub trait LayerMerge: Sized {
116    /// Appends `other` into `self`.
117    ///
118    /// Aperture and tool IDs are remapped to avoid collisions.
119    fn merge(&mut self, other: &Self);
120
121    /// Converts `other` into `Self` and merges it.
122    ///
123    /// Convenience wrapper around [`merge`](Self::merge) that accepts any type
124    /// that can be converted into `Self` via [`Into`].
125    fn merge_from(&mut self, other: impl Into<Self>) {
126        self.merge(&other.into());
127    }
128}
129
130/// Rotation in 90-degree steps.
131pub trait LayerRotate {
132    /// Rotate by `steps` CW quarter-turns about the layer's own centroid.
133    fn rotate(&mut self, steps: i32);
134    /// Rotate by `steps` CW quarter-turns about the origin, then translate by `offset` (mm).
135    fn rebase(&mut self, steps: i32, offset: &Pos);
136}
137
138pub(crate) fn rotate_90(x: f64, y: f64, steps: i32) -> (f64, f64) {
139    match steps.rem_euclid(4) {
140        1 => (y, -x),
141        2 => (-x, -y),
142        3 => (-y, x),
143        _ => (x, y),
144    }
145}
146
147/// Grid replication.
148pub trait LayerStepAndRepeat {
149    /// Tiles the layer `x_repetitions × y_repetitions` times.
150    ///
151    /// The first copy is the original; subsequent copies are offset by
152    /// multiples of `offset` (mm).
153    fn step_and_repeat(&mut self, x_repetitions: u32, y_repetitions: u32, offset: &Pos);
154}
155
156/// Position in mm
157#[derive(Debug, Clone, PartialEq, Display, Default)]
158#[display("x: {x:.3}, y: {y:.3}")]
159#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
160pub struct Pos {
161    pub x: f64,
162    pub y: f64,
163}
164
165/// Size in mm
166#[derive(Debug, Clone, PartialEq, Display, Default)]
167#[display("width: {width:.3}, height: {height:.3}")]
168#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
169pub struct Size {
170    pub width: f64,
171    pub height: f64,
172}
173
174/// And macro to load a single layer statically
175///
176/// Used for loading static assets
177#[macro_export]
178macro_rules! load_layer_data {
179    ($file:expr $(,)?) => {{
180        let data = include_str!($file);
181        let reader = std::io::BufReader::new(std::io::Cursor::new(data));
182        let ty = LayerType::try_from($file.to_string().rsplitn(2, ".").next().unwrap()).unwrap();
183        LayerData::parse(ty, reader).unwrap()
184    }};
185}
186
187/// And macro to load a board statically
188///
189/// Used for loading static assets
190#[macro_export]
191macro_rules! load_board_data {
192    ($path:expr, $(($name:literal, $ty:expr)),* $(,)?) => {{
193        let mut board = Board::empty();
194         $(
195            board.add_layer(Layer {
196                ty: $ty,
197                name: $name.to_string(),
198                data: load_layer_data!(concat!($path, $name)).1
199            });
200         )*
201        board
202    }};
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::board::Board;
209    use std::fs;
210    use std::path::Path;
211
212    #[test]
213    fn it_works() {
214        let folders = ["mobo"];
215        if Path::new("output").exists() {
216            fs::remove_dir_all("output").unwrap();
217        }
218        for folder in folders {
219            let in_path = Path::new("test").join(folder);
220            let out_path = Path::new("output").join(folder);
221            fs::create_dir_all(&out_path).unwrap();
222            println!("Processing folder: {:?}", in_path);
223            let result = Board::from_folder(&in_path).unwrap();
224            for (name, err) in &result.errors {
225                println!("Warning: failed to load '{}': {}", name, err);
226            }
227            let mut board = result.board;
228
229            let (min, max) = board.get_corners();
230            println!(
231                "Transformed Corners: ({}, {}) - ({}, {})",
232                min.x, min.y, max.x, max.y
233            );
234
235            let size = board.get_size();
236
237            board.transform(&Pos {
238                x: 100.0,
239                y: -100.0,
240            });
241
242            board.transform(&Pos {
243                x: -100.0,
244                y: 100.0,
245            });
246
247            let mut copy = board.clone();
248            copy.transform(&Pos {
249                y: size.height + 5.0,
250                x: 0.0,
251            });
252            board.merge(&copy);
253
254            board.write_to_folder(&out_path).unwrap();
255        }
256    }
257}