Skip to content

Commit 78f4d53

Browse files
committed
Add support for load-mirroring, rotation and scaling.
1 parent c2e7c77 commit 78f4d53

3 files changed

Lines changed: 213 additions & 36 deletions

File tree

README.md

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,30 @@ Recently support has been added for the files that DipTrace, KiCad, etc.
2222

2323
### Supported gerber features
2424

25-
| Supported | Feature | Notes |
26-
|----------|----------------------------------|------------------------------------------------|
27-
|| Comments (G04) | |
28-
|| Units (MO) | |
29-
|| Format specification (FS) | |
30-
|| Aperture definition (AD) | |
31-
|| Standard aperture templates | |
32-
|| Aperture macros (AM) | |
33-
|| Select aperture (Dnn) | |
34-
|| Plot state (G01, G02, G03, G75) | |
35-
|| Operations (D01, D02, D03) | |
36-
| | Transformations (LP, LM, LR, LS) | `LM`, `LR`, `LS` missing |
37-
|| Regions (G36/G37) | |
38-
|| Block aperture (AB) | |
39-
|| Step repeat (SR) | |
40-
|| End-of-file (M02) | |
41-
|| File attributes (TF) | |
42-
|| Aperture attributes (TA) | |
43-
|| Object attributes (TO) | |
44-
|| Delete attribute (TD) | |
45-
|| Standard attributes | Full support, including .N, .P, .C, .CXxxx, etc |
46-
|| User defined attributes | |
47-
|| Comment attributes | See spec 2024.5 - 5.1.1, 'G04 #@! ...* |
48-
|| Legacy/deprecated attributes | Partial |
25+
| Supported | Feature | Notes |
26+
|-----------|----------------------------------|-------------------------------------------------|
27+
| | Comments (G04) | |
28+
| | Units (MO) | |
29+
| | Format specification (FS) | |
30+
| | Aperture definition (AD) | |
31+
| | Standard aperture templates | |
32+
| | Aperture macros (AM) | |
33+
| | Select aperture (Dnn) | |
34+
| | Plot state (G01, G02, G03, G75) | |
35+
| | Operations (D01, D02, D03) | |
36+
| | Transformations (LP, LM, LR, LS) | |
37+
| | Regions (G36/G37) | |
38+
| | Block aperture (AB) | |
39+
| | Step repeat (SR) | |
40+
| | End-of-file (M02) | |
41+
| | File attributes (TF) | |
42+
| | Aperture attributes (TA) | |
43+
| | Object attributes (TO) | |
44+
| | Delete attribute (TD) | |
45+
| | Standard attributes | Full support, including .N, .P, .C, .CXxxx, etc |
46+
| | User defined attributes | |
47+
| | Comment attributes | See spec 2024.5 - 5.1.1, 'G04 #@! ...* |
48+
|| Legacy/deprecated attributes | Partial |
4949

5050
Contributions to improve support welcomed!
5151

src/parser.rs

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ use crate::ParseError;
1212
use gerber_types::{
1313
ApertureBlock, ComponentCharacteristics, ComponentDrill, ComponentMounting, ComponentOutline,
1414
CopperType, DrillFunction, DrillRouteType, ExtendedPosition, GenerationSoftware, GerberDate,
15-
GerberError, IPC4761ViaProtection, Ident, Net, NonPlatedDrill, ObjectAttribute,
16-
Pin, PlatedDrill, Position, Profile, ThermalPrimitive, Uuid,
17-
VariableDefinition,
15+
GerberError, IPC4761ViaProtection, Ident, Mirroring, Net, NonPlatedDrill, ObjectAttribute, Pin,
16+
PlatedDrill, Position, Profile, Rotation, Scaling, ThermalPrimitive, Uuid, VariableDefinition,
1817
};
1918
use lazy_regex::*;
2019
use regex::Regex;
@@ -30,6 +29,11 @@ use std::sync::LazyLock;
3029
// constants for the capture names, this would improve the error messages too.
3130
static RE_UNITS: Lazy<Regex> = lazy_regex!(r"%MO(.*)\*%");
3231
static RE_COMMENT: Lazy<Regex> = lazy_regex!(r"G04 (.*)\*");
32+
static RE_LOAD_MIRRORING: Lazy<Regex> = lazy_regex!(r"%LM(?P<mirroring>N|X|Y|XY)\*%");
33+
34+
// Note: scaling cannot be negative, or 0.
35+
static RE_LOAD_SCALING: Lazy<Regex> = lazy_regex!(r"%LS(?P<value>[0-9]+(?:\.[0-9]*)?)\*%");
36+
static RE_LOAD_ROTATION: Lazy<Regex> = lazy_regex!(r"%LR(?P<value>[+-]?[0-9]+(?:\.[0-9]*)?)\*%");
3337
static RE_FORMAT_SPEC: Lazy<Regex> = lazy_regex!(r"%FSLAX(.*)Y(.*)\*%");
3438

3539
/// https://regex101.com/r/YNnrmK/1
@@ -310,11 +314,11 @@ fn parse_line<T: Read>(
310314
_ => Err(ContentError::UnknownCommand {}),
311315
},
312316
// LM
313-
'M' => Err(ContentError::UnsupportedCommand {}),
317+
'M' => parse_load_mirroring(line),
314318
// LR
315-
'R' => Err(ContentError::UnsupportedCommand {}),
319+
'R' => parse_load_rotation(line),
316320
// LS
317-
'S' => Err(ContentError::UnsupportedCommand {}),
321+
'S' => parse_load_scaling(line),
318322
_ => Err(ContentError::UnknownCommand {}),
319323
},
320324
'T' => match linechars.next().ok_or(ContentError::UnknownCommand {})? {
@@ -395,6 +399,82 @@ fn parse_interpolate_move_or_flash(
395399
}
396400
}
397401

402+
fn parse_load_mirroring(line: &str) -> Result<Command, ContentError> {
403+
build_enum_map!(LOAD_MIRRORING_MAP, Mirroring);
404+
405+
match RE_LOAD_MIRRORING.captures(line) {
406+
Some(captures) => {
407+
let value = captures
408+
.name("mirroring")
409+
.ok_or(ContentError::MissingRegexNamedCapture {
410+
regex: RE_LOAD_MIRRORING.clone(),
411+
capture_name: "mirroring".to_string(),
412+
})?
413+
.as_str();
414+
415+
let mirroring = LOAD_MIRRORING_MAP.get(&value.to_lowercase()).ok_or(
416+
ContentError::InvalidParameter {
417+
parameter: value.to_string(),
418+
},
419+
)?;
420+
421+
Ok(ExtendedCode::LoadMirroring(*mirroring).into())
422+
}
423+
None => Err(ContentError::NoRegexMatch {
424+
regex: RE_LOAD_MIRRORING.clone(),
425+
}),
426+
}
427+
}
428+
429+
fn parse_load_scaling(line: &str) -> Result<Command, ContentError> {
430+
match RE_LOAD_SCALING.captures(line) {
431+
Some(captures) => {
432+
let scale = captures
433+
.name("value")
434+
.ok_or(ContentError::MissingRegexNamedCapture {
435+
regex: RE_LOAD_SCALING.clone(),
436+
capture_name: "value".to_string(),
437+
})?
438+
.as_str()
439+
.parse::<f64>()
440+
.map_err(|cause| ContentError::ParseDecimalError { cause })?;
441+
442+
if scale <= 0.0 {
443+
// Gerber spec 2025.05 - 4.9.5 Load Scaling (LS) "<Scale> A decimal > 0."
444+
return Err(ContentError::InvalidParameter {
445+
parameter: scale.to_string(),
446+
});
447+
}
448+
449+
Ok(ExtendedCode::LoadScaling(Scaling { scale }).into())
450+
}
451+
None => Err(ContentError::NoRegexMatch {
452+
regex: RE_LOAD_MIRRORING.clone(),
453+
}),
454+
}
455+
}
456+
457+
fn parse_load_rotation(line: &str) -> Result<Command, ContentError> {
458+
match RE_LOAD_ROTATION.captures(line) {
459+
Some(captures) => {
460+
let rotation = captures
461+
.name("value")
462+
.ok_or(ContentError::MissingRegexNamedCapture {
463+
regex: RE_LOAD_ROTATION.clone(),
464+
capture_name: "value".to_string(),
465+
})?
466+
.as_str()
467+
.parse::<f64>()
468+
.map_err(|cause| ContentError::ParseDecimalError { cause })?;
469+
470+
Ok(ExtendedCode::LoadRotation(Rotation { rotation }).into())
471+
}
472+
None => Err(ContentError::NoRegexMatch {
473+
regex: RE_LOAD_ROTATION.clone(),
474+
}),
475+
}
476+
}
477+
398478
/// parse a Gerber Comment (e.g. 'G04 This is a comment*')
399479
fn parse_comment(line: &str) -> Result<Command, ContentError> {
400480
match RE_COMMENT.captures(line) {

tests/component_tests.rs

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use ::std::collections::HashMap;
21
use gerber_parser::{
32
coordinates_from_gerber, coordinates_offset_from_gerber, parse, partial_coordinates_from_gerber,
43
};
@@ -10,11 +9,13 @@ use gerber_types::{
109
CopperType, DCode, DrillFunction, DrillRouteType, ExtendedCode, ExtendedPosition,
1110
FiducialScope, FileAttribute, FileFunction, FilePolarity, FunctionCode, GCode,
1211
GenerationSoftware, GerberDate, GerberError, IPC4761ViaProtection, Ident, InterpolationMode,
13-
MCode, MacroBoolean, MacroContent, MacroDecimal, MacroInteger, Net, NonPlatedDrill,
14-
ObjectAttribute, Operation, OutlinePrimitive, Part, Pin, PlatedDrill, Polygon,
15-
PolygonPrimitive, Position, Profile, QuadrantMode, Rectangular, SmdPadType, StepAndRepeat,
16-
ThermalPrimitive, Unit, Uuid, VariableDefinition, VectorLinePrimitive,
12+
MCode, MacroBoolean, MacroContent, MacroDecimal, MacroInteger, Mirroring, Net, NonPlatedDrill,
13+
ObjectAttribute, Operation, OutlinePrimitive, Part, Pin, PlatedDrill, Polarity, Polygon,
14+
PolygonPrimitive, Position, Profile, QuadrantMode, Rectangular, Rotation, Scaling, SmdPadType,
15+
StepAndRepeat, ThermalPrimitive, Unit, Uuid, VariableDefinition, VectorLinePrimitive,
1716
};
17+
use std::collections::HashMap;
18+
use strum::VariantArray;
1819
mod util;
1920
use gerber_parser::util::gerber_to_reader;
2021
use util::testing::logging_init;
@@ -548,6 +549,102 @@ fn omitted_coordinate() {
548549
)
549550
}
550551

552+
// See gerber spec 2021-02, section 4.5
553+
#[test]
554+
fn test_load_polarity_scaling_mirroring_rotation() {
555+
// given
556+
let reader = gerber_to_reader(
557+
r#"
558+
%LPD*%
559+
%LPC*%
560+
%LMN*%
561+
%LMX*%
562+
%LMY*%
563+
%LMXY*%
564+
%LR0*%
565+
%LR359.99*%
566+
%LR-359.99*%
567+
%LS0.5*%
568+
%LS99.99*%
569+
"#,
570+
);
571+
572+
// when
573+
parse_and_filter!(reader, commands, filtered_commands, |cmd| matches!(
574+
cmd,
575+
Ok(Command::ExtendedCode(ExtendedCode::LoadPolarity(_)))
576+
| Ok(Command::ExtendedCode(ExtendedCode::LoadMirroring(_)))
577+
| Ok(Command::ExtendedCode(ExtendedCode::LoadScaling(_)))
578+
| Ok(Command::ExtendedCode(ExtendedCode::LoadRotation(_)))
579+
));
580+
581+
// then
582+
assert_eq_commands!(
583+
filtered_commands,
584+
vec![
585+
Ok(Command::ExtendedCode(ExtendedCode::LoadPolarity(
586+
Polarity::Dark
587+
))),
588+
Ok(Command::ExtendedCode(ExtendedCode::LoadPolarity(
589+
Polarity::Clear
590+
))),
591+
Ok(Command::ExtendedCode(ExtendedCode::LoadMirroring(
592+
Mirroring::None
593+
))),
594+
Ok(Command::ExtendedCode(ExtendedCode::LoadMirroring(
595+
Mirroring::X
596+
))),
597+
Ok(Command::ExtendedCode(ExtendedCode::LoadMirroring(
598+
Mirroring::Y
599+
))),
600+
Ok(Command::ExtendedCode(ExtendedCode::LoadMirroring(
601+
Mirroring::XY
602+
))),
603+
Ok(Command::ExtendedCode(ExtendedCode::LoadRotation(
604+
Rotation { rotation: 0.0 }
605+
))),
606+
Ok(Command::ExtendedCode(ExtendedCode::LoadRotation(
607+
Rotation { rotation: 359.99 }
608+
))),
609+
Ok(Command::ExtendedCode(ExtendedCode::LoadRotation(
610+
Rotation { rotation: -359.99 }
611+
))),
612+
Ok(Command::ExtendedCode(ExtendedCode::LoadScaling(Scaling {
613+
scale: 0.5
614+
}))),
615+
Ok(Command::ExtendedCode(ExtendedCode::LoadScaling(Scaling {
616+
scale: 99.99
617+
}))),
618+
]
619+
);
620+
}
621+
622+
// See gerber spec 2021-02, section 4.5
623+
#[test]
624+
fn test_load_scaling_zero() {
625+
// given
626+
let reader = gerber_to_reader(
627+
r#"
628+
%LS0*%
629+
"#,
630+
);
631+
// when
632+
let doc = parse(reader).unwrap();
633+
dump_commands(&doc.commands);
634+
635+
// then
636+
let errors = doc.into_errors();
637+
638+
assert!(matches!(errors.first().unwrap(),
639+
GerberParserErrorWithContext {
640+
error: ContentError::InvalidParameter {
641+
parameter,
642+
},
643+
line: Some((number, content)),
644+
} if parameter.eq("0") && *number == 2 && content.eq("%LS0*%")
645+
));
646+
}
647+
551648
/// Test Step and Repeat command (%SR*%)
552649
#[test]
553650
fn step_and_repeat() {
@@ -841,7 +938,7 @@ fn TA_aperture_attributes() {
841938
($name:ident) => {{
842939
let mut result = vec![ApertureFunction::$name(None)];
843940
result.extend(
844-
IPC4761ViaProtection::values()
941+
<IPC4761ViaProtection as VariantArray>::VARIANTS
845942
.iter()
846943
.cloned()
847944
.map(|value| ApertureFunction::$name(Some(value)))

0 commit comments

Comments
 (0)