Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
73 changes: 58 additions & 15 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ static RE_FORMAT_SPEC: Lazy<Regex> = lazy_regex!(r"%FS[LT][AI]X(.*)Y(.*)\*%");
static RE_APERTURE: Lazy<Regex> =
lazy_regex!(r"%ADD([0-9]+)([._$a-zA-Z][._$a-zA-Z0-9]{0,126})(?:,\s?(.*))?\*%");
static RE_APERTURE_BLOCK: Lazy<Regex> = lazy_regex!(r"%AB(D(?P<code>[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<Regex> =
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<Regex> = lazy_regex!(r"X?(-?[0-9]+)?Y?(-?[0-9]+)?D(0)?[2-3]*");
static RE_IMAGE_NAME: Lazy<Regex> = lazy_regex!(r"%IN(.*)\*%");
static RE_STEP_REPEAT: Lazy<Regex> =
Expand All @@ -68,11 +70,20 @@ static RE_MACRO_DECIMAL: Lazy<Regex> = lazy_regex!(
static RE_MACRO_VARIABLE: Lazy<Regex> =
lazy_regex!(r"\$(?P<number>\d+)\s*=\s*(?P<expression>[^*]+)\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<T: Read> {
line_number: usize,
lines: Lines<BufReader<T>>,
aperture_attributes: HashMap<String, ApertureAttribute>,
object_attributes: HashMap<String, ObjectAttribute>,
modal_operation: ModalOperationMode,
}

impl<T: Read> ParserContext<T> {
Expand All @@ -82,6 +93,7 @@ impl<T: Read> ParserContext<T> {
lines,
aperture_attributes: HashMap::new(),
object_attributes: HashMap::new(),
modal_operation: ModalOperationMode::Undefined,
}
}

Expand All @@ -98,6 +110,22 @@ impl<T: Read> ParserContext<T> {
})
})
}

// 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
Expand Down Expand Up @@ -143,6 +171,7 @@ pub fn parse<T: Read>(reader: BufReader<T>) -> Result<GerberDoc, (GerberDoc, Par
let final_result = match result {
Ok(command) => {
log::trace!("Parsed command: {:?}", command);
parser_context.update_modal_from_command(&command);
Ok(command)
}
Err(ContentError::IoError(error)) => {
Expand Down Expand Up @@ -204,7 +233,7 @@ fn parse_line<T: Read>(
commands.push(parse_interpolate_move_or_flash(
remaining_line,
gerber_doc,
&mut linechars,
parser_context.modal_operation,
));
}
}
Expand All @@ -218,7 +247,7 @@ fn parse_line<T: Read>(
commands.push(parse_interpolate_move_or_flash(
remaining_line,
gerber_doc,
&mut linechars,
parser_context.modal_operation,
));
}
}
Expand All @@ -232,7 +261,7 @@ fn parse_line<T: Read>(
commands.push(parse_interpolate_move_or_flash(
remaining_line,
gerber_doc,
&mut linechars,
parser_context.modal_operation,
));
}
}
Expand Down Expand Up @@ -438,7 +467,7 @@ fn parse_line<T: Read>(
'X' | 'Y' => Ok(vec![parse_interpolate_move_or_flash(
line,
gerber_doc,
&mut linechars,
parser_context.modal_operation,
)]),
'D' => {
// select aperture D<num>* (where num >= 10) or command where num < 10
Expand Down Expand Up @@ -644,17 +673,31 @@ fn parse_axis_select(line: &str) -> Result<Command, ContentError> {
fn parse_interpolate_move_or_flash(
line: &str,
gerber_doc: &mut GerberDoc,
linechars: &mut Chars,
modal: ModalOperationMode,
) -> Result<Command, ContentError> {
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),
}
}

Expand Down
131 changes: 131 additions & 0 deletions tests/component_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading