Skip to content

Commit

Permalink
Fix nested AB parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Aug 19, 2024
1 parent 4f63df0 commit 3939c6f
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 24 deletions.
203 changes: 203 additions & 0 deletions src/pygerber/gerberx3/ast/state_tracking_visitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""`pygerber.gerberx3.node_visitor` contains definition of `StateTrackingVisitor`
class.
"""

from __future__ import annotations

from enum import Enum
from typing import Any, Callable, Optional

from pydantic import BaseModel, Field

from pygerber.gerberx3.ast.nodes import AB, AD, AM
from pygerber.gerberx3.ast.nodes.enums import (
AxisCorrespondence,
CoordinateMode,
ImagePolarity,
Mirroring,
Zeros,
)
from pygerber.gerberx3.ast.nodes.properties.MO import UnitMode
from pygerber.gerberx3.ast.nodes.types import ApertureIdStr, Double, PackedCoordinateStr
from pygerber.gerberx3.ast.visitor import AstVisitor


class _StateModel(BaseModel):
"""Base class for all models representing parts of Gerber state."""


class CoordinateFormat(_StateModel):
"""Coordinate format information."""

zeros: Zeros
coordinate_mode: CoordinateMode

x_integral: int
x_decimal: int

y_integral: int
y_decimal: int

def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)

if self.zeros == Zeros.SKIP_LEADING:
self.unpack_x = self._unpack_skip_leading(self.x_integral, self.x_decimal) # type: ignore[method-assign]
self.unpack_y = self._unpack_skip_leading(self.y_integral, self.y_decimal) # type: ignore[method-assign]
elif self.zeros == Zeros.SKIP_TRAILING:
self.unpack_x = self._unpack_skip_trailing(self.x_integral, self.x_decimal) # type: ignore[method-assign]
self.unpack_y = self._unpack_skip_trailing(self.y_integral, self.y_decimal) # type: ignore[method-assign]
else:
msg = f"Unknown zeros mode: {self.zeros}"
raise ValueError(msg)

def unpack_x(self, coordinate: PackedCoordinateStr, /) -> Double: # noqa: ARG002
"""Unpack X coordinate using the current coordinate format."""
msg = "Coordinate format was not properly set."
raise NotImplementedError(msg)

def unpack_y(self, coordinate: PackedCoordinateStr, /) -> Double: # noqa: ARG002
"""Unpack X coordinate using the current coordinate format."""
msg = "Coordinate format was not properly set."
raise NotImplementedError(msg)

def _unpack_skip_trailing(
self, integer: int, decimal: int
) -> Callable[[PackedCoordinateStr], Double]:
def _(coordinate: PackedCoordinateStr) -> Double:
padded_coordinate = coordinate.ljust((integer + decimal), "0")
integer_value_str = padded_coordinate[:integer]
decimal_value_str = padded_coordinate[integer:]

return float(f"{integer_value_str}.{decimal_value_str}")

return _

def _unpack_skip_leading(
self, integer: int, decimal: int
) -> Callable[[PackedCoordinateStr], Double]:
def _(coordinate: PackedCoordinateStr) -> Double:
padded_coordinate = coordinate.rjust((integer + decimal), "0")
integer_value_str = padded_coordinate[:integer]
decimal_value_str = padded_coordinate[integer:]

return float(f"{integer_value_str}.{decimal_value_str}")

return _


class Attributes(_StateModel):
"""Attributes of the Gerber file."""

aperture_attributes: dict[str, Any] = Field(default_factory=dict)

file_attributes: dict[str, Any] = Field(default_factory=dict)

object_attributes: dict[str, Any] = Field(default_factory=dict)

image_polarity: ImagePolarity = Field(default=None)
"""The name of the image. (Spec reference: 8.1.3)"""

file_name: Optional[str] = Field(default=None)
"""The name of the file. (Spec reference: 8.1.6)"""

axis_correspondence: AxisCorrespondence = Field(default=AxisCorrespondence.AX_BY)
"""The axis correspondence. (Spec reference: 8.1.2)"""


class Transform(_StateModel):
"""Aperture transformations."""

mirroring: Mirroring = Field(default=Mirroring.NONE)
"""Aperture mirroring set with LM command. (Spec reference: 4.9.3)"""

rotation: Double = Field(default=0.0)
"""Aperture rotation set with LR command. (Spec reference: 4.9.4)"""

scaling: Double = Field(default=1.0)
"""Aperture scaling set with LS command. (Spec reference: 4.9.5)"""


class PlotMode(Enum):
"""Plot mode of the Gerber file."""

LINEAR = "LINEAR"
"""Linear interpolation mode."""

ARC = "ARC"
"""Clockwise circular interpolation mode."""

CCW_ARC = "CCW_ARC"
"""Counter-clockwise circular interpolation mode."""


class ApertureStorage(_StateModel):
"""Storage for apertures."""

apertures: dict[ApertureIdStr, AD] = Field(default_factory=dict)
"""Aperture storage."""

blocks: dict[str, AB] = Field(default_factory=dict)
"""Block aperture storage."""

macros: dict[str, AM] = Field(default_factory=dict)
"""Macro definition storage."""


class MacroContext(_StateModel):
"""Macro evaluation context."""


class State(_StateModel):
"""Internal state of the compiler."""

unit_mode: UnitMode = Field(default=UnitMode.METRIC)
"""The draw units used for the Gerber file. (Spec reference: 4.2.1)"""

coordinate_format: Optional[CoordinateFormat] = Field(default=None)
"""The coordinate format specification, including the number of decimals.
(Spec reference: 4.2.2)"""

plot_mode: PlotMode = Field(default=PlotMode.LINEAR)
"""The plot mode. (Spec reference 4.7)"""

current_aperture_id: Optional[ApertureIdStr] = Field(default=None)
"""The ID of currently selected aperture. (Spec reference: 8.6)"""

current_x: Double = Field(default=0.0)
"""Current X coordinate value."""

current_y: Double = Field(default=0.0)
"""Current Y coordinate value."""

coordinate_x: Double = Field(default=0.0)
"""Last X coordinate value set by CoordinateX node."""

coordinate_y: Double = Field(default=0.0)
"""Last Y coordinate value set by CoordinateY node."""

coordinate_i: Double = Field(default=0.0)
"""Last I coordinate value set by CoordinateI node."""

coordinate_j: Double = Field(default=0.0)
"""Last J coordinate value set by CoordinateJ node."""

transform: Transform = Field(default_factory=lambda: Transform)
"""Current aperture transformation parameters."""

apertures: ApertureStorage

macro_context: MacroContext = Field(default_factory=lambda: MacroContext)


class StateTrackingVisitor(AstVisitor):
"""`StateTrackingVisitor` is a visitor class that tracks the internal state
defined in the GerberX3 specification and modifies it according to Gerber
commands.
Additionally, it defines a set of higher level callback methods that extend
interface of `AstVisitor` class.
"""

def __init__(self) -> None:
super().__init__()
40 changes: 20 additions & 20 deletions src/pygerber/gerberx3/parser/pyparsing/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,35 +274,35 @@ def aperture(self) -> pp.ParserElement:

def aperture_block(self) -> pp.ParserElement:
"""Create a parser element capable of parsing aperture blocks."""
block = pp.Forward()
aperture_block = pp.Forward()

aperture_block = (
aperture_block <<= (
(
self.ab_open.set_results_name("open")
+ block
+ pp.ZeroOrMore(
pp.MatchFirst(
[
self.attribute,
self.g_codes,
self.load_commands,
# Technically not valid according to standard.
self.m_codes,
self.properties,
self.d_codes_standalone,
# Other aperture altering commands.
self.macro,
self.step_repeat,
self.add_aperture,
aperture_block,
]
)
).set_results_name("nodes")
+ self.ab_close.set_results_name("close")
)
.set_name("ApertureBlock")
.set_parse_action(self.make_unpack_callback(AB))
)

block <<= pp.ZeroOrMore(
pp.MatchFirst(
[
self.attribute,
self.g_codes,
self.load_commands,
self.m_codes, # Technically not valid according to standard.
self.properties,
self.d_codes_standalone,
# Other aperture altering commands.
self.macro,
self.step_repeat,
self.add_aperture,
]
)
).set_results_name("nodes")

return aperture_block

@pp.cached_property
Expand Down
6 changes: 2 additions & 4 deletions test/assets/gerberx3/ucamco/4.11.4/source.grb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ G04 Define block aperture 100, consisting of two draws and a round dot*
D11*
X-3556000Y17605375D03*
%AB*%
G04 Define block aperture 102, consisting of 2x3 flashes of aperture 101
and 1 flash of D12*
G04 Define block aperture 102, consisting of 2x3 flashes of aperture 101 and 1 flash of D12*
%ABD102*%
G04 Define nested block aperture 101, consisting of 2x2 flashes of
aperture 100*
G04 Define nested block aperture 101, consisting of 2x2 flashes of aperture 100*
%ABD101*%
D100*
X0Y0D03*
Expand Down

0 comments on commit 3939c6f

Please sign in to comment.