diff --git a/src/error.rs b/src/error.rs index e3823d8..4706d7b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -132,6 +132,8 @@ pub enum ContentError { }, #[error("No end of line found, expected line to end with '*%'. line: '{line}'")] NoEndOfLine { line: String }, + #[error("Coordinate data without an operation code (deprecated modal D01) is only valid after a D01.")] + CoordinateDataWithoutOperationCode, } impl ContentError { diff --git a/src/parser.rs b/src/parser.rs index 0281305..055c136 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -52,8 +52,10 @@ static RE_FORMAT_SPEC: Lazy = lazy_regex!(r"%FS[LT][AI]X(.*)Y(.*)\*%"); static RE_APERTURE: Lazy = lazy_regex!(r"%ADD([0-9]+)([._$a-zA-Z][._$a-zA-Z0-9]{0,126})(?:,\s?(.*))?\*%"); static RE_APERTURE_BLOCK: Lazy = lazy_regex!(r"%AB(D(?P[0-9]+))?\*%"); +// The `D(0)?1` suffix is optional to support the deprecated modal-D01 form +// (gerber spec 8.3); the caller has already classified the line as interpolate. static RE_INTERPOLATION: Lazy = - lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?I?(-?[0-9]+)?J?(-?[0-9]+)?D(0)?1\*"); + lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?I?(-?[0-9]+)?J?(-?[0-9]+)?(?:D(0)?1)?\*"); static RE_MOVE_OR_FLASH: Lazy = lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?D(0)?[2-3]*"); static RE_IMAGE_NAME: Lazy = lazy_regex!(r"%IN(.*)\*%"); static RE_STEP_REPEAT: Lazy = @@ -68,11 +70,20 @@ static RE_MACRO_DECIMAL: Lazy = lazy_regex!( static RE_MACRO_VARIABLE: Lazy = lazy_regex!(r"\$(?P\d+)\s*=\s*(?P[^*]+)\s*"); +// Implicit operation code for the deprecated "Coordinate Data without Operation Code" form +// (gerber spec 8.3). Set to `Interpolate` by D01; reset to `Undefined` by D02/D03/aperture select. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ModalOperationMode { + Undefined, + Interpolate, +} + struct ParserContext { line_number: usize, lines: Lines>, aperture_attributes: HashMap, object_attributes: HashMap, + modal_operation: ModalOperationMode, } impl ParserContext { @@ -82,6 +93,7 @@ impl ParserContext { lines, aperture_attributes: HashMap::new(), object_attributes: HashMap::new(), + modal_operation: ModalOperationMode::Undefined, } } @@ -98,6 +110,22 @@ impl ParserContext { }) }) } + + // Update the modal operation mode after a command was parsed (gerber spec 8.3). + fn update_modal_from_command(&mut self, cmd: &Command) { + self.modal_operation = match cmd { + Command::FunctionCode(FunctionCode::DCode(DCode::Operation( + Operation::Interpolate(..), + ))) => ModalOperationMode::Interpolate, + Command::FunctionCode(FunctionCode::DCode(DCode::Operation( + Operation::Move(..) | Operation::Flash(..), + ))) + | Command::FunctionCode(FunctionCode::DCode(DCode::SelectAperture(_))) => { + ModalOperationMode::Undefined + } + _ => return, + }; + } } /// Parse a gerber string (in BufReader) to a GerberDoc @@ -143,6 +171,7 @@ pub fn parse(reader: BufReader) -> Result { log::trace!("Parsed command: {:?}", command); + parser_context.update_modal_from_command(&command); Ok(command) } Err(ContentError::IoError(error)) => { @@ -204,7 +233,7 @@ fn parse_line( commands.push(parse_interpolate_move_or_flash( remaining_line, gerber_doc, - &mut linechars, + parser_context.modal_operation, )); } } @@ -218,7 +247,7 @@ fn parse_line( commands.push(parse_interpolate_move_or_flash( remaining_line, gerber_doc, - &mut linechars, + parser_context.modal_operation, )); } } @@ -232,7 +261,7 @@ fn parse_line( commands.push(parse_interpolate_move_or_flash( remaining_line, gerber_doc, - &mut linechars, + parser_context.modal_operation, )); } } @@ -438,7 +467,7 @@ fn parse_line( 'X' | 'Y' => Ok(vec![parse_interpolate_move_or_flash( line, gerber_doc, - &mut linechars, + parser_context.modal_operation, )]), 'D' => { // select aperture D* (where num >= 10) or command where num < 10 @@ -644,17 +673,31 @@ fn parse_axis_select(line: &str) -> Result { fn parse_interpolate_move_or_flash( line: &str, gerber_doc: &mut GerberDoc, - linechars: &mut Chars, + modal: ModalOperationMode, ) -> Result { - linechars.next_back(); - match linechars - .next_back() - .ok_or(ContentError::UnknownCommand {})? - { - '1' => parse_interpolation(line, gerber_doc), // D01 - '2' => parse_move_or_flash(line, gerber_doc, false), // D02 - '3' => parse_move_or_flash(line, gerber_doc, true), // D03 - _ => Err(ContentError::UnknownCommand {}), + // A trailing `D01`/`D02`/`D03` (or single-digit `D1`/`D2`/`D3`) is an explicit op code. + // Coordinates are digits and signs only, so a 'D' preceding the final 1/2/3 is unambiguous. + // Absence means the deprecated modal-D01 form (gerber spec 8.3). + let bytes = line.strip_suffix('*').unwrap_or(line).as_bytes(); + let dcode = match bytes.last().copied() { + Some(d @ (b'1' | b'2' | b'3')) if bytes.len() >= 2 => { + let len = bytes.len(); + let has_dcode = bytes[len - 2] == b'D' + || (len >= 3 && bytes[len - 2] == b'0' && bytes[len - 3] == b'D'); + has_dcode.then_some(d) + } + _ => None, + }; + + match dcode { + Some(b'1') => parse_interpolation(line, gerber_doc), // D01 + Some(b'2') => parse_move_or_flash(line, gerber_doc, false), // D02 + Some(b'3') => parse_move_or_flash(line, gerber_doc, true), // D03 + Some(_) => unreachable!(), + // Deprecated modal D01 (gerber spec 8.3): `RE_INTERPOLATION` accepts the + // suffix-less form, so dispatch straight to it without re-allocating. + None if modal == ModalOperationMode::Interpolate => parse_interpolation(line, gerber_doc), + None => Err(ContentError::CoordinateDataWithoutOperationCode), } } diff --git a/tests/component_tests.rs b/tests/component_tests.rs index e8fe0ac..32bdc20 100644 --- a/tests/component_tests.rs +++ b/tests/component_tests.rs @@ -437,6 +437,137 @@ fn D01_interpolation_linear() { ) } +/// Test deprecated modal D01 (gerber spec 8.3): coordinate data without an operation code +/// implicitly repeats the last D01 until any other D-code is encountered. +#[test] +fn deprecated_modal_d01_coordinate_data_without_operation_code() { + // given + logging_init(); + + // Example adapted from gerber spec 2021.02 ยง8.3. + let reader = gerber_to_reader( + " + %FSLAX23Y23*% + %MOMM*% + + %ADD10C, 0.01*% + %ADD11C, 0.02*% + + D10* + X700Y1000D01* + G04 Modal D01: coordinate-only line repeats the D01* + X1200Y1000* + X1200Y1300* + + G04 Modal mode survives across aperture changes once re-armed by another D01* + D11* + X1700Y2000D01* + X2200Y2000* + X2200Y2300* + + M02* + ", + ); + + let fs = CoordinateFormat::new(ZeroOmission::Leading, CoordinateMode::Absolute, 2, 3); + + // when + parse_and_filter!(reader, commands, filtered_commands, |cmd| matches!( + cmd, + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate(_, _)) + ))) + )); + + // then + assert_eq_commands!( + filtered_commands, + vec![ + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate( + coordinates_from_gerber(700, 1000, fs).unwrap(), + None, + )) + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate( + coordinates_from_gerber(1200, 1000, fs).unwrap(), + None, + )) + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate( + coordinates_from_gerber(1200, 1300, fs).unwrap(), + None, + )) + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate( + coordinates_from_gerber(1700, 2000, fs).unwrap(), + None, + )) + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate( + coordinates_from_gerber(2200, 2000, fs).unwrap(), + None, + )) + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::Operation(Operation::Interpolate( + coordinates_from_gerber(2200, 2300, fs).unwrap(), + None, + )) + ))), + ] + ) +} + +/// A coordinate-only line is invalid when no D01 is in modal effect: after a D02, D03, +/// or aperture selection (gerber spec 8.3). +#[test] +fn deprecated_modal_d01_invalid_without_preceding_d01() { + // given + logging_init(); + + let reader = gerber_to_reader( + " + %FSLAX23Y23*% + %MOMM*% + + %ADD999C, 0.01*% + + G04 Invalid: aperture selection leaves modal mode undefined* + D999* + X100Y100* + + G04 Invalid: a D02 leaves modal mode undefined* + X200Y200D01* + X300Y300D02* + X400Y400* + + G04 Invalid: a D03 leaves modal mode undefined* + X500Y500D01* + X600Y600D03* + X700Y700* + + M02* + ", + ); + + // when + parse_and_filter!(reader, commands, filtered_commands, |cmd| matches!( + cmd, + Err(GerberParserErrorWithContext { + error: ContentError::CoordinateDataWithoutOperationCode, + .. + }) + )); + + // then + assert_eq!(filtered_commands.len(), 3) +} + /// Test the D01* statements (circular) #[test] #[allow(non_snake_case)]