ReplicateLayout/replicate_layout.py

1507 lines
71 KiB
Python

# -*- 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()