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(©);
253
254 board.write_to_folder(&out_path).unwrap();
255 }
256 }
257}