Argmaster / pygerber

Python implementation of Gerber X3/X2 standard with 2D rendering engine.
https://argmaster.github.io/pygerber/stable
MIT License
55 stars 10 forks source link

PyGerber 3.0.0 comment attributes handling #297

Open Argmaster opened 2 months ago

Argmaster commented 2 months ago

Standard attributes as comments are birefly mentioned in The Gerber Layer Format Specification 2024.05 in section 4.1 Comment (G04), dedicated description is available in section 5.1.1 Comment attributes.

There are at least two ways to approach how this can be handled from perspective of AST node classes:

First approach requires a lot of code duplication, will require separate set of on_<attr-name> callbacks on AstVisitor and in general will unnecessarily complicate implementation, in my humble opition, since comment attributes are bound to behave like the X3 standalone counterparts.

Therefore I would advise choosing second approach. This will require adding is_comment=False for all existing construction calls for attributes in Gerber grammar.

src/pygerber/gerberx3/ast/nodes/attribute/TA.py


class TA(Node):
    """Represents TA Gerber extended command."""

    is_standalone: bool

    @property
    def attribute_name(self) -> str:
        """Get attribute name."""
        raise NotImplementedError

Afterwards, separate grammar definition for all the attributes will be needed, as % signs must be a part of standalone attributes while comment attributes must not contain them.

src/pygerber/gerberx3/parser/pyparsing/grammar.py:582

    @pp.cached_property
    def _ta_user_name(self) -> pp.ParserElement:
        return (
            self._extended_command(
                self._ta
                + self.user_name
                + pp.ZeroOrMore(
                    self.comma
                    + self.field.set_results_name("fields", list_all_matches=True)
                )
            )
            .set_name("TA<UserName>")
            .set_parse_action(self.make_unpack_callback(TA_UserName, is_standalone=True))
        )

G04 with pp.MatchFirst for comment attributes must be placed before general G04.

src/pygerber/gerberx3/parser/pyparsing/grammar.py:1174


    @pp.cached_property
    def g_codes(self) -> pp.ParserElement:
        """Create a parser element capable of parsing G-codes."""
        g04_comment = self._command(
            pp.Regex(r"G0*4") + pp.Opt(self.string),
        ).set_name("G04")

        if self.optimization & Optimization.DISCARD_COMMENTS:
            g04_comment = pp.Suppress(g04_comment)
        else:
            g04_comment = g04_comment.set_parse_action(self.make_unpack_callback(G04))

        def _standalone(cls: Type[Node]) -> pp.ParserElement:
            code = int(cls.__qualname__.lstrip("G"))
            return (
                self._command(pp.Regex(f"G0*{code}"))
                .set_name(cls.__qualname__)
                .set_parse_action(self.make_unpack_callback(cls, is_standalone=True))
            )

        def _non_standalone(cls: Type[Node]) -> pp.ParserElement:
            # We have to account for legacy cases like `G70D02*`, see
            # `G.is_standalone` docstring for more information.
            code = int(cls.__qualname__.lstrip("G"))
            return (
                (
                    pp.Regex(f"G0*{code}")
                    + pp.FollowedBy(pp.one_of(["D", "X", "Y", "I", "J"]))
                )
                .set_name(cls.__qualname__)
                .set_parse_action(self.make_unpack_callback(cls, is_standalone=False))
            ) + self.d_codes_non_standalone

        return pp.MatchFirst(
            [
                # ----------------------------------------------------------
                # COMMENT ATTRIBUTES SHOULD BE INSERTED HERE
                # ----------------------------------------------------------
                g04_comment,
                *(
                    _standalone(cast(Type[Node], cls))
                    for cls in reversed(
                        (
                            G01,
                            G02,
                            G03,
                            G36,
                            G37,
                            G54,
                            G55,
                            G70,
                            G71,
                            G74,
                            G75,
                            G90,
                            G91,
                        )
                    )
                ),
                *(
                    _non_standalone(cast(Type[Node], cls))
                    for cls in reversed(
                        (
                            G01,
                            G02,
                            G03,
                            G36,
                            G37,
                            G54,
                            G55,
                            G70,
                            G71,
                            G74,
                            G75,
                            G90,
                            G91,
                        )
                    )
                ),
            ]
        )

Comment attributes syntax should be defined similarily to standalone attributes:

src/pygerber/gerberx3/parser/pyparsing/grammar.py:558

    @pp.cached_property
    def attribute(self) -> pp.ParserElement:
        """Create a parser element capable of parsing attributes."""
        return pp.MatchFirst(
            [
                self.ta(),
                self.td(),
                self.tf(),
                self.to(),
            ]
        )

src/pygerber/gerberx3/parser/pyparsing/grammar.py:570

    def ta(self) -> pp.ParserElement:
        """Create a parser element capable of parsing TA attributes."""
        return pp.MatchFirst(
            [
                self._ta_user_name,
                self._ta_aper_function,
                self._ta_drill_tolerance,
                self._ta_flash_text,
            ]
        )

But with _comment suffix. eg.

    def ta_comment(self) -> pp.ParserElement:
        """Create a parser element capable of parsing TA comment attributes."""
        return pp.MatchFirst(
            [
                self._ta_user_name_comment,
                self._ta_aper_function_comment,
                self._ta_drill_tolerance_comment,
                self._ta_flash_text_comment,
            ]
        )

Keep in mind that you cannot use self._extended_command as it includes % at the beginning and the end. I suppose example comment attribute definition could look like this:


    @pp.cached_property
    def _ta_user_name_comment(self) -> pp.ParserElement:
        return (
            self._comment_attribute(
                self._ta
                + self.user_name
                + pp.ZeroOrMore(
                    self.comma
                    + self.field.set_results_name("fields", list_all_matches=True)
                )
            )
            .set_name("TA<UserName>")
            .set_parse_action(self.make_unpack_callback(TA_UserName, is_standalone=False))
        )

Where self._comment_attribute has to be implemented and could look similarily to self._extended_command, lets say:

    def _comment_attribute(self, inner: pp.ParserElement) -> pp.ParserElement:
        return Literal("G04") + Literal("#@!") + inner + self._asterisk

Notice that G04 is a separate literal to allow undefined amount of whitespace characters between G04 and #@!. Make sure that I didn't mess up the special sequence, I am not sure that it is #@!

All of this will inevitebly require changes in test suite to account for new is_standalone member of attribute node classes. It would also be good to prepare test assets similar to test/assets/gerberx3/tokens/attribute for comment attributes. Gerber assets created should automatically get included in test suite thanks to how test/gerberx3/test_parser/test_pyparsing/test_parser.py:test_pyparsing_parser_grammar() works.