Skip to content

Commit 50ff315

Browse files
authored
Merge pull request #24 from nicolube/fix/unparsable_coords
Support deprecated modal D01 (gerber spec §8.3): coordinate-only lines after a D01 implicitly repeat it.
2 parents d1998ce + aee41fa commit 50ff315

3 files changed

Lines changed: 191 additions & 15 deletions

File tree

src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ pub enum ContentError {
132132
},
133133
#[error("No end of line found, expected line to end with '*%'. line: '{line}'")]
134134
NoEndOfLine { line: String },
135+
#[error("Coordinate data without an operation code (deprecated modal D01) is only valid after a D01.")]
136+
CoordinateDataWithoutOperationCode,
135137
}
136138

137139
impl ContentError {

src/parser.rs

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ static RE_FORMAT_SPEC: Lazy<Regex> = lazy_regex!(r"%FS[LT][AI]X(.*)Y(.*)\*%");
5252
static RE_APERTURE: Lazy<Regex> =
5353
lazy_regex!(r"%ADD([0-9]+)([._$a-zA-Z][._$a-zA-Z0-9]{0,126})(?:,\s?(.*))?\*%");
5454
static RE_APERTURE_BLOCK: Lazy<Regex> = lazy_regex!(r"%AB(D(?P<code>[0-9]+))?\*%");
55+
// The `D(0)?1` suffix is optional to support the deprecated modal-D01 form
56+
// (gerber spec 8.3); the caller has already classified the line as interpolate.
5557
static RE_INTERPOLATION: Lazy<Regex> =
56-
lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?I?(-?[0-9]+)?J?(-?[0-9]+)?D(0)?1\*");
58+
lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?I?(-?[0-9]+)?J?(-?[0-9]+)?(?:D(0)?1)?\*");
5759
static RE_MOVE_OR_FLASH: Lazy<Regex> = lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?D(0)?[2-3]*");
5860
static RE_IMAGE_NAME: Lazy<Regex> = lazy_regex!(r"%IN(.*)\*%");
5961
static RE_STEP_REPEAT: Lazy<Regex> =
@@ -68,11 +70,20 @@ static RE_MACRO_DECIMAL: Lazy<Regex> = lazy_regex!(
6870
static RE_MACRO_VARIABLE: Lazy<Regex> =
6971
lazy_regex!(r"\$(?P<number>\d+)\s*=\s*(?P<expression>[^*]+)\s*");
7072

73+
// Implicit operation code for the deprecated "Coordinate Data without Operation Code" form
74+
// (gerber spec 8.3). Set to `Interpolate` by D01; reset to `Undefined` by D02/D03/aperture select.
75+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
76+
enum ModalOperationMode {
77+
Undefined,
78+
Interpolate,
79+
}
80+
7181
struct ParserContext<T: Read> {
7282
line_number: usize,
7383
lines: Lines<BufReader<T>>,
7484
aperture_attributes: HashMap<String, ApertureAttribute>,
7585
object_attributes: HashMap<String, ObjectAttribute>,
86+
modal_operation: ModalOperationMode,
7687
}
7788

7889
impl<T: Read> ParserContext<T> {
@@ -82,6 +93,7 @@ impl<T: Read> ParserContext<T> {
8293
lines,
8394
aperture_attributes: HashMap::new(),
8495
object_attributes: HashMap::new(),
96+
modal_operation: ModalOperationMode::Undefined,
8597
}
8698
}
8799

@@ -98,6 +110,22 @@ impl<T: Read> ParserContext<T> {
98110
})
99111
})
100112
}
113+
114+
// Update the modal operation mode after a command was parsed (gerber spec 8.3).
115+
fn update_modal_from_command(&mut self, cmd: &Command) {
116+
self.modal_operation = match cmd {
117+
Command::FunctionCode(FunctionCode::DCode(DCode::Operation(
118+
Operation::Interpolate(..),
119+
))) => ModalOperationMode::Interpolate,
120+
Command::FunctionCode(FunctionCode::DCode(DCode::Operation(
121+
Operation::Move(..) | Operation::Flash(..),
122+
)))
123+
| Command::FunctionCode(FunctionCode::DCode(DCode::SelectAperture(_))) => {
124+
ModalOperationMode::Undefined
125+
}
126+
_ => return,
127+
};
128+
}
101129
}
102130

103131
/// Parse a gerber string (in BufReader) to a GerberDoc
@@ -143,6 +171,7 @@ pub fn parse<T: Read>(reader: BufReader<T>) -> Result<GerberDoc, (GerberDoc, Par
143171
let final_result = match result {
144172
Ok(command) => {
145173
log::trace!("Parsed command: {:?}", command);
174+
parser_context.update_modal_from_command(&command);
146175
Ok(command)
147176
}
148177
Err(ContentError::IoError(error)) => {
@@ -204,7 +233,7 @@ fn parse_line<T: Read>(
204233
commands.push(parse_interpolate_move_or_flash(
205234
remaining_line,
206235
gerber_doc,
207-
&mut linechars,
236+
parser_context.modal_operation,
208237
));
209238
}
210239
}
@@ -218,7 +247,7 @@ fn parse_line<T: Read>(
218247
commands.push(parse_interpolate_move_or_flash(
219248
remaining_line,
220249
gerber_doc,
221-
&mut linechars,
250+
parser_context.modal_operation,
222251
));
223252
}
224253
}
@@ -232,7 +261,7 @@ fn parse_line<T: Read>(
232261
commands.push(parse_interpolate_move_or_flash(
233262
remaining_line,
234263
gerber_doc,
235-
&mut linechars,
264+
parser_context.modal_operation,
236265
));
237266
}
238267
}
@@ -438,7 +467,7 @@ fn parse_line<T: Read>(
438467
'X' | 'Y' => Ok(vec![parse_interpolate_move_or_flash(
439468
line,
440469
gerber_doc,
441-
&mut linechars,
470+
parser_context.modal_operation,
442471
)]),
443472
'D' => {
444473
// select aperture D<num>* (where num >= 10) or command where num < 10
@@ -644,17 +673,31 @@ fn parse_axis_select(line: &str) -> Result<Command, ContentError> {
644673
fn parse_interpolate_move_or_flash(
645674
line: &str,
646675
gerber_doc: &mut GerberDoc,
647-
linechars: &mut Chars,
676+
modal: ModalOperationMode,
648677
) -> Result<Command, ContentError> {
649-
linechars.next_back();
650-
match linechars
651-
.next_back()
652-
.ok_or(ContentError::UnknownCommand {})?
653-
{
654-
'1' => parse_interpolation(line, gerber_doc), // D01
655-
'2' => parse_move_or_flash(line, gerber_doc, false), // D02
656-
'3' => parse_move_or_flash(line, gerber_doc, true), // D03
657-
_ => Err(ContentError::UnknownCommand {}),
678+
// A trailing `D01`/`D02`/`D03` (or single-digit `D1`/`D2`/`D3`) is an explicit op code.
679+
// Coordinates are digits and signs only, so a 'D' preceding the final 1/2/3 is unambiguous.
680+
// Absence means the deprecated modal-D01 form (gerber spec 8.3).
681+
let bytes = line.strip_suffix('*').unwrap_or(line).as_bytes();
682+
let dcode = match bytes.last().copied() {
683+
Some(d @ (b'1' | b'2' | b'3')) if bytes.len() >= 2 => {
684+
let len = bytes.len();
685+
let has_dcode = bytes[len - 2] == b'D'
686+
|| (len >= 3 && bytes[len - 2] == b'0' && bytes[len - 3] == b'D');
687+
has_dcode.then_some(d)
688+
}
689+
_ => None,
690+
};
691+
692+
match dcode {
693+
Some(b'1') => parse_interpolation(line, gerber_doc), // D01
694+
Some(b'2') => parse_move_or_flash(line, gerber_doc, false), // D02
695+
Some(b'3') => parse_move_or_flash(line, gerber_doc, true), // D03
696+
Some(_) => unreachable!(),
697+
// Deprecated modal D01 (gerber spec 8.3): `RE_INTERPOLATION` accepts the
698+
// suffix-less form, so dispatch straight to it without re-allocating.
699+
None if modal == ModalOperationMode::Interpolate => parse_interpolation(line, gerber_doc),
700+
None => Err(ContentError::CoordinateDataWithoutOperationCode),
658701
}
659702
}
660703

tests/component_tests.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,137 @@ fn D01_interpolation_linear() {
437437
)
438438
}
439439

440+
/// Test deprecated modal D01 (gerber spec 8.3): coordinate data without an operation code
441+
/// implicitly repeats the last D01 until any other D-code is encountered.
442+
#[test]
443+
fn deprecated_modal_d01_coordinate_data_without_operation_code() {
444+
// given
445+
logging_init();
446+
447+
// Example adapted from gerber spec 2021.02 §8.3.
448+
let reader = gerber_to_reader(
449+
"
450+
%FSLAX23Y23*%
451+
%MOMM*%
452+
453+
%ADD10C, 0.01*%
454+
%ADD11C, 0.02*%
455+
456+
D10*
457+
X700Y1000D01*
458+
G04 Modal D01: coordinate-only line repeats the D01*
459+
X1200Y1000*
460+
X1200Y1300*
461+
462+
G04 Modal mode survives across aperture changes once re-armed by another D01*
463+
D11*
464+
X1700Y2000D01*
465+
X2200Y2000*
466+
X2200Y2300*
467+
468+
M02*
469+
",
470+
);
471+
472+
let fs = CoordinateFormat::new(ZeroOmission::Leading, CoordinateMode::Absolute, 2, 3);
473+
474+
// when
475+
parse_and_filter!(reader, commands, filtered_commands, |cmd| matches!(
476+
cmd,
477+
Ok(Command::FunctionCode(FunctionCode::DCode(
478+
DCode::Operation(Operation::Interpolate(_, _))
479+
)))
480+
));
481+
482+
// then
483+
assert_eq_commands!(
484+
filtered_commands,
485+
vec![
486+
Ok(Command::FunctionCode(FunctionCode::DCode(
487+
DCode::Operation(Operation::Interpolate(
488+
coordinates_from_gerber(700, 1000, fs).unwrap(),
489+
None,
490+
))
491+
))),
492+
Ok(Command::FunctionCode(FunctionCode::DCode(
493+
DCode::Operation(Operation::Interpolate(
494+
coordinates_from_gerber(1200, 1000, fs).unwrap(),
495+
None,
496+
))
497+
))),
498+
Ok(Command::FunctionCode(FunctionCode::DCode(
499+
DCode::Operation(Operation::Interpolate(
500+
coordinates_from_gerber(1200, 1300, fs).unwrap(),
501+
None,
502+
))
503+
))),
504+
Ok(Command::FunctionCode(FunctionCode::DCode(
505+
DCode::Operation(Operation::Interpolate(
506+
coordinates_from_gerber(1700, 2000, fs).unwrap(),
507+
None,
508+
))
509+
))),
510+
Ok(Command::FunctionCode(FunctionCode::DCode(
511+
DCode::Operation(Operation::Interpolate(
512+
coordinates_from_gerber(2200, 2000, fs).unwrap(),
513+
None,
514+
))
515+
))),
516+
Ok(Command::FunctionCode(FunctionCode::DCode(
517+
DCode::Operation(Operation::Interpolate(
518+
coordinates_from_gerber(2200, 2300, fs).unwrap(),
519+
None,
520+
))
521+
))),
522+
]
523+
)
524+
}
525+
526+
/// A coordinate-only line is invalid when no D01 is in modal effect: after a D02, D03,
527+
/// or aperture selection (gerber spec 8.3).
528+
#[test]
529+
fn deprecated_modal_d01_invalid_without_preceding_d01() {
530+
// given
531+
logging_init();
532+
533+
let reader = gerber_to_reader(
534+
"
535+
%FSLAX23Y23*%
536+
%MOMM*%
537+
538+
%ADD999C, 0.01*%
539+
540+
G04 Invalid: aperture selection leaves modal mode undefined*
541+
D999*
542+
X100Y100*
543+
544+
G04 Invalid: a D02 leaves modal mode undefined*
545+
X200Y200D01*
546+
X300Y300D02*
547+
X400Y400*
548+
549+
G04 Invalid: a D03 leaves modal mode undefined*
550+
X500Y500D01*
551+
X600Y600D03*
552+
X700Y700*
553+
554+
M02*
555+
",
556+
);
557+
558+
// when
559+
parse_and_filter!(reader, commands, filtered_commands, |cmd| matches!(
560+
cmd,
561+
Err(GerberParserErrorWithContext {
562+
error: ContentError::CoordinateDataWithoutOperationCode,
563+
..
564+
})
565+
));
566+
567+
// then
568+
assert_eq!(filtered_commands.len(), 3)
569+
}
570+
440571
/// Test the D01* statements (circular)
441572
#[test]
442573
#[allow(non_snake_case)]

0 commit comments

Comments
 (0)