Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# -*- coding: utf-8 -*-
# flake8: noqa: F405
# MIT license
#
# Copyright (C) 2018-2021 by Dave Vandenbout.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Arrange footprints based on the schematic hierarchy of the design.
Vendored from HierPlace v1.1.0 by Dave Vandenbout (MIT) --
https://github.com/devbisme/HierPlace -- with the following changes for
headless use inside an MCP server:
* removed the hardcoded `sys.path.append('/usr/lib/python3/dist-packages')`
* removed the pcbnew GUI `ActionPlugin` subclass + `.register()` call
(it asserts `PgmOrNull()` when imported outside the pcbnew GUI)
* guarded the trailing `Refresh()` so it is a no-op headless
The placement algorithm (group_modules / pack / hier_place) is unchanged.
"""
import builtins
from collections import defaultdict
from pcbnew import * # noqa: F401,F403 (Module/board API used unqualified, as upstream)
try:
KICAD_VERSION = int(Version().split(".")[0]) # type: ignore[name-defined]
except NameError:
# No version function, so assume this is KiCad 5.
KICAD_VERSION = 5
# Extra spacing placed around bounding boxes of modules and groups of modules
# to provide visual separation.
MODULE_SPACING = 350000
GROUP_SPACING = 5 * MODULE_SPACING
class Module(object):
"""A class to provide extra functions for PCBNEW MODULES."""
def __init__(self, pcbnew_module):
"""Create a Module instance that stores a PCBNEW MODULE instance."""
self.m = pcbnew_module
@property
def ref(self):
"""Return a string with the module's reference id, such as "R3"."""
return self.m.GetReference()
@property
def hier_level(self):
"""Return a string with the hierarchical level of the module."""
try:
path_parts = self.m.GetPath().AsString().split("/")
except AttributeError:
path_parts = self.m.GetPath().split("/")
return "/".join(path_parts[:-1])
@property
def bbox(self):
"""Return an EDA_RECT or BOX2I with the bounding box of the module."""
try:
bb = self.m.GetBoundingBox()
except AttributeError:
bb = self.m.GetFootprintRect()
bb.Inflate(MODULE_SPACING)
return bb
def touches(self, module):
"""Return True if the given module intersects this module."""
bb1 = self.bbox
# Slightly shrink the bounding box so two modules that abut each other
# won't be reported as touching.
bb1.Inflate(-1)
return bb1.Intersects(module.bbox)
@property
def center(self):
"""Return wxPoint containing the centroid of the module."""
return self.bbox.GetCenter()
@property
def tl_corner(self):
"""Return wxPoint containing the top-left corner of the module."""
bb = self.bbox
return wxPoint(bb.GetLeft(), bb.GetTop())
@property
def br_corner(self):
"""Return wxPoint containing the bottom-right corner of the module."""
bb = self.bbox
return wxPoint(bb.GetRight(), bb.GetBottom())
@property
def bl_corner(self):
"""Return wxPoint containing the bottom-left corner of the module."""
bb = self.bbox
return wxPoint(bb.GetLeft(), bb.GetBottom())
@property
def w(self):
"""Return the width of the module."""
return self.bbox.GetWidth()
@property
def h(self):
"""Return the height of the module."""
return self.bbox.GetHeight()
@property
def area(self):
"""Return the area of the module's bounding box."""
return self.bbox.GetArea()
@property
def selected(self):
return self.m.IsSelected()
@property
def locked(self):
"""Return True if the module is locked in place."""
return self.m.IsLocked()
def move(self, dx, dy):
"""Move a module by the given distance in the X and Y directions."""
if not self.locked:
if KICAD_VERSION >= 7:
self.m.Move(VECTOR2I(dx, dy))
else:
self.m.Move(wxPoint(dx, dy))
def set_bl_position(self, point):
"""Set the position of the module's bottom-left corner to the given (X,Y) coordinate."""
if not self.locked:
mv = point - self.bl_corner # Get vector from BL corner to desired point.
self.move(mv.x, mv.y)
class ModuleGroup(list, Module):
"""A class that stores modules or module-groups as a list and also acts like a module."""
def __init__(self, *args):
super(ModuleGroup, self).__init__(*args)
@property
def ref(self):
"""Return a string composed of the reference ids of the modules it contains."""
return "[" + ",".join([m.ref for m in self]) + "]"
@property
def hier_level(self):
"""Return a string with the hierarchical level of the module group."""
return "/".join((self[0].hier_level.split("/"))[:-1])
@property
def bbox(self):
"""Return an EDA_RECT or BOX2I with the bounding box of the group of modules."""
try:
bbox = BOX2I()
except NameError:
bbox = EDA_RECT()
bbox.Move(self[0].center)
for module in self:
bbox.Merge(module.bbox)
bbox.Inflate(GROUP_SPACING)
return bbox
@property
def locked(self):
"""Alsways return False because a module group never contains any locked modules."""
return False
def move(self, dx, dy):
"""Move all the modules in a group by the given distance in the X and Y directions."""
if not self.locked:
for m in self:
m.move(dx, dy)
def group_modules(modules):
"""
Create a dictionary where each entry contains the modules at that level of the hierarchy.
"""
groups = defaultdict(ModuleGroup)
for m in modules:
if not m.locked:
# Add each unlocked module to the group at the same position of the hierarchy.
groups[m.hier_level].append(m)
return groups
def pack(group):
"""
Pack a group of modules into a tight formation.
"""
# Packing starts with the module that's largest in area. The top-left (TL)
# and bottom-right (BR) corners of that module become potential points where
# the lower-left corner of the next-largest module can be placed.
# The point is chosen based upon how much the bounding box of the
# currently-placed modules will expand. When a module is placed,
# the placement point is removed and the TL and BR corners
# of the just-placed module are added to the list of points.
# This proceeds until all the modules are placed in order of decreasing area.
# Create an empty group to store the modules as they are packed.
packed_modules = ModuleGroup()
# Storage for the potential places where a module can be inserted.
placement_pts = [] # Starts off empty.
# Iterate through the modules in order of decreasing area, placing each one
# where it will keep the group within a small area.
for module in sorted(group, key=lambda m: -m.area):
# Skip over locked modules since they can't be moved.
if module.locked:
continue
# Add the module to the packed group. (It still needs to be positioned.)
packed_modules.append(module)
# If there are potential placement points, then go through them looking
# for the one that gives the best packing of the currently-placed modules.
# The only time this is not done will be for the very first module in the
# group which will serve as the anchor the others will gather around.
if placement_pts:
# Iterate through the potential placement points and pick the one that
# least expands the total bounding box.
best_pt, smallest_size = None, float("inf")
for pt in placement_pts:
# Move module bottom-left corner to the placement point.
module.set_bl_position(pt)
# Check to see if the module touches any of the other modules in the packed group.
for pm in packed_modules:
if pm is module:
# Don't check for intersections between the module and itself.
continue
if module.touches(pm):
# Discard this placement point if the module touches one of the others.
break
else:
# If we got here, the placement point doesn't cause this module
# to touch any of the others. Now check the size of the resulting
# group of modules. The size is the sum of the height and width
# plus the difference between the height and width. This
# measure will attempt to keep the area small and square-like.
size = (
packed_modules.h
+ packed_modules.w
+ builtins.abs(packed_modules.h - packed_modules.w)
)
# If this configuration is the smallest so far, store it.
if size <= smallest_size:
smallest_size = size
best_pt = pt
# Place the module at the best placement point that was found.
module.set_bl_position(best_pt)
# Remove the placement point since no other module can be placed there, now.
placement_pts.remove(best_pt)
# Add the top-left and bottom-right corners of the just-placed module
# to the list of potential placement points.
placement_pts.extend([module.tl_corner, module.br_corner])
def hier_place(brd=None):
"""
Remove overlaps of board parts while respecting their hierarchy.
"""
brd = brd or GetBoard()
try:
modules = [Module(m) for m in brd.GetFootprints()]
except AttributeError:
modules = [Module(m) for m in brd.GetModules()]
# Get modules in the PCB that are selected.
# If no modules are selected, then operate on all the modules in the PCB.
selected_modules = [m for m in modules if m.selected] or modules
# Place the modules into groups based on hierarchy.
groups = group_modules(selected_modules)
# Pack all the modules in each group. Then move up a level in the hierarchy
# and pack a group of packed modules into another packing. Proceed upward
# until all the levels of the hierarchy have been packed.
while True:
# Pack the members of each group.
for group in groups.values():
pack(group)
# If there's only one group left, then all the hierarchical levels have
# been packed, so we're done.
if len(groups) <= 1:
break
# Otherwise, collect the packed groups into groups at the next level
# of hierarchy and repeat the loop.
groups = group_modules(groups.values())
# Refresh the GUI if one is attached; a no-op when running headless.
try:
Refresh()
except Exception:
pass
return len(selected_modules)
670d2a9c4This file is pinned to the selected release. Download the original to open it in your design tool.