pointhi/kicad-footprint-generator

View on GitHub
KicadModTree/KicadFileHandler.py

Summary

Maintainability
F
3 days
Test Coverage
# KicadModTree is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# KicadModTree is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with kicad-footprint-generator. If not, see < http://www.gnu.org/licenses/ >.
#
# (C) 2016-2018 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>

from KicadModTree.FileHandler import FileHandler
from KicadModTree.util.kicad_util import *
from KicadModTree.nodes.base.Pad import Pad  # TODO: why .KicadModTree is not enough?
from KicadModTree.nodes.base.Arc import Arc
from KicadModTree.nodes.base.Circle import Circle
from KicadModTree.nodes.base.Line import Line
from KicadModTree.nodes.base.Polygon import Polygon


DEFAULT_LAYER_WIDTH = {'F.SilkS': 0.12,
                       'B.SilkS': 0.12,
                       'F.Fab': 0.10,
                       'B.Fab': 0.10,
                       'F.CrtYd': 0.05,
                       'B.CrtYd': 0.05}

DEFAULT_WIDTH_POLYGON_PAD = 0

DEFAULT_WIDTH = 0.15


def _get_layer_width(layer, width=None):
    if width is not None:
        return width
    else:
        return DEFAULT_LAYER_WIDTH.get(layer, DEFAULT_WIDTH)


class KicadFileHandler(FileHandler):
    r"""Implementation of the FileHandler for .kicad_mod files

    :param kicad_mod:
        Main object representing the footprint
    :type kicad_mod: ``KicadModTree.Footprint``

    :Example:

    >>> from KicadModTree import *
    >>> kicad_mod = Footprint("example_footprint")
    >>> file_handler = KicadFileHandler(kicad_mod)
    >>> file_handler.writeFile('example_footprint.kicad_mod')
    """

    def __init__(self, kicad_mod):
        FileHandler.__init__(self, kicad_mod)

    def serialize(self, **kwargs):
        r"""Get a valid string representation of the footprint in the .kicad_mod format

        :Example:

        >>> from KicadModTree import *
        >>> kicad_mod = Footprint("example_footprint")
        >>> file_handler = KicadFileHandler(kicad_mod)
        >>> print(file_handler.serialize())
        """

        sexpr = ['module', self.kicad_mod.name,
                 ['layer', 'F.Cu'],
                 ['tedit', formatTimestamp(kwargs.get('timestamp'))],
                 SexprSerializer.NEW_LINE
                ]  # NOQA

        if self.kicad_mod.description:
            sexpr.append(['descr', self.kicad_mod.description])
            sexpr.append(SexprSerializer.NEW_LINE)

        if self.kicad_mod.tags:
            sexpr.append(['tags', self.kicad_mod.tags])
            sexpr.append(SexprSerializer.NEW_LINE)

        if self.kicad_mod.attribute:
            sexpr.append(['attr', self.kicad_mod.attribute])
            sexpr.append(SexprSerializer.NEW_LINE)

        if self.kicad_mod.maskMargin:
            sexpr.append(['solder_mask_margin', self.kicad_mod.maskMargin])
            sexpr.append(SexprSerializer.NEW_LINE)

        if self.kicad_mod.pasteMargin:
            sexpr.append(['solder_paste_margin', self.kicad_mod.pasteMargin])
            sexpr.append(SexprSerializer.NEW_LINE)

        if self.kicad_mod.pasteMarginRatio:
            sexpr.append(['solder_paste_ratio', self.kicad_mod.pasteMarginRatio])
            sexpr.append(SexprSerializer.NEW_LINE)

        sexpr.extend(self._serializeTree())

        return str(SexprSerializer(sexpr))

    def _serializeTree(self):
        nodes = self.kicad_mod.serialize()

        grouped_nodes = {}

        for single_node in nodes:
            node_type = single_node.__class__.__name__

            current_nodes = grouped_nodes.get(node_type, [])
            current_nodes.append(single_node)

            grouped_nodes[node_type] = current_nodes

        sexpr = []

        # serialize initial text nodes
        if 'Text' in grouped_nodes:
            reference_nodes = list(filter(lambda node: node.type == 'reference', grouped_nodes['Text']))
            for node in reference_nodes:
                sexpr.append(self._serialize_Text(node))
                sexpr.append(SexprSerializer.NEW_LINE)
                grouped_nodes['Text'].remove(node)

            value_nodes = list(filter(lambda node: node.type == 'value', grouped_nodes['Text']))
            for node in value_nodes:
                sexpr.append(self._serialize_Text(node))
                sexpr.append(SexprSerializer.NEW_LINE)
                grouped_nodes['Text'].remove(node)

        for key, value in sorted(grouped_nodes.items()):
            # check if key is a base node, except Model
            if key not in {'Arc', 'Circle', 'Line', 'Pad', 'Polygon', 'Text'}:
                continue

            # render base nodes
            for node in value:
                sexpr.append(self._callSerialize(node))
                sexpr.append(SexprSerializer.NEW_LINE)

        # serialize 3D Models at the end
        if grouped_nodes.get('Model'):
            for node in grouped_nodes.get('Model'):
                sexpr.append(self._serialize_Model(node))
                sexpr.append(SexprSerializer.NEW_LINE)

        return sexpr

    def _callSerialize(self, node):
        '''
        call the corresponding method to serialize the node
        '''
        method_type = node.__class__.__name__
        method_name = "_serialize_{0}".format(method_type)
        if hasattr(self, method_name):
            return getattr(self, method_name)(node)
        else:
            exception_string = "{name} (node) not found, cannot serialized the node of type {type}"
            raise NotImplementedError(exception_string.format(name=method_name, type=method_type))

    def _serialize_ArcPoints(self, node):
        # in KiCAD, some file attributes of Arc are named not in the way of their real meaning
        center_pos = node.getRealPosition(node.center_pos)
        end_pos = node.getRealPosition(node.start_pos)

        return [
                ['start', center_pos.x, center_pos.y],
                ['end', end_pos.x, end_pos.y],
                ['angle', node.angle]
               ]

    def _serialize_Arc(self, node):
        sexpr = ['fp_arc']
        sexpr += self._serialize_ArcPoints(node)
        sexpr += [
                  ['layer', node.layer],
                  ['width', _get_layer_width(node.layer, node.width)]
                 ]  # NOQA

        return sexpr

    def _serialize_CirclePoints(self, node):
        center_pos = node.getRealPosition(node.center_pos)
        end_pos = node.getRealPosition(node.center_pos + (node.radius, 0))

        return [
                ['center', center_pos.x, center_pos.y],
                ['end', end_pos.x, end_pos.y]
               ]

    def _serialize_Circle(self, node):
        sexpr = ['fp_circle']
        sexpr += self._serialize_CirclePoints(node)
        sexpr += [
                  ['layer', node.layer],
                  ['width', _get_layer_width(node.layer, node.width)]
                 ]  # NOQA

        return sexpr

    def _serialize_LinePoints(self, node):
        start_pos = node.getRealPosition(node.start_pos)
        end_pos = node.getRealPosition(node.end_pos)
        return [
                ['start', start_pos.x, start_pos.y],
                ['end', end_pos.x, end_pos.y]
               ]

    def _serialize_Line(self, node):
        start_pos = node.getRealPosition(node.start_pos)
        end_pos = node.getRealPosition(node.end_pos)

        sexpr = ['fp_line']
        sexpr += self._serialize_LinePoints(node)
        sexpr += [
                ['layer', node.layer],
                 ['width', _get_layer_width(node.layer, node.width)]
                ]  # NOQA

        return sexpr

    def _serialize_Text(self, node):
        sexpr = ['fp_text', node.type, node.text]

        position, rotation = node.getRealPosition(node.at, node.rotation)
        if rotation:
            sexpr.append(['at', position.x, position.y, rotation])
        else:
            sexpr.append(['at', position.x, position.y])

        sexpr.append(['layer', node.layer])
        if node.hide:
            sexpr.append('hide')
        sexpr.append(SexprSerializer.NEW_LINE)

        effects = [
                'effects',
                ['font',
                    ['size', node.size.x, node.size.y],
                    ['thickness', node.thickness]]]

        if node.mirror:
            effects.append(['justify', 'mirror'])

        sexpr.append(effects)
        sexpr.append(SexprSerializer.NEW_LINE)

        return sexpr

    def _serialize_Model(self, node):
        sexpr = ['model', node.filename,
                 SexprSerializer.NEW_LINE,
                 ['at', ['xyz', node.at.x, node.at.y, node.at.z]],
                 SexprSerializer.NEW_LINE,
                 ['scale', ['xyz', node.scale.x, node.scale.y, node.scale.z]],
                 SexprSerializer.NEW_LINE,
                 ['rotate', ['xyz', node.rotate.x, node.rotate.y, node.rotate.z]],
                 SexprSerializer.NEW_LINE
                ]  # NOQA

        return sexpr

    def _serialize_CustomPadPrimitives(self, pad):
        all_primitives = []
        for p in pad.primitives:
            all_primitives.extend(p.serialize())

        grouped_nodes = {}

        for single_node in all_primitives:
            node_type = single_node.__class__.__name__

            current_nodes = grouped_nodes.get(node_type, [])
            current_nodes.append(single_node)

            grouped_nodes[node_type] = current_nodes

        sexpr_primitives = []

        for key, value in sorted(grouped_nodes.items()):
            # check if key is a base node, except Model
            if key not in {'Arc', 'Circle', 'Line', 'Pad', 'Polygon', 'Text'}:
                continue

            # render base nodes
            for p in value:
                if isinstance(p, Polygon):
                    sp = ['gr_poly',
                          self._serialize_PolygonPoints(p, newline_after_pts=True)
                         ]  # NOQA
                elif isinstance(p, Line):
                    sp = ['gr_line'] + self._serialize_LinePoints(p)
                elif isinstance(p, Circle):
                    sp = ['gr_circle'] + self._serialize_CirclePoints(p)
                elif isinstance(p, Arc):
                    sp = ['gr_arc'] + self._serialize_ArcPoints(p)
                else:
                    raise TypeError('Unsuported type of primitive for custom pad.')
                sp.append(['width', DEFAULT_WIDTH_POLYGON_PAD if p.width is None else p.width])
                sexpr_primitives.append(sp)
                sexpr_primitives.append(SexprSerializer.NEW_LINE)

        return sexpr_primitives

    def _serialize_Pad(self, node):
        sexpr = ['pad', node.number, node.type, node.shape]

        position, rotation = node.getRealPosition(node.at, node.rotation)
        if not rotation % 360 == 0:
            sexpr.append(['at', position.x, position.y, rotation])
        else:
            sexpr.append(['at', position.x, position.y])

        sexpr.append(['size', node.size.x, node.size.y])

        if node.type in [Pad.TYPE_THT, Pad.TYPE_NPTH]:
            if node.drill.x == node.drill.y:
                sexpr.append(['drill', node.drill.x])
            else:
                sexpr.append(['drill', 'oval', node.drill.x, node.drill.y])

        sexpr.append(['layers'] + node.layers)
        if node.shape == Pad.SHAPE_ROUNDRECT:
            sexpr.append(['roundrect_rratio', node.radius_ratio])

        if node.shape == Pad.SHAPE_CUSTOM:
            # gr_line, gr_arc, gr_circle or gr_poly
            sexpr.append(SexprSerializer.NEW_LINE)
            sexpr.append(['options',
                         ['clearance', node.shape_in_zone],
                         ['anchor', node.anchor_shape]
                        ])  # NOQA
            sexpr.append(SexprSerializer.NEW_LINE)
            sexpr_primitives = self._serialize_CustomPadPrimitives(node)
            sexpr.append(['primitives', SexprSerializer.NEW_LINE] + sexpr_primitives)

        if node.solder_paste_margin_ratio != 0 or node.solder_mask_margin != 0 or node.solder_paste_margin != 0:
            sexpr.append(SexprSerializer.NEW_LINE)
            if node.solder_mask_margin != 0:
                sexpr.append(['solder_mask_margin', node.solder_mask_margin])
            if node.solder_paste_margin_ratio != 0:
                sexpr.append(['solder_paste_margin_ratio', node.solder_paste_margin_ratio])
            if node.solder_paste_margin != 0:
                sexpr.append(['solder_paste_margin', node.solder_paste_margin])

        return sexpr

    def _serialize_PolygonPoints(self, node, newline_after_pts=False):
        node_points = ['pts']
        if newline_after_pts:
            node_points.append(SexprSerializer.NEW_LINE)
        points_appended = 0
        for n in node.nodes:
            if points_appended >= 4:
                points_appended = 0
                node_points.append(SexprSerializer.NEW_LINE)
            points_appended += 1

            n_pos = node.getRealPosition(n)
            node_points.append(['xy', n_pos.x, n_pos.y])

        return node_points

    def _serialize_Polygon(self, node):
        node_points = self._serialize_PolygonPoints(node)

        sexpr = ['fp_poly',
                 node_points,
                 ['layer', node.layer],
                 ['width', _get_layer_width(node.layer, node.width)]
                ]  # NOQA

        return sexpr