diff --git a/kikit/export.py b/kikit/export.py index 77742ec1..0c16d585 100644 --- a/kikit/export.py +++ b/kikit/export.py @@ -1,21 +1,42 @@ # Based on https://github.com/KiCad/kicad-source-mirror/blob/master/demos/python_scripts_examples/gen_gerber_and_drill_files_board.py -import sys import os +from dataclasses import dataclass + from pcbnewTransition import pcbnew from pcbnew import * + +@dataclass +class LayerToPlot: + name: str + id: int + description: str + + +CuTop = LayerToPlot("CuTop", pcbnew.F_Cu, "Top layer") +CuBottom = LayerToPlot("CuBottom", pcbnew.B_Cu, "Bottom layer") +PasteBottom = LayerToPlot("PasteBottom", pcbnew.B_Paste, "Paste Bottom") +PasteTop = LayerToPlot("PasteTop", pcbnew.F_Paste, "Paste top") +SilkTop = LayerToPlot("SilkTop", pcbnew.F_SilkS, "Silk top") +SilkBottom = LayerToPlot("SilkBottom", pcbnew.B_SilkS, "Silk top") +MaskBottom = LayerToPlot("MaskBottom", pcbnew.B_Mask, "Mask bottom") +MaskTop = LayerToPlot("MaskTop", pcbnew.F_Mask, "Mask top") +EdgeCuts = LayerToPlot("EdgeCuts", pcbnew.Edge_Cuts, "Edges") +CmtUser = LayerToPlot("CmtUser", pcbnew.Cmts_User, "V-CUT") +AdhesiveTop = LayerToPlot("AdhesiveTop", pcbnew.F_Adhes, "Adhesive top") +AdhesiveBottom = LayerToPlot("AdhesiveBottom", pcbnew.B_Adhes, "Adhesive bottom") + fullGerberPlotPlan = [ - # name, id, comment - ("CuTop", F_Cu, "Top layer"), - ("CuBottom", B_Cu, "Bottom layer"), - ("PasteBottom", B_Paste, "Paste Bottom"), - ("PasteTop", F_Paste, "Paste top"), - ("SilkTop", F_SilkS, "Silk top"), - ("SilkBottom", B_SilkS, "Silk top"), - ("MaskBottom", B_Mask, "Mask bottom"), - ("MaskTop", F_Mask, "Mask top"), - ("EdgeCuts", Edge_Cuts, "Edges"), - ("CmtUser", Cmts_User, "V-CUT") + CuTop, + CuBottom, + PasteBottom, + PasteTop, + SilkTop, + SilkBottom, + MaskBottom, + MaskTop, + EdgeCuts, + CmtUser ] exportSettingsJlcpcb = { @@ -50,13 +71,14 @@ } -def hasCopper(plotPlan): - for _, layer, _ in plotPlan: - if layer in [F_Cu, B_Cu]: +def hasCopper(plotPlan: list[LayerToPlot]): + for layer_to_plot in plotPlan: + if layer_to_plot.id in [F_Cu, B_Cu]: return True return False -def gerberImpl(boardfile, outputdir, plot_plan=fullGerberPlotPlan, drilling=True, settings=exportSettingsJlcpcb): + +def gerberImpl(boardfile: str, outputdir: str, plot_plan: list[LayerToPlot]=fullGerberPlotPlan, drilling=True, settings=exportSettingsJlcpcb): """ Export board to gerbers. @@ -99,16 +121,16 @@ def gerberImpl(boardfile, outputdir, plot_plan=fullGerberPlotPlan, drilling=True # prepare the gerber job file jobfile_writer = GERBER_JOBFILE_WRITER(board) - for name, id, comment in plot_plan: - if id <= B_Cu: + for layer_to_plot in plot_plan: + if layer_to_plot.id <= B_Cu: popt.SetSkipPlotNPTH_Pads(True) else: popt.SetSkipPlotNPTH_Pads(False) - pctl.SetLayer(id) - suffix = "" if settings["NoSuffix"] else name - pctl.OpenPlotfile(suffix, PLOT_FORMAT_GERBER, comment) - jobfile_writer.AddGbrFile(id, os.path.basename(pctl.GetPlotFileName())) + pctl.SetLayer(layer_to_plot.id) + suffix = "" if settings["NoSuffix"] else layer_to_plot.name + pctl.OpenPlotfile(suffix, PLOT_FORMAT_GERBER, layer_to_plot.description) + jobfile_writer.AddGbrFile(layer_to_plot.id, os.path.basename(popt.GetPlotFileName())) if pctl.PlotLayer() == False: print("plot error") @@ -176,16 +198,15 @@ def pasteDxfExport(board, plotDir): popt.SetDXFPlotPolygonMode(False) plot_plan = [ - # name, id, comment - ("PasteBottom", B_Paste, "Paste Bottom"), - ("PasteTop", F_Paste, "Paste top"), - ("EdgeCuts", Edge_Cuts, "Edges"), + PasteBottom, + PasteTop, + EdgeCuts ] output = [] - for name, id, comment in plot_plan: - pctl.SetLayer(id) - pctl.OpenPlotfile(name, PLOT_FORMAT_DXF, comment) + for layer_to_plot in plot_plan: + pctl.SetLayer(layer_to_plot.id) + pctl.OpenPlotfile(layer_to_plot.name, PLOT_FORMAT_DXF, layer_to_plot.description) output.append(pctl.GetPlotFileName()) if pctl.PlotLayer() == False: print("plot error") diff --git a/kikit/fab/oshpark.py b/kikit/fab/oshpark.py index 118eebe7..662f8143 100644 --- a/kikit/fab/oshpark.py +++ b/kikit/fab/oshpark.py @@ -2,10 +2,10 @@ import os import shutil from pathlib import Path -from kikit.export import gerberImpl, exportSettingsOSHPark, fullGerberPlotPlan +from kikit.export import gerberImpl, exportSettingsOSHPark, fullGerberPlotPlan, CmtUser from kikit.fab.common import ensurePassingDrc -plotPlanNoVCuts = [(name, id, comment) for name, id, comment in fullGerberPlotPlan if name != "CmtUser"] +plotPlanNoVCuts = [layer_to_plot for layer_to_plot in fullGerberPlotPlan if layer_to_plot.id is not CmtUser.id] def exportOSHPark(board, outputdir, nametemplate, drc): """ diff --git a/kikit/stencil.py b/kikit/stencil.py index 008ae7f2..26d5c2a7 100644 --- a/kikit/stencil.py +++ b/kikit/stencil.py @@ -1,5 +1,7 @@ +import shutil + from pcbnewTransition import pcbnew -from pcbnew import wxPoint +from pcbnew import wxPoint, PCB_SHAPE, BOARD import numpy as np import json from collections import OrderedDict @@ -7,7 +9,7 @@ from kikit.common import * from kikit.defs import * from kikit.substrate import Substrate, extractRings, toShapely, linestringToKicad -from kikit.export import gerberImpl, pasteDxfExport +from kikit.export import gerberImpl, pasteDxfExport, LayerToPlot, PasteTop, PasteBottom, AdhesiveTop, AdhesiveBottom from kikit.export import exportSettingsJlcpcb import solid import solid.utils @@ -23,12 +25,22 @@ MOUNTING_HOLE_R = fromMm(1) HOLE_SPACING = fromMm(20) -def addBottomCounterpart(board, item): + +class StencilType(Enum): + SolderPaste = (PasteTop, PasteBottom) + Adhesive = (AdhesiveTop, AdhesiveBottom) + + def __init__(self, top_layer: LayerToPlot, bottom_layer: LayerToPlot): + self.top_layer = top_layer + self.bottom_layer = bottom_layer + + +def addBottomCounterpart(board: BOARD, item: PCB_SHAPE, stencil_type: StencilType = StencilType.SolderPaste): item = item.Duplicate() - item.SetLayer(Layer.B_Paste) + item.SetLayer(stencil_type.bottom_layer.id) board.Add(item) -def addRoundedCorner(board, center, start, end, thickness): +def addRoundedCorner(board: BOARD, center: wxPoint, start: wxPoint, end: wxPoint, thickness, stencil_type: StencilType = StencilType.SolderPaste): corner = pcbnew.PCB_SHAPE() corner.SetShape(STROKE_T.S_ARC) corner.SetCenter(wxPoint(center[0], center[1])) @@ -48,21 +60,21 @@ def addRoundedCorner(board, center, start, end, thickness): else: corner.SetAngle(fromDegrees(-90)) corner.SetWidth(thickness) - corner.SetLayer(Layer.F_Paste) + corner.SetLayer(stencil_type.top_layer.id) board.Add(corner) - addBottomCounterpart(board, corner) + addBottomCounterpart(board, corner, stencil_type) -def addLine(board, start, end, thickness): +def addLine(board: BOARD, start: wxPoint, end: wxPoint, thickness: int, stencil_type: StencilType = StencilType.SolderPaste): line = pcbnew.PCB_SHAPE() line.SetShape(STROKE_T.S_SEGMENT) line.SetStart(wxPoint(start[0], start[1])) line.SetEnd(wxPoint(end[0], end[1])) line.SetWidth(thickness) - line.SetLayer(Layer.F_Paste) + line.SetLayer(stencil_type.top_layer.id) board.Add(line) - addBottomCounterpart(board, line) + addBottomCounterpart(board, line, stencil_type) -def addBite(board, origin, direction, normal, thickness): +def addBite(board: BOARD, origin: wxPoint, direction: wxPoint, normal: wxPoint, thickness: int, stencil_type: StencilType = StencilType.SolderPaste): """ Adds a bite to the stencil, direction points to the bridge, normal points inside the stencil @@ -73,7 +85,7 @@ def addBite(board, origin, direction, normal, thickness): start = origin end = center + wxPoint(direction[0], direction[1]) # addLine(board, end, end + normal / 2, thickness) - addRoundedCorner(board, center, start, end, thickness) + addRoundedCorner(board, center, start, end, thickness, stencil_type) def numberOfCuts(length, bridgeWidth, bridgeSpacing): """ @@ -83,7 +95,8 @@ def numberOfCuts(length, bridgeWidth, bridgeSpacing): cutLength = (length - (count - 1) * bridgeWidth) / count return count, cutLength -def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance): + +def addFrame(board: BOARD, rect: wxRect, bridgeWidth: int, bridgeSpacing: int, clearance: int, stencil_type: StencilType = StencilType.SolderPaste): """ Add rectangular frame to the board """ @@ -96,7 +109,7 @@ def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance): (bl(rect), wxPoint(0, -R), wxPoint(R, 0)) # BL ] for c, sOffset, eOffset in corners: - addRoundedCorner(board, c + sOffset + eOffset, c + sOffset, c + eOffset, clearance) + addRoundedCorner(board, c + sOffset + eOffset, c + sOffset, c + eOffset, clearance, stencil_type) count, cutLength = numberOfCuts(rect.GetWidth() - 2 * R, bridgeWidth, bridgeSpacing) for i in range(count): @@ -104,16 +117,16 @@ def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance): end = start + cutLength y1, y2 = rect.GetY(), rect.GetY() + rect.GetHeight() - addLine(board, wxPoint(start, y1), wxPoint(end, y1), clearance) + addLine(board, wxPoint(start, y1), wxPoint(end, y1), clearance, stencil_type) if i != 0: - addBite(board, wxPoint(start, y1), wxPoint(-1, 0), wxPoint(0, 1), clearance) + addBite(board, wxPoint(start, y1), wxPoint(-1, 0), wxPoint(0, 1), clearance, stencil_type) if i != count - 1: - addBite(board, wxPoint(end, y1), wxPoint(1, 0), wxPoint(0, 1), clearance) - addLine(board, wxPoint(start, y2), wxPoint(end, y2), clearance) + addBite(board, wxPoint(end, y1), wxPoint(1, 0), wxPoint(0, 1), clearance, stencil_type) + addLine(board, wxPoint(start, y2), wxPoint(end, y2), clearance, stencil_type) if i != 0: - addBite(board, wxPoint(start, y2), wxPoint(-1, 0), wxPoint(0, -1), clearance) + addBite(board, wxPoint(start, y2), wxPoint(-1, 0), wxPoint(0, -1), clearance, stencil_type) if i != count - 1: - addBite(board, wxPoint(end, y2), wxPoint(1, 0), wxPoint(0, -1), clearance) + addBite(board, wxPoint(end, y2), wxPoint(1, 0), wxPoint(0, -1), clearance, stencil_type) count, cutLength = numberOfCuts(rect.GetHeight() - 2 * R, bridgeWidth, bridgeSpacing) for i in range(count): @@ -121,18 +134,19 @@ def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance): end = start + cutLength x1, x2 = rect.GetX(), rect.GetX() + rect.GetWidth() - addLine(board, wxPoint(x1, start), wxPoint(x1, end), clearance) + addLine(board, wxPoint(x1, start), wxPoint(x1, end), clearance, stencil_type) if i != 0: - addBite(board, wxPoint(x1, start), wxPoint(0, -1), wxPoint(1, 0), clearance) + addBite(board, wxPoint(x1, start), wxPoint(0, -1), wxPoint(1, 0), clearance, stencil_type) if i != count - 1: - addBite(board, wxPoint(x1, end), wxPoint(0, 1), wxPoint(1, 0), clearance) - addLine(board, wxPoint(x2, start), wxPoint(x2, end), clearance) + addBite(board, wxPoint(x1, end), wxPoint(0, 1), wxPoint(1, 0), clearance, stencil_type) + addLine(board, wxPoint(x2, start), wxPoint(x2, end), clearance, stencil_type) if i != 0: - addBite(board, wxPoint(x2, start), wxPoint(0, -1), wxPoint(-1, 0), clearance) + addBite(board, wxPoint(x2, start), wxPoint(0, -1), wxPoint(-1, 0), clearance, stencil_type) if i != count - 1: - addBite(board, wxPoint(x2, end), wxPoint(0, 1), wxPoint(-1, 0), clearance) + addBite(board, wxPoint(x2, end), wxPoint(0, 1), wxPoint(-1, 0), clearance, stencil_type) + -def addHole(board, position, radius): +def addHole(board: BOARD, position: wxPoint, radius: int, stencil_type: StencilType = StencilType.SolderPaste): circle = pcbnew.PCB_SHAPE() circle.SetShape(STROKE_T.S_CIRCLE) circle.SetCenter(wxPoint(position[0], position[1])) @@ -142,12 +156,12 @@ def addHole(board, position, radius): else: circle.SetArcStart(wxPoint(position[0], position[1]) + wxPoint(radius/2, 0)) circle.SetWidth(radius) - circle.SetLayer(Layer.F_Paste) + circle.SetLayer(stencil_type.top_layer.id) board.Add(circle) - addBottomCounterpart(board, circle) + addBottomCounterpart(board, circle, stencil_type) -def addJigFrame(board, jigFrameSize, bridgeWidth=fromMm(2), - bridgeSpacing=fromMm(10), clearance=fromMm(0.5)): +def addJigFrame(board: BOARD, jigFrameSize: (int, int), bridgeWidth: int=fromMm(2), + bridgeSpacing: int=fromMm(10), clearance: int=fromMm(0.5), stencil_type: StencilType = StencilType.SolderPaste): """ Given a Pcbnew board finds the board outline and creates a stencil for KiKit's stencil jig. @@ -163,22 +177,22 @@ def addJigFrame(board, jigFrameSize, bridgeWidth=fromMm(2), cutSize = rectByCenter(rectCenter(bBox), jigFrameSize[0] + 2 * (OUTER_BORDER + INNER_BORDER) - fromMm(1), jigFrameSize[1] + 2 * (OUTER_BORDER + INNER_BORDER) - fromMm(1)) - addFrame(board, cutSize, bridgeWidth, bridgeSpacing, clearance) + addFrame(board, cutSize, bridgeWidth, bridgeSpacing, clearance, stencil_type) for i in range(MOUNTING_HOLES_COUNT): x = frameSize.GetX() + OUTER_BORDER / 2 + (i + 1) * (frameSize.GetWidth() - OUTER_BORDER) / (MOUNTING_HOLES_COUNT + 1) - addHole(board, wxPoint(x, OUTER_BORDER / 2 + frameSize.GetY()), MOUNTING_HOLE_R) - addHole(board, wxPoint(x, - OUTER_BORDER / 2 +frameSize.GetY() + frameSize.GetHeight()), MOUNTING_HOLE_R) + addHole(board, wxPoint(x, OUTER_BORDER / 2 + frameSize.GetY()), MOUNTING_HOLE_R, stencil_type) + addHole(board, wxPoint(x, - OUTER_BORDER / 2 +frameSize.GetY() + frameSize.GetHeight()), MOUNTING_HOLE_R, stencil_type) for i in range(MOUNTING_HOLES_COUNT): y = frameSize.GetY() + OUTER_BORDER / 2 + (i + 1) * (frameSize.GetHeight() - OUTER_BORDER) / (MOUNTING_HOLES_COUNT + 1) - addHole(board, wxPoint(OUTER_BORDER / 2 + frameSize.GetX(), y), MOUNTING_HOLE_R) - addHole(board, wxPoint(- OUTER_BORDER / 2 +frameSize.GetX() + frameSize.GetWidth(), y), MOUNTING_HOLE_R) + addHole(board, wxPoint(OUTER_BORDER / 2 + frameSize.GetX(), y), MOUNTING_HOLE_R, stencil_type) + addHole(board, wxPoint(- OUTER_BORDER / 2 +frameSize.GetX() + frameSize.GetWidth(), y), MOUNTING_HOLE_R, stencil_type) PIN_TOLERANCE = fromMm(0.05) - addHole(board, tl(frameSize) + wxPoint(OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE) - addHole(board, tr(frameSize) + wxPoint(-OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE) - addHole(board, br(frameSize) + wxPoint(-OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE) - addHole(board, bl(frameSize) + wxPoint(OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE) + addHole(board, tl(frameSize) + wxPoint(OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE, stencil_type) + addHole(board, tr(frameSize) + wxPoint(-OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE, stencil_type) + addHole(board, br(frameSize) + wxPoint(-OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE, stencil_type) + addHole(board, bl(frameSize) + wxPoint(OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE, stencil_type) def jigMountingHoles(jigFrameSize, origin=wxPoint(0, 0)): """ Get list of all mounting holes in a jig of given size """ @@ -297,20 +311,20 @@ def shapelyToSHAPE_POLY_SET(polygon): p.AddOutline(linestringToKicad(polygon.exterior)) return p -def cutoutComponents(board, components): +def cutoutComponents(board: BOARD, components: list[str], stencil_type: StencilType = StencilType.SolderPaste): topCutout = extractComponentPolygons(components, pcbnew.F_CrtYd) for polygon in topCutout: zone = pcbnew.PCB_SHAPE() zone.SetShape(STROKE_T.S_POLYGON) zone.SetPolyShape(shapelyToSHAPE_POLY_SET(polygon)) - zone.SetLayer(Layer.F_Paste) + zone.SetLayer(stencil_type.top_layer.id) board.Add(zone) bottomCutout = extractComponentPolygons(components, pcbnew.B_CrtYd) for polygon in bottomCutout: zone = pcbnew.PCB_SHAPE() zone.SetShape(STROKE_T.S_POLYGON) zone.SetPolyShape(shapelyToSHAPE_POLY_SET(polygon)) - zone.SetLayer(Layer.B_Paste) + zone.SetLayer(stencil_type.bottom_layer.id) board.Add(zone) def setStencilLayerVisibility(boardName): @@ -356,37 +370,30 @@ def setStencilLayerVisibility(boardName): from pathlib import Path import os -def create(inputboard, outputdir, jigsize, jigthickness, pcbthickness, - registerborder, tolerance, ignore, cutout): +def create(inputboard: str, outputdir: str, jigsize: (int, int), jigthickness: float, pcbthickness: float, + registerborder: (float, float), tolerance: float, ignore: str, cutout: str, type: StencilType = StencilType.SolderPaste): board = pcbnew.LoadBoard(inputboard) - refs = parseReferences(ignore) - removeComponents(board, refs) + removeComponents(board, parseReferences(ignore)) + cutoutComponents(board, getComponents(board, parseReferences(cutout)), type) Path(outputdir).mkdir(parents=True, exist_ok=True) jigsize = (fromMm(jigsize[0]), fromMm(jigsize[1])) - addJigFrame(board, jigsize) - cutoutComponents(board, getComponents(board, parseReferences(cutout))) + addJigFrame(board, jigsize, stencil_type=type) stencilFile = os.path.join(outputdir, "stencil.kicad_pcb") board.Save(stencilFile) - setStencilLayerVisibility(stencilFile) + setStencilLayerVisibility(inputboard) + plot_plan = [type.top_layer, type.bottom_layer] - plotPlan = [ - # name, id, comment - ("PasteBottom", pcbnew.B_Paste, "Paste Bottom"), - ("PasteTop", pcbnew.F_Paste, "Paste top"), - ] # get a copy of exportSettingsJlcpcb dictionary and # exclude the Edge.Cuts layer for creation of stencil gerber files exportSettings = exportSettingsJlcpcb.copy() exportSettings["ExcludeEdgeLayer"] = True gerberDir = os.path.join(outputdir, "gerber") - gerberImpl(stencilFile, gerberDir, plotPlan, False, exportSettings) - gerbers = [os.path.join(gerberDir, x) for x in os.listdir(gerberDir)] - subprocess.check_call(["zip", "-j", - os.path.join(outputdir, "gerbers.zip")] + gerbers) + gerberImpl(stencilFile, gerberDir, plot_plan, False, exportSettings) + shutil.make_archive(os.path.join(outputdir, "gerbers"), "zip", gerberDir) jigthickness = fromMm(jigthickness) pcbthickness = fromMm(pcbthickness) diff --git a/kikit/stencil_ui.py b/kikit/stencil_ui.py index 023064fc..e4c65e26 100644 --- a/kikit/stencil_ui.py +++ b/kikit/stencil_ui.py @@ -1,5 +1,26 @@ -import click +import enum import sys +import typing + +import click +from click import Choice +from kikit.stencil import StencilType + + +# from https://github.com/pallets/click/pull/2210/files#diff-dcb534e6a7591b92836537d4655ddbd2f18e3b293c3420144c30a9ca08f65c4e +class EnumChoice(Choice): + def __init__(self, enum_type: typing.Type[enum.Enum], case_sensitive: bool = True): + super().__init__( + choices=[element.name for element in enum_type], + case_sensitive=case_sensitive, + ) + self.enum_type = enum_type + + def convert(self, value: typing.Any, param: typing.Optional["Parameter"], ctx: typing.Optional["Context"]) -> typing.Any: + value = super().convert(value=value, param=param, ctx=ctx) + if value is None: + return None + return self.enum_type[value] @click.command() @@ -46,6 +67,8 @@ def createPrinted(**kwargs): help="Comma separated list of components references to exclude from the stencil") @click.option("--cutout", type=str, default="", help="Comma separated list of components references to cutout from the stencil based on the courtyard") +@click.option("--type", type=EnumChoice(StencilType, case_sensitive=False), default="solderpaste", + help="Stencil for SolderPaste or Adhesive") def create(**kwargs): """ Create stencil and register elements for manual paste dispensing jig.