class Compiler(StateTrackingVisitor):
"""Compiler for transforming transforming Gerber (AST) to PyGerber rendering VM
commands (RVMC).
"""
MAIN_BUFFER_ID: ClassVar[str] = "%main%"
def __init__(self, *, ignore_program_stop: bool = False) -> None:
super().__init__(ignore_program_stop=ignore_program_stop)
self._buffers: dict[str, CommandBuffer] = {}
self._buffer_stack: list[str] = []
self._contour_buffer: Optional[list[ShapeSegment]] = []
self._create_main_buffer()
def _set_buffer(self, buffer: CommandBuffer) -> None:
"""Register buffer."""
self._buffers[buffer.id_str] = buffer
def _get_buffer(self, id_: str) -> CommandBuffer:
"""Get buffer by id."""
return self._buffers[id_]
def _del_buffer(self, id_: str) -> None:
"""Delete buffer by id."""
del self._buffers[id_]
def _get_buffer_opt(self, id_: str) -> Optional[CommandBuffer]:
"""Get buffer by id."""
return self._buffers.get(id_)
def _get_current_buffer(self) -> CommandBuffer:
return self._get_buffer(self._buffer_stack[-1])
def _append_shape_to_current_buffer(self, command: Shape) -> None:
self._get_current_buffer().append_shape(command)
def _append_paste_to_current_buffer(self, command: PasteLayer) -> None:
self._get_current_buffer().append_paste(command)
def _expand_buffer_to_current_buffer(self, buffer: CommandBuffer) -> None:
for command in buffer.commands:
if isinstance(command, Shape):
self._append_shape_to_current_buffer(command)
elif isinstance(command, PasteLayer):
self._append_paste_to_current_buffer(command)
else:
raise NotImplementedError(type(command))
def _create_main_buffer(self) -> CommandBuffer:
buffer = CommandBuffer(
self.MAIN_BUFFER_ID,
box=None,
origin=Vector(x=0, y=0),
commands=[],
depends_on=set(),
resolved_dependencies=[],
)
assert self.MAIN_BUFFER_ID not in self._buffers
self._set_buffer(buffer)
self._push_buffer(self.MAIN_BUFFER_ID)
return buffer
def _push_buffer(self, id_: str) -> None:
self._buffer_stack.append(id_)
def _pop_buffer(self) -> None:
self._buffer_stack.pop()
def _get_line_thickness(self, line_direction: Vector) -> float:
current_aperture = self.state.current_aperture
if isinstance(current_aperture, ADC):
return current_aperture.diameter
if isinstance(current_aperture, (ADR, ADO)):
angle = line_direction.angle_between(Vector.unit.x)
sin_angle = sin(radians(-angle))
cos_angle = cos(radians(-angle))
return Vector(
x=current_aperture.width * cos_angle,
y=current_aperture.height * sin_angle,
).length()
if isinstance(current_aperture, ADP):
return current_aperture.outer_diameter
raise NotImplementedError(type(current_aperture))
def on_ab(self, node: AB) -> AB:
"""Handle `AB` node."""
aperture_buffer = self._create_aperture_buffer(node.open.aperture_id)
self._push_buffer(aperture_buffer.id_str)
super().on_ab(node)
self._pop_buffer()
return node
def on_sr(self, node: SR) -> SR:
"""Handle `SR` node."""
aperture_buffer = self._create_aperture_buffer(
ApertureIdStr(f"%%SR%{id(node)}%{time.time():.0f}")
)
self._push_buffer(aperture_buffer.id_str)
super().on_sr(node)
self._pop_buffer()
buffer = self._get_buffer(aperture_buffer.id_str)
x_delta = node.open.x_delta
y_delta = node.open.y_delta
for x in range(node.open.x_repeats):
for y in range(node.open.y_repeats):
x_coordinate = self.coordinate_x + (x * x_delta)
y_coordinate = self.coordinate_y + (y * y_delta)
tmp_buffer = self._apply_transform_to_buffer_non_recursive_tmp(
buffer, Matrix3x3.new_translate(x=x_coordinate, y=y_coordinate)
)
self._expand_buffer_to_current_buffer(tmp_buffer)
self._del_buffer(aperture_buffer.id_str)
return node
def on_adc(self, node: ADC) -> ADC:
"""Handle `AD` circle node."""
self.on_ad(node)
aperture_buffer = self._create_aperture_buffer(node.aperture_id)
aperture_buffer.append_shape(
Shape.new_circle(
(0.0, 0.0),
node.diameter,
is_negative=False,
)
)
if node.hole_diameter is not None and node.hole_diameter > 0:
aperture_buffer.append_shape(
Shape.new_circle(
(0.0, 0.0),
min(node.hole_diameter, node.diameter),
is_negative=True,
)
)
return node
def _create_aperture_buffer(self, aperture_id: ApertureIdStr) -> CommandBuffer:
buffer = CommandBuffer(
aperture_id,
box=None,
origin=Vector(x=0, y=0),
commands=[],
depends_on=set(),
resolved_dependencies=[],
)
self._set_buffer(buffer)
return buffer
def on_adr(self, node: ADR) -> ADR:
"""Handle `AD` rectangle node."""
self.on_ad(node)
aperture_buffer = self._create_aperture_buffer(node.aperture_id)
aperture_buffer.append_shape(
Shape.new_rectangle(
(0.0, 0.0),
node.width,
node.height,
is_negative=False,
)
)
if node.hole_diameter is not None and node.hole_diameter > 0:
aperture_buffer.append_shape(
Shape.new_circle(
(0.0, 0.0),
min(node.hole_diameter, node.width, node.height),
is_negative=True,
)
)
return node
def on_ado(self, node: ADO) -> ADO:
"""Handle `AD` obround node."""
self.on_ad(node)
aperture_buffer = self._create_aperture_buffer(node.aperture_id)
aperture_buffer.append_shape(
Shape.new_obround(
(0.0, 0.0),
node.width,
node.height,
is_negative=False,
)
)
if node.hole_diameter is not None and node.hole_diameter > 0:
aperture_buffer.append_shape(
Shape.new_circle(
(0.0, 0.0),
min(node.hole_diameter, node.width, node.height),
is_negative=True,
)
)
return node
def on_adp(self, node: ADP) -> ADP:
"""Handle `AD` polygon node."""
self.on_ad(node)
aperture_buffer = self._create_aperture_buffer(node.aperture_id)
aperture_buffer.append_shape(
Shape.new_polygon(
(0.0, 0.0),
node.outer_diameter,
node.vertices,
node.rotation or 0.0,
is_negative=False,
)
)
if node.hole_diameter is not None and node.hole_diameter > 0:
aperture_buffer.append_shape(
Shape.new_circle(
(0.0, 0.0),
min(node.hole_diameter, node.outer_diameter),
is_negative=True,
)
)
return node
def on_ad_macro(self, node: ADmacro) -> ADmacro:
"""Handle `AD` macro node."""
self.on_ad(node)
aperture_buffer = self._create_aperture_buffer(node.aperture_id)
if node.params is None:
scope = {}
else:
scope = {f"${i + 1}": param for i, param in enumerate(node.params)}
macro = self.state.apertures.macros.get(node.name)
if macro is None:
raise MacroNotDefinedError(node.name)
macro.visit(MacroEvalVisitor(self, aperture_buffer, scope))
return node
def on_draw_line(self, node: D01) -> None: # noqa: ARG002
"""Handle `D01` node in linear interpolation mode."""
start_x = self.state.current_x
start_y = self.state.current_y
start_point = (start_x, start_y)
end_x = self.coordinate_x
end_y = self.coordinate_y
end_point = (end_x, end_y)
thickness = self._get_line_thickness(
Vector.from_tuple((self.coordinate_x, self.coordinate_y))
)
self._append_shape_to_current_buffer(
Shape.new_circle(
start_point,
thickness,
is_negative=self.is_negative,
)
)
self._append_shape_to_current_buffer(
Shape.new_line(
start_point,
end_point,
thickness=thickness,
is_negative=self.is_negative,
),
)
self._append_shape_to_current_buffer(
Shape.new_circle(
end_point,
thickness,
is_negative=self.is_negative,
)
)
def on_draw_cw_arc_mq(self, node: D01) -> None: # noqa: ARG002
"""Handle `D01` node in clockwise circular interpolation multi quadrant mode."""
self._on_draw_arc_mq(Shape.new_cw_arc)
def _on_draw_arc_mq(self, factory_method: _ArcFactory) -> None:
start_x = self.state.current_x
start_y = self.state.current_y
start_point = (start_x, start_y)
end_x = self.coordinate_x
end_y = self.coordinate_y
end_point = (end_x, end_y)
center_x = start_x + self.coordinate_i
center_y = start_y + self.coordinate_j
center = (center_x, center_y)
thickness = self._get_line_thickness(
Vector.from_tuple((self.coordinate_x, self.coordinate_y))
)
self._append_shape_to_current_buffer(
Shape.new_circle(
start_point,
thickness,
is_negative=self.is_negative,
)
)
if start_point == end_point:
length = Vector(x=self.coordinate_i, y=self.coordinate_j).length()
start_point = (center_x - length, center_y)
end_point = (center_x + length, center_y)
self._append_shape_to_current_buffer(
factory_method(
start_point,
end_point,
center,
thickness,
is_negative=self.is_negative,
)
)
self._append_shape_to_current_buffer(
factory_method(
end_point,
start_point,
center,
thickness,
is_negative=self.is_negative,
)
)
else:
self._append_shape_to_current_buffer(
factory_method(
start_point,
end_point,
center,
thickness,
is_negative=self.is_negative,
)
)
self._append_shape_to_current_buffer(
Shape.new_circle(
end_point,
thickness,
is_negative=self.is_negative,
)
)
def on_draw_ccw_arc_mq(self, node: D01) -> None: # noqa: ARG002
"""Handle `D01` node in counter-clockwise circular interpolation multi quadrant
mode.
"""
self._on_draw_arc_mq(Shape.new_ccw_arc)
def on_start_region(self) -> None:
"""Handle start of region."""
super().on_start_region()
self._contour_buffer = []
def on_in_region_draw_line(self, node: D01) -> None: # noqa: ARG002
"""Handle `D01` node in linear interpolation mode in region."""
if self._contour_buffer is None:
raise ContourBufferNotSetError
start_x = self.state.current_x
start_y = self.state.current_y
start_point = (start_x, start_y)
end_x = self.coordinate_x
end_y = self.coordinate_y
end_point = (end_x, end_y)
self._contour_buffer.append(Line.from_tuples(start_point, end_point))
def on_in_region_draw_cw_arc_mq(self, node: D01) -> None: # noqa: ARG002
"""Handle `D01` node in clockwise circular interpolation multi quadrant mode
within region statement.
"""
self._on_in_region_draw_arc_mq(is_clockwise=True)
def _on_in_region_draw_arc_mq(self, *, is_clockwise: bool) -> None:
if self._contour_buffer is None:
raise ContourBufferNotSetError
start_x = self.state.current_x
start_y = self.state.current_y
start_point = (start_x, start_y)
end_x = self.coordinate_x
end_y = self.coordinate_y
end_point = (end_x, end_y)
center_x = start_x + self.coordinate_i
center_y = start_y + self.coordinate_j
center = (center_x, center_y)
if start_point == end_point:
end_point = (center_x + self.coordinate_i, center_y + self.coordinate_j)
self._contour_buffer.append(
Arc.from_tuples(
start_point,
end_point,
center,
clockwise=is_clockwise,
)
)
self._contour_buffer.append(
Arc.from_tuples(
end_point,
start_point,
center,
clockwise=is_clockwise,
)
)
else:
self._contour_buffer.append(
Arc.from_tuples(
start_point,
end_point,
center,
clockwise=is_clockwise,
)
)
def on_in_region_draw_ccw_arc_mq(self, node: D01) -> None: # noqa: ARG002
"""Handle `D01` node in counter-clockwise circular interpolation multi quadrant
mode within region statement.
"""
self._on_in_region_draw_arc_mq(is_clockwise=False)
def on_flush_region(self) -> None:
"""Handle flush region after D02 command or after G37."""
if self._contour_buffer is None:
raise ContourBufferNotSetError
if len(self._contour_buffer) > 0:
self._append_shape_to_current_buffer(
Shape(commands=self._contour_buffer, is_negative=self.is_negative)
)
self._contour_buffer = []
def on_end_region(self) -> None:
"""Handle end of region."""
super().on_end_region()
self._contour_buffer = None
def on_flash_circle(self, node: D03, aperture: ADC) -> None: # noqa: ARG002
"""Handle `D03` node with `ADC` aperture."""
self._on_flash_aperture(aperture.aperture_id)
def _on_flash_aperture(self, aperture_id: ApertureIdStr) -> None:
buffer = self._get_aperture_buffer(aperture_id)
self._append_paste_to_current_buffer(
PasteLayer(
source_layer_id=buffer.layer_id,
center=Vector(x=self.coordinate_x, y=self.coordinate_y),
is_negative=self.is_negative,
),
)
def _get_aperture_buffer(self, aperture_id: str) -> CommandBuffer:
transform = self.state.transform
mirroring_matrix = Matrix3x3.new_reflect(**transform.mirroring.kwargs)
rotation_matrix = Matrix3x3.new_rotate(transform.rotation)
scale_matrix = Matrix3x3.new_scale(transform.scaling, transform.scaling)
transform_matrix = mirroring_matrix @ rotation_matrix @ scale_matrix
return self._get_buffer_with_transform(aperture_id, transform_matrix)
def _get_buffer_with_transform(
self, aperture_id: str, transform_matrix: Matrix3x3
) -> CommandBuffer:
layer_id = f"{aperture_id}%{transform_matrix.tag}"
buffer = self._get_buffer_opt(layer_id)
if buffer is None:
aperture_base_buffer = self._get_buffer(aperture_id)
buffer = self._apply_transform_to_buffer(
aperture_base_buffer, layer_id, transform_matrix
)
assert buffer.id_str == layer_id
self._set_buffer(buffer)
return buffer
def _apply_transform_to_buffer(
self, buffer: CommandBuffer, layer_id: str, transform_matrix: Matrix3x3
) -> CommandBuffer:
commands: list[DrawCmdT] = []
depends_on: set[str] = set()
for cmd in buffer.commands:
if isinstance(cmd, Shape):
commands.append(cmd.transform(transform_matrix))
elif isinstance(cmd, PasteLayer):
aperture_buffer = self._get_aperture_buffer(cmd.source_layer_id.id)
depends_on.add(aperture_buffer.id_str)
commands.append(
PasteLayer(
source_layer_id=LayerID(id=aperture_buffer.id_str),
center=cmd.center.transform(transform_matrix),
is_negative=cmd.is_negative,
)
)
else:
raise NotImplementedError(type(cmd))
return CommandBuffer(
layer_id,
None,
origin=buffer.origin,
commands=commands,
depends_on=depends_on,
resolved_dependencies=[],
)
def on_flash_rectangle(self, node: D03, aperture: ADR) -> None: # noqa: ARG002
"""Handle `D03` node with `ADC` aperture."""
self._on_flash_aperture(aperture.aperture_id)
def on_flash_obround(self, node: D03, aperture: ADO) -> None: # noqa: ARG002
"""Handle `D03` node with `ADO` aperture."""
self._on_flash_aperture(aperture.aperture_id)
def on_flash_polygon(self, node: D03, aperture: ADP) -> None: # noqa: ARG002
"""Handle `D03` node with `ADP` aperture."""
self._on_flash_aperture(aperture.aperture_id)
def on_flash_macro(self, node: D03, aperture: ADmacro) -> None: # noqa: ARG002
"""Handle `D03` node with `ADM` aperture."""
self._on_flash_aperture(aperture.aperture_id)
def on_flash_block(self, node: D03, aperture: AB) -> None: # noqa: ARG002
"""Handle `D03` node with `AB` aperture."""
aperture_id = aperture.open.aperture_id
buffer = self._get_aperture_buffer(aperture_id)
tmp_buffer = self._apply_transform_to_buffer_non_recursive_tmp(
buffer, Matrix3x3.new_translate(x=self.coordinate_x, y=self.coordinate_y)
)
self._expand_buffer_to_current_buffer(tmp_buffer)
def _apply_transform_to_buffer_non_recursive_tmp(
self, buffer: CommandBuffer, transform_matrix: Matrix3x3
) -> CommandBuffer:
commands: list[DrawCmdT] = []
depends_on: set[str] = set()
for cmd in buffer.commands:
if isinstance(cmd, Shape):
commands.append(cmd.transform(transform_matrix))
elif isinstance(cmd, PasteLayer):
depends_on.add(cmd.source_layer_id.id)
commands.append(
PasteLayer(
source_layer_id=cmd.source_layer_id,
center=cmd.center.transform(transform_matrix),
is_negative=cmd.is_negative,
)
)
else:
raise NotImplementedError(type(cmd))
return CommandBuffer(
"%temporary%",
None,
origin=buffer.origin,
commands=commands,
depends_on=depends_on,
resolved_dependencies=[],
)
def _resolve_buffer_submit_order(self) -> list[CommandBuffer]:
buffer_submit_order: list[str] = []
def _(buffer: CommandBuffer) -> None:
for dependency_id in buffer.depends_on:
dependency = self._get_buffer(dependency_id)
_(dependency)
if buffer.id_str in buffer_submit_order:
raise CyclicBufferDependencyError(buffer, dependency)
buffer_submit_order.append(buffer.id_str)
_(self._get_buffer(self.MAIN_BUFFER_ID))
return [self._get_buffer(id_) for id_ in buffer_submit_order]
def _convert_buffers_to_rvmc(self) -> RVMC:
commands: list[Command] = []
buffer_submit_order = self._resolve_buffer_submit_order()
for buffer in buffer_submit_order:
commands.append(StartLayer(id=LayerID(id=buffer.id_str), box=buffer.box))
commands.extend(buffer.commands)
commands.append(EndLayer())
return RVMC(commands=commands)
def compile(self, ast: File) -> RVMC:
"""Compile Gerber AST to RVMC."""
ast.visit(self)
return self._convert_buffers_to_rvmc()