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
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 )