From 5856da0d95ae1fedec02b56833c325a043a50d23 Mon Sep 17 00:00:00 2001 From: Dominic Clifton Date: Fri, 11 Jul 2025 11:08:22 +0200 Subject: [PATCH] Add support for comment attributes. --- CHANGELOG.md | 12 ++++++ examples/polarities-apertures.rs | 15 +++++-- examples/two-boxes.rs | 9 ++-- src/function_codes.rs | 74 +++++++++++++++++++++++++++++--- src/lib.rs | 64 +++++++++++++++++++++++++-- src/types.rs | 18 +++++--- 6 files changed, 170 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ea60b2..2625edeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ Possible log types: - `[fixed]` for any bug fixes. - `[security]` to invite users to upgrade in case of vulnerabilities. +### v0.6.0 (2025-07-10) + +- [added] Added support for G04 'standard comments' where comment attributes are placed in G04 commands. + e.g. 'G04 #@! TA.AperFunction,SMDPad,CuDef*' + This means that when you're looking for attributes, you now have to look in two places: + 1) `Command::ExtendedCode(ExtendedCode::[FileAttribute|ObjectAttribute|ApertureAttribute])` and + 2) `Command::FunctionCode(FunctionCode::GCode(GCode::Comment(CommentContent::Standard(StandardComment::[FileAttribute|ObjectAttribute|ApertureAttribute]))))` + It also means when you're making/serializing gerber files you need to choose where to put the attributes. + Refer to Gerber spec 2024.05 - "4.1 Comment (G04)" and "5.1.1 Comment attributes". + Unfortunately, in 2025, manufacturing files containing comment attributes are still widespread. +- [changed] Removed 'Eq' from `FunctionCode`, due to use of `f64` in attributes (`ExtendedCode` wasn't `Eq` either) + ### v0.5.0 (2025-07-10) - [added] Support for legacy/deprecated gerber commands: `IP`, `MI`, `SF`, `OF`, `IR`, and `AS`. diff --git a/examples/polarities-apertures.rs b/examples/polarities-apertures.rs index 0c88da43..e288a5e8 100644 --- a/examples/polarities-apertures.rs +++ b/examples/polarities-apertures.rs @@ -11,7 +11,10 @@ const VERSION: &'static str = env!("CARGO_PKG_VERSION"); fn main() { let cf = CoordinateFormat::new(2, 6); let commands: Vec = vec![ - FunctionCode::GCode(GCode::Comment("Ucamco ex. 2: Shapes".to_string())).into(), + FunctionCode::GCode(GCode::Comment(CommentContent::String( + "Ucamco ex. 2: Shapes".to_string(), + ))) + .into(), ExtendedCode::CoordinateFormat(cf).into(), ExtendedCode::Unit(Unit::Inches).into(), ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware(GenerationSoftware::new( @@ -25,7 +28,10 @@ fn main() { ))) .into(), ExtendedCode::LoadPolarity(Polarity::Dark).into(), - FunctionCode::GCode(GCode::Comment("Define Apertures".to_string())).into(), + FunctionCode::GCode(GCode::Comment(CommentContent::String( + "Define Apertures".to_string(), + ))) + .into(), ExtendedCode::ApertureMacro(ApertureMacro::new("TARGET125").add_content(MoirePrimitive { center: (0.0.into(), 0.0.into()), diameter: 0.125.into(), @@ -119,7 +125,10 @@ fn main() { aperture: Aperture::Macro("THERMAL80".to_string(), None), }) .into(), - FunctionCode::GCode(GCode::Comment("Start image generation".to_string())).into(), + FunctionCode::GCode(GCode::Comment(CommentContent::String( + "Start image generation".to_string(), + ))) + .into(), FunctionCode::DCode(DCode::SelectAperture(10)).into(), FunctionCode::DCode(DCode::Operation(Operation::Move(Some(Coordinates::new( 0, diff --git a/examples/two-boxes.rs b/examples/two-boxes.rs index 66001b7b..11950935 100644 --- a/examples/two-boxes.rs +++ b/examples/two-boxes.rs @@ -4,8 +4,8 @@ use std::io::stdout; use gerber_types::{ - Aperture, ApertureDefinition, Circle, Command, CoordinateFormat, Coordinates, DCode, - ExtendedCode, FileAttribute, FunctionCode, GCode, GenerationSoftware, GerberCode, + Aperture, ApertureDefinition, Circle, Command, CommentContent, CoordinateFormat, Coordinates, + DCode, ExtendedCode, FileAttribute, FunctionCode, GCode, GenerationSoftware, GerberCode, InterpolationMode, MCode, Operation, Part, Polarity, Unit, }; @@ -14,7 +14,10 @@ const VERSION: &'static str = env!("CARGO_PKG_VERSION"); fn main() { let cf = CoordinateFormat::new(2, 6); let commands: Vec = vec![ - FunctionCode::GCode(GCode::Comment("Ucamco ex. 1: Two square boxes".to_string())).into(), + FunctionCode::GCode(GCode::Comment(CommentContent::String( + "Ucamco ex. 1: Two square boxes".to_string(), + ))) + .into(), ExtendedCode::Unit(Unit::Millimeters).into(), ExtendedCode::CoordinateFormat(cf).into(), ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware(GenerationSoftware::new( diff --git a/src/function_codes.rs b/src/function_codes.rs index bb387171..ae1dafd0 100644 --- a/src/function_codes.rs +++ b/src/function_codes.rs @@ -1,10 +1,10 @@ //! Function code types. -use std::io::Write; - +use crate::attributes; use crate::coordinates::{CoordinateOffset, Coordinates}; use crate::errors::GerberResult; use crate::traits::{GerberCode, PartialGerberCode}; +use std::io::Write; // DCode @@ -26,12 +26,12 @@ impl GerberCode for DCode { // GCode -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum GCode { InterpolationMode(InterpolationMode), RegionMode(bool), QuadrantMode(QuadrantMode), - Comment(String), + Comment(CommentContent), } impl GerberCode for GCode { @@ -46,12 +46,76 @@ impl GerberCode for GCode { } } GCode::QuadrantMode(ref mode) => mode.serialize(writer)?, - GCode::Comment(ref comment) => writeln!(writer, "G04 {}*", comment)?, + GCode::Comment(ref content) => { + write!(writer, "G04 ")?; + content.serialize_partial(writer)?; + writeln!(writer, "*")?; + } }; Ok(()) } } +/// See Gerber spec 2024.05. +/// 1) 4.1 - Comment (G04) +/// 2) 5.1.1 - Comment attributes +#[derive(Debug, Clone, PartialEq)] +pub enum CommentContent { + String(String), + /// "Content starting with ”#@!“ is reserved for standard comments. The purpose of standard + /// comments is to add meta-information in a formally defined manner, without affecting image + /// generation. They can only be used if defined in this specification" + Standard(StandardComment), +} + +impl PartialGerberCode for CommentContent { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + match *self { + CommentContent::String(ref string) => { + write!(writer, "{}", string)?; + } + CommentContent::Standard(ref standard) => { + standard.serialize_partial(writer)?; + } + } + Ok(()) + } +} + +/// See Gerber spec 2024.05. +/// 1) 4.1 - Comment (G04) +/// 2) 5.1.1 - Comment attributes +#[derive(Debug, Clone, PartialEq)] +pub enum StandardComment { + /// TF + FileAttribute(attributes::FileAttribute), + /// TO + ObjectAttribute(attributes::ObjectAttribute), + /// TA + ApertureAttribute(attributes::ApertureAttribute), +} + +impl PartialGerberCode for StandardComment { + fn serialize_partial(&self, writer: &mut W) -> GerberResult<()> { + write!(writer, "#@! ")?; + match *self { + StandardComment::FileAttribute(ref fa) => { + write!(writer, "TF")?; + fa.serialize_partial(writer)?; + } + StandardComment::ObjectAttribute(ref oa) => { + write!(writer, "TO")?; + oa.serialize_partial(writer)?; + } + StandardComment::ApertureAttribute(ref aa) => { + write!(writer, "TA")?; + aa.serialize_partial(writer)?; + } + } + Ok(()) + } +} + // MCode #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index 1f01c31a..c7f81df3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,23 +57,79 @@ mod serialization_tests { #[test] fn test_comment() { //! The serialize method of the GerberCode trait should generate strings. - let comment = GCode::Comment("testcomment".to_string()); + let comment = GCode::Comment(CommentContent::String("testcomment".to_string())); assert_code!(comment, "G04 testcomment*\n"); } + /// `standard comment` is a term defined in the gerber spec. See `2024.05 4.1 Comment (G04)` + #[test] + fn test_standard_comment_with_standard_attributes() { + //! Attributes should be able to be stored in G04 comments starting with `#@!` + let comment = GCode::Comment(CommentContent::Standard( + StandardComment::ApertureAttribute(ApertureAttribute::ApertureFunction( + ApertureFunction::SmdPad(SmdPadType::CopperDefined), + )), + )); + assert_code!(comment, "G04 #@! TA.AperFunction,SMDPad,CuDef*\n"); + + let comment = GCode::Comment(CommentContent::Standard(StandardComment::FileAttribute( + FileAttribute::FileFunction(FileFunction::Profile(Some(Profile::NonPlated))), + ))); + assert_code!(comment, "G04 #@! TF.FileFunction,Profile,NP*\n"); + + let comment = GCode::Comment(CommentContent::Standard(StandardComment::ObjectAttribute( + ObjectAttribute::Component("R1".to_string()), + ))); + assert_code!(comment, "G04 #@! TO.C,R1*\n"); + } + + #[test] + fn test_standard_comment_with_custom_attributes() { + // custom attributes are not prefixed with a `.`. + let comment = GCode::Comment(CommentContent::Standard( + StandardComment::ApertureAttribute(ApertureAttribute::UserDefined { + name: "Example".to_string(), + values: vec!["value1".to_string(), "value2".to_string()], + }), + )); + assert_code!(comment, "G04 #@! TAExample,value1,value2*\n"); + + let comment = GCode::Comment(CommentContent::Standard(StandardComment::FileAttribute( + FileAttribute::UserDefined { + name: "Example".to_string(), + values: vec!["value1".to_string(), "value2".to_string()], + }, + ))); + assert_code!(comment, "G04 #@! TFExample,value1,value2*\n"); + + let comment = GCode::Comment(CommentContent::Standard(StandardComment::ObjectAttribute( + ObjectAttribute::UserDefined { + name: "Example".to_string(), + values: vec!["value1".to_string(), "value2".to_string()], + }, + ))); + assert_code!(comment, "G04 #@! TOExample,value1,value2*\n"); + } + #[test] fn test_vec_of_comments() { //! 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())); + v.push(GCode::Comment(CommentContent::String( + "comment 1".to_string(), + ))); + v.push(GCode::Comment(CommentContent::String( + "another one".to_string(), + ))); assert_code!(v, "G04 comment 1*\nG04 another one*\n"); } #[test] fn test_single_command() { //! A `Command` should implement `GerberCode` - let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment("comment".to_string()))); + let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment(CommentContent::String( + "comment".to_string(), + )))); assert_code!(c, "G04 comment*\n"); } diff --git a/src/types.rs b/src/types.rs index b3cf8431..f18aace1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -47,7 +47,7 @@ macro_rules! impl_command_fromfrom { // Main categories -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum FunctionCode { DCode(function_codes::DCode), GCode(function_codes::GCode), @@ -185,26 +185,30 @@ mod test { use crate::extended_codes::Polarity; use crate::function_codes::GCode; use crate::traits::GerberCode; - use crate::{ApertureBlock, Mirroring, Rotation, Scaling, StepAndRepeat}; + use crate::{ApertureBlock, CommentContent, Mirroring, Rotation, Scaling, StepAndRepeat}; #[test] fn test_debug() { //! The debug representation should work properly. - let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment("test".to_string()))); + let c = Command::FunctionCode(FunctionCode::GCode(GCode::Comment(CommentContent::String( + "test".to_string(), + )))); let debug = format!("{:?}", c); - assert_eq!(debug, "FunctionCode(GCode(Comment(\"test\")))"); + assert_eq!(debug, "FunctionCode(GCode(Comment(String(\"test\"))))"); } #[test] fn test_function_code_serialize() { //! A `FunctionCode` should implement `GerberCode` - let c = FunctionCode::GCode(GCode::Comment("comment".to_string())); + let c = FunctionCode::GCode(GCode::Comment(CommentContent::String( + "comment".to_string(), + ))); assert_code!(c, "G04 comment*\n"); } #[test] fn test_function_code_from_gcode() { - let comment = GCode::Comment("hello".into()); + let comment = GCode::Comment(CommentContent::String("hello".into())); let f1: FunctionCode = FunctionCode::GCode(comment.clone()); let f2: FunctionCode = comment.into(); assert_eq!(f1, f2); @@ -212,7 +216,7 @@ mod test { #[test] fn test_command_from_function_code() { - let comment = FunctionCode::GCode(GCode::Comment("hello".into())); + let comment = FunctionCode::GCode(GCode::Comment(CommentContent::String("hello".into()))); let c1: Command = Command::FunctionCode(comment.clone()); let c2: Command = comment.into(); assert_eq!(c1, c2);