From ff45b5ef03a1ee28fbb0beaa381e81296c255858 Mon Sep 17 00:00:00 2001 From: Hlars Date: Fri, 23 Aug 2024 12:02:52 +0200 Subject: [PATCH] added local fork of gerber-types-rs --- gerber-types-rs/.gitignore | 3 + gerber-types-rs/CHANGELOG.md | 44 + gerber-types-rs/Cargo.toml | 24 + gerber-types-rs/LICENSE-APACHE | 176 ++++ gerber-types-rs/LICENSE-MIT | 19 + gerber-types-rs/README.md | 47 ++ gerber-types-rs/RELEASING.md | 24 + gerber-types-rs/src/attributes.rs | 410 ++++++++++ gerber-types-rs/src/codegen.rs | 106 +++ gerber-types-rs/src/coordinates.rs | 479 +++++++++++ gerber-types-rs/src/errors.rs | 39 + gerber-types-rs/src/extended_codes.rs | 315 +++++++ gerber-types-rs/src/function_codes.rs | 143 ++++ gerber-types-rs/src/lib.rs | 289 +++++++ gerber-types-rs/src/macros.rs | 1089 +++++++++++++++++++++++++ gerber-types-rs/src/test_macros.rs | 25 + gerber-types-rs/src/traits.rs | 19 + gerber-types-rs/src/types.rs | 188 +++++ 18 files changed, 3439 insertions(+) create mode 100644 gerber-types-rs/.gitignore create mode 100644 gerber-types-rs/CHANGELOG.md create mode 100644 gerber-types-rs/Cargo.toml create mode 100644 gerber-types-rs/LICENSE-APACHE create mode 100644 gerber-types-rs/LICENSE-MIT create mode 100644 gerber-types-rs/README.md create mode 100644 gerber-types-rs/RELEASING.md create mode 100644 gerber-types-rs/src/attributes.rs create mode 100644 gerber-types-rs/src/codegen.rs create mode 100644 gerber-types-rs/src/coordinates.rs create mode 100644 gerber-types-rs/src/errors.rs create mode 100644 gerber-types-rs/src/extended_codes.rs create mode 100644 gerber-types-rs/src/function_codes.rs create mode 100644 gerber-types-rs/src/lib.rs create mode 100644 gerber-types-rs/src/macros.rs create mode 100644 gerber-types-rs/src/test_macros.rs create mode 100644 gerber-types-rs/src/traits.rs create mode 100644 gerber-types-rs/src/types.rs diff --git a/gerber-types-rs/.gitignore b/gerber-types-rs/.gitignore new file mode 100644 index 0000000..d4f917d --- /dev/null +++ b/gerber-types-rs/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +*.swp diff --git a/gerber-types-rs/CHANGELOG.md b/gerber-types-rs/CHANGELOG.md new file mode 100644 index 0000000..bfd680f --- /dev/null +++ b/gerber-types-rs/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +This project follows semantic versioning. + +Possible log types: + +- `[added]` for new features. +- `[changed]` for changes in existing functionality. +- `[deprecated]` for once-stable features removed in upcoming releases. +- `[removed]` for deprecated features removed in this release. +- `[fixed]` for any bug fixes. +- `[security]` to invite users to upgrade in case of vulnerabilities. + + +### v0.3.0 (2022-07-05) + +- [fixed] Fix whitespace in G04 comment serialization (#33) +- [changed] Updated dependencies +- [changed] A fixed MSRV was dropped + +Thanks @NemoAndrea for contributions! + +### v0.2.0 (2021-01-06) + +This release requires at least Rust 1.31 (2018 edition). + +- [added] Implement constructors for `Circle` and `Rectangular` +- [added] Derive Clone for all structs and enums (#16) +- [added] Derive PartialEq and Eq where possible +- [added] Implement `From` and `From` for Command +- [added] Impl `From<>` for Command, FunctionCode and ExtendedCode +- [added] New builder-style constructors (#22) +- [added] Support for more FileFunction and FileAttribute variants (#26, #30) +- [changed] Derive Copy for some trivial enums +- [changed] Create new internal `PartialGerberCode` trait (#18) +- [changed] Split up code into more modules +- [changed] Upgraded all dependencies +- [changed] Require Rust 1.31+ + +Thanks @connorkuehl and @twitchyliquid64 for contributions! + +### v0.1.1 (2017-06-10) + +- First crates.io release diff --git a/gerber-types-rs/Cargo.toml b/gerber-types-rs/Cargo.toml new file mode 100644 index 0000000..12dd404 --- /dev/null +++ b/gerber-types-rs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "gerber-types" +version = "0.3.0" +documentation = "https://docs.rs/gerber-types/" +repository = "https://github.com/dbrgn/gerber-types-rs" +license = "MIT OR Apache-2.0" +authors = ["Danilo Bargen "] +description = "Types and code generation for Gerber files (RS-274X)." +readme = "README.md" +keywords = ["gerber", "pcb", "rs274x", "cad", "cam"] +include = [ + "**/*.rs", + "Cargo.toml", + "README.md", + "LICENSE-*", +] +edition = "2018" + +[dependencies] +chrono = "0.4" +conv = "0.3" +num-rational = "0.4" +thiserror = "1" +uuid = "1" diff --git a/gerber-types-rs/LICENSE-APACHE b/gerber-types-rs/LICENSE-APACHE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/gerber-types-rs/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/gerber-types-rs/LICENSE-MIT b/gerber-types-rs/LICENSE-MIT new file mode 100644 index 0000000..05913f1 --- /dev/null +++ b/gerber-types-rs/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2016-2021 Danilo Bargen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/gerber-types-rs/README.md b/gerber-types-rs/README.md new file mode 100644 index 0000000..e69aa37 --- /dev/null +++ b/gerber-types-rs/README.md @@ -0,0 +1,47 @@ +# Rust Gerber Library + +[![Build status][build-status-badge]][build-status] +[![Crates.io][crates-io-badge]][crates-io] + +- [Docs (released)](https://docs.rs/gerber-types/) + +This crate implements the basic building blocks of Gerber X2 (compatible with +Gerber RS-274X) code. It focusses on the low level types (to be used like an +AST) and code generation and does not do any semantic checking. + +For example, you can use an aperture without defining it. This will generate +syntactically valid but semantially invalid Gerber code, but this module won't +complain. + +The plan is to write a high-level wrapper library on top of this. Early drafts +[are in progress](https://github.com/dbrgn/gerber-rs) but the design isn't +fixed yet. + +Current Gerber X2 spec: https://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf + +## Example + +You can find an example in the [`examples` +directory](https://github.com/dbrgn/gerber-types-rs/blob/main/examples/polarities-apertures.rs). +It's still quite verbose, the goal is to make the API a bit more ergonomic in +the future. (This library has a low-level focus though, so it will never get a +high-level API. That is the task of other libraries.) + +To generate Gerber code for that example: + + $ cargo run --example polarities-apertures + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + + +[build-status]: https://github.com/dbrgn/gerber-types-rs/actions?query=workflow%3ACI +[build-status-badge]: https://img.shields.io/github/workflow/status/dbrgn/gerber-types-rs/CI/main +[crates-io]: https://crates.io/crates/gerber-types +[crates-io-badge]: https://img.shields.io/crates/v/gerber-types.svg diff --git a/gerber-types-rs/RELEASING.md b/gerber-types-rs/RELEASING.md new file mode 100644 index 0000000..cb272ef --- /dev/null +++ b/gerber-types-rs/RELEASING.md @@ -0,0 +1,24 @@ +# Releasing + +Set variables: + + $ export VERSION=X.Y.Z + $ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA + +Update version numbers: + + $ vim -p Cargo.toml + +Update changelog: + + $ vim CHANGELOG.md + +Commit & tag: + + $ git commit -S${GPG_KEY} -m "Release v${VERSION}" + $ git tag -s -u ${GPG_KEY} v${VERSION} -m "Version ${VERSION}" + +Publish: + + $ cargo publish + $ git push && git push --tags diff --git a/gerber-types-rs/src/attributes.rs b/gerber-types-rs/src/attributes.rs new file mode 100644 index 0000000..9b4a949 --- /dev/null +++ b/gerber-types-rs/src/attributes.rs @@ -0,0 +1,410 @@ +//! Attributes. + +use std::io::Write; + +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +use crate::errors::GerberResult; +use crate::traits::PartialGerberCode; + +// FileAttribute + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FileAttribute { + Part(Part), + FileFunction(FileFunction), + FilePolarity(FilePolarity), + GenerationSoftware(GenerationSoftware), + CreationDate(DateTime), + ProjectId { + id: String, + guid: Uuid, + revision: String, + }, + Md5(String), + UserDefined { + name: String, + value: Vec, + }, +} + +impl PartialGerberCode for FileAttribute { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + FileAttribute::Part(ref part) => { + write!(writer, "Part,")?; + part.serialize_partial(writer)?; + } + FileAttribute::FileFunction(ref function) => { + write!(writer, "FileFunction,")?; + match function { + FileFunction::Copper { + ref layer, + ref pos, + ref copper_type, + } => { + write!(writer, "Copper,L{},", layer)?; + pos.serialize_partial(writer)?; + if let Some(ref t) = *copper_type { + write!(writer, ",")?; + t.serialize_partial(writer)?; + } + } + FileFunction::Profile(ref plating) => { + write!(writer, "Profile,")?; + plating.serialize_partial(writer)?; + } + FileFunction::Soldermask { ref pos, ref index } => { + write!(writer, "Soldermask,")?; + pos.serialize_partial(writer)?; + if let Some(ref i) = index { + write!(writer, ",{}", *i)?; + } + } + FileFunction::Legend { ref pos, ref index } => { + write!(writer, "Legend,")?; + pos.serialize_partial(writer)?; + if let Some(ref i) = index { + write!(writer, ",{}", *i)?; + } + } + _ => unimplemented!(), + } + } + FileAttribute::GenerationSoftware(ref gs) => { + write!(writer, "GenerationSoftware,")?; + gs.serialize_partial(writer)?; + } + FileAttribute::FilePolarity(ref p) => { + write!(writer, "FilePolarity,")?; + p.serialize_partial(writer)?; + } + FileAttribute::Md5(ref hash) => write!(writer, "MD5,{}", hash)?, + _ => unimplemented!(), + }; + Ok(()) + } +} + +// ApertureAttribute + +#[derive(Debug, Clone, PartialEq)] +pub enum ApertureAttribute { + ApertureFunction(ApertureFunction), + DrillTolerance { plus: f64, minus: f64 }, +} + +// Part + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Part { + /// Single PCB + Single, + /// A.k.a. customer panel, assembly panel, shipping panel, biscuit + Array, + /// A.k.a. working panel, production panel + FabricationPanel, + /// A test coupon + Coupon, + /// None of the above + Other(String), +} + +impl PartialGerberCode for Part { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Part::Single => write!(writer, "Single")?, + Part::Array => write!(writer, "Array")?, + Part::FabricationPanel => write!(writer, "FabricationPanel")?, + Part::Coupon => write!(writer, "Coupon")?, + Part::Other(ref description) => write!(writer, "Other,{}", description)?, + }; + Ok(()) + } +} + +// Position + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Position { + Top, + Bottom, +} + +impl PartialGerberCode for Position { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Position::Top => write!(writer, "Top")?, + Position::Bottom => write!(writer, "Bot")?, + }; + Ok(()) + } +} + +// ExtendedPosition + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExtendedPosition { + Top, + Inner, + Bottom, +} + +impl PartialGerberCode for ExtendedPosition { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + ExtendedPosition::Top => write!(writer, "Top")?, + ExtendedPosition::Inner => write!(writer, "Inr")?, + ExtendedPosition::Bottom => write!(writer, "Bot")?, + }; + Ok(()) + } +} + +// CopperType + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CopperType { + Plane, + Signal, + Mixed, + Hatched, +} + +impl PartialGerberCode for CopperType { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + CopperType::Plane => write!(writer, "Plane")?, + CopperType::Signal => write!(writer, "Signal")?, + CopperType::Mixed => write!(writer, "Mixed")?, + CopperType::Hatched => write!(writer, "Hatched")?, + }; + Ok(()) + } +} + +// Drill + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Drill { + ThroughHole, + Blind, + Buried, +} + +// DrillRouteType + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DrillRouteType { + Drill, + Route, + Mixed, +} + +// Profile + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Profile { + Plated, + NonPlated, +} + +impl PartialGerberCode for Profile { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Profile::Plated => write!(writer, "P")?, + Profile::NonPlated => write!(writer, "NP")?, + }; + Ok(()) + } +} + +// FileFunction + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FileFunction { + Copper { + layer: i32, + pos: ExtendedPosition, + copper_type: Option, + }, + Soldermask { + pos: Position, + index: Option, + }, + Legend { + pos: Position, + index: Option, + }, + Goldmask { + pos: Position, + index: Option, + }, + Silvermask { + pos: Position, + index: Option, + }, + Tinmask { + pos: Position, + index: Option, + }, + Carbonmask { + pos: Position, + index: Option, + }, + Peelablesoldermask { + pos: Position, + index: Option, + }, + Glue { + pos: Position, + index: Option, + }, + Viatenting(Position), + Viafill, + Heatsink(Position), + Paste(Position), + KeepOut(Position), + Pads(Position), + Scoring(Position), + Plated { + from_layer: i32, + to_layer: i32, + drill: Drill, + label: Option, + }, + NonPlated { + from_layer: i32, + to_layer: i32, + drill: Drill, + label: Option, + }, + Profile(Profile), + Drillmap, + FabricationDrawing, + ArrayDrawing, + AssemblyDrawing(Position), + Drawing(String), + Other(String), +} + +// FilePolarity + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FilePolarity { + Positive, + Negative, +} + +impl PartialGerberCode for FilePolarity { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + FilePolarity::Positive => write!(writer, "Positive")?, + FilePolarity::Negative => write!(writer, "Negative")?, + }; + Ok(()) + } +} + +// GenerationSoftware + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GenerationSoftware { + pub vendor: String, + pub application: String, + pub version: Option, +} + +impl GenerationSoftware { + pub fn new>(vendor: S, application: S, version: Option) -> Self { + GenerationSoftware { + vendor: vendor.into(), + application: application.into(), + version: version.map(|s| s.into()), + } + } +} + +impl PartialGerberCode for GenerationSoftware { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match self.version { + Some(ref v) => write!(writer, "{},{},{}", self.vendor, self.application, v)?, + None => write!(writer, "{},{}", self.vendor, self.application)?, + }; + Ok(()) + } +} + +// ApertureFunction + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ApertureFunction { + // Only valid for layers with file function plated or non-plated + ViaDrill, + BackDrill, + ComponentDrill { + press_fit: Option, // TODO is this bool? + }, + CastellatedDrill, + MechanicalDrill { + function: Option, + }, + Slot, + CutOut, + Cavity, + OtherDrill(String), + + // Only valid for layers with file function copper + ComponentPad { + press_fit: Option, // TODO is this bool? + }, + SmdPad(SmdPadType), + BgaPad(SmdPadType), + ConnectorPad, + HeatsinkPad, + ViaPad, + TestPad, + CastellatedPad, + FiducialPad(FiducialScope), + ThermalReliefPad, + WasherPad, + AntiPad, + OtherPad(String), + Conductor, + NonConductor, + CopperBalancing, + Border, + OtherCopper(String), + + // All layers + Profile, + NonMaterial, + Material, + Other(String), +} + +// DrillFunction + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DrillFunction { + BreakOut, + Tooling, + Other, +} + +// SmdPadType + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SmdPadType { + CopperDefined, + SoldermaskDefined, +} + +// FiducialScope + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FiducialScope { + Global, + Local, +} diff --git a/gerber-types-rs/src/codegen.rs b/gerber-types-rs/src/codegen.rs new file mode 100644 index 0000000..5914d1e --- /dev/null +++ b/gerber-types-rs/src/codegen.rs @@ -0,0 +1,106 @@ +//! Generic code generation, e.g. implementations of `PartialGerberCode` for +//! bool or Vec. + +use std::io::Write; + +use crate::errors::GerberResult; +use crate::traits::{GerberCode, PartialGerberCode}; +use crate::types::*; + +/// Implement `PartialGerberCode` for booleans +impl PartialGerberCode for bool { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + if *self { + write!(writer, "1")?; + } else { + write!(writer, "0")?; + }; + Ok(()) + } +} + +/// Implement `GerberCode` for Vectors of types that are `GerberCode`. +impl> GerberCode for Vec { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + for item in self.iter() { + item.serialize(writer)?; + } + Ok(()) + } +} + +/// Implement `PartialGerberCode` for `Option` +impl, W: Write> PartialGerberCode for Option { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + if let Some(ref val) = *self { + val.serialize_partial(writer)?; + } + Ok(()) + } +} + +impl GerberCode for Command { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Command::FunctionCode(ref code) => code.serialize(writer)?, + Command::ExtendedCode(ref code) => code.serialize(writer)?, + }; + Ok(()) + } +} + +impl GerberCode for FunctionCode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + FunctionCode::DCode(ref code) => code.serialize(writer)?, + FunctionCode::GCode(ref code) => code.serialize(writer)?, + FunctionCode::MCode(ref code) => code.serialize(writer)?, + }; + Ok(()) + } +} + +impl GerberCode for ExtendedCode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + ExtendedCode::CoordinateFormat(ref cf) => { + writeln!(writer, "%FSLAX{0}{1}Y{0}{1}*%", cf.integer, cf.decimal)?; + } + ExtendedCode::Unit(ref unit) => { + write!(writer, "%MO")?; + unit.serialize_partial(writer)?; + writeln!(writer, "*%")?; + } + ExtendedCode::ApertureDefinition(ref def) => { + write!(writer, "%ADD")?; + def.serialize_partial(writer)?; + writeln!(writer, "*%")?; + } + ExtendedCode::ApertureMacro(ref am) => { + write!(writer, "%")?; + am.serialize_partial(writer)?; + writeln!(writer, "%")?; + } + ExtendedCode::LoadPolarity(ref polarity) => { + write!(writer, "%LP")?; + polarity.serialize_partial(writer)?; + writeln!(writer, "*%")?; + } + ExtendedCode::StepAndRepeat(ref sar) => { + write!(writer, "%SR")?; + sar.serialize_partial(writer)?; + writeln!(writer, "*%")?; + } + ExtendedCode::FileAttribute(ref attr) => { + write!(writer, "%TF.")?; + attr.serialize_partial(writer)?; + writeln!(writer, "*%")?; + } + ExtendedCode::DeleteAttribute(ref attr) => { + writeln!(writer, "%TD{}*%", attr)?; + } + _ => unimplemented!(), + }; + Ok(()) + } +} diff --git a/gerber-types-rs/src/coordinates.rs b/gerber-types-rs/src/coordinates.rs new file mode 100644 index 0000000..03a5306 --- /dev/null +++ b/gerber-types-rs/src/coordinates.rs @@ -0,0 +1,479 @@ +//! Types for Gerber code generation related to coordinates. + +use std::convert::{From, Into}; +use std::i64; +use std::io::Write; +use std::num::FpCategory; + +use conv::TryFrom; +use num_rational::Ratio; + +use crate::errors::{GerberError, GerberResult}; +use crate::traits::PartialGerberCode; + +// Helper macros + +/// Automatically implement `PartialGerberCode` trait for struct types +/// that are based on `x` and `y` attributes. +macro_rules! impl_xy_partial_gerbercode { + ($class:ty, $x:expr, $y: expr) => { + impl PartialGerberCode for $class { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + if let Some(x) = self.x { + write!(writer, "{}{}", $x, x.gerber(&self.format)?)?; + } + if let Some(y) = self.y { + write!(writer, "{}{}", $y, y.gerber(&self.format)?)?; + } + Ok(()) + } + } + }; +} + +// Types + +/// The coordinate format specifies the number of integer and decimal places in +/// a coordinate number. For example, the `24` format specifies 2 integer and 4 +/// decimal places. The number of decimal places must be 4, 5 or 6. The number +/// of integer places must be not more than 6. Thus the longest representable +/// coordinate number is `nnnnnn.nnnnnn`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct CoordinateFormat { + pub integer: u8, + pub decimal: u8, +} + +impl CoordinateFormat { + pub fn new(integer: u8, decimal: u8) -> Self { + CoordinateFormat { integer, decimal } + } +} + +/// Coordinate numbers are integers conforming to the rules set by the FS +/// command. +/// +/// Coordinate numbers are integers. Explicit decimal points are not allowed. +/// +/// A coordinate number must have at least one character. Zero therefore must +/// be encoded as `0`. +/// +/// The value is stored as a 64 bit integer with 6 decimal places. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct CoordinateNumber { + nano: i64, +} + +impl CoordinateNumber { + pub fn new(nano: i64) -> Self { + CoordinateNumber { nano } + } +} + +const DECIMAL_PLACES_CHARS: u8 = 6; +const DECIMAL_PLACES_FACTOR: i64 = 1_000_000; + +impl TryFrom for CoordinateNumber { + type Err = GerberError; + fn try_from(val: f64) -> Result { + match val.classify() { + FpCategory::Nan => Err(GerberError::ConversionError("Value is NaN".into())), + FpCategory::Infinite => Err(GerberError::ConversionError("Value is infinite".into())), + FpCategory::Zero | FpCategory::Subnormal => Ok(CoordinateNumber { nano: 0 }), + FpCategory::Normal => { + let multiplied = val * DECIMAL_PLACES_FACTOR as f64; + if (multiplied > i64::MAX as f64) || (multiplied < i64::MIN as f64) { + Err(GerberError::ConversionError( + "Value is out of bounds".into(), + )) + } else { + Ok(CoordinateNumber { + nano: multiplied as i64, + }) + } + } + } + } +} + +impl Into for CoordinateNumber { + fn into(self) -> f64 { + (self.nano as f64) / DECIMAL_PLACES_FACTOR as f64 + } +} + +macro_rules! impl_from_integer { + ($class:ty) => { + impl From<$class> for CoordinateNumber { + fn from(val: $class) -> Self { + CoordinateNumber { + nano: val as i64 * DECIMAL_PLACES_FACTOR, + } + } + } + }; +} + +// These are the types we can safely multiply with DECIMAL_PLACES_FACTOR +// without the risk of an overflow. +impl_from_integer!(i8); +impl_from_integer!(i16); +impl_from_integer!(i32); +impl_from_integer!(u8); +impl_from_integer!(u16); + +impl CoordinateNumber { + pub fn gerber(&self, format: &CoordinateFormat) -> Result { + if format.decimal > DECIMAL_PLACES_CHARS { + return Err(GerberError::CoordinateFormatError( + "Invalid precision: Too high!".into(), + )); + } + if self.nano.abs() >= 10_i64.pow((format.integer + DECIMAL_PLACES_CHARS) as u32) { + return Err(GerberError::CoordinateFormatError( + "Number is too large for chosen format!".into(), + )); + } + + let divisor: i64 = 10_i64.pow((DECIMAL_PLACES_CHARS - format.decimal) as u32); + let number: i64 = Ratio::new(self.nano, divisor).round().to_integer(); + Ok(number.to_string()) + } +} + +/// Coordinates are part of an operation. +/// +/// Coordinates are modal. If an X is omitted, the X coordinate of the +/// current point is used. Similar for Y. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Coordinates { + pub x: Option, + pub y: Option, + pub format: CoordinateFormat, +} + +impl Coordinates { + pub fn new(x: T, y: U, format: CoordinateFormat) -> Self + where + T: Into, + U: Into, + { + Coordinates { + x: Some(x.into()), + y: Some(y.into()), + format, + } + } + + pub fn at_x(x: T, format: CoordinateFormat) -> Self + where + T: Into, + { + Coordinates { + x: Some(x.into()), + y: None, + format, + } + } + + pub fn at_y(y: T, format: CoordinateFormat) -> Self + where + T: Into, + { + Coordinates { + x: None, + y: Some(y.into()), + format, + } + } +} + +impl_xy_partial_gerbercode!(Coordinates, "X", "Y"); + +/// Coordinate offsets can be used for interpolate operations in circular +/// interpolation mode. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CoordinateOffset { + pub x: Option, + pub y: Option, + pub format: CoordinateFormat, +} + +impl CoordinateOffset { + pub fn new(x: T, y: U, format: CoordinateFormat) -> Self + where + T: Into, + U: Into, + { + CoordinateOffset { + x: Some(x.into()), + y: Some(y.into()), + format, + } + } + + pub fn at_x(x: T, format: CoordinateFormat) -> Self + where + T: Into, + { + CoordinateOffset { + x: Some(x.into()), + y: None, + format, + } + } + + pub fn at_y(y: T, format: CoordinateFormat) -> Self + where + T: Into, + { + CoordinateOffset { + x: None, + y: Some(y.into()), + format, + } + } +} + +impl_xy_partial_gerbercode!(CoordinateOffset, "I", "J"); + +#[cfg(test)] +mod test { + use super::*; + + use std::f64; + use std::io::BufWriter; + + use conv::TryFrom; + + use crate::traits::PartialGerberCode; + + #[test] + /// Test integer to coordinate number conversion + fn test_from_i8() { + let a = CoordinateNumber { nano: 13000000 }; + let b = CoordinateNumber::from(13i8); + assert_eq!(a, b); + + let c = CoordinateNumber { nano: -99000000 }; + let d = CoordinateNumber::from(-99i8); + assert_eq!(c, d); + } + + #[test] + /// Test integer to coordinate number conversion + fn test_from_i32() { + let a = CoordinateNumber { nano: 13000000 }; + let b = CoordinateNumber::from(13); + assert_eq!(a, b); + + let c = CoordinateNumber { nano: -998000000 }; + let d = CoordinateNumber::from(-998); + assert_eq!(c, d); + } + + #[test] + /// Test float to coordinate number conversion + fn test_try_from_f64_success() { + let a = CoordinateNumber { nano: 1375000i64 }; + let b = CoordinateNumber::try_from(1.375f64).unwrap(); + assert_eq!(a, b); + + let c = CoordinateNumber { + nano: 123456888888i64, + }; + let d = CoordinateNumber::try_from(123456.888888f64).unwrap(); + assert_eq!(c, d); + + let e = CoordinateNumber { nano: 0i64 }; + let f = CoordinateNumber::try_from(0f64).unwrap(); + assert_eq!(e, f); + + let g = CoordinateNumber { nano: -12345678 }; + let h = CoordinateNumber::try_from(-12.345678).unwrap(); + assert_eq!(g, h); + } + + #[test] + /// Test failing float to coordinate number conversion + fn test_try_from_f64_fail() { + let cn1 = CoordinateNumber::try_from(f64::NAN); + assert!(cn1.is_err()); + + let cn2 = CoordinateNumber::try_from(f64::INFINITY); + assert!(cn2.is_err()); + + let cn3 = CoordinateNumber::try_from(f64::MAX - 1.0); + assert!(cn3.is_err()); + + let cn4 = CoordinateNumber::try_from(f64::MIN + 1.0); + assert!(cn4.is_err()); + } + + #[test] + /// Test coordinate number to float conversion + fn test_into_f64() { + let a: f64 = CoordinateNumber { nano: 1375000i64 }.into(); + let b = 1.375f64; + assert_eq!(a, b); + + let c: f64 = CoordinateNumber { + nano: 123456888888i64, + } + .into(); + let d = 123456.888888f64; + assert_eq!(c, d); + + let e: f64 = CoordinateNumber { nano: 0i64 }.into(); + let f = 0f64; + assert_eq!(e, f); + } + + #[test] + /// Test the coordinate number constructor creates correct + /// coordinate numbers. + fn test_coordinate_number_new() { + let nano = 5; + let cn1 = CoordinateNumber::new(nano); + assert_eq!(cn1.nano, nano); + } + + #[test] + /// Test coordinate number to string conversion when it's 0 + fn test_formatted_zero() { + let cf1 = CoordinateFormat::new(6, 6); + let cf2 = CoordinateFormat::new(2, 4); + + let a = CoordinateNumber { nano: 0 }.gerber(&cf1).unwrap(); + let b = CoordinateNumber { nano: 0 }.gerber(&cf2).unwrap(); + assert_eq!(a, "0".to_string()); + assert_eq!(b, "0".to_string()); + } + + #[test] + /// Test coordinate number to string conversion when the decimal part is 0 + fn test_formatted_decimal_zero() { + let cf1 = CoordinateFormat::new(6, 6); + let cf2 = CoordinateFormat::new(2, 4); + + let a = CoordinateNumber { nano: 10000000 }.gerber(&cf1).unwrap(); + let b = CoordinateNumber { nano: 20000000 }.gerber(&cf2).unwrap(); + assert_eq!(a, "10000000".to_string()); + assert_eq!(b, "200000".to_string()); + } + + #[test] + /// Test coordinate number to string conversion + fn test_formatted_65() { + let cf = CoordinateFormat::new(6, 5); + let d = CoordinateNumber { nano: 123456789012 }.gerber(&cf).unwrap(); + assert_eq!(d, "12345678901".to_string()); + } + + #[test] + /// Test coordinate number to string conversion + fn test_formatted_54() { + let cf = CoordinateFormat::new(5, 4); + let d = CoordinateNumber { nano: 12345678901 }.gerber(&cf).unwrap(); + assert_eq!(d, "123456789".to_string()); + } + + #[test] + /// Test coordinate number to string conversion failure + fn test_formatted_number_too_large() { + let cf = CoordinateFormat::new(4, 5); + let d = CoordinateNumber { nano: 12345000000 }.gerber(&cf); + assert!(d.is_err()); + } + + #[test] + /// Test coordinate number to string conversion failure + fn test_formatted_negative_number_too_large() { + let cf = CoordinateFormat::new(4, 5); + let d = CoordinateNumber { nano: -12345000000 }.gerber(&cf); + assert!(d.is_err()); + } + + #[test] + /// Test coordinate number to string conversion (rounding of decimal part) + fn test_formatted_44_rounding() { + let cf = CoordinateFormat::new(4, 4); + let d = CoordinateNumber { nano: 1234432199 }.gerber(&cf).unwrap(); + assert_eq!(d, "12344322".to_string()); + } + + #[test] + /// Test negative coordinate number to string conversion + fn test_formatted_negative_rounding() { + let cf = CoordinateFormat::new(6, 4); + let d = CoordinateNumber { + nano: -123456789099, + } + .gerber(&cf) + .unwrap(); + assert_eq!(d, "-1234567891".to_string()); + } + + #[test] + fn test_coordinates_into() { + let cf = CoordinateFormat::new(2, 4); + let c1 = Coordinates::new(CoordinateNumber::from(1), CoordinateNumber::from(2), cf); + let c2 = Coordinates::new(1, 2, cf); + assert_eq!(c1, c2); + } + + #[test] + fn test_coordinates_into_mixed() { + let cf = CoordinateFormat::new(2, 4); + let c1 = Coordinates::new(CoordinateNumber::from(1), 2, cf); + let c2 = Coordinates::new(1, 2, cf); + assert_eq!(c1, c2); + } + + #[test] + fn test_coordinates() { + macro_rules! assert_coords { + ($coords:expr, $result:expr) => {{ + assert_partial_code!($coords, $result); + }}; + } + let cf44 = CoordinateFormat::new(4, 4); + let cf46 = CoordinateFormat::new(4, 6); + assert_coords!(Coordinates::new(10, 20, cf44), "X100000Y200000"); + assert_coords!( + Coordinates { + x: None, + y: None, + format: cf44 + }, + "" + ); // TODO should we catch this? + assert_coords!(Coordinates::at_x(10, cf44), "X100000"); + assert_coords!(Coordinates::at_y(20, cf46), "Y20000000"); + assert_coords!(Coordinates::new(0, -400, cf44), "X0Y-4000000"); + } + + #[test] + fn test_offset() { + macro_rules! assert_coords { + ($coords:expr, $result:expr) => {{ + assert_partial_code!($coords, $result); + }}; + } + let cf44 = CoordinateFormat::new(4, 4); + let cf55 = CoordinateFormat::new(5, 5); + let cf66 = CoordinateFormat::new(6, 6); + assert_coords!(CoordinateOffset::new(10, 20, cf44), "I100000J200000"); + assert_coords!( + CoordinateOffset { + x: None, + y: None, + format: cf44 + }, + "" + ); // TODO should we catch this? + assert_coords!(CoordinateOffset::at_x(10, cf66), "I10000000"); + assert_coords!(CoordinateOffset::at_y(20, cf55), "J2000000"); + assert_coords!(CoordinateOffset::new(0, -400, cf44), "I0J-4000000"); + } +} diff --git a/gerber-types-rs/src/errors.rs b/gerber-types-rs/src/errors.rs new file mode 100644 index 0000000..15f41fb --- /dev/null +++ b/gerber-types-rs/src/errors.rs @@ -0,0 +1,39 @@ +//! Error types used in the gerber-types library. + +use std::io::Error as IoError; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum GerberError { + #[error("Conversion between two types failed: {0}")] + ConversionError(String), + + #[error("Bad coordinate format: {0}")] + CoordinateFormatError(String), + + #[error("A value is out of range: {0}")] + RangeError(String), + + #[error("Required data is missing: {0}")] + MissingDataError(String), + + #[error("I/O error during code generation")] + IoError(#[from] IoError), +} + +pub type GerberResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_msg() { + let err = GerberError::CoordinateFormatError("Something went wrong".into()); + assert_eq!( + err.to_string(), + "Bad coordinate format: Something went wrong" + ); + } +} diff --git a/gerber-types-rs/src/extended_codes.rs b/gerber-types-rs/src/extended_codes.rs new file mode 100644 index 0000000..72cbdea --- /dev/null +++ b/gerber-types-rs/src/extended_codes.rs @@ -0,0 +1,315 @@ +//! Extended code types. + +use std::io::Write; + +use crate::errors::GerberResult; +use crate::traits::PartialGerberCode; + +// Unit + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Unit { + Inches, + Millimeters, +} + +impl PartialGerberCode for Unit { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Unit::Millimeters => write!(writer, "MM")?, + Unit::Inches => write!(writer, "IN")?, + }; + Ok(()) + } +} + +// ApertureDefinition + +#[derive(Debug, Clone, PartialEq)] +pub struct ApertureDefinition { + pub code: i32, + pub aperture: Aperture, +} + +impl ApertureDefinition { + pub fn new(code: i32, aperture: Aperture) -> Self { + ApertureDefinition { code, aperture } + } +} + +impl PartialGerberCode for ApertureDefinition { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + write!(writer, "{}", self.code)?; + self.aperture.serialize_partial(writer)?; + Ok(()) + } +} + +// Aperture + +#[derive(Debug, Clone, PartialEq)] +pub enum Aperture { + Circle(Circle), + Rectangle(Rectangular), + Obround(Rectangular), + Polygon(Polygon), + Other(String), +} + +impl PartialGerberCode for Aperture { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Aperture::Circle(ref circle) => { + write!(writer, "C,")?; + circle.serialize_partial(writer)?; + } + Aperture::Rectangle(ref rectangular) => { + write!(writer, "R,")?; + rectangular.serialize_partial(writer)?; + } + Aperture::Obround(ref rectangular) => { + write!(writer, "O,")?; + rectangular.serialize_partial(writer)?; + } + Aperture::Polygon(ref polygon) => { + write!(writer, "P,")?; + polygon.serialize_partial(writer)?; + } + Aperture::Other(ref string) => write!(writer, "{}", string)?, + }; + Ok(()) + } +} + +// Circle + +#[derive(Debug, Clone, PartialEq)] +pub struct Circle { + pub diameter: f64, + pub hole_diameter: Option, +} + +impl Circle { + pub fn new(diameter: f64) -> Self { + Circle { + diameter, + hole_diameter: None, + } + } + + pub fn with_hole(diameter: f64, hole_diameter: f64) -> Self { + Circle { + diameter, + hole_diameter: Some(hole_diameter), + } + } +} + +impl PartialGerberCode for Circle { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match self.hole_diameter { + Some(hole_diameter) => { + write!(writer, "{}X{}", self.diameter, hole_diameter)?; + } + None => write!(writer, "{}", self.diameter)?, + }; + Ok(()) + } +} + +// Rectangular + +#[derive(Debug, Clone, PartialEq)] +pub struct Rectangular { + pub x: f64, + pub y: f64, + pub hole_diameter: Option, +} + +impl Rectangular { + pub fn new(x: f64, y: f64) -> Self { + Rectangular { + x, + y, + hole_diameter: None, + } + } + + pub fn with_hole(x: f64, y: f64, hole_diameter: f64) -> Self { + Rectangular { + x, + y, + hole_diameter: Some(hole_diameter), + } + } +} + +impl PartialGerberCode for Rectangular { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match self.hole_diameter { + Some(hole_diameter) => write!(writer, "{}X{}X{}", self.x, self.y, hole_diameter)?, + None => write!(writer, "{}X{}", self.x, self.y)?, + }; + Ok(()) + } +} + +// Polygon + +#[derive(Debug, Clone, PartialEq)] +pub struct Polygon { + pub diameter: f64, + pub vertices: u8, // 3--12 + pub rotation: Option, + pub hole_diameter: Option, +} + +impl Polygon { + pub fn new(diameter: f64, vertices: u8) -> Self { + Polygon { + diameter, + vertices, + rotation: None, + hole_diameter: None, + } + } + + pub fn with_rotation(mut self, angle: f64) -> Self { + self.rotation = Some(angle); + self + } + + pub fn with_diameter(mut self, diameter: f64) -> Self { + self.diameter = diameter; + self + } +} + +impl PartialGerberCode for Polygon { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match (self.rotation, self.hole_diameter) { + (Some(rot), Some(hd)) => { + write!(writer, "{}X{}X{}X{}", self.diameter, self.vertices, rot, hd)? + } + (Some(rot), None) => write!(writer, "{}X{}X{}", self.diameter, self.vertices, rot)?, + (None, Some(hd)) => write!(writer, "{}X{}X0X{}", self.diameter, self.vertices, hd)?, + (None, None) => write!(writer, "{}X{}", self.diameter, self.vertices)?, + }; + Ok(()) + } +} + +// Polarity + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Polarity { + Clear, + Dark, +} + +impl PartialGerberCode for Polarity { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Polarity::Clear => write!(writer, "C")?, + Polarity::Dark => write!(writer, "D")?, + }; + Ok(()) + } +} + +// StepAndRepeat + +#[derive(Debug, Clone, PartialEq)] +pub enum StepAndRepeat { + Open { + repeat_x: u32, + repeat_y: u32, + distance_x: f64, + distance_y: f64, + }, + Close, +} + +impl PartialGerberCode for StepAndRepeat { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + StepAndRepeat::Open { + repeat_x: rx, + repeat_y: ry, + distance_x: dx, + distance_y: dy, + } => write!(writer, "X{}Y{}I{}J{}", rx, ry, dx, dy)?, + StepAndRepeat::Close => {} + }; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_aperture_definition_new() { + let ad1 = ApertureDefinition::new(10, Aperture::Circle(Circle::new(3.0))); + let ad2 = ApertureDefinition { + code: 10, + aperture: Aperture::Circle(Circle::new(3.0)), + }; + assert_eq!(ad1, ad2); + } + + #[test] + fn test_rectangular_new() { + let r1 = Rectangular::new(2.0, 3.0); + let r2 = Rectangular { + x: 2.0, + y: 3.0, + hole_diameter: None, + }; + assert_eq!(r1, r2); + } + + #[test] + fn test_rectangular_with_hole() { + let r1 = Rectangular::with_hole(3.0, 2.0, 1.0); + let r2 = Rectangular { + x: 3.0, + y: 2.0, + hole_diameter: Some(1.0), + }; + assert_eq!(r1, r2); + } + + #[test] + fn test_circle_new() { + let c1 = Circle::new(3.0); + let c2 = Circle { + diameter: 3.0, + hole_diameter: None, + }; + assert_eq!(c1, c2); + } + + #[test] + fn test_circle_with_hole() { + let c1 = Circle::with_hole(3.0, 1.0); + let c2 = Circle { + diameter: 3.0, + hole_diameter: Some(1.0), + }; + assert_eq!(c1, c2); + } + + #[test] + fn test_polygon_new() { + let p1 = Polygon::new(3.0, 4).with_rotation(45.0); + let p2 = Polygon { + diameter: 3.0, + vertices: 4, + rotation: Some(45.0), + hole_diameter: None, + }; + assert_eq!(p1, p2); + } +} diff --git a/gerber-types-rs/src/function_codes.rs b/gerber-types-rs/src/function_codes.rs new file mode 100644 index 0000000..b18411f --- /dev/null +++ b/gerber-types-rs/src/function_codes.rs @@ -0,0 +1,143 @@ +//! Function code types. + +use std::io::Write; + +use crate::coordinates::{CoordinateOffset, Coordinates}; +use crate::errors::GerberResult; +use crate::traits::{GerberCode, PartialGerberCode}; + +// DCode + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DCode { + Operation(Operation), + SelectAperture(i32), +} + +impl GerberCode for DCode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + DCode::Operation(ref operation) => operation.serialize(writer)?, + DCode::SelectAperture(code) => writeln!(writer, "D{}*", code)?, + }; + Ok(()) + } +} + +// GCode + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GCode { + InterpolationMode(InterpolationMode), + RegionMode(bool), + QuadrantMode(QuadrantMode), + Comment(String), +} + +impl GerberCode for GCode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + GCode::InterpolationMode(ref mode) => mode.serialize(writer)?, + GCode::RegionMode(enabled) => { + if enabled { + writeln!(writer, "G36*")?; + } else { + writeln!(writer, "G37*")?; + } + } + GCode::QuadrantMode(ref mode) => mode.serialize(writer)?, + GCode::Comment(ref comment) => writeln!(writer, "G04 {}*", comment)?, + }; + Ok(()) + } +} + +// MCode + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MCode { + EndOfFile, +} + +impl GerberCode for MCode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + MCode::EndOfFile => writeln!(writer, "M02*")?, + }; + Ok(()) + } +} + +// Operation + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Operation { + /// D01 Command + Interpolate(Coordinates, Option), + /// D02 Command + Move(Coordinates), + /// D03 Command + Flash(Coordinates), +} + +impl GerberCode for Operation { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + Operation::Interpolate(ref coords, ref offset) => { + coords.serialize_partial(writer)?; + offset.serialize_partial(writer)?; + writeln!(writer, "D01*")?; + } + Operation::Move(ref coords) => { + coords.serialize_partial(writer)?; + writeln!(writer, "D02*")?; + } + Operation::Flash(ref coords) => { + coords.serialize_partial(writer)?; + writeln!(writer, "D03*")?; + } + }; + Ok(()) + } +} + +// InterpolationMode + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterpolationMode { + Linear, + ClockwiseCircular, + CounterclockwiseCircular, +} + +impl GerberCode for InterpolationMode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + InterpolationMode::Linear => writeln!(writer, "G01*")?, + InterpolationMode::ClockwiseCircular => writeln!(writer, "G02*")?, + InterpolationMode::CounterclockwiseCircular => writeln!(writer, "G03*")?, + }; + Ok(()) + } +} + +// QuadrantMode + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QuadrantMode { + Single, + Multi, +} + +impl GerberCode for QuadrantMode { + fn serialize(&self, writer: &mut W) -> GerberResult<()> { + match *self { + QuadrantMode::Single => writeln!(writer, "G74*")?, + QuadrantMode::Multi => writeln!(writer, "G75*")?, + }; + Ok(()) + } +} + +#[cfg(test)] +mod test {} diff --git a/gerber-types-rs/src/lib.rs b/gerber-types-rs/src/lib.rs new file mode 100644 index 0000000..68b6dc2 --- /dev/null +++ b/gerber-types-rs/src/lib.rs @@ -0,0 +1,289 @@ +//! # Gerber commands +//! +//! This crate implements the basic building blocks of Gerber (RS-274X, aka +//! Extended Gerber version 2) code. It focusses on the low level types and does +//! not do any semantic checking. +//! +//! For example, you can use an aperture without defining it. This will +//! generate syntactically valid but semantially invalid Gerber code, but this +//! module won't complain. +//! +//! ## Traits: GerberCode and PartialGerberCode +//! +//! There are two main traits that are used for code generation: +//! +//! - [`GerberCode`](trait.GerberCode.html) generates a full Gerber code line, +//! terminated with a newline character. +//! - `PartialGerberCode` (internal only) generates Gerber representation of a +//! value, but does not represent a full line of code. +#![allow(clippy::new_without_default)] + +#[cfg(test)] +#[macro_use] +mod test_macros; + +mod attributes; +mod codegen; +mod coordinates; +mod errors; +mod extended_codes; +mod function_codes; +mod macros; +mod traits; +mod types; + +pub use crate::attributes::*; +#[allow(unused)] +pub use crate::codegen::*; +pub use crate::coordinates::*; +pub use crate::errors::*; +pub use crate::extended_codes::*; +pub use crate::function_codes::*; +pub use crate::macros::*; +pub use crate::traits::GerberCode; +pub use crate::types::*; + +#[cfg(test)] +mod test { + use std::io::BufWriter; + + use super::traits::PartialGerberCode; + use super::*; + + #[test] + fn test_serialize() { + //! The serialize method of the GerberCode trait should generate strings. + let comment = GCode::Comment("testcomment".to_string()); + assert_code!(comment, "G04 testcomment*\n"); + } + + #[test] + fn test_vec_serialize() { + //! A `Vec` should also implement `GerberCode`. + let mut v = Vec::new(); + v.push(GCode::Comment("comment 1".to_string())); + v.push(GCode::Comment("another one".to_string())); + assert_code!(v, "G04 comment 1*\nG04 another one*\n"); + } + + #[test] + fn test_command_serialize() { + //! A `Command` should implement `GerberCode` + let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment("comment".to_string()))); + assert_code!(c, "G04 comment*\n"); + } + + #[test] + fn test_interpolation_mode() { + let mut commands = Vec::new(); + let c1 = GCode::InterpolationMode(InterpolationMode::Linear); + let c2 = GCode::InterpolationMode(InterpolationMode::ClockwiseCircular); + let c3 = GCode::InterpolationMode(InterpolationMode::CounterclockwiseCircular); + commands.push(c1); + commands.push(c2); + commands.push(c3); + assert_code!(commands, "G01*\nG02*\nG03*\n"); + } + + #[test] + fn test_region_mode() { + let mut commands = Vec::new(); + commands.push(GCode::RegionMode(true)); + commands.push(GCode::RegionMode(false)); + assert_code!(commands, "G36*\nG37*\n"); + } + + #[test] + fn test_quadrant_mode() { + let mut commands = Vec::new(); + commands.push(GCode::QuadrantMode(QuadrantMode::Single)); + commands.push(GCode::QuadrantMode(QuadrantMode::Multi)); + assert_code!(commands, "G74*\nG75*\n"); + } + + #[test] + fn test_end_of_file() { + let c = MCode::EndOfFile; + assert_code!(c, "M02*\n"); + } + + #[test] + fn test_operation_interpolate() { + let cf = CoordinateFormat::new(2, 5); + let c1 = Operation::Interpolate( + Coordinates::new(1, 2, cf), + Some(CoordinateOffset::new(5, 10, cf)), + ); + assert_code!(c1, "X100000Y200000I500000J1000000D01*\n"); + let c2 = Operation::Interpolate(Coordinates::at_y(-2, CoordinateFormat::new(4, 4)), None); + assert_code!(c2, "Y-20000D01*\n"); + let cf = CoordinateFormat::new(4, 4); + let c3 = Operation::Interpolate( + Coordinates::at_x(1, cf), + Some(CoordinateOffset::at_y(2, cf)), + ); + assert_code!(c3, "X10000J20000D01*\n"); + } + + #[test] + fn test_operation_move() { + let c = Operation::Move(Coordinates::new(23, 42, CoordinateFormat::new(6, 4))); + assert_code!(c, "X230000Y420000D02*\n"); + } + + #[test] + fn test_operation_flash() { + let c = Operation::Flash(Coordinates::new(23, 42, CoordinateFormat::new(4, 4))); + assert_code!(c, "X230000Y420000D03*\n"); + } + + #[test] + fn test_select_aperture() { + let c1 = DCode::SelectAperture(10); + assert_code!(c1, "D10*\n"); + let c2 = DCode::SelectAperture(2147483647); + assert_code!(c2, "D2147483647*\n"); + } + + #[test] + fn test_coordinate_format() { + let c = ExtendedCode::CoordinateFormat(CoordinateFormat::new(2, 5)); + assert_code!(c, "%FSLAX25Y25*%\n"); + } + + #[test] + fn test_unit() { + let c1 = ExtendedCode::Unit(Unit::Millimeters); + let c2 = ExtendedCode::Unit(Unit::Inches); + assert_code!(c1, "%MOMM*%\n"); + assert_code!(c2, "%MOIN*%\n"); + } + + #[test] + fn test_aperture_circle_definition() { + let ad1 = ApertureDefinition { + code: 10, + aperture: Aperture::Circle(Circle { + diameter: 4.0, + hole_diameter: Some(2.0), + }), + }; + let ad2 = ApertureDefinition { + code: 11, + aperture: Aperture::Circle(Circle { + diameter: 4.5, + hole_diameter: None, + }), + }; + assert_partial_code!(ad1, "10C,4X2"); + assert_partial_code!(ad2, "11C,4.5"); + } + + #[test] + fn test_aperture_rectangular_definition() { + let ad1 = ApertureDefinition { + code: 12, + aperture: Aperture::Rectangle(Rectangular { + x: 1.5, + y: 2.25, + hole_diameter: Some(3.8), + }), + }; + let ad2 = ApertureDefinition { + code: 13, + aperture: Aperture::Rectangle(Rectangular { + x: 1.0, + y: 1.0, + hole_diameter: None, + }), + }; + let ad3 = ApertureDefinition { + code: 14, + aperture: Aperture::Obround(Rectangular { + x: 2.0, + y: 4.5, + hole_diameter: None, + }), + }; + assert_partial_code!(ad1, "12R,1.5X2.25X3.8"); + assert_partial_code!(ad2, "13R,1X1"); + assert_partial_code!(ad3, "14O,2X4.5"); + } + + #[test] + fn test_aperture_polygon_definition() { + let ad1 = ApertureDefinition { + code: 15, + aperture: Aperture::Polygon(Polygon { + diameter: 4.5, + vertices: 3, + rotation: None, + hole_diameter: None, + }), + }; + let ad2 = ApertureDefinition { + code: 16, + aperture: Aperture::Polygon(Polygon { + diameter: 5.0, + vertices: 4, + rotation: Some(30.6), + hole_diameter: None, + }), + }; + let ad3 = ApertureDefinition { + code: 17, + aperture: Aperture::Polygon(Polygon { + diameter: 5.5, + vertices: 5, + rotation: None, + hole_diameter: Some(1.8), + }), + }; + assert_partial_code!(ad1, "15P,4.5X3"); + assert_partial_code!(ad2, "16P,5X4X30.6"); + assert_partial_code!(ad3, "17P,5.5X5X0X1.8"); + } + + #[test] + fn test_polarity_serialize() { + let d = ExtendedCode::LoadPolarity(Polarity::Dark); + let c = ExtendedCode::LoadPolarity(Polarity::Clear); + assert_code!(d, "%LPD*%\n"); + assert_code!(c, "%LPC*%\n"); + } + + #[test] + fn test_step_and_repeat_serialize() { + let o = ExtendedCode::StepAndRepeat(StepAndRepeat::Open { + repeat_x: 2, + repeat_y: 3, + distance_x: 2.0, + distance_y: 3.0, + }); + let c = ExtendedCode::StepAndRepeat(StepAndRepeat::Close); + assert_code!(o, "%SRX2Y3I2J3*%\n"); + assert_code!(c, "%SR*%\n"); + } + + #[test] + fn test_delete_attribute_serialize() { + let d = ExtendedCode::DeleteAttribute("foo".into()); + assert_code!(d, "%TDfoo*%\n"); + } + + #[test] + fn test_file_attribute_serialize() { + let part = ExtendedCode::FileAttribute(FileAttribute::Part(Part::Other("foo".into()))); + assert_code!(part, "%TF.Part,Other,foo*%\n"); + + let gensw1 = ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware( + GenerationSoftware::new("Vend0r", "superpcb", None), + )); + assert_code!(gensw1, "%TF.GenerationSoftware,Vend0r,superpcb*%\n"); + + let gensw2 = ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware( + GenerationSoftware::new("Vend0r", "superpcb", Some("1.2.3")), + )); + assert_code!(gensw2, "%TF.GenerationSoftware,Vend0r,superpcb,1.2.3*%\n"); + } +} diff --git a/gerber-types-rs/src/macros.rs b/gerber-types-rs/src/macros.rs new file mode 100644 index 0000000..94acdac --- /dev/null +++ b/gerber-types-rs/src/macros.rs @@ -0,0 +1,1089 @@ +//! Aperture Macros. + +use std::convert::From; +use std::io::Write; + +use crate::errors::{GerberError, GerberResult}; +use crate::traits::PartialGerberCode; + +#[derive(Debug, Clone, PartialEq)] +pub struct ApertureMacro { + pub name: String, + pub content: Vec, +} + +impl ApertureMacro { + pub fn new>(name: S) -> Self { + ApertureMacro { + name: name.into(), + content: Vec::new(), + } + } + + pub fn add_content(mut self, c: C) -> Self + where + C: Into, + { + self.content.push(c.into()); + self + } + + pub fn add_content_mut(&mut self, c: C) + where + C: Into, + { + self.content.push(c.into()); + } +} + +impl PartialGerberCode for ApertureMacro { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + if self.content.is_empty() { + return Err(GerberError::MissingDataError( + "There must be at least 1 content element in an aperture macro".into(), + )); + } + writeln!(writer, "AM{}*", self.name)?; + let mut first = true; + for content in &self.content { + if first { + first = false; + } else { + writeln!(writer)?; + } + content.serialize_partial(writer)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +/// A macro decimal can either be an f64 or a variable placeholder. +pub enum MacroDecimal { + /// A decimal value. + Value(f64), + /// A variable placeholder. + Variable(u32), + /// A variable for arithmetic expressions. + Expression(String), +} + +impl MacroDecimal { + fn is_negative(&self) -> bool { + match *self { + MacroDecimal::Value(v) => v < 0.0, + MacroDecimal::Variable(_) => false, + MacroDecimal::Expression(_) => false, + } + } +} + +impl From for MacroDecimal { + fn from(val: f32) -> Self { + MacroDecimal::Value(val as f64) + } +} + +impl From for MacroDecimal { + fn from(val: f64) -> Self { + MacroDecimal::Value(val) + } +} + +impl PartialGerberCode for MacroDecimal { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + MacroDecimal::Value(ref v) => write!(writer, "{}", v)?, + MacroDecimal::Variable(ref v) => write!(writer, "${}", v)?, + MacroDecimal::Expression(ref v) => write!(writer, "{}", v)?, + }; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MacroContent { + // Primitives + Circle(CirclePrimitive), + VectorLine(VectorLinePrimitive), + CenterLine(CenterLinePrimitive), + Outline(OutlinePrimitive), + Polygon(PolygonPrimitive), + Moire(MoirePrimitive), + Thermal(ThermalPrimitive), + + // Variables + VariableDefinition(VariableDefinition), + + // Comment + Comment(String), +} + +impl PartialGerberCode for MacroContent { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + MacroContent::Circle(ref c) => c.serialize_partial(writer)?, + MacroContent::VectorLine(ref vl) => vl.serialize_partial(writer)?, + MacroContent::CenterLine(ref cl) => cl.serialize_partial(writer)?, + MacroContent::Outline(ref o) => o.serialize_partial(writer)?, + MacroContent::Polygon(ref p) => p.serialize_partial(writer)?, + MacroContent::Moire(ref m) => m.serialize_partial(writer)?, + MacroContent::Thermal(ref t) => t.serialize_partial(writer)?, + MacroContent::Comment(ref s) => write!(writer, "0 {}*", &s)?, + MacroContent::VariableDefinition(ref v) => v.serialize_partial(writer)?, + }; + Ok(()) + } +} + +macro_rules! impl_into { + ($target:ty, $from:ty, $choice:expr) => { + impl From<$from> for $target { + fn from(val: $from) -> $target { + $choice(val) + } + } + }; +} + +impl_into!(MacroContent, CirclePrimitive, MacroContent::Circle); +impl_into!(MacroContent, VectorLinePrimitive, MacroContent::VectorLine); +impl_into!(MacroContent, CenterLinePrimitive, MacroContent::CenterLine); +impl_into!(MacroContent, OutlinePrimitive, MacroContent::Outline); +impl_into!(MacroContent, PolygonPrimitive, MacroContent::Polygon); +impl_into!(MacroContent, MoirePrimitive, MacroContent::Moire); +impl_into!(MacroContent, ThermalPrimitive, MacroContent::Thermal); +impl_into!( + MacroContent, + VariableDefinition, + MacroContent::VariableDefinition +); + +impl> From for MacroContent { + fn from(val: T) -> Self { + MacroContent::Comment(val.into()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CirclePrimitive { + /// Exposure off/on + pub exposure: bool, + + /// Diameter, a decimal >= 0 + pub diameter: MacroDecimal, + + /// X and Y coordinates of center position, decimals + pub center: (MacroDecimal, MacroDecimal), + + /// Rotation angle. + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. the (0, 0) + /// point of macro coordinates. + /// + /// The rotation modifier is optional. The default is no rotation. (We + /// recommend always to set the angle explicitly. + pub angle: Option, +} + +impl CirclePrimitive { + pub fn new(diameter: MacroDecimal) -> Self { + CirclePrimitive { + exposure: true, + diameter, + center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)), + angle: None, + } + } + + pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self { + self.center = center; + self + } + + pub fn exposure_on(mut self, exposure: bool) -> Self { + self.exposure = exposure; + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = Some(angle); + self + } +} + +impl PartialGerberCode for CirclePrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + write!(writer, "1,")?; + self.exposure.serialize_partial(writer)?; + write!(writer, ",")?; + self.diameter.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.1.serialize_partial(writer)?; + if let Some(ref a) = self.angle { + write!(writer, ",")?; + a.serialize_partial(writer)?; + } + write!(writer, "*")?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct VectorLinePrimitive { + /// Exposure off/on + pub exposure: bool, + + /// Line width, a decimal >= 0 + pub width: MacroDecimal, + + /// X and Y coordinates of start point, decimals + pub start: (MacroDecimal, MacroDecimal), + + /// X and Y coordinates of end point, decimals + pub end: (MacroDecimal, MacroDecimal), + + /// Rotation angle of the vector line primitive + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. the (0, 0) + /// point of macro coordinates. + pub angle: MacroDecimal, +} + +impl VectorLinePrimitive { + pub fn new(start: (MacroDecimal, MacroDecimal), end: (MacroDecimal, MacroDecimal)) -> Self { + VectorLinePrimitive { + exposure: true, + width: MacroDecimal::Value(0.0), + start, + end, + angle: MacroDecimal::Value(0.0), + } + } + + pub fn exposure_on(mut self, exposure: bool) -> Self { + self.exposure = exposure; + self + } + + pub fn with_width(mut self, width: MacroDecimal) -> Self { + self.width = width; + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = angle; + self + } +} + +impl PartialGerberCode for VectorLinePrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + write!(writer, "20,")?; + self.exposure.serialize_partial(writer)?; + write!(writer, ",")?; + self.width.serialize_partial(writer)?; + write!(writer, ",")?; + self.start.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.start.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.end.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.end.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.angle.serialize_partial(writer)?; + write!(writer, "*")?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CenterLinePrimitive { + /// Exposure off/on (0/1) + pub exposure: bool, + + /// Rectangle dimensions (width/height) + pub dimensions: (MacroDecimal, MacroDecimal), + + /// X and Y coordinates of center point, decimals + pub center: (MacroDecimal, MacroDecimal), + + /// Rotation angle + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. (0, 0) point + /// of macro coordinates. + pub angle: MacroDecimal, +} + +impl CenterLinePrimitive { + pub fn new(dimensions: (MacroDecimal, MacroDecimal)) -> Self { + CenterLinePrimitive { + exposure: true, + dimensions, + center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)), + angle: MacroDecimal::Value(0.0), + } + } + + pub fn exposure_on(mut self, exposure: bool) -> Self { + self.exposure = exposure; + self + } + + pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self { + self.center = center; + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = angle; + self + } +} + +impl PartialGerberCode for CenterLinePrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + write!(writer, "21,")?; + self.exposure.serialize_partial(writer)?; + write!(writer, ",")?; + self.dimensions.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.dimensions.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.angle.serialize_partial(writer)?; + write!(writer, "*")?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OutlinePrimitive { + /// Exposure off/on (0/1) + pub exposure: bool, + + /// Vector of coordinate pairs. + /// + /// The last coordinate pair must be equal to the first coordinate pair! + pub points: Vec<(MacroDecimal, MacroDecimal)>, + + /// Rotation angle of the outline primitive + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. the (0, 0) + /// point of macro coordinates. + pub angle: MacroDecimal, +} + +impl OutlinePrimitive { + pub fn new() -> Self { + OutlinePrimitive { + exposure: true, + points: Vec::new(), + angle: MacroDecimal::Value(0.0), + } + } + + pub fn from_points(points: Vec<(MacroDecimal, MacroDecimal)>) -> Self { + let mut outline_prim = Self::new(); + outline_prim.points = points; + outline_prim + } + + pub fn add_point(mut self, point: (MacroDecimal, MacroDecimal)) -> Self { + self.points.push(point); + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = angle; + self + } +} + +impl PartialGerberCode for OutlinePrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + // Points invariants + if self.points.len() < 2 { + return Err(GerberError::MissingDataError( + "There must be at least 1 subsequent point in an outline".into(), + )); + } + if self.points.len() > 5001 { + return Err(GerberError::RangeError( + "The maximum number of subsequent points in an outline is 5000".into(), + )); + } + if self.points[0] != self.points[self.points.len() - 1] { + return Err(GerberError::RangeError( + "The last point must be equal to the first point".into(), + )); + } + + write!(writer, "4,")?; + self.exposure.serialize_partial(writer)?; + writeln!(writer, ",{},", self.points.len() - 1)?; + + for &(ref x, ref y) in &self.points { + x.serialize_partial(writer)?; + write!(writer, ",")?; + y.serialize_partial(writer)?; + writeln!(writer, ",")?; + } + self.angle.serialize_partial(writer)?; + write!(writer, "*")?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +/// A polygon primitive is a regular polygon defined by the number of vertices, +/// the center point and the diameter of the circumscribed circle. +pub struct PolygonPrimitive { + /// Exposure off/on (0/1) + pub exposure: bool, + + /// Number of vertices n, 3 <= n <= 12 + pub vertices: u8, + + /// X and Y coordinates of center point, decimals + pub center: (MacroDecimal, MacroDecimal), + + /// Diameter of the circumscribed circle, a decimal >= 0 + pub diameter: MacroDecimal, + + /// Rotation angle of the polygon primitive + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. the (0, 0) + /// point of macro coordinates. The first vertex is on the positive X-axis + /// through the center point when the rotation angle is zero. + /// + /// Note: Rotation is only allowed if the primitive center point coincides + /// with the origin of the macro definition. + pub angle: MacroDecimal, +} + +impl PolygonPrimitive { + pub fn new(vertices: u8) -> Self { + PolygonPrimitive { + exposure: true, + vertices, + center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)), + diameter: MacroDecimal::Value(0.0), + angle: MacroDecimal::Value(0.0), + } + } + + pub fn exposure_on(mut self, exposure: bool) -> Self { + self.exposure = exposure; + self + } + + pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self { + self.center = center; + self + } + + pub fn with_diameter(mut self, diameter: MacroDecimal) -> Self { + self.diameter = diameter; + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = angle; + self + } +} + +impl PartialGerberCode for PolygonPrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + // Vertice count invariants + if self.vertices < 3 { + return Err(GerberError::MissingDataError( + "There must be at least 3 vertices in a polygon".into(), + )); + } + if self.vertices > 12 { + return Err(GerberError::RangeError( + "The maximum number of vertices in a polygon is 12".into(), + )); + } + if self.diameter.is_negative() { + return Err(GerberError::RangeError( + "The diameter must not be negative".into(), + )); + } + write!(writer, "5,")?; + self.exposure.serialize_partial(writer)?; + write!(writer, ",{},", self.vertices)?; + self.center.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.diameter.serialize_partial(writer)?; + write!(writer, ",")?; + self.angle.serialize_partial(writer)?; + write!(writer, "*")?; + Ok(()) + } +} + +/// The moiré primitive is a cross hair centered on concentric rings (annuli). +/// Exposure is always on. +#[derive(Debug, Clone, PartialEq)] +pub struct MoirePrimitive { + /// X and Y coordinates of center point, decimals + pub center: (MacroDecimal, MacroDecimal), + + /// Outer diameter of outer concentric ring, a decimal >= 0 + pub diameter: MacroDecimal, + + /// Ring thickness, a decimal >= 0 + pub ring_thickness: MacroDecimal, + + /// Gap between rings, a decimal >= 0 + pub gap: MacroDecimal, + + /// Maximum number of rings + pub max_rings: u32, + + /// Cross hair thickness, a decimal >= 0 + pub cross_hair_thickness: MacroDecimal, + + /// Cross hair length, a decimal >= 0 + pub cross_hair_length: MacroDecimal, + + /// Rotation angle of the moiré primitive + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. the (0, 0) + /// point of macro coordinates. + /// + /// Note: Rotation is only allowed if the primitive center point coincides + /// with the origin of the macro definition. + pub angle: MacroDecimal, +} + +impl MoirePrimitive { + pub fn new() -> Self { + MoirePrimitive { + center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)), + diameter: MacroDecimal::Value(0.0), + ring_thickness: MacroDecimal::Value(0.0), + gap: MacroDecimal::Value(0.0), + max_rings: 1, + cross_hair_thickness: MacroDecimal::Value(0.0), + cross_hair_length: MacroDecimal::Value(0.0), + angle: MacroDecimal::Value(0.0), + } + } + + pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self { + self.center = center; + self + } + + pub fn with_diameter(mut self, diameter: MacroDecimal) -> Self { + self.diameter = diameter; + self + } + + pub fn with_rings_max(mut self, max_rings: u32) -> Self { + self.max_rings = max_rings; + self + } + + pub fn with_ring_thickness(mut self, thickness: MacroDecimal) -> Self { + self.ring_thickness = thickness; + self + } + + pub fn with_gap(mut self, gap: MacroDecimal) -> Self { + self.gap = gap; + self + } + + pub fn with_cross_thickness(mut self, thickness: MacroDecimal) -> Self { + self.cross_hair_thickness = thickness; + self + } + + pub fn with_cross_length(mut self, length: MacroDecimal) -> Self { + self.cross_hair_length = length; + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = angle; + self + } +} + +impl PartialGerberCode for MoirePrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + // Decimal invariants + if self.diameter.is_negative() { + return Err(GerberError::RangeError( + "Outer diameter of a moiré may not be negative".into(), + )); + } + if self.ring_thickness.is_negative() { + return Err(GerberError::RangeError( + "Ring thickness of a moiré may not be negative".into(), + )); + } + if self.gap.is_negative() { + return Err(GerberError::RangeError( + "Gap of a moiré may not be negative".into(), + )); + } + if self.cross_hair_thickness.is_negative() { + return Err(GerberError::RangeError( + "Cross hair thickness of a moiré may not be negative".into(), + )); + } + if self.cross_hair_length.is_negative() { + return Err(GerberError::RangeError( + "Cross hair length of a moiré may not be negative".into(), + )); + } + write!(writer, "6,")?; + self.center.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.diameter.serialize_partial(writer)?; + write!(writer, ",")?; + self.ring_thickness.serialize_partial(writer)?; + write!(writer, ",")?; + self.gap.serialize_partial(writer)?; + write!(writer, ",{},", self.max_rings)?; + self.cross_hair_thickness.serialize_partial(writer)?; + write!(writer, ",")?; + self.cross_hair_length.serialize_partial(writer)?; + write!(writer, ",")?; + self.angle.serialize_partial(writer)?; + write!(writer, "*")?; + Ok(()) + } +} + +/// The thermal primitive is a ring (annulus) interrupted by four gaps. +/// Exposure is always on. +#[derive(Debug, Clone, PartialEq)] +pub struct ThermalPrimitive { + /// X and Y coordinates of center point, decimals + pub center: (MacroDecimal, MacroDecimal), + + /// Outer diameter, a decimal > inner diameter + pub outer_diameter: MacroDecimal, + + /// Inner diameter, a decimal >= 0 + pub inner_diameter: MacroDecimal, + + /// Gap thickness, a decimal < (outer diameter) / sqrt(2) + pub gap: MacroDecimal, + + /// Rotation angle of the thermal primitive + /// + /// The rotation angle is specified by a decimal, in degrees. The primitive + /// is rotated around the origin of the macro definition, i.e. the (0, 0) + /// point of macro coordinates. The gaps are on the X and Y axes through + /// the center when the rotation angle is zero + /// + /// Note: Rotation is only allowed if the primitive center point coincides + /// with the origin of the macro definition. + pub angle: MacroDecimal, +} + +impl ThermalPrimitive { + pub fn new(inner: MacroDecimal, outer: MacroDecimal, gap: MacroDecimal) -> Self { + ThermalPrimitive { + center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)), + outer_diameter: outer, + inner_diameter: inner, + gap, + angle: MacroDecimal::Value(0.0), + } + } + + pub fn centered_at(mut self, center: (MacroDecimal, MacroDecimal)) -> Self { + self.center = center; + self + } + + pub fn with_angle(mut self, angle: MacroDecimal) -> Self { + self.angle = angle; + self + } +} + +impl PartialGerberCode for ThermalPrimitive { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + // Decimal invariants + if self.inner_diameter.is_negative() { + return Err(GerberError::RangeError( + "Inner diameter of a thermal may not be negative".into(), + )); + } + write!(writer, "7,")?; + self.center.0.serialize_partial(writer)?; + write!(writer, ",")?; + self.center.1.serialize_partial(writer)?; + write!(writer, ",")?; + self.outer_diameter.serialize_partial(writer)?; + write!(writer, ",")?; + self.inner_diameter.serialize_partial(writer)?; + write!(writer, ",")?; + self.gap.serialize_partial(writer)?; + write!(writer, ",")?; + self.angle.serialize_partial(writer)?; + write!(writer, "*")?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VariableDefinition { + number: u32, + expression: String, +} + +impl VariableDefinition { + pub fn new(number: u32, expr: &str) -> Self { + VariableDefinition { + number, + expression: expr.into(), + } + } +} + +impl PartialGerberCode for VariableDefinition { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + write!(writer, "${}={}*", self.number, self.expression)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::io::BufWriter; + + use crate::traits::PartialGerberCode; + + use super::MacroDecimal::{Value, Variable}; + use super::*; + + macro_rules! assert_partial_code { + ($obj:expr, $expected:expr) => { + let mut buf = BufWriter::new(Vec::new()); + $obj.serialize_partial(&mut buf) + .expect("Could not generate Gerber code"); + let bytes = buf.into_inner().unwrap(); + let code = String::from_utf8(bytes).unwrap(); + assert_eq!(&code, $expected); + }; + } + + #[test] + fn test_circle_primitive_codegen() { + let with_angle = CirclePrimitive { + exposure: true, + diameter: Value(1.5), + center: (Value(0.), Value(0.)), + angle: Some(Value(0.)), + }; + assert_partial_code!(with_angle, "1,1,1.5,0,0,0*"); + let no_angle = CirclePrimitive { + exposure: false, + diameter: Value(99.9), + center: (Value(1.1), Value(2.2)), + angle: None, + }; + assert_partial_code!(no_angle, "1,0,99.9,1.1,2.2*"); + } + + #[test] + fn test_vector_line_primitive_codegen() { + let line = VectorLinePrimitive { + exposure: true, + width: Value(0.9), + start: (Value(0.), Value(0.45)), + end: (Value(12.), Value(0.45)), + angle: Value(0.), + }; + assert_partial_code!(line, "20,1,0.9,0,0.45,12,0.45,0*"); + } + + #[test] + fn test_center_line_primitive_codegen() { + let line = CenterLinePrimitive { + exposure: true, + dimensions: (Value(6.8), Value(1.2)), + center: (Value(3.4), Value(0.6)), + angle: Value(30.0), + }; + assert_partial_code!(line, "21,1,6.8,1.2,3.4,0.6,30*"); + } + + #[test] + fn test_outline_primitive_codegen() { + let line = OutlinePrimitive { + exposure: true, + points: vec![ + (Value(0.1), Value(0.1)), + (Value(0.5), Value(0.1)), + (Value(0.5), Value(0.5)), + (Value(0.1), Value(0.5)), + (Value(0.1), Value(0.1)), + ], + angle: Value(0.0), + }; + assert_partial_code!( + line, + "4,1,4,\n0.1,0.1,\n0.5,0.1,\n0.5,0.5,\n0.1,0.5,\n0.1,0.1,\n0*" + ); + } + + #[test] + fn test_polygon_primitive_codegen() { + let line = PolygonPrimitive { + exposure: true, + vertices: 8, + center: (Value(1.5), Value(2.0)), + diameter: Value(8.0), + angle: Value(0.0), + }; + assert_partial_code!(line, "5,1,8,1.5,2,8,0*"); + } + + #[test] + fn test_moire_primitive_codegen() { + let line = MoirePrimitive { + center: (Value(0.0), Value(0.0)), + diameter: Value(5.0), + ring_thickness: Value(0.5), + gap: Value(0.5), + max_rings: 2, + cross_hair_thickness: Value(0.1), + cross_hair_length: Value(6.0), + angle: Value(0.0), + }; + assert_partial_code!(line, "6,0,0,5,0.5,0.5,2,0.1,6,0*"); + } + + #[test] + fn test_thermal_primitive_codegen() { + let line = ThermalPrimitive { + center: (Value(0.0), Value(0.0)), + outer_diameter: Value(8.0), + inner_diameter: Value(6.5), + gap: Value(1.0), + angle: Value(45.0), + }; + assert_partial_code!(line, "7,0,0,8,6.5,1,45*"); + } + + #[test] + fn test_aperture_macro_codegen() { + let am = ApertureMacro::new("CRAZY") + .add_content(MacroContent::Thermal(ThermalPrimitive { + center: (Value(0.0), Value(0.0)), + outer_diameter: Value(0.08), + inner_diameter: Value(0.055), + gap: Value(0.0125), + angle: Value(45.0), + })) + .add_content(MacroContent::Moire(MoirePrimitive { + center: (Value(0.0), Value(0.0)), + diameter: Value(0.125), + ring_thickness: Value(0.01), + gap: Value(0.01), + max_rings: 3, + cross_hair_thickness: Value(0.003), + cross_hair_length: Value(0.150), + angle: Value(0.0), + })); + assert_partial_code!( + am, + "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*" + ); + } + + #[test] + fn test_codegen_with_variable() { + let line = VectorLinePrimitive { + exposure: true, + width: Variable(0), + start: (Variable(1), 0.45.into()), + end: (Value(12.), Variable(2)), + angle: Variable(3), + }; + assert_partial_code!(line, "20,1,$0,$1,0.45,12,$2,$3*"); + } + + #[test] + fn test_macro_decimal_into() { + let a = Value(1.0); + let b: MacroDecimal = 1.0.into(); + assert_eq!(a, b); + let c = Variable(1); + let d = Variable(1); + assert_eq!(c, d); + } + + #[test] + fn test_comment_codegen() { + let comment = MacroContent::Comment("hello world".to_string()); + assert_partial_code!(comment, "0 hello world*"); + } + + #[test] + fn test_variable_definition_codegen() { + let var = VariableDefinition { + number: 17, + expression: "$40+2".to_string(), + }; + assert_partial_code!(var, "$17=$40+2*"); + } + + #[test] + fn test_macrocontent_from_into() { + let a = MacroContent::Comment("hello".into()); + let b: MacroContent = "hello".to_string().into(); + let c: MacroContent = "hello".into(); + assert_eq!(a, b); + assert_eq!(b, c); + } + + #[test] + fn test_circle_primitive_new() { + let c1 = CirclePrimitive::new(Value(3.0)).centered_at((Value(5.0), Value(0.0))); + let c2 = CirclePrimitive { + exposure: true, + diameter: Value(3.0), + center: (Value(5.0), Value(0.0)), + angle: None, + }; + assert_eq!(c1, c2); + } + + #[test] + fn test_vectorline_primitive_new() { + let vl1 = VectorLinePrimitive::new((Value(0.0), Value(5.3)), (Value(3.9), Value(8.5))) + .with_angle(Value(38.0)); + let vl2 = VectorLinePrimitive { + exposure: true, + width: Value(0.0), + start: (Value(0.0), Value(5.3)), + end: (Value(3.9), Value(8.5)), + angle: Value(38.0), + }; + assert_eq!(vl1, vl2); + } + + #[test] + fn test_centerline_primitive_new() { + let cl1 = CenterLinePrimitive::new((Value(3.0), Value(4.5))).exposure_on(false); + let cl2 = CenterLinePrimitive { + exposure: false, + dimensions: (Value(3.0), Value(4.5)), + center: (Value(0.0), Value(0.0)), + angle: Value(0.0), + }; + assert_eq!(cl1, cl2); + } + + #[test] + fn test_outline_primitive_new() { + let op1 = OutlinePrimitive::new() + .add_point((Value(0.0), Value(0.0))) + .add_point((Value(2.0), Value(2.0))) + .add_point((Value(-2.0), Value(-2.0))) + .add_point((Value(0.0), Value(0.0))); + + let pts = vec![ + (Value(0.0), Value(0.0)), + (Value(2.0), Value(2.0)), + (Value(-2.0), Value(-2.0)), + (Value(0.0), Value(0.0)), + ]; + + let op2 = OutlinePrimitive { + exposure: true, + points: pts, + angle: Value(0.0), + }; + assert_eq!(op1, op2); + } + + #[test] + fn test_polygon_primitive_new() { + let pp1 = PolygonPrimitive::new(5) + .with_angle(Value(98.0)) + .with_diameter(Value(5.3)) + .centered_at((Value(1.0), Value(1.0))); + let pp2 = PolygonPrimitive { + exposure: true, + vertices: 5, + angle: Value(98.0), + diameter: Value(5.3), + center: (Value(1.0), Value(1.0)), + }; + assert_eq!(pp1, pp2); + } + + #[test] + fn test_moire_primitive_new() { + let mp1 = MoirePrimitive::new() + .with_diameter(Value(3.0)) + .with_ring_thickness(Value(0.05)) + .with_cross_thickness(Value(0.01)) + .with_cross_length(Value(0.5)) + .with_rings_max(3); + let mp2 = MoirePrimitive { + center: (MacroDecimal::Value(0.0), MacroDecimal::Value(0.0)), + diameter: MacroDecimal::Value(3.0), + ring_thickness: MacroDecimal::Value(0.05), + gap: MacroDecimal::Value(0.0), + max_rings: 3, + cross_hair_thickness: MacroDecimal::Value(0.01), + cross_hair_length: MacroDecimal::Value(0.5), + angle: MacroDecimal::Value(0.0), + }; + assert_eq!(mp1, mp2); + } + + #[test] + fn test_thermal_primitive_new() { + let tp1 = ThermalPrimitive::new(Value(1.0), Value(2.0), Value(1.5)).with_angle(Value(87.3)); + let tp2 = ThermalPrimitive { + inner_diameter: Value(1.0), + outer_diameter: Value(2.0), + gap: Value(1.5), + angle: Value(87.3), + center: (Value(0.0), Value(0.0)), + }; + assert_eq!(tp1, tp2); + } + + #[test] + fn test_variabledefinition_new() { + let vd1 = VariableDefinition::new(3, "Test!"); + let vd2 = VariableDefinition { + number: 3, + expression: "Test!".into(), + }; + assert_eq!(vd1, vd2); + } +} diff --git a/gerber-types-rs/src/test_macros.rs b/gerber-types-rs/src/test_macros.rs new file mode 100644 index 0000000..a8cec44 --- /dev/null +++ b/gerber-types-rs/src/test_macros.rs @@ -0,0 +1,25 @@ +/// Assert that serializing the object generates +/// the specified gerber code. +macro_rules! assert_code { + ($obj:expr, $expected:expr) => { + let mut buf = BufWriter::new(Vec::new()); + $obj.serialize(&mut buf) + .expect("Could not generate Gerber code"); + let bytes = buf.into_inner().unwrap(); + let code = String::from_utf8(bytes).unwrap(); + assert_eq!(&code, $expected); + }; +} + +/// Assert that partially serializing the object generates +/// the specified gerber code. +macro_rules! assert_partial_code { + ($obj:expr, $expected:expr) => { + let mut buf = BufWriter::new(Vec::new()); + $obj.serialize_partial(&mut buf) + .expect("Could not generate Gerber code"); + let bytes = buf.into_inner().unwrap(); + let code = String::from_utf8(bytes).unwrap(); + assert_eq!(&code, $expected); + }; +} diff --git a/gerber-types-rs/src/traits.rs b/gerber-types-rs/src/traits.rs new file mode 100644 index 0000000..f70b040 --- /dev/null +++ b/gerber-types-rs/src/traits.rs @@ -0,0 +1,19 @@ +//! Traits used in gerber-types. + +use std::io::Write; + +use crate::GerberResult; + +/// All types that implement this trait can be converted to a complete Gerber +/// Code line. Generated code should end with a newline. +pub trait GerberCode { + fn serialize(&self, writer: &mut W) -> GerberResult<()>; +} + +/// All types that implement this trait can be converted to a Gerber Code +/// representation. +/// +/// This is a crate-internal trait. +pub trait PartialGerberCode { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()>; +} diff --git a/gerber-types-rs/src/types.rs b/gerber-types-rs/src/types.rs new file mode 100644 index 0000000..b6992a2 --- /dev/null +++ b/gerber-types-rs/src/types.rs @@ -0,0 +1,188 @@ +//! Types for Gerber code generation. +//! +//! All types are stateless, meaning that they contain all information in order +//! to render themselves. This means for example that each `Coordinates` +//! instance contains a reference to the coordinate format to be used. + +use std::convert::From; + +use crate::attributes; +use crate::coordinates; +use crate::extended_codes; +use crate::function_codes; +use crate::macros; + +// Helper macros + +macro_rules! impl_from { + ($from:ty, $target:ty, $variant:expr) => { + impl From<$from> for $target { + fn from(val: $from) -> Self { + $variant(val) + } + } + }; +} + +// Root type + +#[derive(Debug, Clone, PartialEq)] +pub enum Command { + FunctionCode(FunctionCode), + ExtendedCode(ExtendedCode), +} + +impl_from!(FunctionCode, Command, Command::FunctionCode); +impl_from!(ExtendedCode, Command, Command::ExtendedCode); + +macro_rules! impl_command_fromfrom { + ($from:ty, $inner:path) => { + impl From<$from> for Command { + fn from(val: $from) -> Self { + Command::from($inner(val)) + } + } + }; +} + +// Main categories + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionCode { + DCode(function_codes::DCode), + GCode(function_codes::GCode), + MCode(function_codes::MCode), +} + +impl_from!(function_codes::DCode, FunctionCode, FunctionCode::DCode); +impl_from!(function_codes::GCode, FunctionCode, FunctionCode::GCode); +impl_from!(function_codes::MCode, FunctionCode, FunctionCode::MCode); + +impl_command_fromfrom!(function_codes::DCode, FunctionCode::from); +impl_command_fromfrom!(function_codes::GCode, FunctionCode::from); +impl_command_fromfrom!(function_codes::MCode, FunctionCode::from); + +#[derive(Debug, Clone, PartialEq)] +pub enum ExtendedCode { + /// FS + CoordinateFormat(coordinates::CoordinateFormat), + /// MO + Unit(extended_codes::Unit), + /// AD + ApertureDefinition(extended_codes::ApertureDefinition), + /// AM + ApertureMacro(macros::ApertureMacro), + /// LP + LoadPolarity(extended_codes::Polarity), + /// SR + StepAndRepeat(extended_codes::StepAndRepeat), + /// TF + FileAttribute(attributes::FileAttribute), + /// TA + ApertureAttribute(attributes::ApertureAttribute), + /// TD + DeleteAttribute(String), +} + +impl_from!( + coordinates::CoordinateFormat, + ExtendedCode, + ExtendedCode::CoordinateFormat +); +impl_from!(extended_codes::Unit, ExtendedCode, ExtendedCode::Unit); +impl_from!( + extended_codes::ApertureDefinition, + ExtendedCode, + ExtendedCode::ApertureDefinition +); +impl_from!( + macros::ApertureMacro, + ExtendedCode, + ExtendedCode::ApertureMacro +); +impl_from!( + extended_codes::Polarity, + ExtendedCode, + ExtendedCode::LoadPolarity +); +impl_from!( + extended_codes::StepAndRepeat, + ExtendedCode, + ExtendedCode::StepAndRepeat +); +impl_from!( + attributes::FileAttribute, + ExtendedCode, + ExtendedCode::FileAttribute +); +impl_from!( + attributes::ApertureAttribute, + ExtendedCode, + ExtendedCode::ApertureAttribute +); + +impl_command_fromfrom!(coordinates::CoordinateFormat, ExtendedCode::from); +impl_command_fromfrom!(extended_codes::Unit, ExtendedCode::from); +impl_command_fromfrom!(extended_codes::ApertureDefinition, ExtendedCode::from); +impl_command_fromfrom!(macros::ApertureMacro, ExtendedCode::from); +impl_command_fromfrom!(extended_codes::Polarity, ExtendedCode::from); +impl_command_fromfrom!(extended_codes::StepAndRepeat, ExtendedCode::from); +impl_command_fromfrom!(attributes::FileAttribute, ExtendedCode::from); +impl_command_fromfrom!(attributes::ApertureAttribute, ExtendedCode::from); + +#[cfg(test)] +mod test { + use super::*; + + use std::io::BufWriter; + + use crate::extended_codes::Polarity; + use crate::function_codes::GCode; + use crate::traits::GerberCode; + + #[test] + fn test_debug() { + //! The debug representation should work properly. + let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment("test".to_string()))); + let debug = format!("{:?}", c); + assert_eq!(debug, "FunctionCode(GCode(Comment(\"test\")))"); + } + + #[test] + fn test_function_code_serialize() { + //! A `FunctionCode` should implement `GerberCode` + let c = FunctionCode::GCode(GCode::Comment("comment".to_string())); + assert_code!(c, "G04 comment*\n"); + } + + #[test] + fn test_function_code_from_gcode() { + let comment = GCode::Comment("hello".into()); + let f1: FunctionCode = FunctionCode::GCode(comment.clone()); + let f2: FunctionCode = comment.into(); + assert_eq!(f1, f2); + } + + #[test] + fn test_command_from_function_code() { + let comment = FunctionCode::GCode(GCode::Comment("hello".into())); + let c1: Command = Command::FunctionCode(comment.clone()); + let c2: Command = comment.into(); + assert_eq!(c1, c2); + } + + #[test] + fn test_command_from_extended_code() { + let delete_attr = ExtendedCode::DeleteAttribute("hello".into()); + let c1: Command = Command::ExtendedCode(delete_attr.clone()); + let c2: Command = delete_attr.into(); + assert_eq!(c1, c2); + } + + #[test] + fn test_extended_code_from_polarity() { + let e1: ExtendedCode = ExtendedCode::LoadPolarity(Polarity::Dark); + let e2: ExtendedCode = Polarity::Dark.into(); + assert_eq!(e1, e2); + } +}