diff --git a/DATAFORMAT.md b/DATAFORMAT.md index fdd70a0d..ff829d36 100644 --- a/DATAFORMAT.md +++ b/DATAFORMAT.md @@ -101,6 +101,13 @@ pcbdata = { "B": [bomrow1, bomrow2, ...], // numeric IDs of DNP components that are not in BOM "skipped": [id1, id2, ...] + // Fields map is keyed on component ID with values being field data. + // It's order corresponds to order of fields data in config struct. + "fields" { + id1: [field1, field2, ...], + id2: [field1, field2, ...], + ... + } }, // Contains parsed stroke data from newstroke font for // characters used on the pcb. @@ -333,17 +340,7 @@ Footprints are a collection of pads, drawings and some metadata. # bom row struct -Bom row is a 5-tuple (array in js) - -```js -[ - group_component_count, - value, - footprint_name, - reference_set, - extra_data -] -``` +Bom row is a list of reference sets Reference set is array of tuples of (ref, id) where id is just a unique numeric identifier for each footprint that helps avoid @@ -356,13 +353,6 @@ collisions when references are duplicated. ] ``` -Extra data is array of extra field data. It's order corresponds -to order of extra field data in config struct. - -```js -[field1_value, field2_value, ...] -``` - # config struct ```js diff --git a/InteractiveHtmlBom/core/config.py b/InteractiveHtmlBom/core/config.py index aa76a87c..706a2ae8 100644 --- a/InteractiveHtmlBom/core/config.py +++ b/InteractiveHtmlBom/core/config.py @@ -38,8 +38,9 @@ class Config: html_config_fields = [ 'dark_mode', 'show_pads', 'show_fabrication', 'show_silkscreen', 'highlight_pin1', 'redraw_on_drag', 'board_rotation', 'checkboxes', - 'bom_view', 'layer_view', 'extra_fields' + 'bom_view', 'layer_view' ] + default_show_group_fields = ["Value", "Footprint"] # Defaults @@ -70,7 +71,8 @@ class Config: # Extra fields section extra_data_file = None netlist_initial_directory = '' # This is relative to pcb file directory - extra_fields = [] + show_fields = default_show_group_fields + group_fields = default_show_group_fields normalize_field_case = False board_variant_field = '' board_variant_whitelist = [] @@ -79,7 +81,7 @@ class Config: @staticmethod def _split(s): - """Splits string by ',' and drops empty strings from resulting array.""" + """Splits string by ',' and drops empty strings from resulting array""" return [a.replace('\\,', ',') for a in re.split(r'(? None @@ -411,9 +423,15 @@ def set_from_args(self, args): self.include_tracks = args.include_tracks self.include_nets = args.include_nets - # Extra + # Fields self.extra_data_file = args.extra_data_file or args.netlist_file - self.extra_fields = self._split(args.extra_fields) + if args.extra_fields is not None: + self.show_fields = self.default_show_group_fields + \ + self._split(args.extra_fields) + self.group_fields = self.show_fields + else: + self.show_fields = self._split(args.show_fields) + self.group_fields = self._split(args.group_fields) self.normalize_field_case = args.normalize_field_case self.board_variant_field = args.variant_field self.board_variant_whitelist = self._split(args.variants_whitelist) @@ -423,7 +441,5 @@ def set_from_args(self, args): def get_html_config(self): import json d = {f: getattr(self, f) for f in self.html_config_fields} - # Temporary until currently hardcoded columns are made configurable - d["fields"] = ["References"] + self.extra_fields + \ - ["Value", "Footprint", "Quantity"] + d["fields"] = self.show_fields return json.dumps(d) diff --git a/InteractiveHtmlBom/core/ibom.py b/InteractiveHtmlBom/core/ibom.py index 08c543ed..7ee892f4 100644 --- a/InteractiveHtmlBom/core/ibom.py +++ b/InteractiveHtmlBom/core/ibom.py @@ -113,58 +113,78 @@ def natural_sort(lst): # build grouped part list skipped_components = [] part_groups = {} + group_by = set(config.group_fields) + index_to_fields = {} + for i, f in enumerate(pcb_footprints): if skip_component(f, config): skipped_components.append(i) continue # group part refs by value and footprint - norm_value, unit = units.componentValue(f.val, f.ref) + fields = [] + group_key = [] + + for field in config.show_fields: + if field == "Value": + fields.append(f.val) + if "Value" in group_by: + norm_value, unit = units.componentValue(f.val, f.ref) + group_key.append(norm_value) + group_key.append(unit) + elif field == "Footprint": + fields.append(f.footprint) + if "Footprint" in group_by: + group_key.append(f.footprint) + group_key.append(f.attr) + else: + fields.append(f.extra_fields.get(field, '')) + if field in group_by: + group_key.append(f.extra_fields.get(field, '')) + + index_to_fields[i] = fields + refs = part_groups.setdefault(tuple(group_key), []) + refs.append((f.ref, i)) - extras = [] - if config.extra_fields: - extras = [f.extra_fields.get(ef, '') - for ef in config.extra_fields] + bom_table = [] - group_key = (norm_value, unit, tuple(extras), f.footprint, f.attr) - valrefs = part_groups.setdefault(group_key, [f.val, []]) - valrefs[1].append((f.ref, i)) + for _, refs in part_groups.items(): + # Fixup values to normalized string + if "Value" in group_by and "Value" in config.show_fields: + index = config.show_fields.index("Value") + value = index_to_fields[refs[0][1]][index] + for ref in refs: + index_to_fields[ref[1]][index] = value - # build bom table, sort refs - bom_table = [] - for (_, _, extras, footprint, _), valrefs in part_groups.items(): - bom_row = ( - len(valrefs[1]), valrefs[0], footprint, - natural_sort(valrefs[1]), extras) - bom_table.append(bom_row) + bom_table.append(natural_sort(refs)) - # sort table by reference prefix, footprint and quantity + # sort table by reference prefix and quantity def row_sort_key(element): - qty, _, fp, rf, e = element - prefix = re.findall('^[A-Z]*', rf[0][0])[0] + prefix = re.findall('^[A-Z]*', element[0][0])[0] if prefix in config.component_sort_order: ref_ord = config.component_sort_order.index(prefix) else: ref_ord = config.component_sort_order.index('~') - return ref_ord, e, fp, -qty, alphanum_key(rf[0][0]) + return ref_ord, len(element), alphanum_key(element[0][0]) if '~' not in config.component_sort_order: config.component_sort_order.append('~') + bom_table = sorted(bom_table, key=row_sort_key) result = { 'both': bom_table, - 'skipped': skipped_components + 'skipped': skipped_components, + 'fields': index_to_fields } for layer in ['F', 'B']: filtered_table = [] for row in bom_table: - filtered_refs = [ref for ref in row[3] + filtered_refs = [ref for ref in row if pcb_footprints[ref[1]].layer == layer] if filtered_refs: - filtered_table.append((len(filtered_refs), row[1], - row[2], filtered_refs, row[4])) + filtered_table.append(filtered_refs) result[layer] = sorted(filtered_table, key=row_sort_key) diff --git a/InteractiveHtmlBom/core/units.py b/InteractiveHtmlBom/core/units.py index 78d74dce..98bb409b 100644 --- a/InteractiveHtmlBom/core/units.py +++ b/InteractiveHtmlBom/core/units.py @@ -190,9 +190,3 @@ def compareValues(c1, c2): return True # no units for component 2 return False - - -print(compMatch("1µF")) -print(compMatch("1u")) - -print(componentValue("1µ", "C2")) diff --git a/InteractiveHtmlBom/dialog/dialog_base.py b/InteractiveHtmlBom/dialog/dialog_base.py index 41c069b4..0ee40990 100644 --- a/InteractiveHtmlBom/dialog/dialog_base.py +++ b/InteractiveHtmlBom/dialog/dialog_base.py @@ -9,6 +9,7 @@ import wx import wx.xrc +import wx.grid ########################################################################### ## Class SettingsDialogBase @@ -396,17 +397,17 @@ def OnComponentBlacklistRemove( self, event ): ########################################################################### -## Class ExtraFieldsPanelBase +## Class FieldsPanelBase ########################################################################### -class ExtraFieldsPanelBase ( wx.Panel ): +class FieldsPanelBase ( wx.Panel ): def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( -1,-1 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) bSizer42 = wx.BoxSizer( wx.VERTICAL ) - sbSizer7 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Netlist or xml file" ), wx.VERTICAL ) + sbSizer7 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Extra data file" ), wx.VERTICAL ) self.extraDataFilePicker = wx.FilePickerCtrl( sbSizer7.GetStaticBox(), wx.ID_ANY, wx.EmptyString, u"Select a file", u"Netlist and xml files (*.net; *.xml)|*.net;*.xml", wx.DefaultPosition, wx.DefaultSize, wx.FLP_DEFAULT_STYLE|wx.FLP_FILE_MUST_EXIST|wx.FLP_OPEN|wx.FLP_SMALL|wx.FLP_USE_TEXTCTRL|wx.BORDER_SIMPLE ) sbSizer7.Add( self.extraDataFilePicker, 0, wx.EXPAND|wx.BOTTOM|wx.RIGHT|wx.LEFT, 5 ) @@ -414,27 +415,48 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. bSizer42.Add( sbSizer7, 0, wx.ALL|wx.EXPAND, 5 ) - extraFieldsSizer = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Extra fields" ), wx.VERTICAL ) + fieldsSizer = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Fields" ), wx.VERTICAL ) bSizer4 = wx.BoxSizer( wx.HORIZONTAL ) - bSizer6 = wx.BoxSizer( wx.VERTICAL ) + self.fieldsGrid = wx.grid.Grid( fieldsSizer.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) - extraFieldsListChoices = [] - self.extraFieldsList = wx.CheckListBox( extraFieldsSizer.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, extraFieldsListChoices, 0|wx.BORDER_SIMPLE ) - bSizer6.Add( self.extraFieldsList, 1, wx.ALL|wx.EXPAND, 5 ) + # Grid + self.fieldsGrid.CreateGrid( 2, 3 ) + self.fieldsGrid.EnableEditing( True ) + self.fieldsGrid.EnableGridLines( True ) + self.fieldsGrid.EnableDragGridSize( False ) + self.fieldsGrid.SetMargins( 0, 0 ) + # Columns + self.fieldsGrid.AutoSizeColumns() + self.fieldsGrid.EnableDragColMove( False ) + self.fieldsGrid.EnableDragColSize( True ) + self.fieldsGrid.SetColLabelSize( 30 ) + self.fieldsGrid.SetColLabelValue( 0, u"Show" ) + self.fieldsGrid.SetColLabelValue( 1, u"Group" ) + self.fieldsGrid.SetColLabelValue( 2, u"Name" ) + self.fieldsGrid.SetColLabelAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER ) - bSizer4.Add( bSizer6, 1, wx.EXPAND, 5 ) + # Rows + self.fieldsGrid.EnableDragRowSize( False ) + self.fieldsGrid.SetRowLabelSize( 0 ) + self.fieldsGrid.SetRowLabelAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER ) + + # Label Appearance + + # Cell Defaults + self.fieldsGrid.SetDefaultCellAlignment( wx.ALIGN_CENTER, wx.ALIGN_TOP ) + bSizer4.Add( self.fieldsGrid, 1, wx.ALL|wx.EXPAND, 5 ) bSizer5 = wx.BoxSizer( wx.VERTICAL ) - self.m_btnUp = wx.BitmapButton( extraFieldsSizer.GetStaticBox(), wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, wx.BU_AUTODRAW|0 ) + self.m_btnUp = wx.BitmapButton( fieldsSizer.GetStaticBox(), wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, wx.BU_AUTODRAW|0 ) self.m_btnUp.SetMinSize( wx.Size( 30,30 ) ) bSizer5.Add( self.m_btnUp, 0, wx.ALL, 5 ) - self.m_btnDown = wx.BitmapButton( extraFieldsSizer.GetStaticBox(), wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, wx.BU_AUTODRAW|0 ) + self.m_btnDown = wx.BitmapButton( fieldsSizer.GetStaticBox(), wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, wx.BU_AUTODRAW|0 ) self.m_btnDown.SetMinSize( wx.Size( 30,30 ) ) bSizer5.Add( self.m_btnDown, 0, wx.ALL, 5 ) @@ -443,13 +465,13 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. bSizer4.Add( bSizer5, 0, 0, 5 ) - extraFieldsSizer.Add( bSizer4, 1, wx.EXPAND, 5 ) + fieldsSizer.Add( bSizer4, 1, wx.EXPAND, 5 ) - self.normalizeCaseCheckbox = wx.CheckBox( extraFieldsSizer.GetStaticBox(), wx.ID_ANY, u"Normalize field name case", wx.DefaultPosition, wx.DefaultSize, 0 ) - extraFieldsSizer.Add( self.normalizeCaseCheckbox, 0, wx.ALL|wx.EXPAND, 5 ) + self.normalizeCaseCheckbox = wx.CheckBox( fieldsSizer.GetStaticBox(), wx.ID_ANY, u"Normalize field name case", wx.DefaultPosition, wx.DefaultSize, 0 ) + fieldsSizer.Add( self.normalizeCaseCheckbox, 0, wx.ALL|wx.EXPAND, 5 ) - bSizer42.Add( extraFieldsSizer, 2, wx.ALL|wx.EXPAND, 5 ) + bSizer42.Add( fieldsSizer, 2, wx.ALL|wx.EXPAND, 5 ) sbSizer32 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Board variant" ), wx.VERTICAL ) @@ -519,9 +541,10 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx. # Connect Events self.Bind( wx.EVT_SIZE, self.OnSize ) - self.extraDataFilePicker.Bind( wx.EVT_FILEPICKER_CHANGED, self.OnNetlistFileChanged ) - self.m_btnUp.Bind( wx.EVT_BUTTON, self.OnExtraFieldsUp ) - self.m_btnDown.Bind( wx.EVT_BUTTON, self.OnExtraFieldsDown ) + self.extraDataFilePicker.Bind( wx.EVT_FILEPICKER_CHANGED, self.OnExtraDataFileChanged ) + self.fieldsGrid.Bind( wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnGridCellClicked ) + self.m_btnUp.Bind( wx.EVT_BUTTON, self.OnFieldsUp ) + self.m_btnDown.Bind( wx.EVT_BUTTON, self.OnFieldsDown ) self.normalizeCaseCheckbox.Bind( wx.EVT_CHECKBOX, self.OnNetlistFileChanged ) self.boardVariantFieldBox.Bind( wx.EVT_COMBOBOX, self.OnBoardVariantFieldChange ) @@ -533,15 +556,20 @@ def __del__( self ): def OnSize( self, event ): event.Skip() - def OnNetlistFileChanged( self, event ): + def OnExtraDataFileChanged( self, event ): event.Skip() - def OnExtraFieldsUp( self, event ): + def OnGridCellClicked( self, event ): event.Skip() - def OnExtraFieldsDown( self, event ): + def OnFieldsUp( self, event ): event.Skip() + def OnFieldsDown( self, event ): + event.Skip() + + def OnNetlistFileChanged( self, event ): + event.Skip() def OnBoardVariantFieldChange( self, event ): event.Skip() diff --git a/InteractiveHtmlBom/dialog/settings_dialog.py b/InteractiveHtmlBom/dialog/settings_dialog.py index d25b2a58..79095d3d 100644 --- a/InteractiveHtmlBom/dialog/settings_dialog.py +++ b/InteractiveHtmlBom/dialog/settings_dialog.py @@ -2,6 +2,7 @@ import re import wx +import wx.grid from . import dialog_base @@ -34,8 +35,8 @@ def SetSizeHints(self, sz1, sz2): self.SetSizeHintsSz(sz1, sz2) def set_extra_data_path(self, extra_data_file): - self.panel.extra.extraDataFilePicker.Path = extra_data_file - self.panel.extra.OnNetlistFileChanged(None) + self.panel.fields.extraDataFilePicker.Path = extra_data_file + self.panel.fields.OnExtraDataFileChanged(None) # Implementing settings_dialog @@ -47,11 +48,11 @@ def __init__(self, parent, extra_data_func, extra_data_wildcard, self.general = GeneralSettingsPanel(self.notebook, file_name_format_hint) self.html = HtmlSettingsPanel(self.notebook) - self.extra = ExtraFieldsPanel(self.notebook, extra_data_func, - extra_data_wildcard) + self.fields = FieldsPanel(self.notebook, extra_data_func, + extra_data_wildcard) self.notebook.AddPage(self.general, "General") self.notebook.AddPage(self.html, "Html defaults") - self.notebook.AddPage(self.extra, "Extra fields") + self.notebook.AddPage(self.fields, "Fields") def OnExit(self, event): self.GetParent().EndModal(wx.ID_CANCEL) @@ -176,12 +177,13 @@ def OnSize(self, event): self.componentSortOrderBox.SetItems(tmp) -# Implementing ExtraFieldsPanelBase -class ExtraFieldsPanel(dialog_base.ExtraFieldsPanelBase): +# Implementing FieldsPanelBase +class FieldsPanel(dialog_base.FieldsPanelBase): NONE_STRING = '' + FIELDS_GRID_COLUMNS = 3 def __init__(self, parent, extra_data_func, extra_data_wildcard): - dialog_base.ExtraFieldsPanelBase.__init__(self, parent) + dialog_base.FieldsPanelBase.__init__(self, parent) self.extra_data_func = extra_data_func self.extra_field_data = None bitmaps = os.path.join(os.path.dirname(__file__), "bitmaps") @@ -190,6 +192,16 @@ def __init__(self, parent, extra_data_func, extra_data_wildcard): self.m_btnDown.SetBitmap(wx.Bitmap( os.path.join(bitmaps, "btn-arrow-down.png"), wx.BITMAP_TYPE_PNG)) self.set_file_picker_wildcard(extra_data_wildcard) + self._setFieldsList([]) + for i in range(2): + box = self.GetTextExtent(self.fieldsGrid.GetColLabelValue(i)) + if hasattr(box, "x"): + width = box.x + else: + width = box[0] + width = int(width * 1.1 + 5) + self.fieldsGrid.SetColMinimalWidth(i, width) + self.fieldsGrid.SetColSize(i, width) def set_file_picker_wildcard(self, extra_data_wildcard): if extra_data_wildcard is None: @@ -212,34 +224,66 @@ def set_file_picker_wildcard(self, extra_data_wildcard): self.extraDataFilePicker = new_picker self.Layout() - # Handlers for ExtraFieldsPanelBase events. - def OnExtraFieldsUp(self, event): - selection = self.extraFieldsList.Selection - if selection != wx.NOT_FOUND and selection > 0: - item = self.extraFieldsList.GetString(selection) - checked = self.extraFieldsList.IsChecked(selection) - self.extraFieldsList.Delete(selection) - self.extraFieldsList.Insert(item, selection - 1) - if checked: - self.extraFieldsList.Check(selection - 1) - self.extraFieldsList.SetSelection(selection - 1) - - def OnExtraFieldsDown(self, event): - selection = self.extraFieldsList.Selection - size = self.extraFieldsList.Count - if selection != wx.NOT_FOUND and selection < size - 1: - item = self.extraFieldsList.GetString(selection) - checked = self.extraFieldsList.IsChecked(selection) - self.extraFieldsList.Delete(selection) - self.extraFieldsList.Insert(item, selection + 1) - if checked: - self.extraFieldsList.Check(selection + 1) - self.extraFieldsList.SetSelection(selection + 1) - - def OnNetlistFileChanged(self, event): + def _swapRows(self, a, b): + for i in range(self.FIELDS_GRID_COLUMNS): + va = self.fieldsGrid.GetCellValue(a, i) + vb = self.fieldsGrid.GetCellValue(b, i) + self.fieldsGrid.SetCellValue(a, i, vb) + self.fieldsGrid.SetCellValue(b, i, va) + + # Handlers for FieldsPanelBase events. + def OnGridCellClicked(self, event): + self.fieldsGrid.ClearSelection() + self.fieldsGrid.SelectRow(event.Row) + if event.Col < 2: + # toggle checkbox + val = self.fieldsGrid.GetCellValue(event.Row, event.Col) + val = "" if val else "1" + self.fieldsGrid.SetCellValue(event.Row, event.Col, val) + # group shouldn't be enabled without show + if event.Col == 0 and val == "": + self.fieldsGrid.SetCellValue(event.Row, 1, val) + if event.Col == 1 and val == "1": + self.fieldsGrid.SetCellValue(event.Row, 0, val) + + def OnFieldsUp(self, event): + selection = self.fieldsGrid.SelectedRows + if len(selection) == 1 and selection[0] > 0: + self._swapRows(selection[0], selection[0] - 1) + self.fieldsGrid.ClearSelection() + self.fieldsGrid.SelectRow(selection[0] - 1) + + def OnFieldsDown(self, event): + selection = self.fieldsGrid.SelectedRows + size = self.fieldsGrid.NumberRows + if len(selection) == 1 and selection[0] < size - 1: + self._swapRows(selection[0], selection[0] + 1) + self.fieldsGrid.ClearSelection() + self.fieldsGrid.SelectRow(selection[0] + 1) + + def _setFieldsList(self, fields): + if self.fieldsGrid.NumberRows: + self.fieldsGrid.DeleteRows(0, self.fieldsGrid.NumberRows) + self.fieldsGrid.AppendRows(len(fields)) + row = 0 + for f in fields: + self.fieldsGrid.SetCellValue(row, 0, "1") + self.fieldsGrid.SetCellValue(row, 1, "1") + self.fieldsGrid.SetCellRenderer( + row, 0, wx.grid.GridCellBoolRenderer()) + self.fieldsGrid.SetCellRenderer( + row, 1, wx.grid.GridCellBoolRenderer()) + self.fieldsGrid.SetCellValue(row, 2, f) + self.fieldsGrid.SetCellAlignment( + row, 2, wx.ALIGN_LEFT, wx.ALIGN_TOP) + self.fieldsGrid.SetReadOnly(row, 2) + row += 1 + + def OnExtraDataFileChanged(self, event): extra_data_file = self.extraDataFilePicker.Path if not os.path.isfile(extra_data_file): return + self.extra_field_data = None try: self.extra_field_data = self.extra_data_func( @@ -248,9 +292,10 @@ def OnNetlistFileChanged(self, event): pop_error( "Failed to parse file %s\n\n%s" % (extra_data_file, e)) self.extraDataFilePicker.Path = '' + if self.extra_field_data is not None: field_list = list(self.extra_field_data[0]) - self.extraFieldsList.SetItems(field_list) + self._setFieldsList(["Value", "Footprint"] + field_list) field_list.append(self.NONE_STRING) self.boardVariantFieldBox.SetItems(field_list) self.boardVariantFieldBox.SetStringSelection(self.NONE_STRING) @@ -274,10 +319,33 @@ def OnBoardVariantFieldChange(self, event): self.boardVariantBlacklist.SetItems(list(variant_set)) def OnSize(self, event): - # Trick the listCheckBox best size calculations - items = self.extraFieldsList.GetStrings() - checked_items = self.extraFieldsList.GetCheckedStrings() - self.extraFieldsList.SetItems([]) self.Layout() - self.extraFieldsList.SetItems(items) - self.extraFieldsList.SetCheckedStrings(checked_items) + g = self.fieldsGrid + g.SetColSize( + 2, g.GetClientSize().x - g.GetColSize(0) - g.GetColSize(1) - 30) + + def GetShowFields(self): + result = [] + for row in range(self.fieldsGrid.NumberRows): + if self.fieldsGrid.GetCellValue(row, 0) == "1": + result.append(self.fieldsGrid.GetCellValue(row, 2)) + return result + + def GetGroupFields(self): + result = [] + for row in range(self.fieldsGrid.NumberRows): + if self.fieldsGrid.GetCellValue(row, 1) == "1": + result.append(self.fieldsGrid.GetCellValue(row, 2)) + return result + + def SetCheckedFields(self, show, group): + group = [s for s in group if s in show] + current = [] + for row in range(self.fieldsGrid.NumberRows): + current.append(self.fieldsGrid.GetCellValue(row, 2)) + new = [s for s in current if s not in show] + self._setFieldsList(show + new) + for row in range(self.fieldsGrid.NumberRows): + field = self.fieldsGrid.GetCellValue(row, 2) + self.fieldsGrid.SetCellValue(row, 0, "1" if field in show else "") + self.fieldsGrid.SetCellValue(row, 1, "1" if field in group else "") diff --git a/InteractiveHtmlBom/ecad/genericjson.py b/InteractiveHtmlBom/ecad/genericjson.py index 4f3656c7..4f9dbab0 100644 --- a/InteractiveHtmlBom/ecad/genericjson.py +++ b/InteractiveHtmlBom/ecad/genericjson.py @@ -56,7 +56,6 @@ def get_generic_json_pcb(self): return pcb def _verify(self, pcb): - """Spot check the pcb object.""" if len(pcb['pcbdata']['footprints']) != len(pcb['components']): @@ -97,12 +96,12 @@ def parse(self): if board_outline_bbox.initialized(): pcbdata['edges_bbox'] = board_outline_bbox.to_dict() - if self.config.extra_fields: + extra_fields = set(self.config.show_fields) + extra_fields.discard("Footprint") + extra_fields.discard("Value") + if extra_fields: for c in components: - extra_field_data = {} - for f in self.config.extra_fields: - fv = ("" if f not in c.extra_fields else c.extra_fields[f]) - extra_field_data[f] = fv - c.extra_fields = extra_field_data + c.extra_fields = { + f: c.extra_fields.get(f, "") for f in extra_fields} return pcbdata, components diff --git a/InteractiveHtmlBom/ecad/kicad.py b/InteractiveHtmlBom/ecad/kicad.py index dd268edb..901e2947 100644 --- a/InteractiveHtmlBom/ecad/kicad.py +++ b/InteractiveHtmlBom/ecad/kicad.py @@ -567,7 +567,10 @@ def parse(self): from ..errors import ParsingException # Get extra field data from netlist - need_extra_fields = (self.config.extra_fields or + field_set = set(self.config.show_fields) + field_set.discard("Value") + field_set.discard("Footprint") + need_extra_fields = (field_set or self.config.board_variant_whitelist or self.config.board_variant_blacklist or self.config.dnp_field) diff --git a/InteractiveHtmlBom/web/ibom.js b/InteractiveHtmlBom/web/ibom.js index f6e460f3..cade7fe2 100644 --- a/InteractiveHtmlBom/web/ibom.js +++ b/InteractiveHtmlBom/web/ibom.js @@ -182,7 +182,7 @@ function setBomCheckboxState(checkbox, element, references) { } function createCheckboxChangeHandler(checkbox, references, row) { - return function() { + return function () { refsSet = getStoredCheckboxRefs(checkbox); var markWhenChecked = settings.markWhenChecked == checkbox; eventArgs = { @@ -233,7 +233,7 @@ function clearHighlightedFootprints() { } function createRowHighlightHandler(rowid, refs, net) { - return function() { + return function () { if (currentHighlightedRowId) { if (currentHighlightedRowId == rowid) { return; @@ -247,10 +247,10 @@ function createRowHighlightHandler(rowid, refs, net) { drawHighlights(); EventHandler.emitEvent( IBOM_EVENT_TYPES.HIGHLIGHT_EVENT, { - rowid: rowid, - refs: refs, - net: net - }); + rowid: rowid, + refs: refs, + net: net + }); } } @@ -261,37 +261,28 @@ function entryMatches(entry) { } // check refs if (!settings.hiddenColumns.includes("references")) { - for (var ref of entry[3]) { + for (var ref of entry) { if (ref[0].toLowerCase().indexOf(filter) >= 0) { return true; } } } - // check extra fields - if (!settings.hiddenColumns.includes("extrafields")) { - for (var i in config.extra_fields) { - if (entry[4][i].toLowerCase().indexOf(filter) >= 0) { - return true; + // check fields + for (var i in config.fields) { + var f = config.fields[i]; + if (!settings.hiddenColumns.includes(f)) { + for (var ref of entry) { + if (pcbdata.bom.fields[ref[1]][i].toLowerCase().indexOf(filter) >= 0) { + return true; + } } } } - // check value - if (!settings.hiddenColumns.includes("value")) { - if (entry[1].toLowerCase().indexOf(filter) >= 0) { - return true; - } - } - // check footprint - if (!settings.hiddenColumns.includes("footprint")) { - if (entry[2].toLowerCase().indexOf(filter) >= 0) { - return true; - } - } return false; } function findRefInEntry(entry) { - return entry[3].filter(r => r[0].toLowerCase() == reflookup); + return entry.filter(r => r[0].toLowerCase() == reflookup); } function highlightFilter(s) { @@ -318,7 +309,7 @@ function highlightFilter(s) { } function checkboxSetUnsetAllHandler(checkboxname) { - return function() { + return function () { var checkboxnum = 0; while (checkboxnum < settings.checkboxes.length && settings.checkboxes[checkboxnum].toLowerCase() != checkboxname.toLowerCase()) { @@ -361,7 +352,7 @@ function createColumnHeader(name, cls, comparator, is_checkbox = false) { var spacer = document.createElement("div"); spacer.className = "column-spacer"; th.appendChild(spacer); - spacer.onclick = function() { + spacer.onclick = function () { if (currentSortColumn && th !== currentSortColumn) { // Currently sorted by another column currentSortColumn.childNodes[1].classList.remove(currentSortOrder); @@ -373,7 +364,7 @@ function createColumnHeader(name, cls, comparator, is_checkbox = false) { // Already sorted by this column if (currentSortOrder == "asc") { // Sort by this column, descending order - bomSortFunction = function(a, b) { + bomSortFunction = function (a, b) { return -comparator(a, b); } currentSortColumn.childNodes[1].classList.remove("asc"); @@ -440,7 +431,7 @@ function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) var input = document.createElement("input"); input.classList.add("visibility_checkbox"); input.type = "checkbox"; - input.onchange = function(e) { + input.onchange = function (e) { setShowBOMColumn(column, e.target.checked) }; input.checked = !(settings.hiddenColumns.includes(column)); @@ -461,7 +452,7 @@ function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) } tr.appendChild(th); - var checkboxCompareClosure = function(checkbox) { + var checkboxCompareClosure = function (checkbox) { return (a, b) => { var stateA = getCheckboxState(checkbox, a[3]); var stateB = getCheckboxState(checkbox, b[3]); @@ -470,6 +461,15 @@ function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) return 0; } } + var stringFieldCompareClosure = function (fieldIndex) { + return (a, b) => { + var fa = pcbdata.bom.fields[a[0][1]][fieldIndex]; + var fb = pcbdata.bom.fields[b[0][1]][fieldIndex]; + if (fa != fb) return fa > fb ? 1 : -1; + else return 0; + } + } + if (settings.bommode == "netlist") { th = createColumnHeader("Net name", "bom-netname", (a, b) => { if (a > b) return -1; @@ -480,6 +480,8 @@ function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) } else { // Filter hidden columns var columns = settings.columnOrder.filter(e => !settings.hiddenColumns.includes(e)); + var valueIndex = config.fields.indexOf("Value"); + var footprintIndex = config.fields.indexOf("Footprint"); columns.forEach((column) => { if (column === placeHolderColumn) { var n = 1; @@ -490,54 +492,44 @@ function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) tr.appendChild(td); } return; - } - if (column === "checkboxes") { + } else if (column === "checkboxes") { for (var checkbox of settings.checkboxes) { th = createColumnHeader( checkbox, "bom-checkbox", checkboxCompareClosure(checkbox), true); tr.appendChild(th); } - } - if (column === "References") { + } else if (column === "References") { tr.appendChild(createColumnHeader("References", "references", (a, b) => { var i = 0; - while (i < a[3].length && i < b[3].length) { - if (a[3][i] != b[3][i]) return a[3][i] > b[3][i] ? 1 : -1; + while (i < a.length && i < b.length) { + if (a[i] != b[i]) return a[i] > b[i] ? 1 : -1; i++; } - return a[3].length - b[3].length; + return a.length - b.length; })); - } - if (column === "Value") { + } else if (column === "Value") { tr.appendChild(createColumnHeader("Value", "value", (a, b) => { - return valueCompare(a[5], b[5], a[1], b[1]); + var ra = a[0][1], rb = b[0][1]; + return valueCompare( + pcbdata.bom.parsedValues[ra], pcbdata.bom.parsedValues[rb], + pcbdata.bom.fields[ra][valueIndex], pcbdata.bom.fields[rb][valueIndex]); })); - } - if (column === "Footprint") { - tr.appendChild(createColumnHeader("Footprint", "footprint", (a, b) => { - if (a[2] != b[2]) return a[2] > b[2] ? 1 : -1; - else return 0; - })); - } - if (column === "Quantity" && settings.bommode == "grouped") { + return; + } else if (column === "Footprint") { + tr.appendChild(createColumnHeader( + "Footprint", "footprint", stringFieldCompareClosure(footprintIndex))); + } else if (column === "Quantity" && settings.bommode == "grouped") { tr.appendChild(createColumnHeader("Quantity", "quantity", (a, b) => { - return a[3].length - b[3].length; + return a.length - b.length; })); + } else { + // Other fields + var i = config.fields.indexOf(column); + if (i < 0) + return; + tr.appendChild(createColumnHeader( + column, `field${i + 1}`, stringFieldCompareClosure(i))); } - // Extra fields - var extraFieldCompareClosure = function(fieldIndex) { - return (a, b) => { - var fa = a[4][fieldIndex]; - var fb = b[4][fieldIndex]; - if (fa != fb) return fa > fb ? 1 : -1; - else return 0; - } - } - var i = config.extra_fields.indexOf(column); - if (i < 0) - return; - tr.appendChild(createColumnHeader( - column, `extrafield${i+1}`, extraFieldCompareClosure(i))); }); } bomhead.appendChild(tr); @@ -570,10 +562,8 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { // expand bom table expandedTable = [] for (var bomentry of bomtable) { - for (var ref of bomentry[3]) { - expandedTable.push([1, bomentry[1], bomentry[2], - [ref], bomentry[4], bomentry[5] - ]); + for (var ref of bomentry) { + expandedTable.push([ref]); } } bomtable = expandedTable; @@ -607,7 +597,7 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { continue; } } else { - references = bomentry[3]; + references = bomentry; } // Filter hidden columns var columns = settings.columnOrder.filter(e => !settings.hiddenColumns.includes(e)); @@ -621,9 +611,7 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { tr.appendChild(td); } return; - } - // Checkboxes - if (column === "checkboxes") { + } else if (column === "checkboxes") { for (var checkbox of settings.checkboxes) { if (checkbox) { td = document.createElement("TD"); @@ -638,38 +626,26 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { tr.appendChild(td); } } - } - // References - if (column === "References") { + } else if (column === "References") { td = document.createElement("TD"); td.innerHTML = highlightFilter(references.map(r => r[0]).join(", ")); tr.appendChild(td); - } - // Value - if (column === "Value") { - td = document.createElement("TD"); - td.innerHTML = highlightFilter(bomentry[1]); - tr.appendChild(td); - } - // Footprint - if (column === "Footprint") { + } else if (column === "Quantity" && settings.bommode == "grouped") { + // Quantity td = document.createElement("TD"); - td.innerHTML = highlightFilter(bomentry[2]); + td.textContent = references.length; tr.appendChild(td); - } - if (column === "Quantity" && settings.bommode == "grouped") { - // Quantity + } else { + // All the other fields + var field_index = config.fields.indexOf(column) + if (field_index < 0) + return; + var valueSet = new Set(); + references.map(r => r[1]).forEach((id) => valueSet.add(pcbdata.bom.fields[id][field_index])); td = document.createElement("TD"); - td.textContent = bomentry[3].length; + td.innerHTML = highlightFilter(Array.from(valueSet).join(", ")); tr.appendChild(td); } - // Extra fields - var i = config.extra_fields.indexOf(column) - if (i < 0) - return; - td = document.createElement("TD"); - td.innerHTML = highlightFilter(bomentry[4][i]); - tr.appendChild(td); }); } bom.appendChild(tr); @@ -694,11 +670,11 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { } EventHandler.emitEvent( IBOM_EVENT_TYPES.BOM_BODY_CHANGE_EVENT, { - filter: filter, - reflookup: reflookup, - checkboxes: settings.checkboxes, - bommode: settings.bommode, - }); + filter: filter, + reflookup: reflookup, + checkboxes: settings.checkboxes, + bommode: settings.bommode, + }); } function highlightPreviousRow() { @@ -1107,7 +1083,7 @@ function updateCheckboxStats(checkbox) { td.lastChild.innerHTML = checked + "/" + total + " (" + Math.round(percent) + "%)"; } -document.onkeydown = function(e) { +document.onkeydown = function (e) { switch (e.key) { case "n": if (document.activeElement.type == "text") { @@ -1180,7 +1156,7 @@ function hideNetlistButton() { document.getElementById("bom-netlist-btn").style.display = "none"; } -window.onload = function(e) { +window.onload = function (e) { initUtils(); initRender(); initStorage(); diff --git a/InteractiveHtmlBom/web/util.js b/InteractiveHtmlBom/web/util.js index b6e8f46c..f9bcf072 100644 --- a/InteractiveHtmlBom/web/util.js +++ b/InteractiveHtmlBom/web/util.js @@ -177,9 +177,11 @@ function initUtils() { "([GgMmKkUuNnPp])?" + "([0-9]*)" + "(\\b.*)?$", ""); - for (var bom_type of ["both", "F", "B"]) { - for (var row of pcbdata.bom[bom_type]) { - row.push(parseValue(row[1], row[3][0][0])); + if (config.fields.includes("Value")) { + var index = config.fields.indexOf("Value"); + pcbdata.bom["parsedValues"] = {}; + for (var id in pcbdata.bom.fields) { + pcbdata.bom.parsedValues[id] = parseValue(pcbdata.bom.fields[id][index]) } } } @@ -458,6 +460,8 @@ var settings = { renderDnpOutline: false, renderTracks: true, renderZones: true, + columnOrder: [], + hiddenColumns: [], } function initDefaults() { @@ -518,7 +522,7 @@ function initDefaults() { initBooleanSetting("darkmode", config.dark_mode, "darkmodeCheckbox", setDarkMode); initBooleanSetting("highlightpin1", config.highlight_pin1, "highlightpin1Checkbox", setHighlightPin1); - var fields = ["checkboxes"].concat(config.fields); + var fields = ["checkboxes", "References"].concat(config.fields).concat(["Quantity"]); var hcols = JSON.parse(readStorage("hiddenColumns")); if (hcols === null) { hcols = []; diff --git a/settings_dialog.fbp b/settings_dialog.fbp index 206c6795..1e583527 100644 --- a/settings_dialog.fbp +++ b/settings_dialog.fbp @@ -2776,7 +2776,7 @@ wxID_ANY - ExtraFieldsPanelBase + FieldsPanelBase -1,-1 ; ; forward_declare @@ -2796,7 +2796,7 @@ 0 wxID_ANY - Netlist or xml file + Extra data file sbSizer7 wxVERTICAL @@ -2865,7 +2865,7 @@ wxBORDER_SIMPLE - OnNetlistFileChanged + OnExtraDataFileChanged @@ -2876,9 +2876,9 @@ 2 wxID_ANY - Extra fields + Fields - extraFieldsSizer + fieldsSizer wxVERTICAL 1 none @@ -2893,76 +2893,91 @@ none 5 - wxEXPAND + wxALL|wxEXPAND 1 - + + 1 + 1 + 1 + 1 + + + + + 1 + 0 + + + + 1 + + + wxALIGN_CENTER + + wxALIGN_TOP + 0 + 1 + wxALIGN_CENTER + 30 + "Show" "Group" "Name" + wxALIGN_CENTER + 3 + + + 1 + 0 + Dock + 0 + Left + 0 + 1 + 0 + 0 + 1 + 1 + + 1 + + + 1 + 0 + 0 + wxID_ANY + + + + 0 + 0 + + 0 + + + 0 - bSizer6 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - - 1 - extraFieldsList - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - wxBORDER_SIMPLE - - + 1 + fieldsGrid + 1 + + + protected + 1 + + Resizable + wxALIGN_CENTER + 0 + + wxALIGN_CENTER + + 2 + 1 + + ; ; forward_declare + 0 + + + + + OnGridCellClicked @@ -3044,7 +3059,7 @@ - OnExtraFieldsUp + OnFieldsUp @@ -3117,7 +3132,7 @@ - OnExtraFieldsDown + OnFieldsDown