# -*- coding: utf-8 -*- # replicate_layout.py # # Copyright (C) 2019-2022 Mitja Nemec # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import pcbnew from collections import namedtuple from collections import defaultdict import os import logging import itertools import math from difflib import SequenceMatcher try: from .remove_duplicates import remove_duplicates except: from remove_duplicates import remove_duplicates Footprint = namedtuple('Footprint', ['ref', 'fp', 'fp_id', 'sheet_id', 'filename']) logger = logging.getLogger(__name__) Settings = namedtuple('Settings', ['rep_tracks', 'rep_zones', 'rep_text', 'rep_drawings', 'group_layouts', 'group_footprints', 'group_tracks', 'group_zones', 'group_text', 'group_drawings', 'rep_locked_tracks', 'rep_locked_zones', 'rep_locked_text', 'rep_locked_drawings', 'intersecting', 'group_items', 'group_only', 'locked_fps', 'remove'], defaults=[True, True, True, True, False, False, False, False, False, False, True, True, True, True, False, False, False, False, False]) def rotate_around_center(coordinates, angle): """ rotate coordinates for a defined angle in degrees around coordinate center""" new_x = coordinates[0] * math.cos(2 * math.pi * angle / 360) \ - coordinates[1] * math.sin(2 * math.pi * angle / 360) new_y = coordinates[0] * math.sin(2 * math.pi * angle / 360) \ + coordinates[1] * math.cos(2 * math.pi * angle / 360) return int(new_x), int(new_y) def rotate_around_point(old_position, point, angle): """ rotate coordinates for a defined angle in degrees around a point """ # get relative position to point rel_x = old_position[0] - point[0] rel_y = old_position[1] - point[1] # rotate around new_rel_x, new_rel_y = rotate_around_center((rel_x, rel_y), angle) # get absolute position new_position = (new_rel_x + point[0], new_rel_y + point[1]) return new_position def get_index_of_tuple(list_of_tuples, index, value): for pos, t in enumerate(list_of_tuples): if t[index] == value: return pos def update_progress(stage, percentage, message=None): if message is not None: print(message) print(percentage) def flipped_angle(angle): if angle > 0: return 180 - angle else: return -180 - angle class Replicator: def __init__(self, board, src_anchor_fp_ref, update_func=update_progress): self.board = board self.stage = 1 self.max_stages = 0 self.update_progress = update_func self.level = None self.settings = Settings self.src_anchor_fp = None self.src_anchor_fp_group = None self.replicate_locked_footprints = None self.src_sheet = None self.dst_sheets = [] self.dst_groups = [] self.src_footprints = [] self.other_footprints = [] self.src_bounding_box = None self.src_tracks = [] self.src_zones = [] self.src_text = [] self.src_drawings = [] self.connectivity_issues = set() self.pcb_filename = os.path.abspath(board.GetFileName()) self.sch_filename = self.pcb_filename.replace(".kicad_pcb", ".kicad_sch") self.project_folder = os.path.dirname(self.pcb_filename) # construct a list of footprints with all pertinent data logger.info('getting a list of all footprints on board') footprints = board.GetFootprints() self.footprints = [] # get dict_of_sheets from layout data only (through footprint Sheetfile and Sheetname properties) self.dict_of_sheets = {} unique_sheet_ids = set() for fp in footprints: # construct a set of unique sheets from footprint properties path = fp.GetPath().AsString().upper().replace('00000000-0000-0000-0000-0000', '').split("/") sheet_path = path[0:-1] for x in sheet_path: unique_sheet_ids.add(x) sheet_id = self.get_sheet_id(fp) try: sheet_file = fp.GetProperty('Sheetfile') sheet_name = fp.GetProperty('Sheetname') except KeyError: logger.info("Footprint " + fp.GetReference() + " does not have Sheetfile property, it will not be replicated." " Most likely it is only in layout") continue # footprint is in the schematics and has Sheetfile property if sheet_file and sheet_id: self.dict_of_sheets[sheet_id] = [sheet_name, sheet_file] # footprint is in the schematics but has empty Sheetfile properties elif sheet_id: logger.info("Footprint " + fp.GetReference() + " has empty Sheetfile property") raise LookupError("Footprint " + str( fp.GetReference()) + " has empty Sheetfile and Sheetname properties. " "You need to update the layout from schematics") # footprint is on root level else: logger.debug("Footprint " + fp.GetReference() + " on root level") continue # catch corner cases with nested hierarchy, where some hierarchical pages don't have any footprints unique_sheet_ids.remove("") if len(unique_sheet_ids) > len(self.dict_of_sheets): # open root schematics file and parse for other schematics files # This might be prone to errors regarding path discovery # thus it is used only in corner cases schematic_found = {} self.parse_schematic_files(self.sch_filename, schematic_found) self.dict_of_sheets = schematic_found # construct a list of all the footprints for fp in footprints: try: sheet_file = fp.GetProperty('Sheetfile') sheet_name = fp.GetProperty('Sheetname') fp_tuple = Footprint(fp=fp, fp_id=self.get_footprint_id(fp), sheet_id=self.get_sheet_path(fp)[0], filename=self.get_sheet_path(fp)[1], ref=fp.GetReference()) self.footprints.append(fp_tuple) except KeyError: pass # find anchor footprint and it's group self.src_anchor_fp = self.get_fp_by_ref(src_anchor_fp_ref) if self.src_anchor_fp.fp.GetParentGroup(): self.src_anchor_fp_group = self.src_anchor_fp.fp.GetParentGroup().GetName() else: self.src_anchor_fp_group = None # TODO check if there is any other footprint with same ID as anchor footprint # get net-dict # you get the netcode by self.netdict.GetNetItem("netname") self.netdict = self.board.GetNetInfo() def parse_schematic_files(self, filename, dict_of_sheets): with open(filename, encoding='utf-8') as f: contents = f.read().split("\n") filename_dir = os.path.dirname(filename) # find (sheet (at and then look in next few lines for new schematics file for i in range(len(contents)): line = contents[i] if "(sheet (at" in line: sheetname = "" sheetfile = "" sheet_id = "" sn_found = False sf_found = False for j in range(i,i+10): if "(uuid " in contents[j]: path = contents[j].replace("(uuid ", '').rstrip(")").upper().strip() sheet_id = path.replace('00000000-0000-0000-0000-0000', '') if "(property \"Sheet name\"" in contents[j] or "(property \"Sheetname\"" in contents[j]: if "(property \"Sheet name\"" in contents[j]: sheetname = contents[j].replace("(property \"Sheet name\"", '').split("(")[0].replace("\"", "").strip() sn_found = True if "(property \"Sheetname\"" in contents[j]: sheetname = contents[j].replace("(property \"Sheetname\"", '').split("(")[0].replace("\"", "").strip() sn_found = True if "(property \"Sheet file\"" in contents[j] or "(property \"Sheetfile\"" in contents[j]: if "(property \"Sheet file\"" in contents[j]: sheetfile = contents[j].replace("(property \"Sheet file\"", '').split("(")[0].replace("\"", "").strip() sf_found = True if "(property \"Sheetfile\"" in contents[j]: sheetfile = contents[j].replace("(property \"Sheetfile\"", '').split("(")[0].replace("\"", "").strip() sf_found = True # properly handle property not found if not sn_found or not sf_found: logger.info(f'Did not found sheetfile and/or sheetname properties in the schematic file ' f'in {filename} line:{str(i)}') raise LookupError(f'Did not found sheetfile and/or sheetname properties in the schematic file ' f'in {filename} line:{str(i)}. Unsupported schematics file format') sheetfilepath = os.path.join(filename_dir, sheetfile) # here I should find all sheet data dict_of_sheets[sheet_id] = [sheetname, sheetfilepath] # test if newfound file can be opened if not os.path.exists(sheetfilepath): raise LookupError(f'File {sheetfilepath} does not exists. This is either due to error in parsing' f' schematics files, missing schematics file or an error within the schematics') # open a newfound file and look for nested sheets self.parse_schematic_files(sheetfilepath, dict_of_sheets) return def replicate_layout(self, src_anchor_fp, level, dst_sheets, settings, rm_duplicates): logger.info("Starting replication of sheets: " + repr(dst_sheets) + "\non level: " + repr(level) + "\nwith tracks=" + repr(settings.rep_tracks) + ", zone=" + repr(settings.rep_zones) + ", text=" + repr(settings.rep_text) + ", text=" + repr(settings.rep_drawings) + ", intersecting=" + repr(settings.intersecting) + ", remove=" + repr(settings.remove) + ", locked footprints=" + repr(settings.locked_fps) + ", group_only=" + repr(settings.group_only)) self.level = level self.src_anchor_fp = src_anchor_fp self.dst_sheets = dst_sheets self.replicate_locked_footprints = settings.locked_fps self.src_sheet = level if settings.remove: self.max_stages = 2 else: self.max_stages = 0 if settings.rep_tracks: self.max_stages = self.max_stages + 1 if settings.rep_zones: self.max_stages = self.max_stages + 1 if settings.rep_text: self.max_stages = self.max_stages + 1 if settings.rep_drawings: self.max_stages = self.max_stages + 1 if rm_duplicates: self.max_stages = self.max_stages + 1 self.update_progress(self.stage, 0.0, "Preparing for replication") self.prepare_for_replication(level, settings) if settings.remove: logger.info("Removing tracks and zones, before footprint placement") self.stage = 2 self.update_progress(self.stage, 0.0, "Removing zones and tracks") self.remove_zones_tracks(settings.intersecting) self.stage = 3 self.update_progress(self.stage, 0.0, "Replicating footprints") self.replicate_footprints(settings) if settings.remove: logger.info("Removing tracks and zones, after footprint placement") self.stage = 4 self.update_progress(self.stage, 0.0, "Removing zones and tracks") self.remove_zones_tracks(settings.intersecting) if settings.rep_tracks: self.stage = 5 self.update_progress(self.stage, 0.0, "Replicating tracks") self.replicate_tracks(settings) if settings.rep_zones: self.stage = 6 self.update_progress(self.stage, 0.0, "Replicating zones") self.replicate_zones(settings) if settings.rep_text: self.stage = 7 self.update_progress(self.stage, 0.0, "Replicating text") self.replicate_text(settings) if settings.rep_drawings: self.stage = 8 self.update_progress(self.stage, 0.0, "Replicating drawings") self.replicate_drawings(settings) if rm_duplicates: self.stage = 9 self.update_progress(self.stage, 0.0, "Removing duplicates") self.removing_duplicates() # finally at the end refill the zones filler = pcbnew.ZONE_FILLER(self.board) filler.Fill(self.board.Zones()) def prepare_for_replication(self, level, settings): # get a list of source footprints for replication logger.info("Getting the list of source footprints") self.update_progress(self.stage, 0 / 8, None) # if needed filter them by group anchor_sheet_footprints = self.get_footprints_on_sheet(level) self.src_bounding_box = self.get_footprints_bounding_box(anchor_sheet_footprints) self.src_footprints = self.get_footprints_for_replication(level, self.src_bounding_box, settings) excluded_footprints = [fp for fp in anchor_sheet_footprints if fp not in self.src_footprints] # get the rest of the footprints logger.info("Getting the list of all the remaining footprints") self.update_progress(self.stage, 1 / 6, None) self.other_footprints = self.get_footprints_not_on_sheet(level) self.other_footprints.extend(excluded_footprints) # TODO we might need to recalculate bounding box - if so, this has to be ported to highlighting code # get source tracks logger.info("Getting source tracks") self.update_progress(self.stage, 2 / 6, None) self.src_tracks = self.get_tracks_for_replication(level, self.src_bounding_box, settings) # get source zones logger.info("Getting source zones") self.update_progress(self.stage, 3 / 6, None) self.src_zones = self.get_zones_for_replication(level, self.src_bounding_box, settings) # get source text items logger.info("Getting source text items") self.update_progress(self.stage, 4 / 6, None) self.src_text = self.get_text_for_replication(self.src_bounding_box, settings) # get source drawings logger.info("Getting source drawing items") self.update_progress(self.stage, 5 / 6, None) # if needed filter them by group self.src_drawings = self.get_drawings_for_replication(self.src_bounding_box, settings) # create groups for each destination layout if selected if settings.group_layouts: for sheet in self.dst_sheets: dst_group_name = "Replicated Group {}".format(sheet) dst_group = pcbnew.PCB_GROUP(None) dst_group.SetName(dst_group_name) self.board.Add(dst_group) # store destination lauouts' groups self.dst_groups.append(dst_group) @staticmethod def get_footprint_id(footprint): path = footprint.GetPath().AsString().upper().replace('00000000-0000-0000-0000-0000', '').split("/") if len(path) != 1: fp_id = path[-1] # if path is empty, then footprint is not part of schematics else: fp_id = None return fp_id @staticmethod def get_sheet_id(footprint): path = footprint.GetPath().AsString().upper().replace('00000000-0000-0000-0000-0000', '').split("/") if len(path) != 1: sheet_id = path[-2] # if path is empty, then footprint is not part of schematics else: sheet_id = None return sheet_id def get_sheet_path(self, footprint): """ get sheet id """ path = footprint.GetPath().AsString().upper().replace('00000000-0000-0000-0000-0000', '').split("/") if len(path) != 1: sheet_path = path[0:-1] sheet_names = [self.dict_of_sheets[x][0] for x in sheet_path if x in self.dict_of_sheets] sheet_files = [self.dict_of_sheets[x][1] for x in sheet_path if x in self.dict_of_sheets] sheet_path = [sheet_names, sheet_files] else: sheet_path = ["", ""] return sheet_path def get_fp_by_ref(self, ref): for fp in self.footprints: if fp.ref == ref: return fp return None def get_list_of_footprints_with_same_id(self, fp_id): footprints_with_same_id = [] for fp in self.footprints: if fp.fp_id == fp_id: footprints_with_same_id.append(fp) return footprints_with_same_id def get_sheets_to_replicate(self, reference_footprint, level): sheet_id = reference_footprint.sheet_id sheet_file = reference_footprint.filename # find level_id level_file = sheet_file[sheet_id.index(level)] logger.info('constructing a list of sheets suitable for replication on level:' + repr(level) + ", file:" + repr(level_file)) # construct complete hierarchy path up to the level of reference footprint sheet_id_up_to_level = [] for i in range(len(sheet_id)): sheet_id_up_to_level.append(sheet_id[i]) if sheet_id[i] == level: break # get all footprints with same ID footprints_with_same_id = self.get_list_of_footprints_with_same_id(reference_footprint.fp_id) # if hierarchy is deeper, match only the sheets with same hierarchy from root to -1 sheets_on_same_level = [] # go through all the footprints for fp in footprints_with_same_id: # if the footprint is on selected level, it's sheet is added to the list of sheets on this level if level_file in fp.filename: sheet_id_list = [] # create a hierarchy path only up to the level for i in range(len(fp.filename)): sheet_id_list.append(fp.sheet_id[i]) if fp.filename[i] == level_file: break sheets_on_same_level.append(sheet_id_list) # remove duplicates sheets_on_same_level.sort() sheets_on_same_level = list(k for k, _ in itertools.groupby(sheets_on_same_level)) # remove the sheet path for reference footprint for sheet in sheets_on_same_level: if sheet == sheet_id_up_to_level: index = sheets_on_same_level.index(sheet) del sheets_on_same_level[index] break logger.info("suitable sheets are:" + repr(sheets_on_same_level)) return sheets_on_same_level def get_footprints_on_sheet(self, level): footprints_on_sheet = [] level_depth = len(level) for fp in self.footprints: if level == fp.sheet_id[0:level_depth]: footprints_on_sheet.append(fp) return footprints_on_sheet @staticmethod def filter_items_by_group(items, group): items_in_group = [] for item in items: item_group = item.GetParentGroup() if item_group and group == item_group.GetName(): items_in_group.append(item) return items_in_group @staticmethod def filter_footprints_by_group(footprints, group): items_in_group = [] for fp in footprints: fp_group = fp.fp.GetParentGroup() if hasattr(fp_group, 'GetName'): if group and fp_group.GetName(): items_in_group.append(fp) return items_in_group def get_footprints_not_on_sheet(self, level): footprints_not_on_sheet = [] level_depth = len(level) for fp in self.footprints: if level != fp.sheet_id[0:level_depth]: footprints_not_on_sheet.append(fp) return footprints_not_on_sheet @staticmethod def get_nets_from_footprints(footprints): # go through all footprints and their pads and get the nets they are connected to nets = [] for fp in footprints: # get their pads pads = fp.fp.Pads() # get net for pad in pads: nets.append(pad.GetNetname()) # remove duplicates nets_clean = [] for i in nets: if i not in nets_clean: nets_clean.append(i) return nets_clean def get_local_nets(self, src_footprints, other_footprints): # get nets other footprints are connected to other_nets = self.get_nets_from_footprints(other_footprints) # get nets only source footprints are connected to src_nets = self.get_nets_from_footprints(src_footprints) src_local_nets = [] for net in src_nets: if net not in other_nets: src_local_nets.append(net) return src_local_nets @staticmethod def get_footprints_bounding_box(footprints): # get first footprint bounding box bounding_box = footprints[0].fp.GetBoundingBox(False, False) top = bounding_box.GetTop() bottom = bounding_box.GetBottom() left = bounding_box.GetLeft() right = bounding_box.GetRight() # iterate through the rest of the footprints and resize bounding box accordingly for fp in footprints: fp_box = fp.fp.GetBoundingBox(False, False) top = min(top, fp_box.GetTop()) bottom = max(bottom, fp_box.GetBottom()) left = min(left, fp_box.GetLeft()) right = max(right, fp_box.GetRight()) position = pcbnew.VECTOR2I(left, top) size = pcbnew.VECTOR2I(right - left, bottom - top) bounding_box = pcbnew.BOX2I(position, size) return bounding_box def get_tracks(self, bounding_box, containing, exclusive_nets=None): # get_all tracks if exclusive_nets is None: exclusive_nets = [] all_tracks = self.board.GetTracks() tracks = [] # keep only tracks that are within our bounding box for track in all_tracks: track_bb = track.GetBoundingBox() # if track is contained or intersecting the bounding box if (containing and bounding_box.Contains(track_bb)) or \ (not containing and bounding_box.Intersects(track_bb)): tracks.append(track) # even if track is not within the bounding box, but is on the completely local net else: # check if it on a local net if track.GetNetname() in exclusive_nets: # and add it to the tracks.append(track) return tracks def get_zones(self, bounding_box, containing, exclusive_nets=None): if exclusive_nets is None: exclusive_nets = [] # get all zones all_zones = [] for zone_id in range(self.board.GetAreaCount()): all_zones.append(self.board.GetArea(zone_id)) # find all zones which are within the bounding box zones = [] for zone in all_zones: zone_bb = zone.GetBoundingBox() if (containing and bounding_box.Contains(zone_bb)) or \ (not containing and bounding_box.Intersects(zone_bb)): zones.append(zone) # even if track is not within the bounding box, but is on the completely local net else: if zone.GetNetname() in exclusive_nets: # and add it to the zones.append(zone) return zones def get_text_items(self, bounding_box, containing, outside=False): # get all text objects in bounding box all_text = [] for drawing in self.board.GetDrawings(): if not isinstance(drawing, pcbnew.PCB_TEXT): continue if not outside: text_bb = drawing.GetBoundingBox() if containing: if bounding_box.Contains(text_bb): all_text.append(drawing) else: if bounding_box.Intersects(text_bb): all_text.append(drawing) else: text_bb = drawing.GetBoundingBox() if not bounding_box.Contains(text_bb): all_text.append(drawing) return all_text def get_drawings(self, bounding_box, containing, outside=False): # get all drawings in source bounding box all_drawings = [] for drawing in self.board.GetDrawings(): if isinstance(drawing, pcbnew.PCB_TEXT): # text items are handled separately continue if not outside: dwg_bb = drawing.GetBoundingBox() if containing: if bounding_box.Contains(dwg_bb): all_drawings.append(drawing) else: if bounding_box.Intersects(dwg_bb): all_drawings.append(drawing) else: dwg_bb = drawing.GetBoundingBox() if not bounding_box.Contains(dwg_bb): all_drawings.append(drawing) return all_drawings @staticmethod def get_footprint_text_items(footprint): """ get all text item belonging to a footprint """ list_of_items = [footprint.fp.Reference(), footprint.fp.Value()] footprint_items = footprint.fp.GraphicalItems() for item in footprint_items: if type(item) is pcbnew.FP_TEXT: list_of_items.append(item) return list_of_items def get_sheet_anchor_footprint(self, sheet): # get all footprints on this sheet sheet_footprints = self.get_footprints_on_sheet(sheet) # get anchor footprint list_of_possible_anchor_footprints = [] for fp in sheet_footprints: if fp.fp_id == self.src_anchor_fp.fp_id: list_of_possible_anchor_footprints.append(fp) # if there is only one if len(list_of_possible_anchor_footprints) == 1: sheet_anchor_fp = list_of_possible_anchor_footprints[0] # if there are more then one, we're dealing with multiple hierarchy # the correct one is the one who's path is the best match to the sheet path else: list_of_matches = [] for fp in list_of_possible_anchor_footprints: index = list_of_possible_anchor_footprints.index(fp) matches = 0 for item in self.src_anchor_fp.sheet_id: if item in fp.sheet_id: matches = matches + 1 list_of_matches.append((index, matches)) # select the one with most matches index, _ = max(list_of_matches, key=lambda x: x[1]) sheet_anchor_fp = list_of_possible_anchor_footprints[index] return sheet_anchor_fp def get_net_pairs(self, sheet): """ find all net pairs between source sheet and current sheet""" # find all footprints, pads and nets on this sheet sheet_footprints = self.get_footprints_on_sheet(sheet) # find all net pairs via same footprint pads, # first find footprint matches fp_matches = defaultdict(list) for d_fp in sheet_footprints: for s_fp in self.src_footprints: if d_fp.fp_id == s_fp.fp_id: fp_matches[s_fp.ref].append((s_fp, d_fp)) # from matching footprints find closest match if needed fp_pairs = defaultdict(list) for key, value in fp_matches.items(): matches = len(value) if matches == 0: raise LookupError("Could not find at least one matching footprint for: " + key + ".\nPlease make sure that schematics and layout are in sync.") if matches == 1: fp_pairs[key] = value[0] # if more than one match, get the most likely one # this is when replicating a sheet which consist of two or more identical subsheets (multiple hierachy) # the closest match is the one where most of the sheet_id matches if matches > 1: match_len = [] for match in value: match_len.append(len(set(match[0].sheet_id) & set(match[1].sheet_id))) index = match_len.index(max(match_len)) fp_pairs[key] = value[index] # For each pad pair get the net pair, and check if it makes sense connectivity_issues = [] net_pairs = [] for fp_ref, fp_pair in fp_pairs.items(): src_fp_pads = fp_pair[0].fp.Pads() dst_fp_pads = fp_pair[1].fp.Pads() # create a list of pads names and pads s_pads = [] d_pads = [] for pad in src_fp_pads: s_pads.append((pad.GetName(), pad)) for pad in dst_fp_pads: d_pads.append((pad.GetName(), pad)) # sort by pad names s_pads.sort(key=lambda tup: tup[0]) d_pads.sort(key=lambda tup: tup[0]) # add to dict fp_net_pairs = dict(zip([x[0] for x in d_pads] ,list(zip([x[1].GetNetname() for x in s_pads], [x[1].GetNetname() for x in d_pads])))) # go through all net pairs for pad_nr, net_pair in fp_net_pairs.items(): # if net names match if net_pair[0] == net_pair[1]: net_pairs.append(net_pair) continue # get netname depth src_net_path = net_pair[0].split("/") dst_net_path = net_pair[1].split("/") src_net_depth = len(src_net_path) dst_net_depth = len(dst_net_path) net_delta_depth = src_net_depth-dst_net_depth src_fp_depth = len(fp_pair[0].sheet_id) dst_fp_depth = len(fp_pair[1].sheet_id) fp_delta_depth = src_fp_depth - dst_fp_depth # if both nets are local, they should match if (src_net_depth == 1) and (dst_net_depth == 1): net_pairs.append(net_pair) continue # otherwise just look at the net name similarity. And if they are pretty similar be content match_level = self.find_match_level(src_net_path, dst_net_path) if match_level > 0.8: net_pairs.append(net_pair) continue # if I didn't find proper pair, append it anyway but addit to the list for reporting a warnning net_pairs.append(net_pair) logger.warning(f"Significant difference between src net: {src_net_path} and dst net: {dst_net_path}, " f"with src_net_depth={src_net_depth}, dst_net_depth={dst_net_depth}, " f"src_fp_depth={src_fp_depth}, dst_fp_depth={dst_fp_depth}, match level {match_level:.2f}") connectivity_issues.append((fp_pair[1].ref, pad_nr)) if connectivity_issues: """ report_string = "" for item in connectivity_issues: report_string = report_string + f"Footprint {item[0]}, pad {item[1]}\n" logger.info(f"Looks like the design has an exotic connectivity that is not supported by the plugin\n" f"Make sure that you check the connectivity around:\n" + report_string) """ self.connectivity_issues.update(connectivity_issues) # remove duplicates net_pairs_clean = list(set(net_pairs)) logger.info("Net pairs for sheet " + repr(sheet) + " :" + repr(net_pairs_clean)) return net_pairs_clean @staticmethod def find_match_level(netname_a, netname_b): len_nets_1 = len(netname_a) len_nets_2 = len(netname_b) # if both lengths are the same if len_nets_1 == len_nets_2: good_match_count = 0 for i in range(len_nets_1): for j in range(len_nets_2): a = netname_a[i] b = netname_b[j] match_ratio = SequenceMatcher(a=netname_a[i], b=netname_b[j]).ratio() good_match_count = good_match_count + match_ratio # normalize for the lenght return good_match_count / len_nets_1 # otherwise match all of the shortest ones with all of the longest ones else: good_match_count = 0 if len_nets_1 < len_nets_2: for i in range(len_nets_1): for j in range(len_nets_2): a = netname_a[i] b = netname_b[j] match_ratio = SequenceMatcher(a=netname_a[i], b=netname_b[j]).ratio() good_match_count = good_match_count + match_ratio return good_match_count / len_nets_1 else: for i in range(len_nets_2): for j in range(len_nets_1): a = netname_b[i] b = netname_a[j] match_ratio = SequenceMatcher(a=netname_b[i], b=netname_a[j]).ratio() good_match_count = good_match_count + match_ratio return good_match_count / len_nets_2 def replicate_footprints(self, settings): logger.info("Replicating footprints") nr_sheets = len(self.dst_sheets) for st_index in range(nr_sheets): sheet = self.dst_sheets[st_index] progress = st_index / nr_sheets self.update_progress(self.stage, progress, None) logger.info("Replicating footprints on sheet " + repr(sheet)) # get anchor footprint dst_anchor_fp = self.get_sheet_anchor_footprint(sheet) dst_anchor_fp_angle = dst_anchor_fp.fp.GetOrientationDegrees() dst_anchor_fp_position = dst_anchor_fp.fp.GetPosition() src_anchor_fp_angle = self.src_anchor_fp.fp.GetOrientationDegrees() anchor_delta_angle = src_anchor_fp_angle - dst_anchor_fp_angle # go through all footprints src_footprints = self.src_footprints dst_footprints = self.get_footprints_on_sheet(sheet) nr_footprints = len(src_footprints) for fp_index in range(nr_footprints): src_fp = src_footprints[fp_index] progress = progress + (1 / nr_sheets) * (1 / nr_footprints) self.update_progress(self.stage, progress, None) # find proper match in source footprints list_of_possible_dst_footprints = [] for d_fp in dst_footprints: if d_fp.fp_id == src_fp.fp_id: list_of_possible_dst_footprints.append(d_fp) # if there is more than one possible anchor, select the correct one if len(list_of_possible_dst_footprints) == 1: dst_fp = list_of_possible_dst_footprints[0] else: list_of_matches = [] for fp in list_of_possible_dst_footprints: index = list_of_possible_dst_footprints.index(fp) matches = 0 for item in src_fp.sheet_id: if item in fp.sheet_id: matches = matches + 1 list_of_matches.append((index, matches)) # check if list is empty, if it is, then it is highly likely that schematics and pcb are not in sync if not list_of_matches: raise LookupError("Can not find destination footprint for source footprint: " + repr(src_fp.ref) + "\n" + "Most likely, schematics and PCB are not in sync") # select the one with most matches index, _ = max(list_of_matches, key=lambda item: item[1]) dst_fp = list_of_possible_dst_footprints[index] # skip locked footprints if dst_fp.fp.IsLocked() is True and self.replicate_locked_footprints is False: continue # get footprint to clone position src_fp_orientation = src_fp.fp.GetOrientationDegrees() src_fp_pos = src_fp.fp.GetPosition() # get relative position with respect to source anchor src_anchor_pos = self.src_anchor_fp.fp.GetPosition() src_fp_flipped = src_fp.fp.IsFlipped() src_fp_delta_pos = src_fp_pos - src_anchor_pos # new orientation is simple new_orientation = src_fp_orientation - anchor_delta_angle old_pos = src_fp_delta_pos + dst_anchor_fp_position new_pos = rotate_around_point(old_pos, dst_anchor_fp_position, anchor_delta_angle) # convert to tuple of integers new_pos = [int(x) for x in new_pos] dst_fp_pos = pcbnew.VECTOR2I(*new_pos) # place current footprint - only if current footprint is not also the anchor if dst_fp.ref != dst_anchor_fp.ref: dst_fp.fp.SetPosition(dst_fp_pos) if dst_fp.fp.IsFlipped() != src_fp_flipped: dst_fp.fp.Flip(dst_fp.fp.GetPosition(), False) dst_fp.fp.SetOrientationDegrees(new_orientation) # Copy local settings. dst_fp.fp.SetLocalClearance(src_fp.fp.GetLocalClearance()) dst_fp.fp.SetLocalSolderMaskMargin(src_fp.fp.GetLocalSolderMaskMargin()) dst_fp.fp.SetLocalSolderPasteMargin(src_fp.fp.GetLocalSolderPasteMargin()) dst_fp.fp.SetLocalSolderPasteMarginRatio(src_fp.fp.GetLocalSolderPasteMarginRatio()) dst_fp.fp.SetZoneConnection(src_fp.fp.GetZoneConnection()) # add footprints to corresponding layout groups if selected if settings.group_footprints: self.dst_groups[st_index].AddItem(dst_fp.fp) # flip if dst anchor is flipped in regard to src anchor if self.src_anchor_fp.fp.IsFlipped() != dst_anchor_fp.fp.IsFlipped(): # ignore anchor fp if dst_anchor_fp != dst_fp: dst_fp.fp.Flip(dst_anchor_fp_position, False) # src_fp_rel_pos = src_anchor_pos - src_fp_pos delta_angle = dst_anchor_fp_angle + src_anchor_fp_angle dst_fp_rel_pos_rot = rotate_around_center([-src_fp_rel_pos[0], src_fp_rel_pos[1]], -delta_angle) dst_fp_pos = dst_anchor_fp_position + pcbnew.VECTOR2I(dst_fp_rel_pos_rot[0], dst_fp_rel_pos_rot[1]) # also need to change the angle dst_fp.fp.SetPosition(dst_fp_pos) src_fp_flipped_orientation = flipped_angle(src_fp_orientation) flipped_delta = flipped_angle(src_anchor_fp_angle) - dst_anchor_fp_angle new_orientation = src_fp_flipped_orientation - flipped_delta dst_fp.fp.SetOrientationDegrees(new_orientation) dst_fp_orientation = dst_fp.fp.GetOrientationDegrees() dst_fp_flipped = dst_fp.fp.IsFlipped() # replicate also text layout - also for anchor footprint. I am counting that the user is lazy and will # just position the destination anchors and will not edit them # get footprint text src_fp_text_items = self.get_footprint_text_items(src_fp) dst_fp_text_items = self.get_footprint_text_items(dst_fp) # check if both footprints (source and the one for replication) have the same number of text items if len(src_fp_text_items) != len(dst_fp_text_items): raise LookupError( "Source footprint: " + src_fp.ref + " has different number of text items (" + repr( len(src_fp_text_items)) + ")\nthan footprint for replication: " + dst_fp.ref + " (" + repr( len(dst_fp_text_items)) + ")") # replicate each text item src_text: pcbnew.FP_TEXT dst_text: pcbnew.FP_TEXT for src_text in src_fp_text_items: txt_index = src_fp_text_items.index(src_text) src_txt_pos = src_text.GetPosition() src_txt_rel_pos = src_txt_pos - src_fp_pos src_txt_orientation = src_text.GetTextAngleDegrees() delta_angle = dst_fp_orientation - src_fp_orientation dst_text = dst_fp_text_items[txt_index] dst_text.SetLayer(src_text.GetLayer()) # properly set position if src_fp_flipped != dst_fp_flipped: dst_text.Flip(dst_anchor_fp_position, False) dst_txt_rel_pos = [-src_txt_rel_pos[0], src_txt_rel_pos[1]] delta_angle = flipped_angle(src_anchor_fp_angle) - dst_anchor_fp_angle dst_txt_rel_pos_rot = rotate_around_center(dst_txt_rel_pos, delta_angle) dst_txt_pos = dst_fp_pos + pcbnew.VECTOR2I(dst_txt_rel_pos_rot[0], dst_txt_rel_pos_rot[1]) dst_text.SetPosition(dst_txt_pos) dst_text.SetTextAngleDegrees(-src_txt_orientation) dst_text.SetMirrored(not src_text.IsMirrored()) else: dst_txt_rel_pos = rotate_around_center(src_txt_rel_pos, -delta_angle) dst_txt_pos = dst_fp_pos + pcbnew.VECTOR2I(dst_txt_rel_pos[0], dst_txt_rel_pos[1]) dst_text.SetPosition(dst_txt_pos) dst_text.SetTextAngleDegrees(src_txt_orientation) dst_text.SetMirrored(src_text.IsMirrored()) # set text parameters dst_text.SetTextThickness(src_text.GetTextThickness()) dst_text.SetTextWidth(src_text.GetTextWidth()) dst_text.SetTextHeight(src_text.GetTextHeight()) dst_text.SetItalic(src_text.IsItalic()) dst_text.SetBold(src_text.IsBold()) dst_text.SetMultilineAllowed(src_text.IsMultilineAllowed()) dst_text.SetHorizJustify(src_text.GetHorizJustify()) dst_text.SetVertJustify(src_text.GetVertJustify()) dst_text.SetKeepUpright(src_text.IsKeepUpright()) dst_text.SetVisible(src_text.IsVisible()) def replicate_tracks(self, settings): logger.info("Replicating tracks") nr_sheets = len(self.dst_sheets) for st_index in range(nr_sheets): sheet = self.dst_sheets[st_index] progress = st_index / nr_sheets self.update_progress(self.stage, progress, None) logger.info("Replicating tracks on sheet " + repr(sheet)) # get anchor footprint dst_anchor_fp = self.get_sheet_anchor_footprint(sheet) # get source group from source footprint source_group = self.src_anchor_fp.fp.GetParentGroup() dst_anchor_fp_angle = dst_anchor_fp.fp.GetOrientation().AsDegrees() dst_anchor_fp_position = dst_anchor_fp.fp.GetPosition() src_anchor_fp_angle = self.src_anchor_fp.fp.GetOrientation().AsDegrees() src_anchor_fp_position = self.src_anchor_fp.fp.GetPosition() move_vector = dst_anchor_fp_position - src_anchor_fp_position delta_orientation = dst_anchor_fp_angle - src_anchor_fp_angle net_pairs = self.get_net_pairs(sheet) # go through all the tracks nr_tracks = len(self.src_tracks) for track_index in range(nr_tracks): track = self.src_tracks[track_index] progress = progress + (1 / nr_sheets) * (1 / nr_tracks) self.update_progress(self.stage, progress, None) # get from which net we are cloning from_net_name = track.GetNetname() # find to net tup = [item for item in net_pairs if item[0] == from_net_name] # if net was not found, then the track is not part of this sheet and should not be cloned if not tup: pass else: to_net_name = tup[0][1] to_net_code = self.netdict.GetNetItem(to_net_name).GetNetCode() #to_net_item = self.netdict.GetNetItem(to_net_name) # make a duplicate, move it, rotate it, select proper net and add it to the board new_track = track.Duplicate().Cast() new_track.SetNetCode(to_net_code) #new_track.SetNet(to_net_item) new_track.Move(move_vector) if self.src_anchor_fp.fp.IsFlipped() != dst_anchor_fp.fp.IsFlipped(): new_track.Flip(dst_anchor_fp_position, False) delta_angle = flipped_angle(src_anchor_fp_angle) - dst_anchor_fp_angle rot_angle = delta_angle - 180 new_track.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(-rot_angle, pcbnew.DEGREES_T)) else: new_track.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(delta_orientation, pcbnew.DEGREES_T)) # prevent tracks from being added into source group if source_group is not None: source_group.RemoveItem(new_track) # add tracks to corresponding layout groups if selected if settings.group_tracks: self.dst_groups[st_index].AddItem(new_track) self.board.Add(new_track) def replicate_zones(self, settings): """ method which replicates zones""" logger.info("Replicating zones") # start cloning nr_sheets = len(self.dst_sheets) for st_index in range(nr_sheets): sheet = self.dst_sheets[st_index] progress = st_index / nr_sheets self.update_progress(self.stage, progress, None) logger.info("Replicating zones on sheet " + repr(sheet)) # get anchor footprint dst_anchor_fp = self.get_sheet_anchor_footprint(sheet) dst_anchor_fp_angle = dst_anchor_fp.fp.GetOrientation().AsDegrees() dst_anchor_fp_position = dst_anchor_fp.fp.GetPosition() # get source group from source footprint source_group = self.src_anchor_fp.fp.GetParentGroup() src_anchor_fp_angle = self.src_anchor_fp.fp.GetOrientation().AsDegrees() src_anchor_fp_position = self.src_anchor_fp.fp.GetPosition() move_vector = dst_anchor_fp_position - src_anchor_fp_position delta_orientation = dst_anchor_fp_angle - src_anchor_fp_angle net_pairs = self.get_net_pairs(sheet) # go through all the zones nr_zones = len(self.src_zones) for zone_index in range(nr_zones): zone = self.src_zones[zone_index] progress = progress + (1 / nr_sheets) * (1 / nr_zones) self.update_progress(self.stage, progress, None) # get from which net we are cloning from_net_name = zone.GetNetname() # if zone is not on copper layer it does not matter on which net it is if not zone.IsOnCopperLayer(): tup = [('', '')] else: if from_net_name: tup = [item for item in net_pairs if item[0] == from_net_name] # With proper layout I don't see why this should happen # TODO find a case when this happens in order to log it with proper message if len(tup) == 0: logger.info("When replicating zone from source net " + repr(from_net_name) + " we did not find matching destination net") tup = [('', '')] # if source zone does not have a netname defined then destination zone also does not need it else: tup = [('', '')] # there is no net if not tup: # Allow keepout zones to be cloned. if not zone.IsOnCopperLayer(): tup = [('', '')] # start the clone to_net_name = tup[0][1] if to_net_name == u'': to_net_code = 0 to_net_item = self.board.FindNet(0) else: to_net_code = self.netdict.GetNetItem(to_net_name).GetNetCode() #to_net_item = self.netdict.GetNetItem(to_net_name) # make a duplicate, move it, rotate it, select proper net and add it to the board new_zone = zone.Duplicate().Cast() new_zone.Move(move_vector) new_zone.SetNetCode(to_net_code) #new_zone.SetNet(to_net_item) if self.src_anchor_fp.fp.IsFlipped() != dst_anchor_fp.fp.IsFlipped(): new_zone.Flip(dst_anchor_fp_position, False) delta_angle = flipped_angle(src_anchor_fp_angle) - dst_anchor_fp_angle rot_angle = delta_angle - 180 new_zone.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(-rot_angle, pcbnew.DEGREES_T)) else: new_zone.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(delta_orientation, pcbnew.DEGREES_T)) # prevent zones from being added into source group if source_group is not None: source_group.RemoveItem(new_zone) # add zones to corresponding layout groups if selected if settings.group_zones: self.dst_groups[st_index].AddItem(new_zone) self.board.Add(new_zone) def replicate_text(self, settings): logger.info("Replicating text") # start cloning nr_sheets = len(self.dst_sheets) for st_index in range(nr_sheets): sheet = self.dst_sheets[st_index] progress = st_index / nr_sheets self.update_progress(self.stage, progress, None) logger.info("Replicating text on sheet " + repr(sheet)) # get anchor footprint dst_anchor_fp = self.get_sheet_anchor_footprint(sheet) dst_anchor_fp_position = dst_anchor_fp.fp.GetPosition() dst_anchor_fp_angle = dst_anchor_fp.fp.GetOrientation().AsDegrees() # get source group from source footprint source_group = self.src_anchor_fp.fp.GetParentGroup() src_anchor_fp_angle = self.src_anchor_fp.fp.GetOrientation().AsDegrees() src_anchor_fp_position = self.src_anchor_fp.fp.GetPosition() move_vector = dst_anchor_fp_position - src_anchor_fp_position delta_orientation = dst_anchor_fp_angle - src_anchor_fp_angle nr_text = len(self.src_text) for text_index in range(nr_text): text = self.src_text[text_index] progress = progress + (1 / nr_sheets) * (1 / nr_text) self.update_progress(self.stage, progress, None) new_text = text.Duplicate().Cast() new_text.Move(move_vector) if self.src_anchor_fp.fp.IsFlipped() != dst_anchor_fp.fp.IsFlipped(): new_text.Flip(dst_anchor_fp_position, False) delta_angle = flipped_angle(src_anchor_fp_angle) - dst_anchor_fp_angle rot_angle = delta_angle - 180 new_text.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(-rot_angle, pcbnew.DEGREES_T)) else: new_text.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(delta_orientation, pcbnew.DEGREES_T)) # prevent text from being added into source group if source_group is not None: source_group.RemoveItem(new_text) # add text to corresponding layout groups if selected if settings.group_text: self.dst_groups[st_index].AddItem(new_text) self.board.Add(new_text) def replicate_drawings(self, settings): logger.info("Replicating drawings") nr_sheets = len(self.dst_sheets) for st_index in range(nr_sheets): sheet = self.dst_sheets[st_index] progress = st_index / nr_sheets self.update_progress(self.stage, progress, None) logger.info("Replicating drawings on sheet " + repr(sheet)) # get anchor footprint dst_anchor_fp = self.get_sheet_anchor_footprint(sheet) dst_anchor_fp_position = dst_anchor_fp.fp.GetPosition() dst_anchor_fp_angle = dst_anchor_fp.fp.GetOrientation().AsDegrees() # get source group from source footprint source_group = self.src_anchor_fp.fp.GetParentGroup() src_anchor_fp_angle = self.src_anchor_fp.fp.GetOrientation().AsDegrees() src_anchor_fp_position = self.src_anchor_fp.fp.GetPosition() move_vector = dst_anchor_fp_position - src_anchor_fp_position delta_orientation = dst_anchor_fp_angle - src_anchor_fp_angle # go through all the drawings nr_drawings = len(self.src_drawings) for dw_index in range(nr_drawings): drawing = self.src_drawings[dw_index] progress = progress + (1 / nr_sheets) * (1 / nr_drawings) self.update_progress(self.stage, progress, None) new_drawing = drawing.Duplicate().Cast() new_drawing.Move(move_vector) if self.src_anchor_fp.fp.IsFlipped() != dst_anchor_fp.fp.IsFlipped(): new_drawing.Flip(dst_anchor_fp_position, False) delta_angle = flipped_angle(src_anchor_fp_angle) - dst_anchor_fp_angle rot_angle = delta_angle - 180 new_drawing.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(-rot_angle, pcbnew.DEGREES_T)) else: new_drawing.Rotate(dst_anchor_fp_position, pcbnew.EDA_ANGLE(delta_orientation, pcbnew.DEGREES_T)) # prevent drawings from being added into source group if source_group is not None: source_group.RemoveItem(new_drawing) # add drawings to corresponding layout groups if selected if settings.group_drawings: self.dst_groups[st_index].AddItem(new_drawing) self.board.Add(new_drawing) def remove_zones_tracks(self, intersecting): for index in range(len(self.dst_sheets)): sheet = self.dst_sheets[index] self.update_progress(self.stage, index / len(self.dst_sheets), None) # get footprints on a sheet fp_sheet = self.get_footprints_on_sheet(sheet) # get bounding box bounding_box = self.get_footprints_bounding_box(fp_sheet) logger.info(f"Remove bounding box top:{bounding_box.GetTop()}, bottom:{bounding_box.GetBottom()}, " f"Left:{bounding_box.GetLeft()}, Right:{bounding_box.GetRight()}") # remove only tracks which are within the bounding box # or they are connected to a net that is completely local to the sheet nets_on_sheet = self.get_nets_from_footprints(fp_sheet) fp_not_on_sheet = self.get_footprints_not_on_sheet(sheet) other_nets = self.get_nets_from_footprints(fp_not_on_sheet) nets_exclusively_on_sheet = [net for net in nets_on_sheet if net not in other_nets] # remove items # TODO refactor out the old selection code tracks_for_removal = self.get_tracks(bounding_box, not intersecting, nets_exclusively_on_sheet) for track in tracks_for_removal: # minus the tracks in source bounding box if track not in self.src_tracks: self.board.RemoveNative(track) zones_for_removal = self.get_zones(bounding_box, not intersecting, nets_exclusively_on_sheet) for zone in zones_for_removal: # minus the zones in source bounding box if zone not in self.src_zones: self.board.RemoveNative(zone) for text_item in self.get_text_items(bounding_box, not intersecting): self.board.RemoveNative(text_item) for drawing in self.get_drawings(bounding_box, not intersecting): self.board.RemoveNative(drawing) def removing_duplicates(self): remove_duplicates(self.board) def get_footprints_for_replication(self, level, bounding_box, settings): src_fps = self.get_footprints_on_sheet(level) fps_for_replication = [] for fp in src_fps: if not fp.fp.IsLocked() or settings.rep_locked_drawings: if settings.group_only: if fp.fp.GetParentGroup(): if fp.fp.GetParentGroup().GetName() == self.src_anchor_fp_group: fps_for_replication.append(fp) else: fps_for_replication.append(fp) return fps_for_replication def get_tracks_for_replication(self, level, bounding_box, settings): tracks_for_replication = [] # get all tracks all_tracks = self.board.GetTracks() src_fps = self.get_footprints_on_sheet(level) nets_on_sheet = self.get_nets_from_footprints(src_fps) fp_not_on_sheet = self.get_footprints_not_on_sheet(level) other_nets = self.get_nets_from_footprints(fp_not_on_sheet) nets_exclusively_on_sheet = [net for net in nets_on_sheet if net not in other_nets] common_nets_on_sheet = [net for net in nets_on_sheet if net not in nets_exclusively_on_sheet] logger.info(f"Filtering list of tracks") if settings.group_only: # get all tracks that are in the group and on sheet nets (including common) for t in all_tracks: if not t.IsLocked() or settings.rep_locked_tracks: if t.GetParentGroup(): logger.info(f"Track group: {t.GetParentGroup().GetName()}, src group:{self.src_anchor_fp_group}") if t.GetParentGroup().GetName() == self.src_anchor_fp_group: if t.GetNetname() in nets_on_sheet: tracks_for_replication.append(t) else: for t in all_tracks: if not t.IsLocked() or settings.rep_locked_tracks: t_bb = t.GetBoundingBox() if (settings.intersecting and bounding_box.Intersects(t_bb)) or \ (not settings.intersecting and bounding_box.Contains(t_bb)): # append those tracks which are inside bounding box and on sheet nets (including common) if t.GetNetname() in nets_on_sheet: tracks_for_replication.append(t) # outside tracks else: # append those which are on sheet exclusive nets if t.GetNetname() in nets_exclusively_on_sheet: tracks_for_replication.append(t) # those which are on other nets, append only if they are in group and if the user wants to else: if settings.group_items and t.GetNetname() in nets_on_sheet: if t.GetParentGroup(): if self.src_anchor_fp_group == t.GetParentGroup().GetName(): tracks_for_replication.append(t) return tracks_for_replication def get_zones_for_replication(self, level, bounding_box, settings): zones_for_replication = [] # get all zones all_zones = [] for zone_id in range(self.board.GetAreaCount()): all_zones.append(self.board.GetArea(zone_id)) src_fps = self.get_footprints_on_sheet(level) nets_on_sheet = self.get_nets_from_footprints(src_fps) fp_not_on_sheet = self.get_footprints_not_on_sheet(level) other_nets = self.get_nets_from_footprints(fp_not_on_sheet) nets_exclusively_on_sheet = [net for net in nets_on_sheet if net not in other_nets] common_nets_on_sheet = [net for net in nets_on_sheet if net not in nets_exclusively_on_sheet] if settings.group_only: # get all zones that are in the group and on sheet nets (including common) for z in all_zones: if not z.IsLocked() or settings.rep_locked_zones: if z.GetParentGroup(): if z.GetParentGroup().GetName() == self.src_anchor_fp_group: if z.GetNetname() in nets_on_sheet: zones_for_replication.append(z) else: for z in all_zones: if not z.IsLocked() or settings.rep_locked_zones: z_bb = z.GetBoundingBox() if (settings.intersecting and bounding_box.Intersects(z_bb)) or \ (not settings.intersecting and bounding_box.Contains(z_bb)): # append those zones which are inside bounding box and on sheet nets (including common) if z.GetNetname() in nets_on_sheet or z.GetIsRuleArea(): zones_for_replication.append(z) # outside zones else: # append those which are on sheet exclusive nets if z.GetNetname() in nets_exclusively_on_sheet: zones_for_replication.append(z) # those which are on other nets, append only if they are in group and if the user wants to else: if settings.group_items and (z.GetNetname() in nets_on_sheet or z.GetIsRuleArea()): if z.GetParentGroup(): if self.src_anchor_fp_group == z.GetParentGroup().GetName(): zones_for_replication.append(z) return zones_for_replication def get_text_for_replication(self, bounding_box, settings): text_items_for_replication = [] # get all drawings on PCB text_items = [] for t_i in self.board.GetDrawings(): if isinstance(t_i, pcbnew.PCB_TEXT): # text items are handled separately text_items.append(t_i) # if group only if settings.group_only: # get all drawings, and select only those belonging to group for t_i in text_items: if t_i.GetParentGroup(): if self.src_anchor_fp_group == t_i.GetParentGroup().GetName(): if not t_i.IsLocked() or settings.rep_locked_text: text_items_for_replication.append(t_i) else: for t_i in text_items: t_i_bb = t_i.GetBoundingBox() if settings.intersecting: # append those drawings which are inside bounding box if bounding_box.Intersects(t_i_bb): if not t_i.IsLocked() or settings.rep_locked_text: text_items_for_replication.append(t_i) # append outside drawings append only if required else: if settings.group_items: if t_i.GetParentGroup(): if self.src_anchor_fp_group == t_i.GetParentGroup().GetName(): if not t_i.IsLocked() or settings.rep_locked_drawings: text_items_for_replication.append(t_i) else: if bounding_box.Contains(t_i_bb): if not t_i.IsLocked() or settings.rep_locked_drawings: text_items_for_replication.append(t_i) else: if settings.group_items: if t_i.GetParentGroup(): if self.src_anchor_fp_group == t_i.GetParentGroup().GetName(): if not t_i.IsLocked() or settings.rep_locked_drawings: text_items_for_replication.append(t_i) return text_items_for_replication def get_drawings_for_replication(self, bounding_box, settings): drawings_for_replication = [] # get all drawings on PCB drawings = [] for d in self.board.GetDrawings(): if not isinstance(d, pcbnew.PCB_TEXT): # text items are handled separately drawings.append(d) # if group only if settings.group_only: # get all drawings, and select only those belonging to group for d in drawings: if d.GetParentGroup(): if self.src_anchor_fp_group == d.GetParentGroup().GetName(): if not d.IsLocked() or settings.rep_locked_drawings: drawings_for_replication.append(d) else: for d in drawings: d_bb = d.GetBoundingBox() if settings.intersecting: # append those drawings which are inside bounding box if bounding_box.Intersects(d_bb): if not d.IsLocked() or settings.rep_locked_drawings: drawings_for_replication.append(d) # append outside drawings append only if required else: if settings.group_items: if d.GetParentGroup(): if self.src_anchor_fp_group == d.GetParentGroup().GetName(): if not d.IsLocked() or settings.rep_locked_drawings: drawings_for_replication.append(d) else: if bounding_box.Contains(d_bb): if not d.IsLocked() or settings.rep_locked_drawings: drawings_for_replication.append(d) else: if settings.group_items: if d.GetParentGroup(): if self.src_anchor_fp_group == d.GetParentGroup().GetName(): if not d.IsLocked() or settings.rep_locked_drawings: drawings_for_replication.append(d) return drawings_for_replication def highlight_set_level(self, level, settings): logger.info(f"Level selected: {repr(level)}") # find level bounding box src_fps = self.get_footprints_on_sheet(level) fps_bb = self.get_footprints_bounding_box(src_fps) # set highlight on all the footprints fps = self.get_footprints_for_replication(level, fps_bb, settings) for fp in fps: self.fp_set_highlight(fp.fp) # set highlight on other items highlighted_items = [] if settings.rep_tracks: tracks = self.get_tracks_for_replication(level, fps_bb, settings) for t in tracks: t.SetBrightened() highlighted_items.append(t) if settings.rep_zones: zones = self.get_zones_for_replication(level, fps_bb, settings) for z in zones: z.SetBrightened() highlighted_items.append(z) if settings.rep_text: text_items = self.get_text_for_replication(fps_bb, settings) for t in text_items: t.SetBrightened() highlighted_items.append(t) if settings.rep_drawings: drawings = self.get_drawings_for_replication(fps_bb, settings) for d in drawings: d.SetBrightened() highlighted_items.append(d) return fps, highlighted_items def highlight_clear_level(self, fps, items): # set highlight on all the footprints for fp in fps: self.fp_clear_highlight(fp.fp) # set highlight on other items for item in items: item.ClearBrightened() @staticmethod def fp_set_highlight(fp): pads_list = fp.Pads() for pad in pads_list: pad.SetBrightened() drawings = fp.GraphicalItems() for item in drawings: item.SetBrightened() @staticmethod def fp_clear_highlight(fp): pads_list = fp.Pads() for pad in pads_list: pad.ClearBrightened() drawings = fp.GraphicalItems() for item in drawings: item.ClearBrightened()