From de9628e5c1064f5f37571a7fa25501b94e769170 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 23 Oct 2020 14:17:03 -0300 Subject: [PATCH] Added columns configuration for position files. You can customize which columns are used, their names and order. Closes #22 --- CHANGELOG.md | 2 + README.md | 4 + docs/samples/generic_plot.kibot.yaml | 6 ++ kibot/out_position.py | 84 ++++++++++++++----- tests/test_plot/test_position.py | 11 +++ .../simple_position_csv_cols.kibot.yaml | 20 +++++ 6 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 tests/yaml_samples/simple_position_csv_cols.kibot.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index aa101c24..c91eb215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Help for filters and variants. - Support for new `pcbnew_do export` options. - Filters for KiBot warnings. +- Columns in position files can be selected, renamed and sorted as you + like. ### Fixed - KiBom variants when using multiple variants and a components used more diff --git a/README.md b/README.md index 4ee40d69..16af0aa8 100644 --- a/README.md +++ b/README.md @@ -1070,6 +1070,10 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `position` output. * Valid keys: + - `columns`: [list(dict)|list(string)] which columns are included in the output. + * Valid keys: + - `id`: [string=''] [Ref,Val,Package,PosX,PosY,Rot,Side] Internal name. + - `name`: [string=''] Name to use in the outut file. The id is used when empty. - `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted. A short-cut to use for simple cases where a variant is an overkill. - `format`: [string='ASCII'] [ASCII,CSV] format for the position file. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 3c922a83..9ecaa081 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -698,6 +698,12 @@ outputs: type: 'position' dir: 'Example/position_dir' options: + # [list(dict)|list(string)] which columns are included in the output + columns: + # [string=''] [Ref,Val,Package,PosX,PosY,Rot,Side] Internal name + id: '' + # [string=''] Name to use in the outut file. The id is used when empty + name: '' # [string|list(string)=''] Name of the filter to mark components as not fitted. # A short-cut to use for simple cases where a variant is an overkill dnf_filter: '' diff --git a/kibot/out_position.py b/kibot/out_position.py index d6777ae2..b4671a2f 100644 --- a/kibot/out_position.py +++ b/kibot/out_position.py @@ -8,15 +8,35 @@ import operator from datetime import datetime from pcbnew import IU_PER_MM, IU_PER_MILS +from collections import OrderedDict from .gs import GS from .misc import UI_SMD, UI_VIRTUAL, KICAD_VERSION_5_99, MOD_THROUGH_HOLE, MOD_SMD, MOD_EXCLUDE_FROM_POS_FILES +from .optionable import Optionable from .out_base import VariantOptions +from .error import KiPlotConfigurationError from .macros import macros, document, output_class # noqa: F401 from . import log logger = log.get_logger(__name__) +class PosColumns(Optionable): + """ Which columns we want and its names """ + def __init__(self): + super().__init__() + self._unkown_is_error = True + with document: + self.id = '' + """ [Ref,Val,Package,PosX,PosY,Rot,Side] Internal name """ + self.name = '' + """ Name to use in the outut file. The id is used when empty """ + + def config(self): + super().config() + if not self.id: + raise KiPlotConfigurationError("Missing or empty `id` in columns list ({})".format(str(self._tree))) + + class PositionOptions(VariantOptions): def __init__(self): with document: @@ -30,8 +50,28 @@ class PositionOptions(VariantOptions): """ output file name (%i='top_pos'|'bottom_pos'|'both_pos', %x='pos'|'csv') """ self.units = 'millimeters' """ [millimeters,inches] units used for the positions """ + self.columns = PosColumns + """ [list(dict)|list(string)] which columns are included in the output """ super().__init__() + def config(self): + super().config() + if isinstance(self.columns, type): + # Default list of columns + self.columns = OrderedDict([('Ref', 'Ref'), ('Val', 'Val'), ('Package', 'Package'), ('PosX', 'PosX'), + ('PosY', 'PosY'), ('Rot', 'Rot'), ('Side', 'Side')]) + else: + new_columns = OrderedDict() + for col in self.columns: + if isinstance(col, str): + # Just a string, add to the list of used + new_name = new_col = col + else: + new_col = col.id + new_name = col.name if col.name else new_col + new_columns[new_col] = new_name + self.columns = new_columns + def _do_position_plot_ascii(self, board, output_dir, columns, modulesStr, maxSizes): topf = None botf = None @@ -141,8 +181,7 @@ class PositionOptions(VariantOptions): def run(self, output_dir, board): super().run(output_dir, board) - columns = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"] - colcount = len(columns) + columns = self.columns.values() # Note: the parser already checked the units are milimeters or inches conv = 1.0 if self.units == 'millimeters': @@ -168,24 +207,31 @@ class PositionOptions(VariantOptions): # If passed check the position options if (self.only_smd and is_pure_smd(m)) or (not self.only_smd and is_not_virtual(m)): center = m.GetCenter() - # See PLACE_FILE_EXPORTER::GenPositionData() in - # export_footprints_placefile.cpp for C++ version of this. - modules.append([ - "{}".format(ref), - "{}".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") - ]) - + # KiCad: PLACE_FILE_EXPORTER::GenPositionData() in export_footprints_placefile.cpp + row = [] + for k in self.columns: + if k == 'Ref': + row.append(ref) + elif k == 'Val': + row.append(m.GetValue()) + elif k == 'Package': + row.append(str(m.GetFPID().GetLibItemName())) # pcbnew.UTF8 type + elif k == 'PosX': + row.append("{:.4f}".format(center.x * conv)) + elif k == 'PosY': + row.append("{:.4f}".format(-center.y * conv)) + elif k == 'Rot': + row.append("{:.4f}".format(m.GetOrientationDegrees())) + elif k == 'Side': + row.append("bottom" if m.IsFlipped() else "top") + modules.append(row) # 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])) - + maxlengths = [] + for col, name in enumerate(columns): + max_l = len(name) + for row in modules: + max_l = max(max_l, len(row[col])) + maxlengths.append(max_l) # Note: the parser already checked the format is ASCII or CSV if self.format == 'ASCII': self._do_position_plot_ascii(board, output_dir, columns, modules, maxlengths) diff --git a/tests/test_plot/test_position.py b/tests/test_plot/test_position.py index 9066df1d..ad245805 100644 --- a/tests/test_plot/test_position.py +++ b/tests/test_plot/test_position.py @@ -119,6 +119,17 @@ def test_3Rs_position_csv(): ctx.clean_up() +def test_position_csv_cols(): + ctx = context.TestContext('test_position_csv_cols', '3Rs', 'simple_position_csv_cols', POS_DIR) + ctx.run() + pos_top = ctx.get_pos_top_csv_filename() + pos_bot = ctx.get_pos_bot_csv_filename() + ctx.expect_out_file(pos_top) + ctx.expect_out_file(pos_bot) + assert ctx.search_in_file(pos_top, ["Ref,Value,Center X"]) is not None + ctx.clean_up() + + def test_3Rs_position_unified_csv(): """ Also test the quiet mode """ ctx = context.TestContext('3Rs_position_unified_csv', '3Rs', 'simple_position_unified_csv', POS_DIR) diff --git a/tests/yaml_samples/simple_position_csv_cols.kibot.yaml b/tests/yaml_samples/simple_position_csv_cols.kibot.yaml new file mode 100644 index 00000000..93285452 --- /dev/null +++ b/tests/yaml_samples/simple_position_csv_cols.kibot.yaml @@ -0,0 +1,20 @@ +# Example KiBot config file for a basic 2-layer board +kibot: + version: 1 + +outputs: + + - name: 'position' + type: position + dir: positiondir + options: + format: CSV # CSV or ASCII format + units: millimeters # millimeters or inches + separate_files_for_front_and_back: true + only_smd: true + columns: + - "Ref" + - id: Val + name: Value + - id: PosX + name: "Center X"