Example #1
0
 def __init__(self):
     """
     Initializes empty panel.
     """
     self.board = pcbnew.BOARD()
     self.boardCounter = 0
     self.boardSubstrate = Substrate([]) # Keep substrate in internal representation,
                                         # Draw it just before saving
     self.hVCuts = set() # Keep V-cuts as numbers and append them just before saving
     self.vVCuts = set() # to make them truly span the whole panel
Example #2
0
 def __init__(self):
     """
     Initializes empty panel.
     """
     self.board = pcbnew.BOARD()
     self.substrates = [] # Substrates of the individual boards; e.g. for masking
     self.boardCounter = 0
     self.boardSubstrate = Substrate([]) # Keep substrate in internal representation,
                                         # Draw it just before saving
     self.hVCuts = set() # Keep V-cuts as numbers and append them just before saving
     self.vVCuts = set() # to make them truly span the whole panel
     self.copperLayerCount = None
     self.zonesToRefill = pcbnew.ZONES()
Example #3
0
    def appendBoard(self,
                    filename,
                    destination,
                    sourceArea=None,
                    origin=Origin.Center,
                    rotationAngle=0,
                    shrink=False,
                    tolerance=0,
                    bufferOutline=fromMm(0.001),
                    netRenamer=None,
                    refRenamer=None):
        """
        Appends a board to the panel.

        The sourceArea (wxRect) of the board specified by filename is extracted
        and placed at destination (wxPoint). The source area (wxRect) can be
        auto detected if it is not provided. Only board items which fit entirely
        into the source area are selected. You can also specify rotation. Both
        translation and rotation origin are specified by origin. Origin
        specifies which point of the sourceArea is used for translation and
        rotation (origin it is placed to destination). It is possible to specify
        coarse source area and automatically shrink it if shrink is True.
        Tolerance enlarges (even shrinked) source area - useful for inclusion of
        filled zones which can reach out of the board edges.

        You can also specify functions which will rename the net and ref names.
        By default, nets are renamed to "Board_{n}-{orig}", refs are unchanged.
        The renamers are given board seq number and original name

        Returns bounding box (wxRect) of the extracted area placed at the
        destination and the extracted substrate of the board.
        """
        board = LoadBoard(filename)
        thickness = board.GetDesignSettings().GetBoardThickness()
        if self.boardCounter == 0:
            self.board.GetDesignSettings().SetBoardThickness(thickness)
        else:
            panelThickness = self.board.GetDesignSettings().GetBoardThickness()
            if panelThickness != thickness:
                raise PanelError(f"Cannot append board {filename} as its " \
                                 f"thickness ({toMm(thickness)} mm) differs from " \
                                 f"thickness of the panel ({toMm(panelThickness)}) mm")
        self.boardCounter += 1
        self.inheritCopperLayers(board)

        if not sourceArea:
            sourceArea = findBoardBoundingBox(board)
        elif shrink:
            sourceArea = findBoardBoundingBox(board, sourceArea)
        enlargedSourceArea = expandRect(sourceArea, tolerance)
        originPoint = getOriginCoord(origin, sourceArea)
        translation = wxPoint(destination[0] - originPoint[0],
                              destination[1] - originPoint[1])

        if netRenamer is None:
            netRenamer = lambda x, y: self._uniquePrefix() + y
        renameNets(board, lambda x: netRenamer(self.boardCounter, x))
        if refRenamer is not None:
            renameRefs(board, lambda x: refRenamer(self.boardCounter, x))

        drawings = collectItems(board.GetDrawings(), enlargedSourceArea)
        footprints = collectFootprints(board.GetFootprints(),
                                       enlargedSourceArea)
        tracks = collectItems(board.GetTracks(), enlargedSourceArea)
        zones = collectItems(board.Zones(), enlargedSourceArea)

        edges = []
        for footprint in footprints:
            footprint.Rotate(originPoint, rotationAngle)
            footprint.Move(translation)
            edges += removeCutsFromFootprint(footprint)
            appendItem(self.board, footprint)
        for track in tracks:
            track.Rotate(originPoint, rotationAngle)
            track.Move(translation)
            appendItem(self.board, track)
        for zone in zones:
            zone.Rotate(originPoint, rotationAngle)
            zone.Move(translation)
            appendItem(self.board, zone)
        for netId in board.GetNetInfo().NetsByNetcode():
            self.board.Add(board.GetNetInfo().GetNetItem(netId))

        # Treat drawings differently since they contains board edges
        for drawing in drawings:
            drawing.Rotate(originPoint, rotationAngle)
            drawing.Move(translation)
        edges += [edge for edge in drawings if isBoardEdge(edge)]
        otherDrawings = [edge for edge in drawings if not isBoardEdge(edge)]
        try:
            s = Substrate(edges, bufferOutline)
            self.boardSubstrate.union(s)
            self.substrates.append(s)
        except substrate.PositionError as e:
            point = undoTransformation(e.point, rotationAngle, originPoint,
                                       translation)
            raise substrate.PositionError(filename + ": " + e.origMessage,
                                          point)
        for drawing in otherDrawings:
            appendItem(self.board, drawing)
        return findBoundingBox(edges)
Example #4
0
class Panel:
    """
    Basic interface for panel building. Instance of this class represents a
    single panel. You can append boards, add substrate pieces, make cuts or add
    holes to the panel. Once you finish, you have to save the panel to a file.
    """
    def __init__(self):
        """
        Initializes empty panel.
        """
        self.board = pcbnew.BOARD()
        self.substrates = [
        ]  # Substrates of the individual boards; e.g. for masking
        self.boardCounter = 0
        self.boardSubstrate = Substrate(
            [])  # Keep substrate in internal representation,
        # Draw it just before saving
        self.hVCuts = set(
        )  # Keep V-cuts as numbers and append them just before saving
        self.vVCuts = set()  # to make them truly span the whole panel
        self.copperLayerCount = None
        self.zonesToRefill = pcbnew.ZONES()

    def save(self, filename):
        """
        Saves the panel to a file.
        """
        for edge in self.boardSubstrate.serialize():
            self.board.Add(edge)
        vcuts = self._renderVCutH() + self._renderVCutV()
        for cut in vcuts:
            self.board.Add(cut)
        fillerTool = pcbnew.ZONE_FILLER(self.board)
        fillerTool.Fill(self.zonesToRefill)
        self.board.Save(filename)
        for edge in collectEdges(self.board, "Edge.Cuts"):
            self.board.Remove(edge)
        for cut in vcuts:
            self.board.Remove(cut)

    def _uniquePrefix(self):
        return "Board_{}-".format(self.boardCounter)

    def inheritDesignSettings(self, boardFilename):
        """
        Inherit design settings from the given board specified by a filename
        """
        b = pcbnew.LoadBoard(boardFilename)
        self.setDesignSettings(b.GetDesignSettings())

    def setDesignSettings(self, designSettings):
        """
        Set design settings
        """
        self.board.SetDesignSettings(designSettings)

    def inheritProperties(self, boardFilename):
        """
        Inherit text properties from a board specified by a properties
        """
        b = pcbnew.LoadBoard(boardFilename)
        self.setDesignSettings(b.GetDesignSettings())

    def setProperties(self, properties):
        """
        Set text properties cached in the board
        """
        self.board.SetProperties(designSettings)

    def appendBoard(self,
                    filename,
                    destination,
                    sourceArea=None,
                    origin=Origin.Center,
                    rotationAngle=0,
                    shrink=False,
                    tolerance=0,
                    bufferOutline=fromMm(0.001),
                    netRenamer=None,
                    refRenamer=None):
        """
        Appends a board to the panel.

        The sourceArea (wxRect) of the board specified by filename is extracted
        and placed at destination (wxPoint). The source area (wxRect) can be
        auto detected if it is not provided. Only board items which fit entirely
        into the source area are selected. You can also specify rotation. Both
        translation and rotation origin are specified by origin. Origin
        specifies which point of the sourceArea is used for translation and
        rotation (origin it is placed to destination). It is possible to specify
        coarse source area and automatically shrink it if shrink is True.
        Tolerance enlarges (even shrinked) source area - useful for inclusion of
        filled zones which can reach out of the board edges.

        You can also specify functions which will rename the net and ref names.
        By default, nets are renamed to "Board_{n}-{orig}", refs are unchanged.
        The renamers are given board seq number and original name

        Returns bounding box (wxRect) of the extracted area placed at the
        destination and the extracted substrate of the board.
        """
        board = LoadBoard(filename)
        thickness = board.GetDesignSettings().GetBoardThickness()
        if self.boardCounter == 0:
            self.board.GetDesignSettings().SetBoardThickness(thickness)
        else:
            panelThickness = self.board.GetDesignSettings().GetBoardThickness()
            if panelThickness != thickness:
                raise PanelError(f"Cannot append board {filename} as its " \
                                 f"thickness ({toMm(thickness)} mm) differs from " \
                                 f"thickness of the panel ({toMm(panelThickness)}) mm")
        self.boardCounter += 1
        self.inheritCopperLayers(board)

        if not sourceArea:
            sourceArea = findBoardBoundingBox(board)
        elif shrink:
            sourceArea = findBoardBoundingBox(board, sourceArea)
        enlargedSourceArea = expandRect(sourceArea, tolerance)
        originPoint = getOriginCoord(origin, sourceArea)
        translation = wxPoint(destination[0] - originPoint[0],
                              destination[1] - originPoint[1])

        if netRenamer is None:
            netRenamer = lambda x, y: self._uniquePrefix() + y
        renameNets(board, lambda x: netRenamer(self.boardCounter, x))
        if refRenamer is not None:
            renameRefs(board, lambda x: refRenamer(self.boardCounter, x))

        drawings = collectItems(board.GetDrawings(), enlargedSourceArea)
        footprints = collectFootprints(board.GetFootprints(),
                                       enlargedSourceArea)
        tracks = collectItems(board.GetTracks(), enlargedSourceArea)
        zones = collectItems(board.Zones(), enlargedSourceArea)

        edges = []
        for footprint in footprints:
            footprint.Rotate(originPoint, rotationAngle)
            footprint.Move(translation)
            edges += removeCutsFromFootprint(footprint)
            appendItem(self.board, footprint)
        for track in tracks:
            track.Rotate(originPoint, rotationAngle)
            track.Move(translation)
            appendItem(self.board, track)
        for zone in zones:
            zone.Rotate(originPoint, rotationAngle)
            zone.Move(translation)
            appendItem(self.board, zone)
        for netId in board.GetNetInfo().NetsByNetcode():
            self.board.Add(board.GetNetInfo().GetNetItem(netId))

        # Treat drawings differently since they contains board edges
        for drawing in drawings:
            drawing.Rotate(originPoint, rotationAngle)
            drawing.Move(translation)
        edges += [edge for edge in drawings if isBoardEdge(edge)]
        otherDrawings = [edge for edge in drawings if not isBoardEdge(edge)]
        try:
            s = Substrate(edges, bufferOutline)
            self.boardSubstrate.union(s)
            self.substrates.append(s)
        except substrate.PositionError as e:
            point = undoTransformation(e.point, rotationAngle, originPoint,
                                       translation)
            raise substrate.PositionError(filename + ": " + e.origMessage,
                                          point)
        for drawing in otherDrawings:
            appendItem(self.board, drawing)
        return findBoundingBox(edges)

    def appendSubstrate(self, substrate):
        """
        Append a piece of substrate or a list of pieces to the panel. Substrate
        can be either wxRect or Shapely polygon. Newly appended corners can be
        rounded by specifying non-zero filletRadius.
        """
        polygon = toPolygon(substrate)
        self.boardSubstrate.union(polygon)

    def addVCutH(self, pos):
        """
        Adds a horizontal V-CUT at pos (integer in KiCAD units).
        """
        self.hVCuts.add(pos)

    def addVCutV(self, pos):
        """
        Adds a horizontal V-CUT at pos (integer in KiCAD units).
        """
        self.vVCuts.add(pos)

    def _setVCutSegmentStyle(self, segment, layer):
        segment.SetShape(STROKE_T.S_SEGMENT)
        segment.SetLayer(layer)
        segment.SetWidth(fromMm(0.4))

    def _setVCutLabelStyle(self, label, layer):
        label.SetText("V-CUT")
        label.SetLayer(layer)
        label.SetTextThickness(fromMm(0.4))
        label.SetTextSize(pcbnew.wxSizeMM(2, 2))
        label.SetHorizJustify(EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT)

    def _renderVCutV(self, layer=Layer.Cmts_User):
        """ return list of PCB_SHAPE V-Cuts """
        bBox = self.boardSubstrate.boundingBox()
        minY, maxY = bBox.GetY() - fromMm(
            3), bBox.GetY() + bBox.GetHeight() + fromMm(3)
        segments = []
        for cut in self.vVCuts:
            segment = pcbnew.PCB_SHAPE()
            self._setVCutSegmentStyle(segment, layer)
            segment.SetStart(pcbnew.wxPoint(cut, minY))
            segment.SetEnd(pcbnew.wxPoint(cut, maxY))
            segments.append(segment)

            label = pcbnew.PCB_TEXT(segment)
            self._setVCutLabelStyle(label, layer)
            label.SetPosition(wxPoint(cut, minY - fromMm(3)))
            label.SetTextAngle(900)
            segments.append(label)
        return segments

    def _renderVCutH(self, layer=Layer.Cmts_User):
        """ return list of PCB_SHAPE V-Cuts """
        bBox = self.boardSubstrate.boundingBox()
        minX, maxX = bBox.GetX() - fromMm(
            3), bBox.GetX() + bBox.GetWidth() + fromMm(3)
        segments = []
        for cut in self.hVCuts:
            segment = pcbnew.PCB_SHAPE()
            self._setVCutSegmentStyle(segment, layer)
            segment.SetStart(pcbnew.wxPoint(minX, cut))
            segment.SetEnd(pcbnew.wxPoint(maxX, cut))
            segments.append(segment)

            label = pcbnew.PCB_TEXT(segment)
            self._setVCutLabelStyle(label, layer)
            label.SetPosition(wxPoint(maxX + fromMm(3), cut))
            segments.append(label)
        return segments

    def _boardGridPos(self, destination, i, j, boardSize, horSpace, verSpace):
        return wxPoint(destination[0] + j * (boardSize.GetWidth() + horSpace),
                       destination[1] + i * (boardSize.GetHeight() + verSpace))

    def _placeBoardsInGrid(self, boardfile, rows, cols, destination,
                           sourceArea, tolerance, verSpace, horSpace, rotation,
                           netRenamer, refRenamer):
        """
        Create a grid of boards, return source board size aligned at the top
        left corner
        """
        boardSize = wxRect(0, 0, 0, 0)
        topLeftSize = None
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace,
                                      verSpace)
            boardSize = self.appendBoard(boardfile,
                                         dest,
                                         sourceArea=sourceArea,
                                         tolerance=tolerance,
                                         origin=Origin.TopLeft,
                                         rotationAngle=rotation,
                                         netRenamer=netRenamer,
                                         refRenamer=refRenamer)
            if not topLeftSize:
                topLeftSize = boardSize
        return topLeftSize

    def _makeFullHorizontalTabs(self, destination, rows, cols, boardSize,
                                verSpace, horSpace, outerVerSpace,
                                outerHorSpace, forceOuterCuts):
        """
        Crate full tabs for given grid.

        Return tab body, list of cut edges and list of fillet candidates as a
        tuple.
        """
        width = cols * boardSize.GetWidth() + (cols - 1) * horSpace
        height = rows * boardSize.GetHeight() + (rows - 1) * verSpace
        polygons = []
        cuts = []
        for i in range(cols - 1):
            pos = (i + 1) * boardSize.GetWidth() + i * horSpace
            tl = destination + wxPoint(pos, -outerVerSpace)
            tr = destination + wxPoint(pos + horSpace, -outerVerSpace)
            br = destination + wxPoint(pos + horSpace, height + outerVerSpace)
            bl = destination + wxPoint(pos, height + outerVerSpace)
            if horSpace > 0:
                polygon = Polygon([tl, tr, br, bl])
                polygons.append(polygon)
                cuts.append(LineString([tl, bl]))
            cuts.append(LineString([br, tr]))

        if outerHorSpace > 0:
            # Outer tabs
            polygons.append(
                Polygon([
                    destination + wxPoint(-outerHorSpace, -outerVerSpace),
                    destination + wxPoint(0, -outerVerSpace),
                    destination + wxPoint(0, height + outerVerSpace),
                    destination +
                    wxPoint(-outerHorSpace, height + outerVerSpace)
                ]))
            polygons.append(
                Polygon([
                    destination +
                    wxPoint(width + outerHorSpace, -outerVerSpace),
                    destination + wxPoint(width, -outerVerSpace),
                    destination + wxPoint(width, height + outerVerSpace),
                    destination +
                    wxPoint(width + outerHorSpace, height + outerVerSpace)
                ]))
        if forceOuterCuts or outerHorSpace > 0:
            cuts.append(
                LineString([
                    destination + wxPoint(0, height + outerVerSpace),
                    destination + wxPoint(0, -outerVerSpace)
                ]))
            cuts.append(
                LineString([
                    destination + wxPoint(width, -outerVerSpace),
                    destination + wxPoint(width, height + outerVerSpace)
                ]))
        return polygons, cuts

    def _makeFullVerticalTabs(self, destination, rows, cols, boardSize,
                              verSpace, horSpace, outerVerSpace, outerHorSpace,
                              forceOuterCuts):
        """
        Crate full tabs for given grid.

        Return tab body, list of cut edges and list of fillet candidates as a
        tuple.
        """
        width = cols * boardSize.GetWidth() + (cols - 1) * horSpace
        height = rows * boardSize.GetHeight() + (rows - 1) * verSpace
        polygons = []
        cuts = []
        for i in range(rows - 1):
            pos = (i + 1) * boardSize.GetHeight() + i * verSpace
            tl = destination + wxPoint(-outerHorSpace, pos)
            tr = destination + wxPoint(width + outerHorSpace, pos)
            br = destination + wxPoint(width + outerHorSpace, pos + verSpace)
            bl = destination + wxPoint(-outerHorSpace, pos + verSpace)
            if verSpace > 0:
                polygon = Polygon([tl, tr, br, bl])
                polygons.append(polygon)
                cuts.append(LineString([tr, tl]))
            cuts.append(LineString([bl, br]))
        if outerVerSpace > 0:
            # Outer tabs
            polygons.append(
                Polygon([
                    destination + wxPoint(-outerHorSpace, 0),
                    destination + wxPoint(-outerHorSpace, -outerVerSpace),
                    destination +
                    wxPoint(outerHorSpace + width, -outerVerSpace),
                    destination + wxPoint(outerHorSpace + width, 0)
                ]))
            polygons.append(
                Polygon([
                    destination + wxPoint(-outerHorSpace, height),
                    destination +
                    wxPoint(-outerHorSpace, height + outerVerSpace),
                    destination +
                    wxPoint(outerHorSpace + width, height + outerVerSpace),
                    destination + wxPoint(outerHorSpace + width, height)
                ]))
        if forceOuterCuts or outerVerSpace > 0:
            cuts.append(
                LineString([
                    destination + wxPoint(-outerHorSpace, 0),
                    destination + wxPoint(width + outerHorSpace, 0)
                ]))
            cuts.append(
                LineString([
                    destination + wxPoint(width + outerHorSpace, height),
                    destination + wxPoint(-outerHorSpace, height)
                ]))
        return polygons, cuts

    def _tabSpacing(self, width, count):
        """
        Given a width of board edge and tab count, return an iterable with tab
        offsets.
        """
        return [width * i / (count + 1) for i in range(1, count + 1)]

    def _makeVerGridTabs(self, destination, rows, cols, boardSize, verSpace,
                         horSpace, verTabWidth, horTabWidth, verTabCount,
                         horTabCount, outerVerTabThickness,
                         outerHorTabThickness):
        polygons = []
        cuts = []
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace,
                                      verSpace)
            if (i != 0 and verSpace > 0
                ) or outerVerTabThickness > 0:  # Add tabs to the top side
                tabThickness = outerVerTabThickness if i == 0 else verSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetWidth(),
                                               verTabCount):
                    t, f = self.boardSubstrate.tab(
                        dest + wxPoint(tabPos, -tabThickness), [0, 1],
                        verTabWidth)
                    polygons.append(t)
                    cuts.append(f)
            if (i != rows - 1 and verSpace > 0
                ) or outerVerTabThickness > 0:  # Add tabs to the bottom side
                tabThickness = outerVerTabThickness if i == rows - 1 else verSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetWidth(),
                                               verTabCount):
                    origin = dest + wxPoint(
                        tabPos,
                        boardSize.GetHeight() + tabThickness)
                    t, f = self.boardSubstrate.tab(origin, [0, -1],
                                                   verTabWidth)
                    polygons.append(t)
                    cuts.append(f)
        return polygons, cuts

    def _makeHorGridTabs(self, destination, rows, cols, boardSize, verSpace,
                         horSpace, verTabWidth, horTabWidth, verTabCount,
                         horTabCount, outerVerTabThickness,
                         outerHorTabThickness):
        polygons = []
        cuts = []
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace,
                                      verSpace)
            if (j != 0 and horSpace > 0
                ) or outerHorTabThickness > 0:  # Add tabs to the left side
                tabThickness = outerHorTabThickness if j == 0 else horSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetHeight(),
                                               horTabCount):
                    t, f = self.boardSubstrate.tab(
                        dest + wxPoint(-tabThickness, tabPos), [1, 0],
                        horTabWidth)
                    polygons.append(t)
                    cuts.append(f)
            if (j != cols - 1 and horSpace > 0
                ) or outerHorTabThickness > 0:  # Add tabs to the right side
                tabThickness = outerHorTabThickness if j == cols - 1 else horSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetHeight(),
                                               horTabCount):
                    origin = dest + wxPoint(
                        boardSize.GetWidth() + tabThickness, tabPos)
                    t, f = self.boardSubstrate.tab(origin, [-1, 0],
                                                   horTabWidth)
                    polygons.append(t)
                    cuts.append(f)
        return polygons, cuts

    def makeGrid(self,
                 boardfile,
                 rows,
                 cols,
                 destination,
                 sourceArea=None,
                 tolerance=0,
                 verSpace=0,
                 horSpace=0,
                 verTabCount=1,
                 horTabCount=1,
                 verTabWidth=0,
                 horTabWidth=0,
                 outerVerTabThickness=0,
                 outerHorTabThickness=0,
                 rotation=0,
                 forceOuterCutsH=False,
                 forceOuterCutsV=False,
                 netRenamePattern="Board_{n}-{orig}",
                 refRenamePattern="Board_{n}-{orig}"):
        """
        Creates a grid of boards (row x col) as a panel at given destination
        separated by V-CUTS. The source can be either extracted automatically or
        from given sourceArea. There can be a spacing between the individual
        board (verSpacing, horSpacing) and the tab width can be adjusted
        (verTabWidth, horTabWidth). Also, the user can control whether to append
        the outer tabs (e.g. to connect it to a frame) by setting
        outerVerTabsWidth and outerHorTabsWidth.

        Returns a tuple - wxRect with the panel bounding box (excluding
        outerTabs) and a list of cuts (list of lines) to make. You can use the
        list to either create a V-CUTS via makeVCuts or mouse bites via
        makeMouseBites.
        """
        netRenamer = lambda x, y: netRenamePattern.format(n=x, orig=y)
        refRenamer = lambda x, y: refRenamePattern.format(n=x, orig=y)
        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
                                            sourceArea, tolerance, verSpace,
                                            horSpace, rotation, netRenamer,
                                            refRenamer)
        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
        tabs, cuts = [], []

        if verTabCount != 0:
            if verTabWidth == 0:
                t, c = self._makeFullVerticalTabs(gridDest, rows, cols,
                                                  boardSize, verSpace,
                                                  horSpace,
                                                  outerVerTabThickness,
                                                  outerHorTabThickness,
                                                  forceOuterCutsV)
            else:
                t, c = self._makeVerGridTabs(gridDest, rows, cols, boardSize,
                                             verSpace, horSpace, verTabWidth,
                                             horTabWidth, verTabCount,
                                             horTabCount, outerVerTabThickness,
                                             outerHorTabThickness)
            tabs += t
            cuts += c

        if horTabCount != 0:
            if horTabWidth == 0:
                t, c = self._makeFullHorizontalTabs(gridDest, rows, cols,
                                                    boardSize, verSpace,
                                                    horSpace,
                                                    outerVerTabThickness,
                                                    outerHorTabThickness,
                                                    forceOuterCutsH)
            else:
                t, c = self._makeHorGridTabs(gridDest, rows, cols, boardSize,
                                             verSpace, horSpace, verTabWidth,
                                             horTabWidth, verTabCount,
                                             horTabCount, outerVerTabThickness,
                                             outerHorTabThickness)
            tabs += t
            cuts += c

        tabs = list([t.buffer(fromMm(0.001), join_style=2) for t in tabs])
        self.appendSubstrate(tabs)

        return (wxRect(gridDest[0], gridDest[1],
                       cols * boardSize.GetWidth() + (cols - 1) * horSpace,
                       rows * boardSize.GetHeight() + (rows - 1) * verSpace),
                cuts)

    def makeTightGrid(self,
                      boardfile,
                      rows,
                      cols,
                      destination,
                      verSpace,
                      horSpace,
                      slotWidth,
                      width,
                      height,
                      sourceArea=None,
                      tolerance=0,
                      verTabWidth=0,
                      horTabWidth=0,
                      verTabCount=1,
                      horTabCount=1,
                      rotation=0,
                      netRenamePattern="Board_{n}-{orig}",
                      refRenamePattern="Board_{n}-{orig}"):
        """
        Creates a grid of boards just like `makeGrid`, however, it creates a
        milled slot around perimeter of each board and 4 tabs.
        """
        netRenamer = lambda x, y: netRenamePattern.format(n=x, orig=y)
        refRenamer = lambda x, y: refRenamePattern.format(n=x, orig=y)
        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
                                            sourceArea, tolerance, verSpace,
                                            horSpace, rotation, netRenamer,
                                            refRenamer)
        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
        panelSize = wxRect(
            gridDest[0], gridDest[1],
            cols * boardSize.GetWidth() + (cols - 1) * horSpace,
            rows * boardSize.GetHeight() + (rows - 1) * verSpace)

        tabs, cuts = [], []
        if verTabCount != 0:
            t, c = self._makeVerGridTabs(gridDest, rows, cols, boardSize,
                                         verSpace, horSpace, verTabWidth,
                                         horTabWidth, verTabCount, horTabCount,
                                         slotWidth, slotWidth)
            tabs += t
            cuts += c
        if horTabCount != 0:
            t, c = self._makeHorGridTabs(gridDest, rows, cols, boardSize,
                                         verSpace, horSpace, verTabWidth,
                                         horTabWidth, verTabCount, horTabCount,
                                         slotWidth, slotWidth)
            tabs += t
            cuts += c

        xDiff = (width - panelSize.GetWidth()) // 2
        if xDiff < 0:
            raise RuntimeError("The frame is to small")
        yDiff = (height - panelSize.GetHeight()) // 2
        if yDiff < 0:
            raise RuntimeError("The frame is to small")
        outerRect = expandRect(panelSize, xDiff, yDiff)
        outerRing = rectToRing(outerRect)
        frame = Polygon(outerRing)
        frame = frame.difference(
            self.boardSubstrate.exterior().buffer(slotWidth))
        self.appendSubstrate(frame)

        tabs = list([t.buffer(fromMm(0.001), join_style=2) for t in tabs])
        self.appendSubstrate(tabs)

        if verTabCount != 0 or horTabCount != 0:
            self.boardSubstrate.removeIslands()

        return (outerRect, cuts)

    def makeFrame(self, innerArea, width, height, offset):
        """
        Adds a frame around given `innerArea` (`wxRect`), which can be obtained,
        e.g., by `makeGrid`, with given `width` and `height`. Space with width
        `offset` is added around the `innerArea`.
        """
        innerAreaExpanded = expandRect(innerArea, offset - fromMm(0.01))
        xDiff = (width - innerAreaExpanded.GetWidth()) // 2
        if xDiff < 0:
            raise RuntimeError("The frame is to small")
        yDiff = (height - innerAreaExpanded.GetHeight()) // 2
        if yDiff < 0:
            raise RuntimeError("The frame is to small")
        innerRing = rectToRing(innerAreaExpanded)
        outerRect = expandRect(innerAreaExpanded, xDiff, yDiff)
        outerRing = rectToRing(outerRect)
        polygon = Polygon(outerRing, [innerRing])
        frame_cuts_v = self.makeFrameCutsV(innerArea, innerAreaExpanded,
                                           outerRect)
        frame_cuts_h = self.makeFrameCutsH(innerArea, innerAreaExpanded,
                                           outerRect)
        self.appendSubstrate(polygon)
        return (outerRect, frame_cuts_v, frame_cuts_h)

    def makeRailsTb(self, thickness):
        """
        Adds a rail to top and bottom.
        """
        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
        topRail = box(minx, maxy - fromMm(0.001), maxx, maxy + thickness)
        bottomRail = box(minx, miny + fromMm(0.001), maxx, miny - thickness)
        self.appendSubstrate(topRail)
        self.appendSubstrate(bottomRail)

    def makeRailsLr(self, thickness):
        """
        Adds a rail to left and right.
        """
        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
        leftRail = box(minx - thickness + fromMm(0.001), miny, minx, maxy)
        rightRail = box(maxx - fromMm(0.001), miny, maxx + thickness, maxy)
        self.appendSubstrate(leftRail)
        self.appendSubstrate(rightRail)

    def makeFrameCutsV(self, innerArea, innerAreaExpanded, outerArea):
        x_coords = [innerArea.GetX(), innerArea.GetX() + innerArea.GetWidth()]
        y_coords = sorted([
            innerAreaExpanded.GetY(),
            innerAreaExpanded.GetY() + innerAreaExpanded.GetHeight(),
            outerArea.GetY(),
            outerArea.GetY() + outerArea.GetHeight()
        ])
        cuts = [[(x_coord, y_coords[0]), (x_coord, y_coords[1])]
                for x_coord in x_coords]
        cuts += [[(x_coord, y_coords[2]), (x_coord, y_coords[3])]
                 for x_coord in x_coords]
        return map(LineString, cuts)

    def makeFrameCutsH(self, innerArea, innerAreaExpanded, outerArea):
        y_coords = [innerArea.GetY(), innerArea.GetY() + innerArea.GetHeight()]
        x_coords = sorted([
            innerAreaExpanded.GetX(),
            innerAreaExpanded.GetX() + innerAreaExpanded.GetWidth(),
            outerArea.GetX(),
            outerArea.GetX() + outerArea.GetWidth()
        ])
        cuts = [[(x_coords[0], y_coord), (x_coords[1], y_coord)]
                for y_coord in y_coords]
        cuts += [[(x_coords[2], y_coord), (x_coords[3], y_coord)]
                 for y_coord in y_coords]
        return map(LineString, cuts)

    def makeVCuts(self, cuts, boundCurves=False):
        """
        Take a list of lines to cut and performs V-CUTS. When boundCurves is
        set, approximate curved cuts by a line from the first and last point.
        Otherwise, raise an exception.
        """
        for cut in cuts:
            if len(cut.simplify(fromMm(0.01)).coords) > 2 and not boundCurves:
                raise RuntimeError("Cannot V-Cut a curve")
            start = roundPoint(cut.coords[0])
            end = roundPoint(cut.coords[-1])
            if start.x == end.x or (abs(start.x - end.x) <= fromMm(0.5)
                                    and boundCurves):
                self.addVCutV((start.x + end.x) / 2)
            elif start.y == end.y or (abs(start.y - end.y) <= fromMm(0.5)
                                      and boundCurves):
                self.addVCutH((start.y + end.y) / 2)
            else:
                raise RuntimeError(
                    "Cannot perform V-Cut which is not horizontal or vertical")

    def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25)):
        """
        Take a list of cuts and perform mouse bites.
        """
        bloatedSubstrate = prep(
            self.boardSubstrate.substrates.buffer(fromMm(0.01)))
        for cut in cuts:
            cut = cut.simplify(
                fromMm(0.001))  # Remove self-intersecting geometry
            offsetCut = cut.parallel_offset(offset, "left")
            length = offsetCut.length
            count = int(length / spacing) + 1
            for i in range(count):
                if count == 1:
                    hole = offsetCut.interpolate(0.5, normalized=True)
                else:
                    hole = offsetCut.interpolate(i * length / (count - 1))
                if bloatedSubstrate.intersects(hole.buffer(0.8 * diameter /
                                                           2)):
                    self.addNPTHole(wxPoint(hole.x, hole.y), diameter)

    def addNPTHole(self, position, diameter):
        """
        Add a drilled non-plated hole to the position (`wxPoint`) with given
        diameter.
        """
        module = pcbnew.PCB_IO().FootprintLoad(KIKIT_LIB, "NPTH")
        module.SetPosition(position)
        for pad in module.Pads():
            pad.SetDrillSize(pcbnew.wxSize(diameter, diameter))
            pad.SetSize(pcbnew.wxSize(diameter, diameter))
        self.board.Add(module)

    def addFiducial(self,
                    position,
                    copperDiameter,
                    openingDiameter,
                    bottom=False):
        """
        Add fiducial, i.e round copper pad with solder mask opening to the position (`wxPoint`),
        with given copperDiameter and openingDiameter. By setting bottom to True, the fiducial
        is placed on bottom side.
        """
        module = pcbnew.PCB_IO().FootprintLoad(KIKIT_LIB, "Fiducial")
        module.SetPosition(position)
        if (bottom):
            if pcbnew_compatibility.isV6(pcbnew_compatibility.pcbnewVersion):
                module.Flip(position, False)
            else:
                module.Flip(position)
        for pad in module.Pads():
            pad.SetSize(pcbnew.wxSize(copperDiameter, copperDiameter))
            pad.SetLocalSolderMaskMargin(
                int((openingDiameter - copperDiameter) / 2))
            pad.SetLocalClearance(int((openingDiameter - copperDiameter) / 2))
        self.board.Add(module)

    def addFiducials(self, horizontalOffset, verticalOffset, copperDiameter,
                     openingDiameter):
        """
        Add 3 fiducials to the top-left, top-right and bottom-left corner of the
        board. This function expects there is enough space on the
        board/frame/rail to place the feature.

        The offsets are measured from the outer edges of the substrate.
        """
        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
        topLeft = wxPoint(minx + horizontalOffset, miny + verticalOffset)
        topRight = wxPoint(maxx - horizontalOffset, miny + verticalOffset)
        bottomLeft = wxPoint(minx + horizontalOffset, maxy - verticalOffset)
        for pos in [topLeft, topRight, bottomLeft]:
            self.addFiducial(pos, copperDiameter, openingDiameter, False)
            self.addFiducial(pos, copperDiameter, openingDiameter, True)

    def addTooling(self, horizontalOffset, verticalOffset, diameter):
        """
        Add 3 tooling holes to the top-left, top-right and bottom-left corner of
        the board. This function expects there is enough space on the
        board/frame/rail to place the feature.

        The offsets are measured from the outer edges of the substrate.
        """
        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
        topLeft = wxPoint(minx + horizontalOffset, miny + verticalOffset)
        topRight = wxPoint(maxx - horizontalOffset, miny + verticalOffset)
        bottomLeft = wxPoint(minx + horizontalOffset, maxy - verticalOffset)
        for pos in [topLeft, topRight, bottomLeft]:
            self.addNPTHole(pos, diameter)

    def addMillFillets(self, millRadius):
        """
        Add fillets to inner conernes which will be produced a by mill with
        given radius.
        """
        self.boardSubstrate.millFillets(millRadius)

    def layerToTabs(self, layerName, tabWidth):
        """
        Take all line pcbshapes from the given layer and convert them to
        tabs.

        The tabs are created by placing a tab origin into the line starting
        point and spanning the tab in the line direction up to the line length.
        Therefore, it is necessary that the line is long enough to penetarete
        the boardoutline.

        The lines are deleted from panel.

        Returns list of tabs substrates and a list of cuts to perform.
        """
        lines = [
            element for element in self.board.GetDrawings()
            if isinstance(element, pcbnew.PCB_SHAPE) and element.GetShape() ==
            STROKE_T.S_SEGMENT and element.GetLayerName() == layerName
        ]
        tabs, cuts = [], []
        for line in lines:
            origin = line.GetStart()
            direction = line.GetEnd() - line.GetStart()
            tab, cut = self.boardSubstrate.tab(origin, direction, tabWidth,
                                               line.GetLength())
            tabs.append(tab)
            cuts.append(cut)
            self.board.Remove(line)
        return tabs, cuts

    def inheritCopperLayers(self, board):
        """
        Update the panel's layer count to match the design being panelized.
        Raise an error if this is attempted twice with inconsistent layer count
        boards.
        """
        if (self.copperLayerCount is None):
            self.copperLayerCount = board.GetCopperLayerCount()
            self.board.SetCopperLayerCount(self.copperLayerCount)

        elif (self.copperLayerCount != board.GetCopperLayerCount()):
            raise RuntimeError(
                "Attempting to panelize boards together of mixed layer counts")

    def copperFillNonBoardAreas(self):
        """
        Fill top and bottom layers with copper on unused areas of the panel
        (frame, rails and tabs)
        """
        if not self.boardSubstrate.isSinglePiece():
            raise RuntimeError(
                "The substrate has to be a single piece to fill unused areas")
        increaseZonePriorities(self.board)

        zoneContainer = pcbnew.ZONE(self.board)
        boundary = self.boardSubstrate.exterior().boundary
        zoneContainer.Outline().AddOutline(linestringToKicad(boundary))
        for substrate in self.substrates:
            boundary = substrate.exterior().boundary
            zoneContainer.Outline().AddHole(linestringToKicad(boundary))
        zoneContainer.SetPriority(0)

        zoneContainer.SetLayer(Layer.F_Cu)
        self.board.Add(zoneContainer)
        self.zonesToRefill.append(zoneContainer)

        zoneContainer = zoneContainer.Duplicate()
        zoneContainer.SetLayer(Layer.B_Cu)
        self.board.Add(zoneContainer)
        self.zonesToRefill.append(zoneContainer)
Example #5
0
def createOffsetPolygon(board, offset):
    outerSubstrate = Substrate(collectEdges(board, "Edge.Cuts"))
    outerSubstrate.substrates = outerSubstrate.substrates.buffer(offset)
    return outerSubstrate.exterior()
Example #6
0
def createOuterPolygon(board, jigFrameSize, outerBorder):
    bBox = findBoardBoundingBox(board)
    centerpoint = rectCenter(bBox)
    holes = jigMountingHoles(jigFrameSize, centerpoint)

    outerSubstrate = Substrate(collectEdges(board, "Edge.Cuts"))
    outerSubstrate.substrates = outerSubstrate.substrates.buffer(outerBorder)
    tabs = []
    for hole in holes:
        tab, _ = outerSubstrate.tab(hole, centerpoint - hole, INNER_BORDER,
                                    fromMm(1000))
        tabs.append(tab)
    outerSubstrate.union(tabs)
    outerSubstrate.union([Point(x).buffer(INNER_BORDER / 2) for x in holes])
    outerSubstrate.millFillets(fromMm(3))
    return outerSubstrate.exterior(), holes
Example #7
0
    def appendBoard(self,
                    filename,
                    destination,
                    sourceArea=None,
                    origin=Origin.Center,
                    rotationAngle=0,
                    shrink=False,
                    tolerance=0):
        """
        Appends a board to the panel.

        The sourceArea (wxRect) of the board specified by filename is extracted
        and placed at destination (wxPoint). The source area (wxRect) can be
        auto detected if it is not provided. Only board items which fit entirely
        into the source area are selected. You can also specify rotation. Both
        translation and rotation origin are specified by origin. Origin
        specifies which point of the sourceArea is used for translation and
        rotation (origin it is placed to destination). It is possible to specify
        coarse source area and automatically shrink it if shrink is True.
        Tolerance enlarges (even shrinked) source area - useful for inclusion of
        filled zones which can reach out of the board edges.

        Returns bounding box (wxRect) of the extracted area placed at the
        destination.
        """
        board = LoadBoard(filename)
        self.boardCounter += 1

        if not sourceArea:
            sourceArea = findBoardBoundingBox(board)
        elif shrink:
            sourceArea = findBoardBoundingBox(board, sourceArea)
        tightSourceArea = findBoardBoundingBox(
            board, expandRect(sourceArea, fromMm(1)))
        enlargedSourceArea = expandRect(sourceArea, tolerance)
        originPoint = getOriginCoord(origin, sourceArea)
        translation = wxPoint(destination[0] - originPoint[0],
                              destination[1] - originPoint[1])
        tightSourceArea = translateRect(tightSourceArea, translation)

        self._makeNetNamesUnique(board)

        drawings = collectItems(board.GetDrawings(), enlargedSourceArea)
        modules = collectItems(board.GetModules(), enlargedSourceArea)
        tracks = collectItems(board.GetTracks(), enlargedSourceArea)
        zones = collectItems(board.Zones(), enlargedSourceArea)

        for module in modules:
            module.Rotate(originPoint, rotationAngle)
            module.Move(translation)
            appendItem(self.board, module)
        for track in tracks:
            track.Rotate(originPoint, rotationAngle)
            track.Move(translation)
            appendItem(self.board, track)
        for zone in zones:
            zone.Rotate(originPoint, rotationAngle)
            zone.Move(translation)
            appendItem(self.board, zone)
        for netId in board.GetNetInfo().NetsByNetcode():
            self.board.Add(board.GetNetInfo().GetNetItem(netId))

        # Treat drawings differently since they contains board edges
        for drawing in drawings:
            drawing.Rotate(originPoint, rotationAngle)
            drawing.Move(translation)
        edges = [
            edge for edge in drawings if edge.GetLayerName() == "Edge.Cuts"
        ]
        otherDrawings = [
            edge for edge in drawings if edge.GetLayerName() != "Edge.Cuts"
        ]
        self.boardSubstrate.union(Substrate(edges))
        for drawing in otherDrawings:
            appendItem(self.board, drawing)
        return tightSourceArea
Example #8
0
class Panel:
    """
    Basic interface for panel building. Instance of this class represents a
    single panel. You can append boards, add substrate pieces, make cuts or add
    holes to the panel. Once you finish, you have to save the panel to a file.
    """
    def __init__(self):
        """
        Initializes empty panel.
        """
        self.board = pcbnew.BOARD()
        self.boardCounter = 0
        self.boardSubstrate = Substrate(
            [])  # Keep substrate in internal representation,
        # Draw it just before saving
        self.hVCuts = set(
        )  # Keep V-cuts as numbers and append them just before saving
        self.vVCuts = set()  # to make them truly span the whole panel

    def save(self, filename):
        """
        Saves the panel to a file.
        """
        for edge in self.boardSubstrate.serialize():
            self.board.Add(edge)
        vcuts = self._renderVCutH() + self._renderVCutV()
        for cut in vcuts:
            self.board.Add(cut)
        self.board.Save(filename)
        for edge in collectEdges(self.board, "Edge.Cuts"):
            self.board.Remove(edge)
        for cut in vcuts:
            self.board.Remove(cut)

    def _uniquePrefix(self):
        return "Board_{}-".format(self.boardCounter)

    def appendBoard(self,
                    filename,
                    destination,
                    sourceArea=None,
                    origin=Origin.Center,
                    rotationAngle=0,
                    shrink=False,
                    tolerance=0):
        """
        Appends a board to the panel.

        The sourceArea (wxRect) of the board specified by filename is extracted
        and placed at destination (wxPoint). The source area (wxRect) can be
        auto detected if it is not provided. Only board items which fit entirely
        into the source area are selected. You can also specify rotation. Both
        translation and rotation origin are specified by origin. Origin
        specifies which point of the sourceArea is used for translation and
        rotation (origin it is placed to destination). It is possible to specify
        coarse source area and automatically shrink it if shrink is True.
        Tolerance enlarges (even shrinked) source area - useful for inclusion of
        filled zones which can reach out of the board edges.

        Returns bounding box (wxRect) of the extracted area placed at the
        destination.
        """
        board = LoadBoard(filename)
        self.boardCounter += 1

        if not sourceArea:
            sourceArea = findBoardBoundingBox(board)
        elif shrink:
            sourceArea = findBoardBoundingBox(board, sourceArea)
        tightSourceArea = findBoardBoundingBox(
            board, expandRect(sourceArea, fromMm(1)))
        enlargedSourceArea = expandRect(sourceArea, tolerance)
        originPoint = getOriginCoord(origin, sourceArea)
        translation = wxPoint(destination[0] - originPoint[0],
                              destination[1] - originPoint[1])
        tightSourceArea = translateRect(tightSourceArea, translation)

        self._makeNetNamesUnique(board)

        drawings = collectItems(board.GetDrawings(), enlargedSourceArea)
        modules = collectItems(board.GetModules(), enlargedSourceArea)
        tracks = collectItems(board.GetTracks(), enlargedSourceArea)
        zones = collectItems(board.Zones(), enlargedSourceArea)

        for module in modules:
            module.Rotate(originPoint, rotationAngle)
            module.Move(translation)
            appendItem(self.board, module)
        for track in tracks:
            track.Rotate(originPoint, rotationAngle)
            track.Move(translation)
            appendItem(self.board, track)
        for zone in zones:
            zone.Rotate(originPoint, rotationAngle)
            zone.Move(translation)
            appendItem(self.board, zone)
        for netId in board.GetNetInfo().NetsByNetcode():
            self.board.Add(board.GetNetInfo().GetNetItem(netId))

        # Treat drawings differently since they contains board edges
        for drawing in drawings:
            drawing.Rotate(originPoint, rotationAngle)
            drawing.Move(translation)
        edges = [
            edge for edge in drawings if edge.GetLayerName() == "Edge.Cuts"
        ]
        otherDrawings = [
            edge for edge in drawings if edge.GetLayerName() != "Edge.Cuts"
        ]
        self.boardSubstrate.union(Substrate(edges))
        for drawing in otherDrawings:
            appendItem(self.board, drawing)
        return tightSourceArea

    def appendSubstrate(self, substrate, filletRadius=0):
        """
        Append a piece of substrate to the panel. Substrate can be either wxRect
        or Shapely polygon. Newly appended corners can be rounded by specifying
        non-zero filletRadius.
        """
        polygon = toPolygon(substrate)
        if filletRadius == 0:
            self.boardSubstrate.union(polygon)
            return
        filletCandidates = self.boardSubstrate.boundary().intersection(
            polygon.boundary)
        self.boardSubstrate.union(polygon)
        for geom in filletCandidates:
            if not isinstance(geom, Point):
                continue
            # We found a candidate point for fillet, try to round it
            self.boardSubstrate.fillet(geom, filletRadius)

    def addVCutH(self, pos):
        """
        Adds a horizontal V-CUT at pos (integer in KiCAD units).
        """
        self.hVCuts.add(pos)

    def addVCutV(self, pos):
        """
        Adds a horizontal V-CUT at pos (integer in KiCAD units).
        """
        self.vVCuts.add(pos)

    def _setVCutSegmentStyle(self, segment, layer):
        segment.SetShape(STROKE_T.S_SEGMENT)
        segment.SetLayer(layer)
        segment.SetWidth(fromMm(0.4))

    def _setVCutLabelStyle(self, label, layer):
        label.SetText("V-CUT")
        label.SetLayer(layer)
        label.SetThickness(fromMm(0.4))
        label.SetTextSize(pcbnew.wxSizeMM(2, 2))
        label.SetHorizJustify(EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT)

    def _renderVCutV(self, layer=Layer.Cmts_User):
        """ return list of DRAWSEGMENT V-Cuts """
        bBox = self.boardSubstrate.boundingBox()
        minY, maxY = bBox.GetY() - fromMm(
            3), bBox.GetY() + bBox.GetHeight() + fromMm(3)
        segments = []
        for cut in self.vVCuts:
            segment = pcbnew.DRAWSEGMENT()
            self._setVCutSegmentStyle(segment, layer)
            segment.SetStart(pcbnew.wxPoint(cut, minY))
            segment.SetEnd(pcbnew.wxPoint(cut, maxY))
            segments.append(segment)

            label = pcbnew.TEXTE_PCB(segment)
            self._setVCutLabelStyle(label, layer)
            label.SetPosition(wxPoint(cut, minY - fromMm(3)))
            label.SetTextAngle(900)
            segments.append(label)
        return segments

    def _renderVCutH(self, layer=Layer.Cmts_User):
        """ return list of DRAWSEGMENT V-Cuts """
        bBox = self.boardSubstrate.boundingBox()
        minX, maxX = bBox.GetX() - fromMm(
            3), bBox.GetX() + bBox.GetWidth() + fromMm(3)
        segments = []
        for cut in self.hVCuts:
            segment = pcbnew.DRAWSEGMENT()
            self._setVCutSegmentStyle(segment, layer)
            segment.SetStart(pcbnew.wxPoint(minX, cut))
            segment.SetEnd(pcbnew.wxPoint(maxX, cut))
            segments.append(segment)

            label = pcbnew.TEXTE_PCB(segment)
            self._setVCutLabelStyle(label, layer)
            label.SetPosition(wxPoint(maxX + fromMm(3), cut))
            segments.append(label)
        return segments

    def _makeNetNamesUnique(self, board):
        prefix = self._uniquePrefix()
        originalNetNames = collectNetNames(board)
        netinfo = board.GetNetInfo()

        newNetMapping = {"": netinfo.GetNetItem("")}
        for name in originalNetNames:
            newNet = pcbnew.NETINFO_ITEM(board, prefix + name)
            newNetMapping[name] = newNet
            board.Add(newNet)

        remapNets(board.GetPads(), newNetMapping)
        remapNets(board.GetTracks(), newNetMapping)
        remapNets(board.Zones(), newNetMapping)

        for name in originalNetNames:
            if name != "":
                board.RemoveNative(netinfo.GetNetItem(name))

    def _boardGridPos(self, destination, i, j, boardSize, horSpace, verSpace):
        # Remove 0.001 mm to compensate for numerical imprecision when
        # merging coincident edges of boards
        return wxPoint(
            destination[0] + j *
            (boardSize.GetWidth() + horSpace - FromMM(0.001)), destination[1] +
            i * (boardSize.GetHeight() + verSpace - FromMM(0.001)))

    def _placeBoardsInGrid(self, boardfile, rows, cols, destination,
                           sourceArea, tolerance, verSpace, horSpace):
        """
        Create a grid of boards, return source board size aligned at the top
        left corner
        """
        boardSize = wxRect(0, 0, 0, 0)
        topLeftSize = None
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace,
                                      verSpace)
            boardSize = self.appendBoard(boardfile,
                                         dest,
                                         sourceArea=sourceArea,
                                         tolerance=tolerance,
                                         origin=Origin.TopLeft)
            if not topLeftSize:
                topLeftSize = boardSize
        return topLeftSize

    def _singleTabSize(self, boardSize, expectedSize, spaceSize, last):
        """ Returns offset from board edge, and tab size"""
        if expectedSize == 0:  # Full tab, add tab to the spacing
            if not last:
                return 0, boardSize + spaceSize
            return 0, boardSize
        # Smaller tab than board, shrink it!
        shrinkSize = boardSize - expectedSize
        if shrinkSize < 0:
            raise RuntimeError(
                "Tab size is larger ({}) thank board size ({})".format(
                    expectedSize, boardSize))
        return shrinkSize // 2, boardSize - shrinkSize

    def _makeSingleInnerTabs(self, destination, rows, cols, boardSize,
                             verSpace, horSpace, verTabWidth, horTabWidth,
                             radius):
        """
        Create inner tabs in board grid by placing exactly one in the middle
        """
        lastRow = lambda x: x == rows - 1
        lastCol = lambda x: x == cols - 1
        cuts = []
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace,
                                      verSpace)
            if not lastRow(i):
                # Add bottom tab
                xOffset, width = self._singleTabSize(boardSize.GetWidth(),
                                                     verTabWidth, horSpace,
                                                     lastCol(j))
                tab = wxRect(dest[0] + xOffset,
                             dest[1] + boardSize.GetHeight(), width, verSpace)
                cuts.append(
                    ((tab.GetX(), tab.GetY()), (tab.GetX() + tab.GetWidth(),
                                                tab.GetY())))
                if tab.GetHeight() != 0:
                    cuts.append(((tab.GetX(), tab.GetY() + tab.GetHeight()),
                                 (tab.GetX() + tab.GetWidth(),
                                  tab.GetY() + tab.GetHeight())))
                    tab = expandRect(tab, FromMM(0.001))
                    self.appendSubstrate(tab, radius)
            if not lastCol(j):
                # Add right tab
                yOffset, height = self._singleTabSize(boardSize.GetHeight(),
                                                      horTabWidth, verSpace,
                                                      lastRow(i))
                tab = wxRect(dest[0] + boardSize.GetWidth(), dest[1] + yOffset,
                             horSpace, height)
                cuts.append(((tab.GetX(), tab.GetY()),
                             (tab.GetX(), tab.GetY() + tab.GetHeight())))
                if tab.GetHeight() != 0:
                    cuts.append(((tab.GetX() + tab.GetWidth(),
                                  tab.GetY()), (tab.GetX() + tab.GetWidth(),
                                                tab.GetY() + tab.GetHeight())))
                    tab = expandRect(tab, FromMM(0.001))
                    self.appendSubstrate(tab, radius)
        return cuts

    def _makeSingleOuterVerTabs(self, destination, rows, cols, boardSize,
                                verSpace, horSpace, verTabWidth, horTabWidth,
                                radius, outerVerTabThickness,
                                outerHorTabThickness):
        lastCol = lambda x: x == cols - 1
        cuts = []

        def appendVerticalTab(destination, i, outerVerTabThickness):
            dest = self._boardGridPos(destination, 0, i, boardSize, horSpace,
                                      verSpace)
            spaceSize = outerHorTabThickness if lastCol(i) else horSpace
            xOffset, width = self._singleTabSize(boardSize.GetWidth(),
                                                 verTabWidth, spaceSize, False)
            tab = wxRect(dest[0] + xOffset, dest[1] - outerVerTabThickness,
                         width, outerVerTabThickness)
            if i == 0 and verTabWidth == 0:
                tab.SetX(tab.GetX() - outerHorTabThickness)
                tab.SetWidth(tab.GetWidth() + outerHorTabThickness)
            cuts.append(
                ((tab.GetX(), tab.GetY() + tab.GetHeight()),
                 (tab.GetX() + tab.GetWidth(), tab.GetY() + tab.GetHeight())))
            tab = normalizeRect(tab)
            tab = expandRect(tab, FromMM(0.001))
            self.appendSubstrate(tab, radius)

        for i in range(cols):
            appendVerticalTab(destination, i, outerVerTabThickness)
            dest = self._boardGridPos(destination, rows - 1, 0, boardSize,
                                      horSpace, verSpace)
            appendVerticalTab(dest + wxPoint(0, boardSize.GetHeight()), i,
                              -outerVerTabThickness)
        return cuts

    def _makeSingleOuterHorTabs(self, destination, rows, cols, boardSize,
                                verSpace, horSpace, verTabWidth, horTabWidth,
                                radius, outerVerTabThickness,
                                outerHorTabThickness):
        lastRow = lambda x: x == rows - 1
        cuts = []

        def appendHorizontalTab(destination, i, outerHorTabThickness):
            dest = self._boardGridPos(destination, i, 0, boardSize, horSpace,
                                      verSpace)
            spaceSize = outerVerTabThickness if lastRow(i) else verSpace
            yOffset, height = self._singleTabSize(boardSize.GetHeight(),
                                                  horTabWidth, spaceSize,
                                                  False)
            tab = wxRect(dest[0] - outerHorTabThickness, dest[1] + yOffset,
                         outerHorTabThickness, height)
            if i == 0 and horTabWidth == 0:
                tab.SetY(tab.GetY() - outerVerTabThickness)
                tab.SetHeight(tab.GetHeight() + outerVerTabThickness)
            cuts.append(
                ((tab.GetX() + tab.GetWidth(), tab.GetY()),
                 (tab.GetX() + tab.GetWidth(), tab.GetY() + tab.GetHeight())))
            tab = normalizeRect(tab)
            tab = expandRect(tab, FromMM(0.001))
            self.appendSubstrate(tab, radius)

        for i in range(rows):
            appendHorizontalTab(destination, i, outerHorTabThickness)
            dest = self._boardGridPos(destination, 0, cols - 1, boardSize,
                                      horSpace, verSpace)
            appendHorizontalTab(dest + wxPoint(boardSize.GetWidth(), 0), i,
                                -outerHorTabThickness)
        return cuts

    def makeGrid(self,
                 boardfile,
                 rows,
                 cols,
                 destination,
                 sourceArea=None,
                 tolerance=0,
                 radius=0,
                 verSpace=0,
                 horSpace=0,
                 verTabWidth=0,
                 horTabWidth=0,
                 outerVerTabThickness=0,
                 outerHorTabThickness=0):
        """
        Creates a grid of boards (row x col) as a panel at given destination
        separated by V-CUTS. The source can be either extract automatically of
        from given sourceArea. There can be a spacing between the individual
        board (verSpacing, horSpacing) and the tab width can be adjusted
        (verTabWidth, horTabWidth). Also the user can control whether append the
        outer tabs (e.g. to connect it to a frame) by setting outerVerTabsWidth
        and outerHorTabsWidth.

        Returns a tuple - wxRect with the panel bounding box (excluding
        outerTabs) and a list of cuts (list of lines) to make. You can use the
        list to either create a V-CUTS via makeVCuts or mouse bites via
        makeMouseBites.
        """
        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
                                            sourceArea, tolerance, verSpace,
                                            horSpace)
        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
        cuts = self._makeSingleInnerTabs(gridDest, rows, cols, boardSize,
                                         verSpace, horSpace, verTabWidth,
                                         horTabWidth, radius)

        if outerVerTabThickness > 0:
            cuts += self._makeSingleOuterVerTabs(gridDest, rows, cols,
                                                 boardSize, verSpace, horSpace,
                                                 verTabWidth, horTabWidth,
                                                 radius, outerVerTabThickness,
                                                 outerHorTabThickness)

        if outerHorTabThickness > 0:
            cuts += self._makeSingleOuterHorTabs(gridDest, rows, cols,
                                                 boardSize, verSpace, horSpace,
                                                 verTabWidth, horTabWidth,
                                                 radius, outerVerTabThickness,
                                                 outerHorTabThickness)

        return (wxRect(gridDest[0], gridDest[1],
                       cols * boardSize.GetWidth() + (cols - 1) * horSpace,
                       rows * boardSize.GetHeight() + (rows - 1) * verSpace),
                cuts)

    def makeTightGrid(self,
                      boardfile,
                      rows,
                      cols,
                      destination,
                      verSpace,
                      horSpace,
                      slotWidth,
                      width,
                      height,
                      sourceArea=None,
                      tolerance=0,
                      radius=0,
                      verTabWidth=0,
                      horTabWidth=0):
        """
        Creates a grid of boards just like `makeGrid`, however, it creates a
        milled slot around perimeter of each board and 4 tabs.
        """
        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
                                            sourceArea, tolerance, verSpace,
                                            horSpace)
        panelSize = wxRect(
            destination[0], destination[1],
            cols * boardSize.GetWidth() + (cols - 1) * horSpace,
            rows * boardSize.GetHeight() + (rows - 1) * verSpace)

        xDiff = (width - panelSize.GetWidth()) // 2
        if xDiff < 0:
            raise RuntimeError("The frame is to small")
        yDiff = (height - panelSize.GetHeight()) // 2
        if yDiff < 0:
            raise RuntimeError("The frame is to small")
        outerRect = expandRect(panelSize, xDiff, yDiff)
        outerRing = rectToRing(outerRect)
        frame = Polygon(outerRing)
        frame = frame.difference(
            self.boardSubstrate.exterior().buffer(slotWidth))
        self.appendSubstrate(frame)

        cuts = self._makeSingleInnerTabs(destination, rows, cols, boardSize,
                                         verSpace, horSpace, verTabWidth,
                                         horTabWidth, radius)
        cuts += self._makeSingleOuterVerTabs(destination, rows, cols,
                                             boardSize, verSpace, horSpace,
                                             verTabWidth, horTabWidth, radius,
                                             slotWidth, slotWidth)
        cuts += self._makeSingleOuterHorTabs(destination, rows, cols,
                                             boardSize, verSpace, horSpace,
                                             verTabWidth, horTabWidth, radius,
                                             slotWidth, slotWidth)
        return (outerRect, cuts)

    def makeFrame(self, innerArea, width, height, offset, radius=0):
        """
        Adds a frame around given `innerArea` (`wxRect`), which can be obtained,
        e.g., by `makeGrid`, with given `width` and `height`. Space with width
        `offset` is added around the `innerArea`.
        """
        innerArea = expandRect(innerArea, offset)
        innerArea = expandRect(innerArea, -fromMm(0.01))
        xDiff = (width - innerArea.GetWidth()) // 2
        if xDiff < 0:
            raise RuntimeError("The frame is to small")
        yDiff = (height - innerArea.GetHeight()) // 2
        if yDiff < 0:
            raise RuntimeError("The frame is to small")
        innerRing = rectToRing(innerArea)
        outerRect = expandRect(innerArea, xDiff, yDiff)
        outerRing = rectToRing(outerRect)
        polygon = Polygon(outerRing, [innerRing])
        self.appendSubstrate(polygon, radius)
        for x, y in innerRing:
            self.boardSubstrate.fillet(Point(x, y), radius)
        return outerRect

    def makeVCuts(self, cuts):
        """
        Take a list of lines to cut and performs V-CUTS
        """
        for start, end in cuts:
            if start[0] == end[0]:
                self.addVCutV(start[0])
            elif start[1] == end[1]:
                self.addVCutH(start[1])
            else:
                raise RuntimeError(
                    "Cannot perform V-Cut which is not horizontal or vertical")

    def makeMouseBites(self, cuts, diameter, spacing):
        """
        Take a list of cuts and perform mouse bites.
        """
        for start, end in cuts:
            start, end = np.array(start), np.array(end)
            dir = end - start
            count = int(np.linalg.norm(dir) / spacing) + 1
            for i in range(count):
                hole = start + i * dir / (count - 1)
                self.addNPTHole(wxPoint(hole[0], hole[1]), diameter)

    def addNPTHole(self, position, diameter):
        """
        Add a drilled non-plated hole to the position (`wxPoint`) with given
        diameter.
        """
        module = pcbnew.PCB_IO().FootprintLoad(KIKIT_LIB, "NPTH")
        module.SetPosition(position)
        for pad in module.Pads():
            pad.SetDrillSize(pcbnew.wxSize(diameter, diameter))
            pad.SetSize(pcbnew.wxSize(diameter, diameter))
        self.board.Add(module)
Example #9
0
class Panel:
    """
    Basic interface for panel building. Instance of this class represents a
    single panel. You can append boards, add substrate pieces, make cuts or add
    holes to the panel. Once you finish, you have to save the panel to a file.
    """
    def __init__(self):
        """
        Initializes empty panel.
        """
        self.board = pcbnew.BOARD()
        self.boardCounter = 0
        self.boardSubstrate = Substrate([]) # Keep substrate in internal representation,
                                            # Draw it just before saving
        self.hVCuts = set() # Keep V-cuts as numbers and append them just before saving
        self.vVCuts = set() # to make them truly span the whole panel

    def save(self, filename):
        """
        Saves the panel to a file.
        """
        for edge in self.boardSubstrate.serialize():
            self.board.Add(edge)
        vcuts = self._renderVCutH() + self._renderVCutV()
        for cut in vcuts:
            self.board.Add(cut)
        self.board.Save(filename)
        for edge in collectEdges(self.board, "Edge.Cuts"):
            self.board.Remove(edge)
        for cut in vcuts:
            self.board.Remove(cut)

    def _uniquePrefix(self):
        return "Board_{}-".format(self.boardCounter)

    def appendBoard(self, filename, destination, sourceArea=None,
                    origin=Origin.Center, rotationAngle=0, shrink=False,
                    tolerance=0, bufferOutline=fromMm(0.001)):
        """
        Appends a board to the panel.

        The sourceArea (wxRect) of the board specified by filename is extracted
        and placed at destination (wxPoint). The source area (wxRect) can be
        auto detected if it is not provided. Only board items which fit entirely
        into the source area are selected. You can also specify rotation. Both
        translation and rotation origin are specified by origin. Origin
        specifies which point of the sourceArea is used for translation and
        rotation (origin it is placed to destination). It is possible to specify
        coarse source area and automatically shrink it if shrink is True.
        Tolerance enlarges (even shrinked) source area - useful for inclusion of
        filled zones which can reach out of the board edges.

        Returns bounding box (wxRect) of the extracted area placed at the
        destination.
        """
        board = LoadBoard(filename)
        self.boardCounter += 1

        if not sourceArea:
            sourceArea = findBoardBoundingBox(board)
        elif shrink:
            sourceArea = findBoardBoundingBox(board, sourceArea)
        enlargedSourceArea = expandRect(sourceArea, tolerance)
        originPoint = getOriginCoord(origin, sourceArea)
        translation = wxPoint(destination[0] - originPoint[0],
                              destination[1] - originPoint[1])

        self._makeNetNamesUnique(board)

        drawings = collectItems(board.GetDrawings(), enlargedSourceArea)
        modules = collectItems(board.GetModules(), enlargedSourceArea)
        tracks = collectItems(board.GetTracks(), enlargedSourceArea)
        zones = collectItems(board.Zones(), enlargedSourceArea)

        for module in modules:
            module.Rotate(originPoint, rotationAngle)
            module.Move(translation)
            appendItem(self.board, module)
        for track in tracks:
            track.Rotate(originPoint, rotationAngle)
            track.Move(translation)
            appendItem(self.board, track)
        for zone in zones:
            zone.Rotate(originPoint, rotationAngle)
            zone.Move(translation)
            appendItem(self.board, zone)
        for netId in board.GetNetInfo().NetsByNetcode():
            self.board.Add(board.GetNetInfo().GetNetItem(netId))

        # Treat drawings differently since they contains board edges
        for drawing in drawings:
            drawing.Rotate(originPoint, rotationAngle)
            drawing.Move(translation)
        edges = [edge for edge in drawings if edge.GetLayerName() == "Edge.Cuts"]
        otherDrawings = [edge for edge in drawings if edge.GetLayerName() != "Edge.Cuts"]
        try:
            self.boardSubstrate.union(Substrate(edges, bufferOutline))
        except substrate.PositionError as e:
            point = undoTransformation(e.point, rotationAngle, originPoint, translation)
            raise substrate.PositionError(filename + ": " + e.origMessage, point)
        for drawing in otherDrawings:
            appendItem(self.board, drawing)
        return findBoundingBox(edges)

    def appendSubstrate(self, substrate):
        """
        Append a piece of substrate or a list of pieces to the panel. Substrate
        can be either wxRect or Shapely polygon. Newly appended corners can be
        rounded by specifying non-zero filletRadius.
        """
        polygon = toPolygon(substrate)
        self.boardSubstrate.union(polygon)

    def addVCutH(self, pos):
        """
        Adds a horizontal V-CUT at pos (integer in KiCAD units).
        """
        self.hVCuts.add(pos)

    def addVCutV(self, pos):
        """
        Adds a horizontal V-CUT at pos (integer in KiCAD units).
        """
        self.vVCuts.add(pos)

    def _setVCutSegmentStyle(self, segment, layer):
        segment.SetShape(STROKE_T.S_SEGMENT)
        segment.SetLayer(layer)
        segment.SetWidth(fromMm(0.4))

    def _setVCutLabelStyle(self, label, layer):
        label.SetText("V-CUT")
        label.SetLayer(layer)
        label.SetThickness(fromMm(0.4))
        label.SetTextSize(pcbnew.wxSizeMM(2, 2))
        label.SetHorizJustify(EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT)

    def _renderVCutV(self, layer=Layer.Cmts_User):
        """ return list of DRAWSEGMENT V-Cuts """
        bBox = self.boardSubstrate.boundingBox()
        minY, maxY = bBox.GetY() - fromMm(3), bBox.GetY() + bBox.GetHeight() + fromMm(3)
        segments = []
        for cut in self.vVCuts:
            segment = pcbnew.DRAWSEGMENT()
            self._setVCutSegmentStyle(segment, layer)
            segment.SetStart(pcbnew.wxPoint(cut, minY))
            segment.SetEnd(pcbnew.wxPoint(cut, maxY))
            segments.append(segment)

            label = pcbnew.TEXTE_PCB(segment)
            self._setVCutLabelStyle(label, layer)
            label.SetPosition(wxPoint(cut, minY - fromMm(3)))
            label.SetTextAngle(900)
            segments.append(label)
        return segments

    def _renderVCutH(self, layer=Layer.Cmts_User):
        """ return list of DRAWSEGMENT V-Cuts """
        bBox = self.boardSubstrate.boundingBox()
        minX, maxX = bBox.GetX() - fromMm(3), bBox.GetX() + bBox.GetWidth() + fromMm(3)
        segments = []
        for cut in self.hVCuts:
            segment = pcbnew.DRAWSEGMENT()
            self._setVCutSegmentStyle(segment, layer)
            segment.SetStart(pcbnew.wxPoint(minX, cut))
            segment.SetEnd(pcbnew.wxPoint(maxX, cut))
            segments.append(segment)

            label = pcbnew.TEXTE_PCB(segment)
            self._setVCutLabelStyle(label, layer)
            label.SetPosition(wxPoint(maxX + fromMm(3), cut))
            segments.append(label)
        return segments

    def _makeNetNamesUnique(self, board):
        prefix = self._uniquePrefix()
        originalNetNames = collectNetNames(board)
        netinfo = board.GetNetInfo()

        newNetMapping = { "": netinfo.GetNetItem("") }
        for name in originalNetNames:
            newNet = pcbnew.NETINFO_ITEM(board, prefix + name)
            newNetMapping[name] = newNet
            board.Add(newNet)

        remapNets(board.GetPads(), newNetMapping)
        remapNets(board.GetTracks(), newNetMapping)
        remapNets(board.Zones(), newNetMapping)

        for name in originalNetNames:
            if name != "":
                board.RemoveNative(netinfo.GetNetItem(name))

    def _boardGridPos(self, destination, i, j, boardSize, horSpace, verSpace):
        return wxPoint(destination[0] + j * (boardSize.GetWidth() + horSpace),
                       destination[1] + i * (boardSize.GetHeight() + verSpace))

    def _placeBoardsInGrid(self, boardfile, rows, cols, destination, sourceArea, tolerance,
                  verSpace, horSpace, rotation):
        """
        Create a grid of boards, return source board size aligned at the top
        left corner
        """
        boardSize = wxRect(0, 0, 0, 0)
        topLeftSize = None
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace, verSpace)
            boardSize = self.appendBoard(boardfile, dest, sourceArea=sourceArea,
                                         tolerance=tolerance, origin=Origin.TopLeft, rotationAngle=rotation)
            if not topLeftSize:
                topLeftSize = boardSize
        return topLeftSize

    def _makeFullHorizontalTabs(self, destination, rows, cols, boardSize,
                                verSpace, horSpace, outerVerSpace, outerHorSpace):
        """
        Crate full tabs for given grid.

        Return tab body, list of cut edges and list of fillet candidates as a
        tuple.
        """
        width = cols * boardSize.GetWidth() + (cols - 1) * horSpace
        height = rows * boardSize.GetHeight() + (rows - 1) * verSpace
        polygons = []
        cuts = []
        for i in range(cols - 1):
            pos = (i + 1) * boardSize.GetWidth() + i * horSpace
            tl = destination + wxPoint(pos, -outerVerSpace)
            tr = destination + wxPoint(pos + horSpace, -outerVerSpace)
            br = destination + wxPoint(pos + horSpace, height + outerVerSpace)
            bl = destination + wxPoint(pos, height + outerVerSpace)
            if horSpace > 0:
                polygon = Polygon([tl, tr, br, bl])
                polygons.append(polygon)
                cuts.append(LineString([tl, bl]))
            cuts.append(LineString([br, tr]))
        if outerHorSpace > 0:
            # Outer tabs
            polygons.append(Polygon([
                destination + wxPoint(-outerHorSpace, -outerVerSpace),
                destination + wxPoint(0, -outerVerSpace),
                destination + wxPoint(0, height + outerVerSpace),
                destination + wxPoint(-outerHorSpace, height + outerVerSpace)]))
            polygons.append(Polygon([
                destination + wxPoint(width + outerHorSpace, -outerVerSpace),
                destination + wxPoint(width, -outerVerSpace),
                destination + wxPoint(width, height + outerVerSpace),
                destination + wxPoint(width + outerHorSpace, height + outerVerSpace)]))

            cuts.append(LineString([destination + wxPoint(0, height + outerVerSpace),
                destination + wxPoint(0, -outerVerSpace)]))
            cuts.append(LineString([destination + wxPoint(width, -outerVerSpace),
                destination + wxPoint(width, height + outerVerSpace)]))
        return polygons, cuts

    def _makeFullVerticalTabs(self, destination, rows, cols, boardSize,
                                   verSpace, horSpace, outerVerSpace, outerHorSpace):
        """
        Crate full tabs for given grid.

        Return tab body, list of cut edges and list of fillet candidates as a
        tuple.
        """
        width = cols * boardSize.GetWidth() + (cols - 1) * horSpace
        height = rows * boardSize.GetHeight() + (rows - 1) * verSpace
        polygons = []
        cuts = []
        for i in range(rows - 1):
            pos = (i + 1) * boardSize.GetHeight() + i * verSpace
            tl = destination + wxPoint(-outerHorSpace, pos)
            tr = destination + wxPoint(width + outerHorSpace, pos)
            br = destination + wxPoint(width + outerHorSpace, pos + verSpace)
            bl = destination + wxPoint(-outerHorSpace, pos + verSpace)
            if verSpace > 0:
                polygon = Polygon([tl, tr, br, bl])
                polygons.append(polygon)
                cuts.append(LineString([tr, tl]))
            cuts.append(LineString([bl, br]))
        if outerVerSpace > 0:
            # Outer tabs
            polygons.append(Polygon([
                destination + wxPoint(-outerHorSpace, 0),
                destination + wxPoint(-outerHorSpace, -outerVerSpace),
                destination + wxPoint(outerHorSpace + width, - outerVerSpace),
                destination + wxPoint(outerHorSpace + width, 0)]))
            polygons.append(Polygon([
                destination + wxPoint(-outerHorSpace, height),
                destination + wxPoint(-outerHorSpace, height + outerVerSpace),
                destination + wxPoint(outerHorSpace + width, height + outerVerSpace),
                destination + wxPoint(outerHorSpace + width, height)]))
            cuts.append(LineString([destination + wxPoint(-outerHorSpace, 0),
                destination + wxPoint(width + outerHorSpace, 0)]))
            cuts.append(LineString([destination + wxPoint(width + outerHorSpace, height),
                destination + wxPoint(-outerHorSpace, height)]))
        return polygons, cuts

    def _tabSpacing(self, width, count):
        """
        Given a width of board edge and tab count, return an iterable with tab
        offsets.
        """
        return [width * i / (count + 1) for i in range(1, count + 1)]

    def _makeVerGridTabs(self, destination, rows, cols, boardSize, verSpace,
                      horSpace, verTabWidth, horTabWidth, verTabCount,
                      horTabCount, outerVerTabThickness, outerHorTabThickness):
        polygons = []
        cuts = []
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace, verSpace)
            if (i != 0 and verSpace > 0) or outerVerTabThickness > 0: # Add tabs to the top side
                tabThickness = outerVerTabThickness if i == 0 else verSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetWidth(), verTabCount):
                    t, f = self.boardSubstrate.tab(
                        dest + wxPoint(tabPos, -tabThickness), [0, 1], verTabWidth)
                    polygons.append(t)
                    cuts.append(f)
            if (i != rows - 1 and verSpace > 0) or outerVerTabThickness > 0: # Add tabs to the bottom side
                tabThickness = outerVerTabThickness if i == rows - 1 else verSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetWidth(), verTabCount):
                    origin = dest + wxPoint(tabPos, boardSize.GetHeight() + tabThickness)
                    t, f = self.boardSubstrate.tab(origin, [0, -1], verTabWidth)
                    polygons.append(t)
                    cuts.append(f)
        return polygons, cuts

    def _makeHorGridTabs(self, destination, rows, cols, boardSize, verSpace,
                      horSpace, verTabWidth, horTabWidth, verTabCount,
                      horTabCount, outerVerTabThickness, outerHorTabThickness):
        polygons = []
        cuts = []
        for i, j in product(range(rows), range(cols)):
            dest = self._boardGridPos(destination, i, j, boardSize, horSpace, verSpace)
            if (j != 0 and horSpace > 0) or outerHorTabThickness > 0: # Add tabs to the left side
                tabThickness = outerHorTabThickness if j == 0 else horSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetHeight(), horTabCount):
                    t, f = self.boardSubstrate.tab(
                        dest + wxPoint(-tabThickness, tabPos), [1, 0], horTabWidth)
                    polygons.append(t)
                    cuts.append(f)
            if (j != cols - 1 and horSpace > 0) or outerHorTabThickness > 0: # Add tabs to the right side
                tabThickness = outerHorTabThickness if j == cols - 1 else horSpace / 2
                for tabPos in self._tabSpacing(boardSize.GetHeight(), horTabCount):
                    origin = dest + wxPoint(boardSize.GetWidth() + tabThickness, tabPos)
                    t, f = self.boardSubstrate.tab(
                        origin, [-1, 0], horTabWidth)
                    polygons.append(t)
                    cuts.append(f)
        return polygons, cuts

    def makeGrid(self, boardfile, rows, cols, destination, sourceArea=None,
                 tolerance=0, verSpace=0, horSpace=0, verTabCount=1,
                 horTabCount=1, verTabWidth=0, horTabWidth=0,
                 outerVerTabThickness=0, outerHorTabThickness=0, rotation=0):
        """
        Creates a grid of boards (row x col) as a panel at given destination
        separated by V-CUTS. The source can be either extracted automatically or
        from given sourceArea. There can be a spacing between the individual
        board (verSpacing, horSpacing) and the tab width can be adjusted
        (verTabWidth, horTabWidth). Also, the user can control whether to append
        the outer tabs (e.g. to connect it to a frame) by setting
        outerVerTabsWidth and outerHorTabsWidth.

        Returns a tuple - wxRect with the panel bounding box (excluding
        outerTabs) and a list of cuts (list of lines) to make. You can use the
        list to either create a V-CUTS via makeVCuts or mouse bites via
        makeMouseBites.
        """
        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
                                    sourceArea, tolerance, verSpace, horSpace, rotation)
        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
        tabs, cuts = [], []

        if verTabWidth == 0:
            t, c = self._makeFullVerticalTabs(gridDest, rows, cols,
                boardSize, verSpace, horSpace, outerVerTabThickness, outerHorTabThickness)
        else:
            t, c = self._makeVerGridTabs(gridDest, rows, cols, boardSize,
                verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
                horTabCount, outerVerTabThickness, outerHorTabThickness)
        tabs += t
        cuts += c

        if horTabWidth == 0:
            t, c = self._makeFullHorizontalTabs(gridDest, rows, cols,
                boardSize, verSpace, horSpace, outerVerTabThickness, outerHorTabThickness)
        else:
            t, c = self._makeHorGridTabs(gridDest, rows, cols, boardSize,
                verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
                horTabCount, outerVerTabThickness, outerHorTabThickness)
        tabs += t
        cuts += c

        tabs = list([t.buffer(fromMm(0.001), join_style=2) for t in tabs])
        self.appendSubstrate(tabs)

        return (wxRect(gridDest[0], gridDest[1],
                       cols * boardSize.GetWidth() + (cols - 1) * horSpace,
                       rows * boardSize.GetHeight() + (rows - 1) * verSpace),
                cuts)


    def makeTightGrid(self, boardfile, rows, cols, destination, verSpace,
                      horSpace, slotWidth, width, height, sourceArea=None,
                      tolerance=0, verTabWidth=0, horTabWidth=0,
                      verTabCount=1, horTabCount=1, rotation=0):
        """
        Creates a grid of boards just like `makeGrid`, however, it creates a
        milled slot around perimeter of each board and 4 tabs.
        """
        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
                                    sourceArea, tolerance, verSpace, horSpace, rotation)
        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
        panelSize = wxRect(destination[0], destination[1],
                       cols * boardSize.GetWidth() + (cols - 1) * horSpace,
                       rows * boardSize.GetHeight() + (rows - 1) * verSpace)

        tabs, cuts = [], []
        t, c = self._makeVerGridTabs(gridDest, rows, cols, boardSize,
                verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
                horTabCount, slotWidth, slotWidth)
        tabs += t
        cuts += c
        t, c = self._makeHorGridTabs(gridDest, rows, cols, boardSize,
                verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
                horTabCount, slotWidth, slotWidth)
        tabs += t
        cuts += c

        xDiff = (width - panelSize.GetWidth()) // 2
        if xDiff < 0:
            raise RuntimeError("The frame is to small")
        yDiff = (height - panelSize.GetHeight()) // 2
        if yDiff < 0:
            raise RuntimeError("The frame is to small")
        outerRect = expandRect(panelSize, xDiff, yDiff)
        outerRing = rectToRing(outerRect)
        frame = Polygon(outerRing)
        frame = frame.difference(self.boardSubstrate.exterior().buffer(slotWidth))
        self.appendSubstrate(frame)

        tabs = list([t.buffer(fromMm(0.001), join_style=2) for t in tabs])
        self.appendSubstrate(tabs)

        self.boardSubstrate.removeIslands()

        return (outerRect, cuts)


    def makeFrame(self, innerArea, width, height, offset):
        """
        Adds a frame around given `innerArea` (`wxRect`), which can be obtained,
        e.g., by `makeGrid`, with given `width` and `height`. Space with width
        `offset` is added around the `innerArea`.
        """
        innerArea = expandRect(innerArea, offset)
        innerArea = expandRect(innerArea, -fromMm(0.01))
        xDiff = (width - innerArea.GetWidth()) // 2
        if xDiff < 0:
            raise RuntimeError("The frame is to small")
        yDiff = (height - innerArea.GetHeight()) // 2
        if yDiff < 0:
            raise RuntimeError("The frame is to small")
        innerRing = rectToRing(innerArea)
        outerRect = expandRect(innerArea, xDiff, yDiff)
        outerRing = rectToRing(outerRect)
        polygon = Polygon(outerRing, [innerRing])
        self.appendSubstrate(polygon)
        return outerRect

    def makeVCuts(self, cuts, boundCurves=False):
        """
        Take a list of lines to cut and performs V-CUTS. When boundCurves is
        set, approximate curved cuts by a line from the first and last point.
        Otherwise, raise an exception.
        """
        for cut in cuts:
            if len(cut.simplify(fromMm(0.01)).coords) > 2 and not boundCurves:
                raise RuntimeError("Cannot V-Cut a curve")
            start = roundPoint(cut.coords[0])
            end = roundPoint(cut.coords[-1])
            if start.x == end.x or (abs(start.x - end.x) <= fromMm(0.5) and boundCurves):
                self.addVCutV((start.x + end.x) / 2)
            elif start.y == end.y or (abs(start.y - end.y) <= fromMm(0.5) and boundCurves):
                self.addVCutH((start.y + end.y) / 2)
            else:
                raise RuntimeError("Cannot perform V-Cut which is not horizontal or vertical")

    def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25)):
        """
        Take a list of cuts and perform mouse bites.
        """
        bloatedSubstrate = prep(self.boardSubstrate.substrates.buffer(fromMm(0.1)))
        for cut in cuts:
            cut = cut.simplify(fromMm(0.001)) # Remove self-intersecting geometry
            offsetCut = cut.parallel_offset(offset, "left")
            length = offsetCut.length
            count = int(length / spacing) + 1
            for i in range(count):
                if count == 1:
                    hole = offsetCut.interpolate(0.5, normalized=True)
                else:
                    hole = offsetCut.interpolate( i * length / (count - 1) )
                if bloatedSubstrate.contains(hole.buffer(0.8 * diameter / 2)):
                    self.addNPTHole(wxPoint(hole.x, hole.y), diameter)

    def addNPTHole(self, position, diameter):
        """
        Add a drilled non-plated hole to the position (`wxPoint`) with given
        diameter.
        """
        module = pcbnew.PCB_IO().FootprintLoad(KIKIT_LIB, "NPTH")
        module.SetPosition(position)
        for pad in module.Pads():
            pad.SetDrillSize(pcbnew.wxSize(diameter, diameter))
            pad.SetSize(pcbnew.wxSize(diameter, diameter))
        self.board.Add(module)

    def addMillFillets(self, millRadius):
        """
        Add fillets to inner conernes which will be produced a by mill with
        given radius.
        """
        self.boardSubstrate.millFillets(millRadius)