From a9c8b9a6bf5b9d2c3c820cf73154b3d9fe249b6a Mon Sep 17 00:00:00 2001 From: Romain Deterre Date: Tue, 15 Oct 2019 22:00:36 -0400 Subject: [PATCH] Add support for plotting position files --- src/kiplot/config_reader.py | 20 ++- src/kiplot/kiplot.py | 166 +++++++++++++++++++ src/kiplot/plot_config.py | 19 +++ tests/yaml_samples/simple_2layer.kiplot.yaml | 11 +- 4 files changed, 214 insertions(+), 2 deletions(-) diff --git a/src/kiplot/config_reader.py b/src/kiplot/config_reader.py index b21c1a5f4..92cd5ded1 100644 --- a/src/kiplot/config_reader.py +++ b/src/kiplot/config_reader.py @@ -326,6 +326,24 @@ def _parse_out_opts(self, otype, options): 'to': 'mirror_y_axis', 'required': lambda opts: True, }, + { + 'key': 'format', + 'types': ['position'], + 'to': 'format', + 'required': lambda opts: True, + }, + { + 'key': 'units', + 'types': ['position'], + 'to': 'units', + 'required': lambda opts: True, + }, + { + 'key': 'separate_files_for_front_and_back', + 'types': ['position'], + 'to': 'separate_files_for_front_and_back', + 'required': lambda opts: True, + } ] po = PC.OutputOptions(otype) @@ -411,7 +429,7 @@ def _parse_output(self, o_obj): raise YamlError("Output needs a type") if otype not in ['gerber', 'ps', 'hpgl', 'dxf', 'pdf', 'svg', - 'gerb_drill', 'excellon']: + 'gerb_drill', 'excellon', 'position']: raise YamlError("Unknown output type: {}".format(otype)) try: diff --git a/src/kiplot/kiplot.py b/src/kiplot/kiplot.py index a8f4bb4b3..1a249baef 100644 --- a/src/kiplot/kiplot.py +++ b/src/kiplot/kiplot.py @@ -2,6 +2,7 @@ Main Kiplot code """ +from datetime import datetime import logging import os @@ -51,6 +52,8 @@ def plot(self, brd_file): self._do_layer_plot(board, pc, op) elif self._output_is_drill(op): self._do_drill_plot(board, pc, op) + elif self._output_is_position(op): + self._do_position_plot(board, pc, op) else: raise PlotError("Don't know how to plot type {}" .format(op.options.type)) @@ -85,6 +88,9 @@ def _output_is_drill(self, output): PCfg.OutputOptions.GERB_DRILL, ] + def _output_is_position(self, output): + return output.options.type == PCfg.OutputOptions.POSITION + def _get_layer_plot_format(self, output): """ Gets the Pcbnew plot format for a given KiPlot output type @@ -221,6 +227,160 @@ def _do_drill_plot(self, board, plot_ctrl, output): drill_writer.GenDrillReportFile(drill_report_file) + def _do_position_plot_ascii(self, board, plot_ctrl, output, columns, modulesStr, maxSizes): + to = output.options.type_options + outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory() + if not os.path.exists(outdir): + os.makedirs(outdir) + name = os.path.splitext(os.path.basename(board.GetFileName()))[0] + + topf = None + botf = None + bothf = None + if to.separate_files_for_front_and_back: + topf = open(os.path.join(outdir, "{}-top.pos".format(name)), 'w') + botf = open(os.path.join(outdir, "{}-bottom.pos".format(name)), + 'w') + else: + bothf = open(os.path.join(outdir, "{}-both.pos").format(name), 'w') + + files = [f for f in [topf, botf, bothf] if f is not None] + for f in files: + f.write('### Module positions - created on {} ###\n'.format( + datetime.now().strftime("%a %d %b %Y %I:%M:%S %p %Z") + )) + f.write('### Printed by KiPlot\n') + unit = {'millimeters': 'mm', + 'inches': 'in'}[to.units] + f.write('## Unit: {}, Angle = deg\n'.format(unit)) + + if topf is not None: + topf.write('## Side: top\n') + if botf is not None: + botf.write('## Side: bottom\n') + if bothf is not None: + bothf.write('## Side: both\n') + + for f in files: + f.write('# ') + for idx, col in enumerate(columns): + if idx > 0: + f.write(" ") + f.write("{0: <{width}}".format(col, width=maxSizes[idx])) + f.write('\n') + + # Account for the "# " at the start of the comment column + maxSizes[0] = maxSizes[0] + 2 + + for m in modulesStr: + fle = bothf + if fle is None: + if m[-1] == "top": + fle = topf + else: + fle = botf + for idx, col in enumerate(m): + if idx > 0: + fle.write(" ") + fle.write("{0: <{width}}".format(col, width=maxSizes[idx])) + fle.write("\n") + + for f in files: + f.write("## End\n") + + if topf is not None: + topf.close() + if botf is not None: + botf.close() + if bothf is not None: + bothf.close() + + def _do_position_plot_csv(self, board, plot_ctrl, output, columns, modulesStr): + to = output.options.type_options + outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory() + if not os.path.exists(outdir): + os.makedirs(outdir) + name = os.path.splitext(os.path.basename(board.GetFileName()))[0] + + topf = None + botf = None + bothf = None + if to.separate_files_for_front_and_back: + topf = open(os.path.join(outdir, "{}-top-pos.csv".format(name)), + 'w') + botf = open(os.path.join(outdir, "{}-bottom-pos.csv".format(name)), + 'w') + else: + bothf = open(os.path.join(outdir, "{}-both-pos.csv").format(name), + 'w') + + files = [f for f in [topf, botf, bothf] if f is not None] + + for f in files: + f.write(",".join(columns)) + f.write("\n") + + for m in modulesStr: + fle = bothf + if fle is None: + if m[-1] == "top": + fle = topf + else: + fle = botf + fle.write(",".join('"{}"'.format(e) for e in m)) + fle.write("\n") + + if topf is not None: + topf.close() + if botf is not None: + botf.close() + if bothf is not None: + bothf.close() + + def _do_position_plot(self, board, plot_ctrl, output): + to = output.options.type_options + + columns = ["ref", "val", "package", "posx", "posy", "rot", "side"] + colcount = len(columns) + + conv = 1.0 + if to.units == 'millimeters': + conv = 1.0 / pcbnew.IU_PER_MM + elif to.units == 'inches': + conv = 0.001 / pcbnew.IU_PER_MILS + else: + raise PlotError('Invalid units: {}'.format(to.units)) + + # Format all strings + modules = [] + for m in board.GetModules(): + center = m.GetCenter() + # See PLACE_FILE_EXPORTER::GenPositionData() in + # export_footprints_placefile.cpp for C++ version of this. + modules.append([ + "{}".format(m.GetReference()), + "{}".format(m.GetValue()), + "{}".format(m.GetFPID().GetLibItemName()), + "{:.4f}".format(center.x * conv), + "{:.4f}".format(center.y * conv), + "{:.4f}".format(m.GetOrientationDegrees()), + "{}".format("bottom" if m.IsFlipped() else "top") + ]) + + # Find max width for all columns + maxlengths = [0] * colcount + for row in range(len(modules)): + for col in range(colcount): + maxlengths[col] = max(maxlengths[col], len(modules[row][col])) + + if to.format.lower() == 'ascii': + self._do_position_plot_ascii(board, plot_ctrl, output, columns, modules, + maxlengths) + elif to.format.lower() == 'csv': + self._do_position_plot_csv(board, plot_ctrl, output, columns, modules) + else: + raise PlotError("Format is invalid: {}".format(to.format)) + def _configure_gerber_opts(self, po, output): # true if gerber @@ -271,6 +431,10 @@ def _configure_svg_opts(self, po, output): assert(output.options.type == PCfg.OutputOptions.SVG) # pdf_opts = output.options.type_options + def _configure_position_opts(self, po, output): + + assert(output.options.type == PCfg.OutputOptions.POSITION) + def _configure_output_dir(self, plot_ctrl, output): po = plot_ctrl.GetPlotOptions() @@ -324,6 +488,8 @@ def _configure_plot_ctrl(self, plot_ctrl, output): self._configure_pdf_opts(po, output) elif output.options.type == PCfg.OutputOptions.HPGL: self._configure_hpgl_opts(po, output) + elif output.options.type == PCfg.OutputOptions.POSITION: + self._configure_position_opts(po, output) po.SetDrillMarksType(opts.drill_marks) diff --git a/src/kiplot/plot_config.py b/src/kiplot/plot_config.py index 8bbead09c..257103f24 100644 --- a/src/kiplot/plot_config.py +++ b/src/kiplot/plot_config.py @@ -361,6 +361,22 @@ def __init__(self): self.type = None +class PositionOptions(TypeOptions): + + def __init__(self): + self.format = None + self.units = None + self.separate_files_for_front_and_back = None + + def validate(self): + errs = [] + if self.format not in ["ASCII", "CSV"]: + errs.append("Format must be either ASCII or CSV") + if self.units not in ["millimeters", "inches"]: + errs.append("Units must be either millimeters or inches") + return errs + + class OutputOptions(object): GERBER = 'gerber' @@ -372,6 +388,7 @@ class OutputOptions(object): EXCELLON = 'excellon' GERB_DRILL = 'gerb_drill' + POSITION = 'position' def __init__(self, otype): self.type = otype @@ -392,6 +409,8 @@ def __init__(self, otype): self.type_options = ExcellonOptions() elif otype == self.GERB_DRILL: self.type_options = GerberDrillOptions() + elif otype == self.POSITION: + self.type_options = PositionOptions() else: self.type_options = None diff --git a/tests/yaml_samples/simple_2layer.kiplot.yaml b/tests/yaml_samples/simple_2layer.kiplot.yaml index df3a68389..a38f1ac2a 100644 --- a/tests/yaml_samples/simple_2layer.kiplot.yaml +++ b/tests/yaml_samples/simple_2layer.kiplot.yaml @@ -33,4 +33,13 @@ outputs: - layer: F.Cu suffix: F_Cu - layer: F.SilkS - suffix: F_SilkS \ No newline at end of file + suffix: F_SilkS + + - name: 'position' + comment: "Pick and place file" + type: position + dir: positiondir + options: + format: ASCII # CSV or ASCII format + units: millimeters # millimeters or inches + separate_files_for_front_and_back: true