You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
xgm220p-tex/plot.py

233 lines
8.5 KiB
Python

#!/usr/bin/env kipy
__copyright__ = 'Copyright 2020 ALT-TEKNIK LLC'
from argparse import ArgumentParser
from os import path, rename
from os.path import dirname, realpath
from pcbnew import B_Cu, B_Mask, Edge_Cuts, EXCELLON_WRITER, F_Cu, F_Fab, \
F_Mask, F_Paste, F_SilkS, FILLED, FromMils, LoadBoard, LSET, \
PCB_PLOT_PARAMS, PLOT_CONTROLLER, PLOT_FORMAT_GERBER, PLOT_FORMAT_PDF, \
TEXTE_PCB
from uuid import uuid1
_UNNAMED_PAD = ''
class _PadExclusion:
def __init__(self, ref, name=None):
self.__ref = ref
self.__name = (None if name is None
else self.__normalize_name(str(name)))
def exclude_from_layer(self, board, layer_num):
count = 0
layer_mask = ~2**layer_num
for pad in board.FindModuleByReference(self.__ref).Pads():
if ((self.__name is None or
self.__name == self.__normalize_name(pad.GetName())) and
pad.IsOnLayer(layer_num)):
pad_mask = int(pad.GetLayerSet().FmtHex().replace('_', ''), 16)
pad_hex = hex(pad_mask & layer_mask)
layers = LSET()
layers.ParseHex(pad_hex, len(pad_hex))
pad.SetLayerSet(layers)
count += 1
if count:
if self.__name:
descr = 'pad {}'.format(self.__name)
else:
descr = 'pad' if count == 1 else 'pads'
if self.__name is not None:
descr = 'unnamed {}'.format(descr)
print(' Excluded {} {}'.format(self.__ref, descr))
@staticmethod
def __normalize_name(name):
return _UNNAMED_PAD if name == '~' or name.isspace() else name
_BOARD_NAME = 'bgtex'
_VERSION_TOKEN = 'X.Y.Z'
_GERBER_EXT_OVERRIDES = {F_Fab: 'gm1', Edge_Cuts: 'gko'}
class _Layer:
def __init__(self, num, descr, **kwargs):
self.__num = num
self.__descr = descr
if kwargs.get('is_pdf'):
self.__use_aux_origin = False
self.__fmt = PLOT_FORMAT_PDF
self.__fmt_descr = 'PDF'
else:
self.__use_aux_origin = True
self.__fmt = PLOT_FORMAT_GERBER
self.__fmt_descr = 'Gerber'
scale = kwargs.get('scale')
self.__scale = 1.0 if not scale else scale
suffix = kwargs.get('suffix')
self.__suffix = '' if suffix is None else suffix
pad_exclusions = kwargs.get('pad_exclusions')
self.__pad_exclusions = (() if pad_exclusions is None
else pad_exclusions)
def plot(self, board_file, output_dir, version=None):
# Append the suffix for Gerber layers with file extension overrides to
# ease renaming after plotting
suffix = self.__suffix
override_gerber_ext = (self.__fmt == PLOT_FORMAT_GERBER and
self.__num in _GERBER_EXT_OVERRIDES)
if override_gerber_ext:
uuid = str(uuid1())
suffix = '{}-{}'.format(suffix, uuid) if suffix else uuid
# Indicate status
print('Plotting {} {}...'.format(self.__descr, self.__fmt_descr))
# Load the board
board = LoadBoard(board_file)
# Replace version text
if version is not None:
for drawing in board.GetDrawings():
if (drawing.IsOnLayer(self.__num) and
isinstance(drawing, TEXTE_PCB)):
text = drawing.GetText()
if _VERSION_TOKEN in text:
drawing.SetText(text.replace(_VERSION_TOKEN, version))
print(' Replaced version text')
# Apply pad exclusions
for pad_exclusion in self.__pad_exclusions:
pad_exclusion.exclude_from_layer(board, self.__num)
# Create a plot controller
controller = PLOT_CONTROLLER(board)
# Set the plot options
opts = controller.GetPlotOptions()
opts.SetOutputDirectory(output_dir)
opts.SetPlotFrameRef(False)
opts.SetPlotValue(False)
opts.SetPlotReference(True)
opts.SetPlotInvisibleText(False)
opts.SetExcludeEdgeLayer(True)
opts.SetSubtractMaskFromSilk(False)
opts.SetPlotViaOnMaskLayer(False)
opts.SetSkipPlotNPTH_Pads(True)
opts.SetUseAuxOrigin(self.__use_aux_origin)
opts.SetDrillMarksType(PCB_PLOT_PARAMS.NO_DRILL_SHAPE)
opts.SetAutoScale(False)
opts.SetScale(self.__scale)
opts.SetPlotMode(FILLED)
opts.SetLineWidth(FromMils(0.1))
opts.SetMirror(False)
opts.SetNegative(False)
if self.__fmt == PLOT_FORMAT_GERBER:
opts.SetUseGerberProtelExtensions(not override_gerber_ext)
opts.SetCreateGerberJobFile(False)
opts.SetGerberPrecision(6)
opts.SetUseGerberX2format(False)
opts.SetIncludeGerberNetlistInfo(False)
# Plot the layer
controller.SetColorMode(False)
controller.SetLayer(self.__num)
controller.OpenPlotfile(suffix, self.__fmt, self.__descr)
controller.PlotLayer()
controller.ClosePlot()
# Rename Gerber layers with file extension overrides
if override_gerber_ext:
src = '{}-{}.gbr'.format(_BOARD_NAME, suffix)
dest = _BOARD_NAME
if self.__suffix:
dest = '{}-{}'.format(dest, self.__suffix)
dest = '{}.{}'.format(dest, _GERBER_EXT_OVERRIDES[self.__num])
rename(path.join(output_dir, src), path.join(output_dir, dest))
_OUTPUT_DIR = 'plots'
_PDF_SCALE = 5.0
_DEFAULT_PASTE_PAD_EXCLUSIONS = (
_PadExclusion('C1'),
_PadExclusion('R1'),
_PadExclusion('R4'),
_PadExclusion('U3', _UNNAMED_PAD)
)
_MIN_PASTE_PAD_EXCLUSIONS = (
_PadExclusion('C1'),
_PadExclusion('C3'),
_PadExclusion('C4'),
_PadExclusion('C5'),
_PadExclusion('J2'),
_PadExclusion('R1'),
_PadExclusion('R4'),
_PadExclusion('R5'),
_PadExclusion('R6'),
_PadExclusion('R7'),
_PadExclusion('R8'),
_PadExclusion('R9'),
_PadExclusion('R10'),
_PadExclusion('U2'),
_PadExclusion('U3')
)
_WSON_PASTE_PAD_EXCLUSIONS = (
_PadExclusion('C1'),
_PadExclusion('R1'),
_PadExclusion('R4')
)
_DEFAULT_MASK_PAD_EXCLUSIONS = _PadExclusion('U3', 9),
_LAYERS = (
_Layer(F_Fab, 'F.Fab'),
_Layer(F_Fab, 'F.Fab', is_pdf=True, scale=_PDF_SCALE, suffix='assembly'),
_Layer(F_Paste, 'F.Paste', pad_exclusions=_DEFAULT_PASTE_PAD_EXCLUSIONS),
_Layer(F_Paste, 'MIN F.Paste', suffix='min',
pad_exclusions=_MIN_PASTE_PAD_EXCLUSIONS),
_Layer(F_Paste, 'WSON F.Paste', suffix='wson',
pad_exclusions=_WSON_PASTE_PAD_EXCLUSIONS),
_Layer(F_SilkS, 'F.SilkS'),
_Layer(F_Mask, 'F.Mask', pad_exclusions=_DEFAULT_MASK_PAD_EXCLUSIONS),
_Layer(F_Mask, 'WSON F.Mask', suffix='wson'),
_Layer(F_Cu, 'F.Cu'),
_Layer(B_Cu, 'B.Cu'),
_Layer(B_Mask, 'B.Mask'),
_Layer(Edge_Cuts, 'Edge.Cuts')
)
def main(version=None):
# Canonicalize the board filename and output directory
project_dir = realpath(dirname(__file__))
board_file = '{}.kicad_pcb'.format(_BOARD_NAME)
abs_board_file = path.join(project_dir, board_file)
output_dir = realpath(path.join(project_dir, _OUTPUT_DIR))
# Plot the layers
for layer in _LAYERS:
layer.plot(abs_board_file, output_dir, version)
# Write the drill file
print('Writing Excellon drill file...')
board = LoadBoard(abs_board_file)
writer = EXCELLON_WRITER(board)
writer.SetOptions(False, # aMirror
False, # aMinimalHeader
board.GetDesignSettings().m_AuxOrigin, # aOffset
True) # aMerge_PTH_NPTH
writer.SetRouteModeForOvalHoles(True)
writer.SetFormat(True, # aMetric
EXCELLON_WRITER.DECIMAL_FORMAT) # aZerosFmt
writer.CreateDrillandMapFilesSet(output_dir, # aPlotDirectory
True, # aGenDrill
False) # aGenMap
rename(path.join(output_dir, '{}.drl'.format(_BOARD_NAME)),
path.join(output_dir, '{}.xln'.format(_BOARD_NAME)))
# Indicate status
print('Plotted {}'.format(board_file))
if __name__ == '__main__':
parser = ArgumentParser(description='creates BGTEX board plots')
parser.add_argument('version', nargs='?', help='the board version')
args = parser.parse_args()
main(args.version)