245 lines
7.5 KiB
Python
245 lines
7.5 KiB
Python
|
from .pack_algo import PackingAlgorithm
|
||
|
from .geometry import Rectangle
|
||
|
import itertools
|
||
|
import collections
|
||
|
import operator
|
||
|
|
||
|
|
||
|
first_item = operator.itemgetter(0)
|
||
|
|
||
|
|
||
|
|
||
|
class MaxRects(PackingAlgorithm):
|
||
|
|
||
|
def __init__(self, width, height, rot=True, *args, **kwargs):
|
||
|
super(MaxRects, self).__init__(width, height, rot, *args, **kwargs)
|
||
|
|
||
|
def _rect_fitness(self, max_rect, width, height):
|
||
|
"""
|
||
|
Arguments:
|
||
|
max_rect (Rectangle): Destination max_rect
|
||
|
width (int, float): Rectangle width
|
||
|
height (int, float): Rectangle height
|
||
|
|
||
|
Returns:
|
||
|
None: Rectangle couldn't be placed into max_rect
|
||
|
integer, float: fitness value
|
||
|
"""
|
||
|
if width <= max_rect.width and height <= max_rect.height:
|
||
|
return 0
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def _select_position(self, w, h):
|
||
|
"""
|
||
|
Find max_rect with best fitness for placing a rectangle
|
||
|
of dimentsions w*h
|
||
|
|
||
|
Arguments:
|
||
|
w (int, float): Rectangle width
|
||
|
h (int, float): Rectangle height
|
||
|
|
||
|
Returns:
|
||
|
(rect, max_rect)
|
||
|
rect (Rectangle): Placed rectangle or None if was unable.
|
||
|
max_rect (Rectangle): Maximal rectangle were rect was placed
|
||
|
"""
|
||
|
if not self._max_rects:
|
||
|
return None, None
|
||
|
|
||
|
# Normal rectangle
|
||
|
fitn = ((self._rect_fitness(m, w, h), w, h, m) for m in self._max_rects
|
||
|
if self._rect_fitness(m, w, h) is not None)
|
||
|
|
||
|
# Rotated rectangle
|
||
|
fitr = ((self._rect_fitness(m, h, w), h, w, m) for m in self._max_rects
|
||
|
if self._rect_fitness(m, h, w) is not None)
|
||
|
|
||
|
if not self.rot:
|
||
|
fitr = []
|
||
|
|
||
|
fit = itertools.chain(fitn, fitr)
|
||
|
|
||
|
try:
|
||
|
_, w, h, m = min(fit, key=first_item)
|
||
|
except ValueError:
|
||
|
return None, None
|
||
|
|
||
|
return Rectangle(m.x, m.y, w, h), m
|
||
|
|
||
|
def _generate_splits(self, m, r):
|
||
|
"""
|
||
|
When a rectangle is placed inside a maximal rectangle, it stops being one
|
||
|
and up to 4 new maximal rectangles may appear depending on the placement.
|
||
|
_generate_splits calculates them.
|
||
|
|
||
|
Arguments:
|
||
|
m (Rectangle): max_rect rectangle
|
||
|
r (Rectangle): rectangle placed
|
||
|
|
||
|
Returns:
|
||
|
list : list containing new maximal rectangles or an empty list
|
||
|
"""
|
||
|
new_rects = []
|
||
|
|
||
|
if r.left > m.left:
|
||
|
new_rects.append(Rectangle(m.left, m.bottom, r.left-m.left, m.height))
|
||
|
if r.right < m.right:
|
||
|
new_rects.append(Rectangle(r.right, m.bottom, m.right-r.right, m.height))
|
||
|
if r.top < m.top:
|
||
|
new_rects.append(Rectangle(m.left, r.top, m.width, m.top-r.top))
|
||
|
if r.bottom > m.bottom:
|
||
|
new_rects.append(Rectangle(m.left, m.bottom, m.width, r.bottom-m.bottom))
|
||
|
|
||
|
return new_rects
|
||
|
|
||
|
def _split(self, rect):
|
||
|
"""
|
||
|
Split all max_rects intersecting the rectangle rect into up to
|
||
|
4 new max_rects.
|
||
|
|
||
|
Arguments:
|
||
|
rect (Rectangle): Rectangle
|
||
|
|
||
|
Returns:
|
||
|
split (Rectangle list): List of rectangles resulting from the split
|
||
|
"""
|
||
|
max_rects = collections.deque()
|
||
|
|
||
|
for r in self._max_rects:
|
||
|
if r.intersects(rect):
|
||
|
max_rects.extend(self._generate_splits(r, rect))
|
||
|
else:
|
||
|
max_rects.append(r)
|
||
|
|
||
|
# Add newly generated max_rects
|
||
|
self._max_rects = list(max_rects)
|
||
|
|
||
|
def _remove_duplicates(self):
|
||
|
"""
|
||
|
Remove every maximal rectangle contained by another one.
|
||
|
"""
|
||
|
contained = set()
|
||
|
for m1, m2 in itertools.combinations(self._max_rects, 2):
|
||
|
if m1.contains(m2):
|
||
|
contained.add(m2)
|
||
|
elif m2.contains(m1):
|
||
|
contained.add(m1)
|
||
|
|
||
|
# Remove from max_rects
|
||
|
self._max_rects = [m for m in self._max_rects if m not in contained]
|
||
|
|
||
|
def fitness(self, width, height):
|
||
|
"""
|
||
|
Metric used to rate how much space is wasted if a rectangle is placed.
|
||
|
Returns a value greater or equal to zero, the smaller the value the more
|
||
|
'fit' is the rectangle. If the rectangle can't be placed, returns None.
|
||
|
|
||
|
Arguments:
|
||
|
width (int, float): Rectangle width
|
||
|
height (int, float): Rectangle height
|
||
|
|
||
|
Returns:
|
||
|
int, float: Rectangle fitness
|
||
|
None: Rectangle can't be placed
|
||
|
"""
|
||
|
assert(width > 0 and height > 0)
|
||
|
|
||
|
rect, max_rect = self._select_position(width, height)
|
||
|
if rect is None:
|
||
|
return None
|
||
|
|
||
|
# Return fitness
|
||
|
return self._rect_fitness(max_rect, rect.width, rect.height)
|
||
|
|
||
|
def add_rect(self, width, height, rid=None):
|
||
|
"""
|
||
|
Add rectangle of widthxheight dimensions.
|
||
|
|
||
|
Arguments:
|
||
|
width (int, float): Rectangle width
|
||
|
height (int, float): Rectangle height
|
||
|
rid: Optional rectangle user id
|
||
|
|
||
|
Returns:
|
||
|
Rectangle: Rectangle with placemente coordinates
|
||
|
None: If the rectangle couldn be placed.
|
||
|
"""
|
||
|
assert(width > 0 and height >0)
|
||
|
|
||
|
# Search best position and orientation
|
||
|
rect, _ = self._select_position(width, height)
|
||
|
if not rect:
|
||
|
return None
|
||
|
|
||
|
# Subdivide all the max rectangles intersecting with the selected
|
||
|
# rectangle.
|
||
|
self._split(rect)
|
||
|
|
||
|
# Remove any max_rect contained by another
|
||
|
self._remove_duplicates()
|
||
|
|
||
|
# Store and return rectangle position.
|
||
|
rect.rid = rid
|
||
|
self.rectangles.append(rect)
|
||
|
return rect
|
||
|
|
||
|
def reset(self):
|
||
|
super(MaxRects, self).reset()
|
||
|
self._max_rects = [Rectangle(0, 0, self.width, self.height)]
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
class MaxRectsBl(MaxRects):
|
||
|
|
||
|
def _select_position(self, w, h):
|
||
|
"""
|
||
|
Select the position where the y coordinate of the top of the rectangle
|
||
|
is lower, if there are severtal pick the one with the smallest x
|
||
|
coordinate
|
||
|
"""
|
||
|
fitn = ((m.y+h, m.x, w, h, m) for m in self._max_rects
|
||
|
if self._rect_fitness(m, w, h) is not None)
|
||
|
fitr = ((m.y+w, m.x, h, w, m) for m in self._max_rects
|
||
|
if self._rect_fitness(m, h, w) is not None)
|
||
|
|
||
|
if not self.rot:
|
||
|
fitr = []
|
||
|
|
||
|
fit = itertools.chain(fitn, fitr)
|
||
|
|
||
|
try:
|
||
|
_, _, w, h, m = min(fit, key=first_item)
|
||
|
except ValueError:
|
||
|
return None, None
|
||
|
|
||
|
return Rectangle(m.x, m.y, w, h), m
|
||
|
|
||
|
|
||
|
class MaxRectsBssf(MaxRects):
|
||
|
"""Best Sort Side Fit minimize short leftover side"""
|
||
|
def _rect_fitness(self, max_rect, width, height):
|
||
|
if width > max_rect.width or height > max_rect.height:
|
||
|
return None
|
||
|
|
||
|
return min(max_rect.width-width, max_rect.height-height)
|
||
|
|
||
|
class MaxRectsBaf(MaxRects):
|
||
|
"""Best Area Fit pick maximal rectangle with smallest area
|
||
|
where the rectangle can be placed"""
|
||
|
def _rect_fitness(self, max_rect, width, height):
|
||
|
if width > max_rect.width or height > max_rect.height:
|
||
|
return None
|
||
|
|
||
|
return (max_rect.width*max_rect.height)-(width*height)
|
||
|
|
||
|
|
||
|
class MaxRectsBlsf(MaxRects):
|
||
|
"""Best Long Side Fit minimize long leftover side"""
|
||
|
def _rect_fitness(self, max_rect, width, height):
|
||
|
if width > max_rect.width or height > max_rect.height:
|
||
|
return None
|
||
|
|
||
|
return max(max_rect.width-width, max_rect.height-height)
|