diff --git a/ViaStitching/FillArea.py b/ViaStitching/FillArea.py index 97f09b3..8e192a0 100755 --- a/ViaStitching/FillArea.py +++ b/ViaStitching/FillArea.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # # FillArea.py # @@ -28,8 +29,8 @@ import os import random import pprint -"""# This script fill all areas with Via (Via Stitching) is the area is on -# a specific net (by default /GND fallback to GND) +""" +# This script fills all areas of a specific net with Vias (Via Stitching) # # # Usage in pcbnew's python console: @@ -42,7 +43,7 @@ FillArea.FillArea().Run() # Other example: -# You can add modifications to parameters by adding functions call: +# You can add modifications to parameters by adding functions calls: FillArea.FillArea().SetDebug().SetNetname("GND").SetStepMM(1.27).SetSizeMM(0.6).SetDrillMM(0.3).SetClearanceMM(0.2).Run() # with @@ -59,6 +60,17 @@ FillArea.FillArea().SetDebug().SetNetname("GND").SetStepMM(1.27).SetSizeMM(0.6). """ +class ViaObject: + + """ + ViaObject holds all information of a single Via + """ + + def __init__(self, x, y, pos_x, pos_y): + self.X = x + self.Y = y + self.PosX = pos_x + self.PosY = pos_y class FillArea: @@ -67,6 +79,15 @@ class FillArea: pads and keepout areas """ + REASON_OK = 0 + REASON_NO_SIGNAL = 1 + REASON_OTHER_SIGNAL = 2 + REASON_KEEPOUT = 3 + REASON_TRACK = 4 + REASON_PAD = 5 + REASON_DRAWING = 6 + REASON_STEP = 7 + def __init__(self, filename=None): self.filename = None self.clearance = 0 @@ -124,7 +145,7 @@ class FillArea: return self def SetNetname(self, netname): - self.netname = netname + self.netname = netname.upper() return self def SetStepMM(self, s): @@ -150,23 +171,48 @@ class FillArea: def SetClearanceMM(self, s): self.clearance = float(FromMM(s)) return self - + + def GetReasonSymbol(self, reason): + if isinstance(reason, ViaObject): + return "X" + if reason == self.REASON_NO_SIGNAL: + return " " + if reason == self.REASON_OTHER_SIGNAL: + return "O" + if reason == self.REASON_KEEPOUT: + return "K" + if reason == self.REASON_TRACK: + return "T" + if reason == self.REASON_PAD: + return "P" + if reason == self.REASON_DRAWING: + return "D" + if reason == self.REASON_STEP: + return "-" + + return str(reason) + def PrintRect(self, rectangle): """debuging tool Print board in ascii art """ - for y in range(rectangle[0].__len__()): - for x in range(rectangle.__len__()): - print("%X" % rectangle[x][y], end='') - print() - print() - - def CleanupFootprint(self): - """ - cleanup temp footprint - """ - if self.tmp_dir and os.path.isdir(self.tmp_dir): - shutil.rmtree(self.tmp_dir) + print("_" * (len(rectangle)+2)) + for y in range(len(rectangle[0])): + print("|", end='') + for x in range(len(rectangle)): + print("%s" % self.GetReasonSymbol(rectangle[x][y]), end='') + print("|") + print("_" * (len(rectangle)+2)) + print(''' +OK = 'X' +NO_SIGNAL = ' ' +OTHER_SIGNAL = 'O' +KEEPOUT = 'K' +TRACK = 'T' +PAD = 'P' +DRAWING = 'D' +STEP = '-' +''') def AddVia(self, position, x, y): m = VIA(self.pcb) @@ -186,116 +232,192 @@ class FillArea: if not area.GetIsKeepout(): area.BuildFilledSolidAreasPolygons(self.pcb) + def CheckViaInAllAreas(self, via, all_areas): + ''' + Checks if an existing Via collides with another area + ''' + # Enum all area + for area in all_areas: + area_layer = area.GetLayer() + area_clearance = area.GetClearance() + area_priority = area.GetPriority() + is_keepout_area = area.GetIsKeepout() + is_target_net = (area.GetNetname().upper() == self.netname) + + if (not is_target_net): # Only process areas that are not in the target net + offset = max(self.clearance, area_clearance) + self.size / 2 # Offset is half the size of the via plus the clearance of the via or the area + for dx in [-offset, offset]: + for dy in [-offset, offset]: # All 4 corners of the via are testet (upper, lower, left, right) but not the center + point_to_test = wxPoint(via.PosX + dx, via.PosY + dy) + + hit_test_area = area.HitTestFilledArea(point_to_test) # Collides with a filled area + hit_test_edge = area.HitTestForEdge(point_to_test) # Collides with an edge/corner + hit_test_zone = area.HitTestInsideZone(point_to_test) # Is inside a zone (e.g. KeepOut) + + if is_keepout_area and (hit_test_area or hit_test_edge or hit_test_zone): + return self.REASON_KEEPOUT # Collides with keepout + + elif (hit_test_area or hit_test_edge): + return self.REASON_OTHER_SIGNAL # Collides with another signal (e.g. on another layer) + + elif hit_test_zone: + # Check if the zone is higher priority than other zones of the target net in the same point + target_areas_on_same_layer = filter(lambda x: ((x.GetPriority() > area_priority) and (x.GetLayer() == area_layer) and (x.GetNetname().upper() == self.netname)), all_areas) + for area_with_higher_priority in target_areas_on_same_layer: + if area_with_higher_priority.HitTestInsideZone(point_to_test): + break # Area of target net has higher priority on this layer + else: + return self.REASON_OTHER_SIGNAL # Collides with another signal (e.g. on another layer) + + return self.REASON_OK + + def ClearViaInStepSize(self, rectangle, x, y, distance): + ''' + Stepsize==0 + O O O O O O O O O + O O O O O O O O O + O O O O O O O O O + O O O O O O O O O + O O O O O O O O O + O O O O O O O O O + O O O O O O O O O + + Standard + O O O O O + + O O O O O + + O O O O O + + O O O O O + + Star + O O O O O + O O O O + O O O O O + O O O O + O O O O O + O O O O + O O O O O + ''' + for x_pos in range(x-distance, x+distance+1): + if (x_pos >= 0) and (x_pos < len(rectangle)): + distance_y = distance-abs(x-x_pos) if self.star else distance # Star or Standard shape + for y_pos in range(y-distance_y, y+distance_y+1): + if (y_pos >= 0) and (y_pos < len(rectangle[0])): + if (x_pos == x) and (y_pos == y): + continue + rectangle[x_pos][y_pos] = self.REASON_STEP + def Run(self): """ Launch the process """ if self.delete_vias: - for via in self.pcb.GetTracks(): + target_tracks = filter(lambda x: (x.GetNetname().upper() == self.netname), self.pcb.GetTracks()) + for via in target_tracks: if via.Type() == PCB_VIA_T: if via.GetTimeStamp() == 33: - if self.debug: - print("* Deleting via %u" % via.GetTimeStamp()) self.pcb.RemoveNative(via) self.RefillBoardAreas() - return # no need to run the rest of logic + return # no need to run the rest of logic - lboard = self.pcb.ComputeBoundingBox() - rectangle = [] + lboard = self.pcb.ComputeBoundingBox(True) origin = lboard.GetPosition() - # Create an initial rectangle: all is off + # Create an initial rectangle: all is set to "REASON_NO_SIGNAL" # get a margin to avoid out of range - # Values: - # 0 => position is ok for via - # != 0 => position is not ok. - # Number is for debuging: check what feature is disabling this position - l_clearance = self.step + self.clearance + self.size - x_limit = int((lboard.GetWidth() + l_clearance) / self.step) + 1 - y_limit = int((lboard.GetHeight() + l_clearance) / self.step) + 1 - for x in range(0, x_limit): - rectangle.append([]) - for y in range(0, y_limit): - rectangle[x].append(0x8) + l_clearance = self.clearance + self.size + x_limit = int((lboard.GetWidth() + l_clearance) / l_clearance) + 1 + y_limit = int((lboard.GetHeight() + l_clearance) / l_clearance) + 1 + + rectangle = [[self.REASON_NO_SIGNAL]*y_limit for i in xrange(x_limit)] - if self.debug: - print("Initial rectangle") - self.PrintRect(rectangle) + all_pads = self.pcb.GetPads() + all_tracks = self.pcb.GetTracks() + all_drawings = filter(lambda x: x.GetClass() == 'PTEXT' and self.pcb.GetLayerID(x.GetLayerName()) in (F_Cu, B_Cu), self.pcb.DrawingsList()) + all_areas = [self.pcb.GetArea(i) for i in xrange(self.pcb.GetAreaCount())] + target_areas = filter(lambda x: (x.GetNetname().upper() == self.netname), all_areas) # KeepOuts are filtered because they have no name - # Enum all area - for i in range(self.pcb.GetAreaCount()): - area = self.pcb.GetArea(i) - # If use only selected area mode - # only_selected_area | is selected | keepout | result - # False | X | X | True - # True | True | X | True - # True | False | True | True - # True | False | False | False - if not (self.only_selected_area and not area.IsSelected() and not area.GetIsKeepout()): - # Handle only area on same target net of keepout are - if area.GetNetname() == self.netname or area.GetIsKeepout(): - keepOutMode = area.GetIsKeepout() - for y in range(rectangle[0].__len__()): - for x in range(rectangle.__len__()): - testResult = not keepOutMode # = False if is Keepout - offset = (self.clearance) + self.size / 2 - # For keepout area: Deny Via - # For same net area: Allow if not denied by keepout - current_x = origin.x + (x * self.step) - current_y = origin.y + (y * self.step) + via_list = [] # Create a list of existing vias => faster than scanning through the whole rectangle + max_target_area_clearance = 0 + + # Enum all target areas (Search possible positions for vias on the target net) + for area in target_areas: + print ("Processing Target Area: %s, LayerName: %s..." % (area.GetNetname(), area.GetLayerName())) + + is_selected_area = area.IsSelected() + area_clearance = area.GetClearance() + if max_target_area_clearance < area_clearance: + max_target_area_clearance = area_clearance + + if (not self.only_selected_area) or (self.only_selected_area and is_selected_area): # All areas or only the selected area + for x in xrange(len(rectangle)): # Check every possible point in the virtual coordinate system + for y in xrange(len(rectangle[0])): + if rectangle[x][y] == self.REASON_NO_SIGNAL: # No other "target area" found yet => go on with processing + current_x = origin.x + (x * l_clearance) # Center of the via + current_y = origin.y + (y * l_clearance) + + test_result = True # Start with true, if a check fails, it is set to false + + offset = max(self.clearance, area_clearance) + self.size / 2 # Offset is half the size of the via plus the clearance of the via or the area for dx in [-offset, offset]: - for dy in [-offset, offset]: - point_to_test = wxPoint(current_x + dx, - current_y + dy) - r = area.HitTestInsideZone(point_to_test) - t = area.HitTestForEdge(point_to_test) - r = r and not t - if keepOutMode: - testResult |= r - else: - testResult &= r - - if testResult: - if keepOutMode: - rectangle[x][y] = 0x1 - else: - # Allow only if it's first step disabling - # ie: keepout are keeped - if rectangle[x][y] == 0x8: - rectangle[x][y] = 0 + for dy in [-offset, offset]: # All 4 corners of the via are testet (upper, lower, left, right) but not the center + point_to_test = wxPoint(current_x + dx, current_y + dy) + hit_test_area = area.HitTestFilledArea(point_to_test) # Collides with a filled area + hit_test_edge = area.HitTestForEdge(point_to_test) # Collides with an edge/corner + + test_result &= (hit_test_area and not hit_test_edge) # test_result only remains true if the via is inside an area and not on an edge + if test_result: + via_obj = ViaObject(x=x, y=y, pos_x=current_x, pos_y=current_y) # Create a via object with information about the via and place it in the rectangle + rectangle[x][y] = via_obj + via_list.append(via_obj) + if self.debug: - print("Post Area handling") + print("\nPost target areas:") self.PrintRect(rectangle) + + # Enum all vias + print ("Processing all vias of target area...") + for via in via_list: + reason = self.CheckViaInAllAreas(via, all_areas) + if reason != self.REASON_OK: + rectangle[via.X][via.Y] = reason + + if self.debug: + print("\nPost areas:") + self.PrintRect(rectangle) + + # Same job with all pads => all pads on all layers + print ("Processing all pads...") + for pad in all_pads: + local_offset = max(pad.GetClearance(), self.clearance, max_target_area_clearance) + (self.size / 2) + max_size = max(pad.GetSize().x, pad.GetSize().y) + + start_x = int(floor(((pad.GetPosition().x - (max_size / 2.0 + local_offset)) - origin.x) / l_clearance)) + stop_x = int(ceil(((pad.GetPosition().x + (max_size / 2.0 + local_offset)) - origin.x) / l_clearance)) - # Same job with all pads - for pad in self.pcb.GetPads(): - local_offset = max(pad.GetClearance(), - self.clearance) + self.size / 2 - max_size = max(pad.GetSize().x, pad.GetSize().y) - start_x = int(floor(((pad.GetPosition().x - (max_size / 2.0 + - local_offset)) - origin.x) / self.step)) - stop_x = int(ceil(((pad.GetPosition().x + (max_size / 2.0 + - local_offset)) - origin.x) / self.step)) - - start_y = int(floor(((pad.GetPosition().y - (max_size / 2.0 + - local_offset)) - origin.y) / self.step)) - stop_y = int(ceil(((pad.GetPosition().y + (max_size / 2.0 + - local_offset)) - origin.y) / self.step)) + start_y = int(floor(((pad.GetPosition().y - (max_size / 2.0 + local_offset)) - origin.y) / l_clearance)) + stop_y = int(ceil(((pad.GetPosition().y + (max_size / 2.0 + local_offset)) - origin.y) / l_clearance)) for x in range(start_x, stop_x + 1): for y in range(start_y, stop_y + 1): - start_rect = wxPoint(origin.x + (self.step * x) - - local_offset, - origin.y + (self.step * y) - - local_offset) - size_rect = wxSize(2 * local_offset, 2 * local_offset) - if pad.HitTest(EDA_RECT(start_rect, size_rect), False): - rectangle[x][y] |= 0x2 - - # Same job with tracks - for track in self.pcb.GetTracks(): + if isinstance(rectangle[x][y], ViaObject): + start_rect = wxPoint(origin.x + (l_clearance * x) - local_offset, + origin.y + (l_clearance * y) - local_offset) + size_rect = wxSize(2 * local_offset, 2 * local_offset) + if pad.HitTest(EDA_RECT(start_rect, size_rect), False): + rectangle[x][y] = self.REASON_PAD + + if self.debug: + print("\nPost pads:") + self.PrintRect(rectangle) + + # Same job with tracks => all tracks on all layers + print ("Processing all tracks...") + for track in all_tracks: start_x = track.GetStart().x start_y = track.GetStart().y @@ -317,89 +439,77 @@ class FillArea: opx = stop_x opy = stop_y - clearance = max(track.GetClearance(), self.clearance) + \ - self.size / 2 + track.GetWidth() / 2 + clearance = max(track.GetClearance(), self.clearance, max_target_area_clearance) + (self.size / 2) + (track.GetWidth() / 2) + + start_x = int(floor(((start_x - clearance) - origin.x) / l_clearance)) + stop_x = int(ceil(((stop_x + clearance) - origin.x) / l_clearance)) - start_x = int(floor(((start_x - clearance) - - origin.x) / self.step)) - stop_x = int(ceil(((stop_x + clearance) - origin.x) / self.step)) - - start_y = int(floor(((start_y - clearance) - - origin.y) / self.step)) - stop_y = int(ceil(((stop_y + clearance) - origin.y) / self.step)) + start_y = int(floor(((start_y - clearance) - origin.y) / l_clearance)) + stop_y = int(ceil(((stop_y + clearance) - origin.y) / l_clearance)) for x in range(start_x, stop_x + 1): for y in range(start_y, stop_y + 1): - start_rect = wxPoint(origin.x + (self.step * x) - - clearance, - origin.y + (self.step * y) - - clearance) - size_rect = wxSize(2 * clearance, 2 * clearance) - if track.HitTest(EDA_RECT(start_rect, size_rect), False): - rectangle[x][y] |= 0x4 - + if isinstance(rectangle[x][y], ViaObject): + start_rect = wxPoint(origin.x + (l_clearance * x) - clearance, + origin.y + (l_clearance * y) - clearance) + size_rect = wxSize(2 * clearance, 2 * clearance) + if track.HitTest(EDA_RECT(start_rect, size_rect), False): + rectangle[x][y] = self.REASON_TRACK + if self.debug: - print("Post Pad + tracks") + print("\nPost tracks:") self.PrintRect(rectangle) - + # Same job with existing text - for draw in self.pcb.DrawingsList(): - if (draw.GetClass() == 'PTEXT' and - self.pcb.GetLayerID(draw.GetLayerName()) in (F_Cu, B_Cu)): - inter = float(self.clearance + self.size) - bbox = draw.GetBoundingBox() - start_x = int(floor(((bbox.GetPosition().x - inter) - - origin.x) / self.step)) - stop_x = int(ceil(((bbox.GetPosition().x + - (bbox.GetSize().x + inter)) - - origin.x) / self.step)) + print ("Processing all existing drawings...") + for draw in all_drawings: + inter = float(self.clearance + self.size) + bbox = draw.GetBoundingBox() + + start_x = int(floor(((bbox.GetPosition().x - inter) - origin.x) / l_clearance)) + stop_x = int(ceil(((bbox.GetPosition().x + (bbox.GetSize().x + inter)) - origin.x) / l_clearance)) - start_y = int(floor(((bbox.GetPosition().y - inter) - - origin.y) / self.step)) - stop_y = int(ceil(((bbox.GetPosition().y + - (bbox.GetSize().y + inter)) - - origin.y) / self.step)) - - for x in range(start_x, stop_x + 1): - for y in range(start_y, stop_y + 1): - rectangle[x][y] |= 0xA + start_y = int(floor(((bbox.GetPosition().y - inter) - origin.y) / l_clearance)) + stop_y = int(ceil(((bbox.GetPosition().y + (bbox.GetSize().y + inter)) - origin.y) / l_clearance)) + for x in range(start_x, stop_x + 1): + for y in range(start_y, stop_y + 1): + rectangle[x][y] = self.REASON_DRAWING + if self.debug: - print("Post Drawnings: final result (0: mean ok for a via)") + print("Post Drawnings:") self.PrintRect(rectangle) - - for y in range(rectangle[0].__len__()): - for x in range(rectangle.__len__()): - if rectangle[x][y] == 0: - ran_x = 0 - ran_y = 0 + + print ("Remove vias to guarantee step size...") + clear_distance = 0 + if self.step != 0.0: + clear_distance = int((self.step+l_clearance) / l_clearance) # How much "via steps" should be removed around a via (round up) + + for x in xrange(len(rectangle)): + for y in xrange(len(rectangle[0])): + if isinstance(rectangle[x][y], ViaObject): + if clear_distance: + self.ClearViaInStepSize(rectangle, x, y, clear_distance) + + via = rectangle[x][y] + ran_x = 0 + ran_y = 0 + if self.random: - ran_x = (random.random() * self.step / 2.0) - \ - (self.step / 4.0) - ran_y = (random.random() * self.step / 2.0) - \ - (self.step / 4.0) - if self.star: - if y % 2: - if ((x + 1) % 2): - self.AddVia( - wxPoint(origin.x + (self.step * x) + ran_x, - origin.y + (self.step * y) + ran_y), x, y) - else: - if (x % 2): - self.AddVia( - wxPoint(origin.x + (self.step * x) + ran_x, - origin.y + (self.step * y) + ran_y), x, y) - else: - self.AddVia( - wxPoint(origin.x + (self.step * x) + ran_x, - origin.y + (self.step * y) + ran_y), x, y) + ran_x = (random.random() * l_clearance / 2.0) - (l_clearance / 4.0) + ran_y = (random.random() * l_clearance / 2.0) - (l_clearance / 4.0) + + self.AddVia(wxPoint(via.PosX + ran_x, via.PosY + ran_y), via.X, via.Y) + + if self.debug: + print("\nFinal result:") + self.PrintRect(rectangle) self.RefillBoardAreas() if self.filename: self.pcb.Save(self.filename) - self.CleanupFootprint() - + print ("Done!") if __name__ == '__main__': if len(sys.argv) < 2: diff --git a/ViaStitching/FillAreaAction.py b/ViaStitching/FillAreaAction.py index 8068745..bf26bfb 100644 --- a/ViaStitching/FillAreaAction.py +++ b/ViaStitching/FillAreaAction.py @@ -41,19 +41,18 @@ class FillAreaAction(pcbnew.ActionPlugin): a.m_SizeMM.SetValue("0.46") a.m_StepMM.SetValue("2.54") a.m_DrillMM.SetValue("0.2") - a.m_Netname.SetValue("auto") + a.m_Netname.SetValue("GND") a.m_ClearanceMM.SetValue("0.2") a.m_Star.SetValue(True) modal_result = a.ShowModal() if modal_result == wx.ID_OK: - fill = FillArea.FillArea() try: + fill = FillArea.FillArea() fill.SetStepMM(float(a.m_StepMM.GetValue())) fill.SetSizeMM(float(a.m_SizeMM.GetValue())) fill.SetDrillMM(float(a.m_DrillMM.GetValue())) fill.SetClearanceMM(float(a.m_ClearanceMM.GetValue())) - if a.m_Netname.GetValue() != "auto": - fill.SetNetname(a.m_Netname.GetValue()) + fill.SetNetname(a.m_Netname.GetValue()) if a.m_Debug.IsChecked(): fill.SetDebug() if a.m_Random.IsChecked(): @@ -68,6 +67,7 @@ class FillAreaAction(pcbnew.ActionPlugin): elif modal_result == wx.ID_DELETE: try: fill = FillArea.FillArea() + fill.SetNetname(a.m_Netname.GetValue()) if a.m_Debug.IsChecked(): fill.SetDebug() fill.DeleteVias()