From c4f176d3f43a020abeaa9d2500237d8ff77e5e3f Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 23 Oct 2023 14:51:15 +0200 Subject: Improve error handling, add runtime options --- main_dialog.fbp | 416 +++++++++++++++++++++++++++++++++++++++++++++++++- mesh_dialog.py | 142 ++++++++++++----- mesh_plugin_dialog.py | 39 ++++- 3 files changed, 556 insertions(+), 41 deletions(-) diff --git a/main_dialog.fbp b/main_dialog.fbp index 9852735..d97f4c0 100644 --- a/main_dialog.fbp +++ b/main_dialog.fbp @@ -48,7 +48,7 @@ MainDialog - 632,458 + 632,580 wxCLOSE_BOX|wxDEFAULT_DIALOG_STYLE|wxMINIMIZE_BOX|wxRESIZE_BORDER|wxSTAY_ON_TOP ; ; forward_declare Security Mesh Generator Plugin @@ -62,6 +62,68 @@ bSizer1 wxVERTICAL none + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + <font color="red"><b>Warning: Board outline not found</b></font> + 1 + + 0 + + + 0 + + 1 + m_warningLabel + 1 + + + protected + 1 + + Resizable + 1 + + wxALIGN_CENTER_HORIZONTAL + ; ; forward_declare + 0 + + + + + -1 + + 5 wxEXPAND @@ -1333,6 +1395,358 @@ + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Respect zone keepouts + + 0 + + + 0 + + 1 + m_useKeepoutCheckbox + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Respect board outline + + 0 + + + 0 + + 1 + m_useOutlineCheckbox + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Save layout visualizations + + 0 + + + 0 + + 1 + m_vizCheckbox + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Visualization output directory + 0 + + 0 + + + 0 + + 1 + m_staticText14 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_vizTextfield + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + diff --git a/mesh_dialog.py b/mesh_dialog.py index 9812bba..17dcf6a 100644 --- a/mesh_dialog.py +++ b/mesh_dialog.py @@ -8,6 +8,7 @@ from itertools import count, islice import json import re from os import path +import os import wx @@ -30,16 +31,20 @@ class AbortError(SystemError): @dataclasses.dataclass class GeneratorSettings: - edge_clearance: float = 1.5 # mm - anchor: str = None # Footprint designator - chamfer: float = 0.0 # unit fraction - mask_layer_id: int = 0 # kicad layer id, populated later - random_seed: str = None - randomness: float = 1.0 + edge_clearance: float = 1.5 # mm + anchor: str = None # Footprint designator + chamfer: float = 0.0 # unit fraction + mask_layer_id: int = 0 # kicad layer id, populated later + random_seed: str = None + randomness: float = 1.0 + use_keepouts: bool = True + use_outline: bool = True + save_visualization: bool = True + visualization_path: str = 'mesh_visualizations' def serialize(self): d = dataclasses.asdict(self) - d['kimesh_settings_version'] = '2.0.0' + d['kimesh_settings_version'] = '2.1.0' return json.dumps(d).encode() @classmethod @@ -47,7 +52,7 @@ class GeneratorSettings: d = json.loads(data.decode()) version = d.pop('kimesh_settings_version') vtup = tuple(map(int, version.split('.'))) - if vtup > (2, 0, 0): + if vtup > (2, 1, 0): raise cls.VersionError("Project kimesh settings file is too new for this plugin's version.") return cls(**d) @@ -69,6 +74,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): self.nets = { str(wxs) for wxs, netinfo in board.GetNetsByName().items() } self.update_net_label(None) + self.update_outline_warning() self.Fit() @@ -104,9 +110,64 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): self.m_seedInput.Value = settings.random_seed or '' self.m_randomnessSpin.Value = settings.randomness*100.0 self.m_edgeClearanceSpin.Value = settings.edge_clearance + self.m_useOutlineCheckbox.Value = settings.use_outline + self.m_useKeepoutCheckbox.Value = settings.use_keepouts + self.m_vizTextfield.Value = settings.visualization_path + self.m_vizCheckbox.Value = settings.save_visualization self.SetMinSize(self.GetSize()) + @contextmanager + def viz(self, filename): + if self.m_vizCheckbox.Value: + val = self.m_vizTextfield.Value + project_dir = path.dirname(self.board.GetFileName()) + if val: + val = path.join(project_dir, val) + if not os.path.isdir(val): + os.mkdir(val) + filename = path.join(val, filename) + + filename = path.join(project_dir, filename) + with open(filename, 'w') as f: + wrapper = DebugOutputWrapper(f) + yield wrapper + wrapper.save() + + else: + wrapper = DebugOutputWrapper(None) + yield wrapper + + def board_has_outline(self): + # KiCad's API is absolutely insane. As long as the board has an outline, the board outline function works + # alright. Now imagine the Edge.Cuts layer is empty. What would be a sane thing to do? I guess raising an error + # would be the best, with the second best being to return something like the hull of all objects on the other + # layers. Alas, KiCad doesn't do either. Instead, KiCad returns the union of the shapes of all objects on the + # **VISIBLE** layers, so the result of that outline function changes with which layers the user has set to + # visible. Whyyyyy :( + # + # We have to work around this to avoid presenting the user with a foot-gun in case they hide their mesh + # definition layer. + # + edge_cuts = self.board.GetLayerID('Edge.Cuts') + outline_objs = [] + for drawing in self.board.GetDrawings(): + if drawing.GetLayer() == edge_cuts: + return True + else: + return False + + def update_outline_warning(self): + outlines = pcbnew.SHAPE_POLY_SET() + self.board.GetBoardPolygonOutlines(outlines) + board_outlines = list(self.poly_set_to_shapely(outlines)) + board_mask = shapely.ops.unary_union(board_outlines) + + if not self.board_has_outline() or board_mask.is_empty: + self.m_warningLabel.SetLabelMarkup('Warning: Board outline not found') + else: + self.m_warningLabel.SetLabelMarkup('') + def get_matching_nets(self): prefix = self.m_net_prefix.Value return { net for net in self.nets if net.startswith(prefix) } @@ -170,7 +231,11 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): chamfer = float(self.m_chamferSpin.Value)/100.0, mask_layer_id = self.m_maskLayerChoice.GetSelection(), random_seed = str(self.m_seedInput.Value) or None, - randomness = float(self.m_randomnessSpin.Value)/100.0) + randomness = float(self.m_randomnessSpin.Value)/100.0, + use_outline = self.m_useOutlineCheckbox.Value, + use_keepouts = self.m_useKeepoutCheckbox.Value, + visualization_path = self.m_vizTextfield.Value, + save_visualization = self.m_vizCheckbox.Value) except ValueError as e: return wx.MessageDialog(self, "Invalid input value: {}.".format(e), "Invalid input").ShowModal() @@ -198,29 +263,37 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): keepouts.append(zone.Outline()) print(f'Found {len(keepouts)} keepout areas.') - outlines = pcbnew.SHAPE_POLY_SET() - self.board.GetBoardPolygonOutlines(outlines) - board_outlines = list(self.poly_set_to_shapely(outlines)) - board_mask = shapely.ops.unary_union(board_outlines) - board_mask = board_mask.buffer(-settings.edge_clearance) - print('board outline bounds:', board_mask.bounds) - if board_mask.is_empty: - return wx.MessageDialog(self, "Error: Could not find the board outline, or board edge clearance is set too high.").ShowModal() + if self.board_has_outline() and self.m_useOutlineCheckbox.Value: # Avoid foot-gun due to insane API. See note in the function. + outlines = pcbnew.SHAPE_POLY_SET() + self.board.GetBoardPolygonOutlines(outlines) + board_outlines = list(self.poly_set_to_shapely(outlines)) + board_mask = shapely.ops.unary_union(board_outlines) + mask = board_mask.buffer(-settings.edge_clearance) + print('board outline bounds:', mask.bounds) + if mask.is_empty: + return wx.MessageDialog(self, "Error: Board edge clearance is set too high. There is nothing left for the mesh after applying clearance.").ShowModal() + else: + mask = None zone_outlines = [ outline for zone in mesh_zones for outline in self.poly_set_to_shapely(zone) ] zone_mask = shapely.ops.unary_union(zone_outlines) if zone_mask.is_empty: - mask = board_mask + return wx.MessageDialog(self, "Error: Empty mesh outline on mesh outline layer. Make sure the mesh outline is defined with polygon objects only. Other shapes are not supported yet.").ShowModal() + elif mask is None: + mask = zone_mask else: - mask = zone_mask.intersection(board_mask) + mask = zone_mask.intersection(mask) print('Mesh mask bounds:', zone_mask.bounds) - keepout_outlines = [ outline for zone in keepouts for outline in self.poly_set_to_shapely(zone) ] - keepout_mask = shapely.ops.unary_union(keepout_outlines) - if not keepout_mask.is_empty: - mask = shapely.difference(mask, keepout_mask) - print('keepout mask bounds:', keepout_mask.bounds) - print('resulting mask bounds:', mask.bounds) + if self.m_useKeepoutCheckbox.Value: + keepout_outlines = [ outline for zone in keepouts for outline in self.poly_set_to_shapely(zone) ] + keepout_mask = shapely.ops.unary_union(keepout_outlines) + if not keepout_mask.is_empty: + mask = shapely.difference(mask, keepout_mask) + print('keepout mask bounds:', keepout_mask.bounds) + print('resulting mask bounds:', mask.bounds) + if mask.is_empty: + return wx.MessageDialog(self, "Error: After applying all keepouts, and intersecting with the board's outline, the mesh outline is empty.") try: def warn(msg): @@ -307,7 +380,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): grid.append(row) num_valid = 0 - with DebugOutput('dbg_grid.svg') as dbg: + with self.viz('mesh_grid.svg') as dbg: dbg.add(mask, color='#00000020') for y, row in enumerate(grid, start=grid_y0): @@ -389,10 +462,10 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): not_visited = { (x, y) for x in range(grid_x0, grid_x0+grid_cols) for y in range(grid_y0, grid_y0+grid_rows) if is_valid(grid[y-grid_y0][x-grid_x0]) } num_to_visit = len(not_visited) track_count = 0 - with DebugOutput('dbg_cells.svg') as dbg_cells,\ - DebugOutput('dbg_composite.svg') as dbg_composite,\ - DebugOutput('dbg_tiles.svg') as dbg_tiles,\ - DebugOutput('dbg_traces.svg') as dbg_traces: + with self.viz('mesh_cells.svg') as dbg_cells,\ + self.viz('mesh_composite.svg') as dbg_composite,\ + self.viz('mesh_tiles.svg') as dbg_tiles,\ + self.viz('mesh_traces.svg') as dbg_traces: dbg_cells.add(mask, color='#00000020') dbg_composite.add(mask, color='#00000020') dbg_traces.add(mask, color='#00000020') @@ -425,7 +498,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): i = 0 past_tiles = {} def dump_output(i): - with DebugOutput(f'per-tile/step{i}.svg') as dbg_per_tile: + with self.viz(f'per-tile/step{i}.svg') as dbg_per_tile: dbg_per_tile.add(mask, color='#00000020') for foo in anchor_outlines: dbg_per_tile.add(foo, color='#00000080', stroke_width=0.05, stroke_color='#00000000') @@ -607,13 +680,6 @@ def virihex(val, max=1.0, alpha=1.0): r, g, b, a = [ int(round(0xff*c)) for c in [r, g, b, alpha] ] return f'#{r:02x}{g:02x}{b:02x}{a:02x}' -@contextmanager -def DebugOutput(filename): - with open(filename, 'w') as f: - wrapper = DebugOutputWrapper(f) - yield wrapper - wrapper.save() - class DebugOutputWrapper: def __init__(self, f): self.f = f diff --git a/mesh_plugin_dialog.py b/mesh_plugin_dialog.py index 6223481..790caa8 100644 --- a/mesh_plugin_dialog.py +++ b/mesh_plugin_dialog.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ########################################################################### -## Python code generated with wxFormBuilder (version 3.10.1-367-gf0e67a69) +## Python code generated with wxFormBuilder (version 3.10.1-380-gf48f2659) ## http://www.wxformbuilder.org/ ## ## PLEASE DO *NOT* EDIT THIS FILE! @@ -17,12 +17,18 @@ import wx.xrc class MainDialog ( wx.Dialog ): def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Security Mesh Generator Plugin", pos = wx.DefaultPosition, size = wx.Size( 632,458 ), style = wx.CLOSE_BOX|wx.DEFAULT_DIALOG_STYLE|wx.MINIMIZE_BOX|wx.RESIZE_BORDER|wx.STAY_ON_TOP ) + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Security Mesh Generator Plugin", pos = wx.DefaultPosition, size = wx.Size( 632,580 ), style = wx.CLOSE_BOX|wx.DEFAULT_DIALOG_STYLE|wx.MINIMIZE_BOX|wx.RESIZE_BORDER|wx.STAY_ON_TOP ) self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) bSizer1 = wx.BoxSizer( wx.VERTICAL ) + self.m_warningLabel = wx.StaticText( self, wx.ID_ANY, u"Warning: Board outline not found", wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTER_HORIZONTAL ) + self.m_warningLabel.SetLabelMarkup( u"Warning: Board outline not found" ) + self.m_warningLabel.Wrap( -1 ) + + bSizer1.Add( self.m_warningLabel, 0, wx.ALL|wx.EXPAND, 5 ) + fgSizer1 = wx.FlexGridSizer( 0, 2, 0, 0 ) fgSizer1.AddGrowableCol( 1 ) fgSizer1.SetFlexibleDirection( wx.BOTH ) @@ -138,6 +144,35 @@ class MainDialog ( wx.Dialog ): fgSizer1.Add( bSizer12, 1, wx.EXPAND, 5 ) + fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_useKeepoutCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Respect zone keepouts", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_useKeepoutCheckbox.SetValue(True) + fgSizer1.Add( self.m_useKeepoutCheckbox, 0, wx.ALL, 5 ) + + + fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_useOutlineCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Respect board outline", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_useOutlineCheckbox.SetValue(True) + fgSizer1.Add( self.m_useOutlineCheckbox, 0, wx.ALL, 5 ) + + + fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_vizCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Save layout visualizations", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_vizCheckbox.SetValue(True) + fgSizer1.Add( self.m_vizCheckbox, 0, wx.ALL, 5 ) + + self.m_staticText14 = wx.StaticText( self, wx.ID_ANY, u"Visualization output directory", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText14.Wrap( -1 ) + + fgSizer1.Add( self.m_staticText14, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT|wx.ALL, 5 ) + + self.m_vizTextfield = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) + fgSizer1.Add( self.m_vizTextfield, 0, wx.ALL|wx.EXPAND, 5 ) + + bSizer1.Add( fgSizer1, 1, wx.EXPAND, 5 ) bSizer99 = wx.BoxSizer( wx.HORIZONTAL ) -- cgit