Update Files

This commit is contained in:
2025-01-22 16:18:30 +01:00
parent ed4603cf95
commit a36294b518
16718 changed files with 2960346 additions and 0 deletions

View File

@ -0,0 +1,23 @@
from .guillotine import GuillotineBssfSas, GuillotineBssfLas, \
GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \
GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \
GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \
GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \
GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \
GuillotineBafMinas
from .maxrects import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf
from .skyline import SkylineMwf, SkylineMwfl, SkylineBl, \
SkylineBlWm, SkylineMwfWm, SkylineMwflWm
from .packer import SORT_AREA, SORT_PERI, SORT_DIFF, SORT_SSIDE, \
SORT_LSIDE, SORT_RATIO, SORT_NONE
from .packer import PackerBNF, PackerBFF, PackerBBF, PackerOnlineBNF, \
PackerOnlineBFF, PackerOnlineBBF, PackerGlobal, newPacker, \
PackingMode, PackingBin, float2dec

View File

@ -0,0 +1,148 @@
import heapq # heapq.heappush, heapq.heappop
from .packer import newPacker, PackingMode, PackingBin, SORT_LSIDE
from .skyline import SkylineBlWm
class Enclose(object):
def __init__(self, rectangles=[], max_width=None, max_height=None, rotation=True):
"""
Arguments:
rectangles (list): Rectangle to be enveloped
[(width1, height1), (width2, height2), ...]
max_width (number|None): Enveloping rectangle max allowed width.
max_height (number|None): Enveloping rectangle max allowed height.
rotation (boolean): Enable/Disable rectangle rotation.
"""
# Enclosing rectangle max width
self._max_width = max_width
# Encloseing rectangle max height
self._max_height = max_height
# Enable or disable rectangle rotation
self._rotation = rotation
# Default packing algorithm
self._pack_algo = SkylineBlWm
# rectangles to enclose [(width, height), (width, height, ...)]
self._rectangles = []
for r in rectangles:
self.add_rect(*r)
def _container_candidates(self):
"""Generate container candidate list
Returns:
tuple list: [(width1, height1), (width2, height2), ...]
"""
if not self._rectangles:
return []
if self._rotation:
sides = sorted(side for rect in self._rectangles for side in rect)
max_height = sum(max(r[0], r[1]) for r in self._rectangles)
min_width = max(min(r[0], r[1]) for r in self._rectangles)
max_width = max_height
else:
sides = sorted(r[0] for r in self._rectangles)
max_height = sum(r[1] for r in self._rectangles)
min_width = max(r[0] for r in self._rectangles)
max_width = sum(sides)
if self._max_width and self._max_width < max_width:
max_width = self._max_width
if self._max_height and self._max_height < max_height:
max_height = self._max_height
assert(max_width>min_width)
# Generate initial container widths
candidates = [max_width, min_width]
width = 0
for s in reversed(sides):
width += s
candidates.append(width)
width = 0
for s in sides:
width += s
candidates.append(width)
candidates.append(max_width)
candidates.append(min_width)
# Remove duplicates and widths too big or small
seen = set()
seen_add = seen.add
candidates = [x for x in candidates if not(x in seen or seen_add(x))]
candidates = [x for x in candidates if not(x>max_width or x<min_width)]
# Remove candidates too small to fit all the rectangles
min_area = sum(r[0]*r[1] for r in self._rectangles)
return [(c, max_height) for c in candidates if c*max_height>=min_area]
def _refine_candidate(self, width, height):
"""
Use bottom-left packing algorithm to find a lower height for the
container.
Arguments:
width
height
Returns:
tuple (width, height, PackingAlgorithm):
"""
packer = newPacker(PackingMode.Offline, PackingBin.BFF,
pack_algo=self._pack_algo, sort_algo=SORT_LSIDE,
rotation=self._rotation)
packer.add_bin(width, height)
for r in self._rectangles:
packer.add_rect(*r)
packer.pack()
# Check all rectangles where packed
if len(packer[0]) != len(self._rectangles):
return None
# Find highest rectangle
new_height = max(packer[0], key=lambda x: x.top).top
return(width, new_height, packer)
def generate(self):
# Generate initial containers
candidates = self._container_candidates()
if not candidates:
return None
# Refine candidates and return the one with the smaller area
containers = [self._refine_candidate(*c) for c in candidates]
containers = [c for c in containers if c]
if not containers:
return None
width, height, packer = min(containers, key=lambda x: x[0]*x[1])
packer.width = width
packer.height = height
return packer
def add_rect(self, width, height):
"""
Add anoter rectangle to be enclosed
Arguments:
width (number): Rectangle width
height (number): Rectangle height
"""
self._rectangles.append((width, height))

View File

@ -0,0 +1,344 @@
from math import sqrt
class Point(object):
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return (self.x == other.x and self.y == other.y)
def __repr__(self):
return "P({}, {})".format(self.x, self.y)
def distance(self, point):
"""
Calculate distance to another point
"""
return sqrt((self.x-point.x)**2+(self.y-point.y)**2)
def distance_squared(self, point):
return (self.x-point.x)**2+(self.y-point.y)**2
class Segment(object):
__slots__ = ('start', 'end')
def __init__(self, start, end):
"""
Arguments:
start (Point): Segment start point
end (Point): Segment end point
"""
assert(isinstance(start, Point) and isinstance(end, Point))
self.start = start
self.end = end
def __eq__(self, other):
if not isinstance(other, self.__class__):
None
return self.start==other.start and self.end==other.end
def __repr__(self):
return "S({}, {})".format(self.start, self.end)
@property
def length_squared(self):
"""Faster than length and useful for some comparisons"""
return self.start.distance_squared(self.end)
@property
def length(self):
return self.start.distance(self.end)
@property
def top(self):
return max(self.start.y, self.end.y)
@property
def bottom(self):
return min(self.start.y, self.end.y)
@property
def right(self):
return max(self.start.x, self.end.x)
@property
def left(self):
return min(self.start.x, self.end.x)
class HSegment(Segment):
"""Horizontal Segment"""
def __init__(self, start, length):
"""
Create an Horizontal segment given its left most end point and its
length.
Arguments:
- start (Point): Starting Point
- length (number): segment length
"""
assert(isinstance(start, Point) and not isinstance(length, Point))
super(HSegment, self).__init__(start, Point(start.x+length, start.y))
@property
def length(self):
return self.end.x-self.start.x
class VSegment(Segment):
"""Vertical Segment"""
def __init__(self, start, length):
"""
Create a Vertical segment given its bottom most end point and its
length.
Arguments:
- start (Point): Starting Point
- length (number): segment length
"""
assert(isinstance(start, Point) and not isinstance(length, Point))
super(VSegment, self).__init__(start, Point(start.x, start.y+length))
@property
def length(self):
return self.end.y-self.start.y
class Rectangle(object):
"""Basic rectangle primitive class.
x, y-> Lower right corner coordinates
width -
height -
"""
__slots__ = ('width', 'height', 'x', 'y', 'rid')
def __init__(self, x, y, width, height, rid = None):
"""
Args:
x (int, float):
y (int, float):
width (int, float):
height (int, float):
rid (int):
"""
assert(height >=0 and width >=0)
self.width = width
self.height = height
self.x = x
self.y = y
self.rid = rid
@property
def bottom(self):
"""
Rectangle bottom edge y coordinate
"""
return self.y
@property
def top(self):
"""
Rectangle top edge y coordiante
"""
return self.y+self.height
@property
def left(self):
"""
Rectangle left ednge x coordinate
"""
return self.x
@property
def right(self):
"""
Rectangle right edge x coordinate
"""
return self.x+self.width
@property
def corner_top_l(self):
return Point(self.left, self.top)
@property
def corner_top_r(self):
return Point(self.right, self.top)
@property
def corner_bot_r(self):
return Point(self.right, self.bottom)
@property
def corner_bot_l(self):
return Point(self.left, self.bottom)
def __lt__(self, other):
"""
Compare rectangles by area (used for sorting)
"""
return self.area() < other.area()
def __eq__(self, other):
"""
Equal rectangles have same area.
"""
if not isinstance(other, self.__class__):
return False
return (self.width == other.width and \
self.height == other.height and \
self.x == other.x and \
self.y == other.y)
def __hash__(self):
return hash((self.x, self.y, self.width, self.height))
def __iter__(self):
"""
Iterate through rectangle corners
"""
yield self.corner_top_l
yield self.corner_top_r
yield self.corner_bot_r
yield self.corner_bot_l
def __repr__(self):
return "R({}, {}, {}, {})".format(self.x, self.y, self.width, self.height)
def area(self):
"""
Rectangle area
"""
return self.width * self.height
def move(self, x, y):
"""
Move Rectangle to x,y coordinates
Arguments:
x (int, float): X coordinate
y (int, float): Y coordinate
"""
self.x = x
self.y = y
def contains(self, rect):
"""
Tests if another rectangle is contained by this one
Arguments:
rect (Rectangle): The other rectangle
Returns:
bool: True if it is container, False otherwise
"""
return (rect.y >= self.y and \
rect.x >= self.x and \
rect.y+rect.height <= self.y+self.height and \
rect.x+rect.width <= self.x+self.width)
def intersects(self, rect, edges=False):
"""
Detect intersections between this and another Rectangle.
Parameters:
rect (Rectangle): The other rectangle.
edges (bool): True to consider rectangles touching by their
edges or corners to be intersecting.
(Should have been named include_touching)
Returns:
bool: True if the rectangles intersect, False otherwise
"""
if edges:
if (self.bottom > rect.top or self.top < rect.bottom or\
self.left > rect.right or self.right < rect.left):
return False
else:
if (self.bottom >= rect.top or self.top <= rect.bottom or
self.left >= rect.right or self.right <= rect.left):
return False
return True
def intersection(self, rect, edges=False):
"""
Returns the rectangle resulting of the intersection between this and another
rectangle. If the rectangles are only touching by their edges, and the
argument 'edges' is True the rectangle returned will have an area of 0.
Returns None if there is no intersection.
Arguments:
rect (Rectangle): The other rectangle.
edges (bool): If True Rectangles touching by their edges are
considered to be intersection. In this case a rectangle of
0 height or/and width will be returned.
Returns:
Rectangle: Intersection.
None: There was no intersection.
"""
if not self.intersects(rect, edges=edges):
return None
bottom = max(self.bottom, rect.bottom)
left = max(self.left, rect.left)
top = min(self.top, rect.top)
right = min(self.right, rect.right)
return Rectangle(left, bottom, right-left, top-bottom)
def join(self, other):
"""
Try to join a rectangle to this one, if the result is also a rectangle
and the operation is successful and this rectangle is modified to the union.
Arguments:
other (Rectangle): Rectangle to join
Returns:
bool: True when successfully joined, False otherwise
"""
if self.contains(other):
return True
if other.contains(self):
self.x = other.x
self.y = other.y
self.width = other.width
self.height = other.height
return True
if not self.intersects(other, edges=True):
return False
# Other rectangle is Up/Down from this
if self.left == other.left and self.width == other.width:
y_min = min(self.bottom, other.bottom)
y_max = max(self.top, other.top)
self.y = y_min
self.height = y_max-y_min
return True
# Other rectangle is Right/Left from this
if self.bottom == other.bottom and self.height == other.height:
x_min = min(self.left, other.left)
x_max = max(self.right, other.right)
self.x = x_min
self.width = x_max-x_min
return True
return False

View File

@ -0,0 +1,368 @@
from .pack_algo import PackingAlgorithm
from .geometry import Rectangle
import itertools
import operator
class Guillotine(PackingAlgorithm):
"""Implementation of several variants of Guillotine packing algorithm
For a more detailed explanation of the algorithm used, see:
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
"""
def __init__(self, width, height, rot=True, merge=True, *args, **kwargs):
"""
Arguments:
width (int, float):
height (int, float):
merge (bool): Optional keyword argument
"""
self._merge = merge
super(Guillotine, self).__init__(width, height, rot, *args, **kwargs)
def _add_section(self, section):
"""Adds a new section to the free section list, but before that and if
section merge is enabled, tries to join the rectangle with all existing
sections, if successful the resulting section is again merged with the
remaining sections until the operation fails. The result is then
appended to the list.
Arguments:
section (Rectangle): New free section.
"""
section.rid = 0
plen = 0
while self._merge and self._sections and plen != len(self._sections):
plen = len(self._sections)
self._sections = [s for s in self._sections if not section.join(s)]
self._sections.append(section)
def _split_horizontal(self, section, width, height):
"""For an horizontal split the rectangle is placed in the lower
left corner of the section (section's xy coordinates), the top
most side of the rectangle and its horizontal continuation,
marks the line of division for the split.
+-----------------+
| |
| |
| |
| |
+-------+---------+
|#######| |
|#######| |
|#######| |
+-------+---------+
If the rectangle width is equal to the the section width, only one
section is created over the rectangle. If the rectangle height is
equal to the section height, only one section to the right of the
rectangle is created. If both width and height are equal, no sections
are created.
"""
# First remove the section we are splitting so it doesn't
# interfere when later we try to merge the resulting split
# rectangles, with the rest of free sections.
#self._sections.remove(section)
# Creates two new empty sections, and returns the new rectangle.
if height < section.height:
self._add_section(Rectangle(section.x, section.y+height,
section.width, section.height-height))
if width < section.width:
self._add_section(Rectangle(section.x+width, section.y,
section.width-width, height))
def _split_vertical(self, section, width, height):
"""For a vertical split the rectangle is placed in the lower
left corner of the section (section's xy coordinates), the
right most side of the rectangle and its vertical continuation,
marks the line of division for the split.
+-------+---------+
| | |
| | |
| | |
| | |
+-------+ |
|#######| |
|#######| |
|#######| |
+-------+---------+
If the rectangle width is equal to the the section width, only one
section is created over the rectangle. If the rectangle height is
equal to the section height, only one section to the right of the
rectangle is created. If both width and height are equal, no sections
are created.
"""
# When a section is split, depending on the rectangle size
# two, one, or no new sections will be created.
if height < section.height:
self._add_section(Rectangle(section.x, section.y+height,
width, section.height-height))
if width < section.width:
self._add_section(Rectangle(section.x+width, section.y,
section.width-width, section.height))
def _split(self, section, width, height):
"""
Selects the best split for a section, given a rectangle of dimmensions
width and height, then calls _split_vertical or _split_horizontal,
to do the dirty work.
Arguments:
section (Rectangle): Section to split
width (int, float): Rectangle width
height (int, float): Rectangle height
"""
raise NotImplementedError
def _section_fitness(self, section, width, height):
"""The subclass for each one of the Guillotine selection methods,
BAF, BLSF.... will override this method, this is here only
to asure a valid value return if the worst happens.
"""
raise NotImplementedError
def _select_fittest_section(self, w, h):
"""Calls _section_fitness for each of the sections in free section
list. Returns the section with the minimal fitness value, all the rest
is boilerplate to make the fitness comparison, to rotatate the rectangles,
and to take into account when _section_fitness returns None because
the rectangle couldn't be placed.
Arguments:
w (int, float): Rectangle width
h (int, float): Rectangle height
Returns:
(section, was_rotated): Returns the tuple
section (Rectangle): Section with best fitness
was_rotated (bool): The rectangle was rotated
"""
fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections
if self._section_fitness(s, w, h) is not None)
fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections
if self._section_fitness(s, h, w) is not None)
if not self.rot:
fitr = []
fit = itertools.chain(fitn, fitr)
try:
_, sec, rot = min(fit, key=operator.itemgetter(0))
except ValueError:
return None, None
return sec, rot
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)
# Obtain the best section to place the rectangle.
section, rotated = self._select_fittest_section(width, height)
if not section:
return None
if rotated:
width, height = height, width
# Remove section, split and store results
self._sections.remove(section)
self._split(section, width, height)
# Store rectangle in the selected position
rect = Rectangle(section.x, section.y, width, height, rid)
self.rectangles.append(rect)
return rect
def fitness(self, width, height):
"""
In guillotine algorithm case, returns the min of the fitness of all
free sections, for the given dimension, both normal and rotated
(if rotation enabled.)
"""
assert(width > 0 and height > 0)
# Get best fitness section.
section, rotated = self._select_fittest_section(width, height)
if not section:
return None
# Return fitness of returned section, with correct dimmensions if the
# the rectangle was rotated.
if rotated:
return self._section_fitness(section, height, width)
else:
return self._section_fitness(section, width, height)
def reset(self):
super(Guillotine, self).reset()
self._sections = []
self._add_section(Rectangle(0, 0, self.width, self.height))
class GuillotineBaf(Guillotine):
"""Implements Best Area Fit (BAF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return section.area()-width*height
class GuillotineBlsf(Guillotine):
"""Implements Best Long Side Fit (BLSF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return max(section.width-width, section.height-height)
class GuillotineBssf(Guillotine):
"""Implements Best Short Side Fit (BSSF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return min(section.width-width, section.height-height)
class GuillotineSas(Guillotine):
"""Implements Short Axis Split (SAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if section.width < section.height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineLas(Guillotine):
"""Implements Long Axis Split (LAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if section.width >= section.height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineSlas(Guillotine):
"""Implements Short Leftover Axis Split (SLAS) selection rule for
Guillotine algorithm.
"""
def _split(self, section, width, height):
if section.width-width < section.height-height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineLlas(Guillotine):
"""Implements Long Leftover Axis Split (LLAS) selection rule for
Guillotine algorithm.
"""
def _split(self, section, width, height):
if section.width-width >= section.height-height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineMaxas(Guillotine):
"""Implements Max Area Axis Split (MAXAS) selection rule for Guillotine
algorithm. Maximize the larger area == minimize the smaller area.
Tries to make the rectangles more even-sized.
"""
def _split(self, section, width, height):
if width*(section.height-height) <= height*(section.width-width):
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineMinas(Guillotine):
"""Implements Min Area Axis Split (MINAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if width*(section.height-height) >= height*(section.width-width):
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
# Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one
# Axis split, and one selection criteria.
class GuillotineBssfSas(GuillotineBssf, GuillotineSas):
pass
class GuillotineBssfLas(GuillotineBssf, GuillotineLas):
pass
class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas):
pass
class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas):
pass
class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas):
pass
class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas):
pass
class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas):
pass
class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas):
pass
class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas):
pass
class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas):
pass
class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas):
pass
class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas):
pass
class GuillotineBafSas(GuillotineBaf, GuillotineSas):
pass
class GuillotineBafLas(GuillotineBaf, GuillotineLas):
pass
class GuillotineBafSlas(GuillotineBaf, GuillotineSlas):
pass
class GuillotineBafLlas(GuillotineBaf, GuillotineLlas):
pass
class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas):
pass
class GuillotineBafMinas(GuillotineBaf, GuillotineMinas):
pass

View File

@ -0,0 +1,244 @@
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)

View File

@ -0,0 +1,140 @@
from .geometry import Rectangle
class PackingAlgorithm(object):
"""PackingAlgorithm base class"""
def __init__(self, width, height, rot=True, bid=None, *args, **kwargs):
"""
Initialize packing algorithm
Arguments:
width (int, float): Packing surface width
height (int, float): Packing surface height
rot (bool): Rectangle rotation enabled or disabled
bid (string|int|...): Packing surface identification
"""
self.width = width
self.height = height
self.rot = rot
self.rectangles = []
self.bid = bid
self._surface = Rectangle(0, 0, width, height)
self.reset()
def __len__(self):
return len(self.rectangles)
def __iter__(self):
return iter(self.rectangles)
def _fits_surface(self, width, height):
"""
Test surface is big enough to place a rectangle
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
Returns:
boolean: True if it could be placed, False otherwise
"""
assert(width > 0 and height > 0)
if self.rot and (width > self.width or height > self.height):
width, height = height, width
if width > self.width or height > self.height:
return False
else:
return True
def __getitem__(self, key):
"""
Return rectangle in selected position.
"""
return self.rectangles[key]
def used_area(self):
"""
Total area of rectangles placed
Returns:
int, float: Area
"""
return sum(r.area() for r in self)
def fitness(self, width, height, rot = False):
"""
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
rot (bool): Enable rectangle rotation
Returns:
int, float: Rectangle fitness
None: Rectangle can't be placed
"""
raise NotImplementedError
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.
"""
raise NotImplementedError
def rect_list(self):
"""
Returns a list with all rectangles placed into the surface.
Returns:
List: Format [(x, y, width, height, rid), ...]
"""
rectangle_list = []
for r in self:
rectangle_list.append((r.x, r.y, r.width, r.height, r.rid))
return rectangle_list
def validate_packing(self):
"""
Check for collisions between rectangles, also check all are placed
inside surface.
"""
surface = Rectangle(0, 0, self.width, self.height)
for r in self:
if not surface.contains(r):
raise Exception("Rectangle placed outside surface")
rectangles = [r for r in self]
if len(rectangles) <= 1:
return
for r1 in range(0, len(rectangles)-2):
for r2 in range(r1+1, len(rectangles)-1):
if rectangles[r1].intersects(rectangles[r2]):
raise Exception("Rectangle collision detected")
def is_empty(self):
# Returns true if there is no rectangles placed.
return not bool(len(self))
def reset(self):
self.rectangles = [] # List of placed Rectangles.

View File

@ -0,0 +1,580 @@
from .maxrects import MaxRectsBssf
import operator
import itertools
import collections
import decimal
# Float to Decimal helper
def float2dec(ft, decimal_digits):
"""
Convert float (or int) to Decimal (rounding up) with the
requested number of decimal digits.
Arguments:
ft (float, int): Number to convert
decimal (int): Number of digits after decimal point
Return:
Decimal: Number converted to decima
"""
with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_UP
places = decimal.Decimal(10)**(-decimal_digits)
return decimal.Decimal.from_float(float(ft)).quantize(places)
# Sorting algos for rectangle lists
SORT_AREA = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: r[0]*r[1]) # Sort by area
SORT_PERI = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: r[0]+r[1]) # Sort by perimeter
SORT_DIFF = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: abs(r[0]-r[1])) # Sort by Diff
SORT_SSIDE = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: (min(r[0], r[1]), max(r[0], r[1]))) # Sort by short side
SORT_LSIDE = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: (max(r[0], r[1]), min(r[0], r[1]))) # Sort by long side
SORT_RATIO = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: r[0]/r[1]) # Sort by side ratio
SORT_NONE = lambda rectlist: list(rectlist) # Unsorted
class BinFactory(object):
def __init__(self, width, height, count, pack_algo, *args, **kwargs):
self._width = width
self._height = height
self._count = count
self._pack_algo = pack_algo
self._algo_kwargs = kwargs
self._algo_args = args
self._ref_bin = None # Reference bin used to calculate fitness
self._bid = kwargs.get("bid", None)
def _create_bin(self):
return self._pack_algo(self._width, self._height, *self._algo_args, **self._algo_kwargs)
def is_empty(self):
return self._count<1
def fitness(self, width, height):
if not self._ref_bin:
self._ref_bin = self._create_bin()
return self._ref_bin.fitness(width, height)
def fits_inside(self, width, height):
# Determine if rectangle widthxheight will fit into empty bin
if not self._ref_bin:
self._ref_bin = self._create_bin()
return self._ref_bin._fits_surface(width, height)
def new_bin(self):
if self._count > 0:
self._count -= 1
return self._create_bin()
else:
return None
def __eq__(self, other):
return self._width*self._height == other._width*other._height
def __lt__(self, other):
return self._width*self._height < other._width*other._height
def __str__(self):
return "Bin: {} {} {}".format(self._width, self._height, self._count)
class PackerBNFMixin(object):
"""
BNF (Bin Next Fit): Only one open bin at a time. If the rectangle
doesn't fit, close the current bin and go to the next.
"""
def add_rect(self, width, height, rid=None):
while True:
# if there are no open bins, try to open a new one
if len(self._open_bins)==0:
# can we find an unopened bin that will hold this rect?
new_bin = self._new_open_bin(width, height, rid=rid)
if new_bin is None:
return None
# we have at least one open bin, so check if it can hold this rect
rect = self._open_bins[0].add_rect(width, height, rid=rid)
if rect is not None:
return rect
# since the rect doesn't fit, close this bin and try again
closed_bin = self._open_bins.popleft()
self._closed_bins.append(closed_bin)
class PackerBFFMixin(object):
"""
BFF (Bin First Fit): Pack rectangle in first bin it fits
"""
def add_rect(self, width, height, rid=None):
# see if this rect will fit in any of the open bins
for b in self._open_bins:
rect = b.add_rect(width, height, rid=rid)
if rect is not None:
return rect
while True:
# can we find an unopened bin that will hold this rect?
new_bin = self._new_open_bin(width, height, rid=rid)
if new_bin is None:
return None
# _new_open_bin may return a bin that's too small,
# so we have to double-check
rect = new_bin.add_rect(width, height, rid=rid)
if rect is not None:
return rect
class PackerBBFMixin(object):
"""
BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness
"""
# only create this getter once
first_item = operator.itemgetter(0)
def add_rect(self, width, height, rid=None):
# Try packing into open bins
fit = ((b.fitness(width, height), b) for b in self._open_bins)
fit = (b for b in fit if b[0] is not None)
try:
_, best_bin = min(fit, key=self.first_item)
best_bin.add_rect(width, height, rid)
return True
except ValueError:
pass
# Try packing into one of the empty bins
while True:
# can we find an unopened bin that will hold this rect?
new_bin = self._new_open_bin(width, height, rid=rid)
if new_bin is None:
return False
# _new_open_bin may return a bin that's too small,
# so we have to double-check
if new_bin.add_rect(width, height, rid):
return True
class PackerOnline(object):
"""
Rectangles are packed as soon are they are added
"""
def __init__(self, pack_algo=MaxRectsBssf, rotation=True):
"""
Arguments:
pack_algo (PackingAlgorithm): What packing algo to use
rotation (bool): Enable/Disable rectangle rotation
"""
self._rotation = rotation
self._pack_algo = pack_algo
self.reset()
def __iter__(self):
return itertools.chain(self._closed_bins, self._open_bins)
def __len__(self):
return len(self._closed_bins)+len(self._open_bins)
def __getitem__(self, key):
"""
Return bin in selected position. (excluding empty bins)
"""
if not isinstance(key, int):
raise TypeError("Indices must be integers")
size = len(self) # avoid recalulations
if key < 0:
key += size
if not 0 <= key < size:
raise IndexError("Index out of range")
if key < len(self._closed_bins):
return self._closed_bins[key]
else:
return self._open_bins[key-len(self._closed_bins)]
def _new_open_bin(self, width=None, height=None, rid=None):
"""
Extract the next empty bin and append it to open bins
Returns:
PackingAlgorithm: Initialized empty packing bin.
None: No bin big enough for the rectangle was found
"""
factories_to_delete = set() #
new_bin = None
for key, binfac in self._empty_bins.items():
# Only return the new bin if the rect fits.
# (If width or height is None, caller doesn't know the size.)
if not binfac.fits_inside(width, height):
continue
# Create bin and add to open_bins
new_bin = binfac.new_bin()
if new_bin is None:
continue
self._open_bins.append(new_bin)
# If the factory was depleted mark for deletion
if binfac.is_empty():
factories_to_delete.add(key)
break
# Delete marked factories
for f in factories_to_delete:
del self._empty_bins[f]
return new_bin
def add_bin(self, width, height, count=1, **kwargs):
# accept the same parameters as PackingAlgorithm objects
kwargs['rot'] = self._rotation
bin_factory = BinFactory(width, height, count, self._pack_algo, **kwargs)
self._empty_bins[next(self._bin_count)] = bin_factory
def rect_list(self):
rectangles = []
bin_count = 0
for abin in self:
for rect in abin:
rectangles.append((bin_count, rect.x, rect.y, rect.width, rect.height, rect.rid))
bin_count += 1
return rectangles
def bin_list(self):
"""
Return a list of the dimmensions of the bins in use, that is closed
or open containing at least one rectangle
"""
return [(b.width, b.height) for b in self]
def validate_packing(self):
for b in self:
b.validate_packing()
def reset(self):
# Bins fully packed and closed.
self._closed_bins = collections.deque()
# Bins ready to pack rectangles
self._open_bins = collections.deque()
# User provided bins not in current use
self._empty_bins = collections.OrderedDict() # O(1) deletion of arbitrary elem
self._bin_count = itertools.count()
class Packer(PackerOnline):
"""
Rectangles aren't packed untils pack() is called
"""
def __init__(self, pack_algo=MaxRectsBssf, sort_algo=SORT_NONE,
rotation=True):
"""
"""
super(Packer, self).__init__(pack_algo=pack_algo, rotation=rotation)
self._sort_algo = sort_algo
# User provided bins and Rectangles
self._avail_bins = collections.deque()
self._avail_rect = collections.deque()
# Aux vars used during packing
self._sorted_rect = []
def add_bin(self, width, height, count=1, **kwargs):
self._avail_bins.append((width, height, count, kwargs))
def add_rect(self, width, height, rid=None):
self._avail_rect.append((width, height, rid))
def _is_everything_ready(self):
return self._avail_rect and self._avail_bins
def pack(self):
self.reset()
if not self._is_everything_ready():
# maybe we should throw an error here?
return
# Add available bins to packer
for b in self._avail_bins:
width, height, count, extra_kwargs = b
super(Packer, self).add_bin(width, height, count, **extra_kwargs)
# If enabled sort rectangles
self._sorted_rect = self._sort_algo(self._avail_rect)
# Start packing
for r in self._sorted_rect:
super(Packer, self).add_rect(*r)
class PackerBNF(Packer, PackerBNFMixin):
"""
BNF (Bin Next Fit): Only one open bin, if rectangle doesn't fit
go to next bin and close current one.
"""
pass
class PackerBFF(Packer, PackerBFFMixin):
"""
BFF (Bin First Fit): Pack rectangle in first bin it fits
"""
pass
class PackerBBF(Packer, PackerBBFMixin):
"""
BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness
"""
pass
class PackerOnlineBNF(PackerOnline, PackerBNFMixin):
"""
BNF Bin Next Fit Online variant
"""
pass
class PackerOnlineBFF(PackerOnline, PackerBFFMixin):
"""
BFF Bin First Fit Online variant
"""
pass
class PackerOnlineBBF(PackerOnline, PackerBBFMixin):
"""
BBF Bin Best Fit Online variant
"""
pass
class PackerGlobal(Packer, PackerBNFMixin):
"""
GLOBAL: For each bin pack the rectangle with the best fitness.
"""
first_item = operator.itemgetter(0)
def __init__(self, pack_algo=MaxRectsBssf, rotation=True):
"""
"""
super(PackerGlobal, self).__init__(pack_algo=pack_algo,
sort_algo=SORT_NONE, rotation=rotation)
def _find_best_fit(self, pbin):
"""
Return best fitness rectangle from rectangles packing _sorted_rect list
Arguments:
pbin (PackingAlgorithm): Packing bin
Returns:
key of the rectangle with best fitness
"""
fit = ((pbin.fitness(r[0], r[1]), k) for k, r in self._sorted_rect.items())
fit = (f for f in fit if f[0] is not None)
try:
_, rect = min(fit, key=self.first_item)
return rect
except ValueError:
return None
def _new_open_bin(self, remaining_rect):
"""
Extract the next bin where at least one of the rectangles in
rem
Arguments:
remaining_rect (dict): rectangles not placed yet
Returns:
PackingAlgorithm: Initialized empty packing bin.
None: No bin big enough for the rectangle was found
"""
factories_to_delete = set() #
new_bin = None
for key, binfac in self._empty_bins.items():
# Only return the new bin if at least one of the remaining
# rectangles fit inside.
a_rectangle_fits = False
for _, rect in remaining_rect.items():
if binfac.fits_inside(rect[0], rect[1]):
a_rectangle_fits = True
break
if not a_rectangle_fits:
factories_to_delete.add(key)
continue
# Create bin and add to open_bins
new_bin = binfac.new_bin()
if new_bin is None:
continue
self._open_bins.append(new_bin)
# If the factory was depleted mark for deletion
if binfac.is_empty():
factories_to_delete.add(key)
break
# Delete marked factories
for f in factories_to_delete:
del self._empty_bins[f]
return new_bin
def pack(self):
self.reset()
if not self._is_everything_ready():
return
# Add available bins to packer
for b in self._avail_bins:
width, height, count, extra_kwargs = b
super(Packer, self).add_bin(width, height, count, **extra_kwargs)
# Store rectangles into dict for fast deletion
self._sorted_rect = collections.OrderedDict(
enumerate(self._sort_algo(self._avail_rect)))
# For each bin pack the rectangles with lowest fitness until it is filled or
# the rectangles exhausted, then open the next bin where at least one rectangle
# will fit and repeat the process until there aren't more rectangles or bins
# available.
while len(self._sorted_rect) > 0:
# Find one bin where at least one of the remaining rectangles fit
pbin = self._new_open_bin(self._sorted_rect)
if pbin is None:
break
# Pack as many rectangles as possible into the open bin
while True:
# Find 'fittest' rectangle
best_rect_key = self._find_best_fit(pbin)
if best_rect_key is None:
closed_bin = self._open_bins.popleft()
self._closed_bins.append(closed_bin)
break # None of the remaining rectangles can be packed in this bin
best_rect = self._sorted_rect[best_rect_key]
del self._sorted_rect[best_rect_key]
PackerBNFMixin.add_rect(self, *best_rect)
# Packer factory
class Enum(tuple):
__getattr__ = tuple.index
PackingMode = Enum(["Online", "Offline"])
PackingBin = Enum(["BNF", "BFF", "BBF", "Global"])
def newPacker(mode=PackingMode.Offline,
bin_algo=PackingBin.BBF,
pack_algo=MaxRectsBssf,
sort_algo=SORT_AREA,
rotation=True):
"""
Packer factory helper function
Arguments:
mode (PackingMode): Packing mode
Online: Rectangles are packed as soon are they are added
Offline: Rectangles aren't packed untils pack() is called
bin_algo (PackingBin): Bin selection heuristic
pack_algo (PackingAlgorithm): Algorithm used
rotation (boolean): Enable or disable rectangle rotation.
Returns:
Packer: Initialized packer instance.
"""
packer_class = None
# Online Mode
if mode == PackingMode.Online:
sort_algo=None
if bin_algo == PackingBin.BNF:
packer_class = PackerOnlineBNF
elif bin_algo == PackingBin.BFF:
packer_class = PackerOnlineBFF
elif bin_algo == PackingBin.BBF:
packer_class = PackerOnlineBBF
else:
raise AttributeError("Unsupported bin selection heuristic")
# Offline Mode
elif mode == PackingMode.Offline:
if bin_algo == PackingBin.BNF:
packer_class = PackerBNF
elif bin_algo == PackingBin.BFF:
packer_class = PackerBFF
elif bin_algo == PackingBin.BBF:
packer_class = PackerBBF
elif bin_algo == PackingBin.Global:
packer_class = PackerGlobal
sort_algo=None
else:
raise AttributeError("Unsupported bin selection heuristic")
else:
raise AttributeError("Unknown packing mode.")
if sort_algo:
return packer_class(pack_algo=pack_algo, sort_algo=sort_algo,
rotation=rotation)
else:
return packer_class(pack_algo=pack_algo, rotation=rotation)

View File

@ -0,0 +1,303 @@
import collections
import itertools
import operator
import heapq
import copy
from .pack_algo import PackingAlgorithm
from .geometry import Point as P
from .geometry import HSegment, Rectangle
from .waste import WasteManager
class Skyline(PackingAlgorithm):
""" Class implementing Skyline algorithm as described by
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
_skyline: stores all the segments at the top of the skyline.
_waste: Handles all wasted sections.
"""
def __init__(self, width, height, rot=True, *args, **kwargs):
"""
_skyline is the list used to store all the skyline segments, each
one is a list with the format [x, y, width] where x is the x
coordinate of the left most point of the segment, y the y coordinate
of the segment, and width the length of the segment. The initial
segment is allways [0, 0, surface_width]
Arguments:
width (int, float):
height (int, float):
rot (bool): Enable or disable rectangle rotation
"""
self._waste_management = False
self._waste = WasteManager(rot=rot)
super(Skyline, self).__init__(width, height, rot, merge=False, *args, **kwargs)
def _placement_points_generator(self, skyline, width):
"""Returns a generator for the x coordinates of all the placement
points on the skyline for a given rectangle.
WARNING: In some cases could be duplicated points, but it is faster
to compute them twice than to remove them.
Arguments:
skyline (list): Skyline HSegment list
width (int, float): Rectangle width
Returns:
generator
"""
skyline_r = skyline[-1].right
skyline_l = skyline[0].left
# Placements using skyline segment left point
ppointsl = (s.left for s in skyline if s.left+width <= skyline_r)
# Placements using skyline segment right point
ppointsr = (s.right-width for s in skyline if s.right-width >= skyline_l)
# Merge positions
return heapq.merge(ppointsl, ppointsr)
def _generate_placements(self, width, height):
"""
Generate a list with
Arguments:
skyline (list): SkylineHSegment list
width (number):
Returns:
tuple (Rectangle, fitness):
Rectangle: Rectangle in valid position
left_skyline: Index for the skyline under the rectangle left edge.
right_skyline: Index for the skyline under the rectangle right edte.
"""
skyline = self._skyline
points = collections.deque()
left_index = right_index = 0 # Left and right side skyline index
support_height = skyline[0].top
support_index = 0
placements = self._placement_points_generator(skyline, width)
for p in placements:
# If Rectangle's right side changed segment, find new support
if p+width > skyline[right_index].right:
for right_index in range(right_index+1, len(skyline)):
if skyline[right_index].top >= support_height:
support_index = right_index
support_height = skyline[right_index].top
if p+width <= skyline[right_index].right:
break
# If left side changed segment.
if p >= skyline[left_index].right:
left_index +=1
# Find new support if the previous one was shifted out.
if support_index < left_index:
support_index = left_index
support_height = skyline[left_index].top
for i in range(left_index, right_index+1):
if skyline[i].top >= support_height:
support_index = i
support_height = skyline[i].top
# Add point if there is enought room at the top
if support_height+height <= self.height:
points.append((Rectangle(p, support_height, width, height),\
left_index, right_index))
return points
def _merge_skyline(self, skylineq, segment):
"""
Arguments:
skylineq (collections.deque):
segment (HSegment):
"""
if len(skylineq) == 0:
skylineq.append(segment)
return
if skylineq[-1].top == segment.top:
s = skylineq[-1]
skylineq[-1] = HSegment(s.start, s.length+segment.length)
else:
skylineq.append(segment)
def _add_skyline(self, rect):
"""
Arguments:
seg (Rectangle):
"""
skylineq = collections.deque([]) # Skyline after adding new one
for sky in self._skyline:
if sky.right <= rect.left or sky.left >= rect.right:
self._merge_skyline(skylineq, sky)
continue
if sky.left < rect.left and sky.right > rect.left:
# Skyline section partially under segment left
self._merge_skyline(skylineq,
HSegment(sky.start, rect.left-sky.left))
sky = HSegment(P(rect.left, sky.top), sky.right-rect.left)
if sky.left < rect.right:
if sky.left == rect.left:
self._merge_skyline(skylineq,
HSegment(P(rect.left, rect.top), rect.width))
# Skyline section partially under segment right
if sky.right > rect.right:
self._merge_skyline(skylineq,
HSegment(P(rect.right, sky.top), sky.right-rect.right))
sky = HSegment(sky.start, rect.right-sky.left)
if sky.left >= rect.left and sky.right <= rect.right:
# Skyline section fully under segment, account for wasted space
if self._waste_management and sky.top < rect.bottom:
self._waste.add_waste(sky.left, sky.top,
sky.length, rect.bottom - sky.top)
else:
# Segment
self._merge_skyline(skylineq, sky)
# Aaaaand ..... Done
self._skyline = list(skylineq)
def _rect_fitness(self, rect, left_index, right_index):
return rect.top
def _select_position(self, width, height):
"""
Search for the placement with the bes fitness for the rectangle.
Returns:
tuple (Rectangle, fitness) - Rectangle placed in the fittest position
None - Rectangle couldn't be placed
"""
positions = self._generate_placements(width, height)
if self.rot and width != height:
positions += self._generate_placements(height, width)
if not positions:
return None, None
return min(((p[0], self._rect_fitness(*p))for p in positions),
key=operator.itemgetter(1))
def fitness(self, width, height):
"""Search for the best fitness
"""
assert(width > 0 and height >0)
if width > max(self.width, self.height) or\
height > max(self.height, self.width):
return None
# If there is room in wasted space, FREE PACKING!!
if self._waste_management:
if self._waste.fitness(width, height) is not None:
return 0
# Get best fitness segment, for normal rectangle, and for
# rotated rectangle if rotation is enabled.
rect, fitness = self._select_position(width, height)
return fitness
def add_rect(self, width, height, rid=None):
"""
Add new rectangle
"""
assert(width > 0 and height > 0)
if width > max(self.width, self.height) or\
height > max(self.height, self.width):
return None
rect = None
# If Waste managment is enabled, first try to place the rectangle there
if self._waste_management:
rect = self._waste.add_rect(width, height, rid)
# Get best possible rectangle position
if not rect:
rect, _ = self._select_position(width, height)
if rect:
self._add_skyline(rect)
if rect is None:
return None
# Store rectangle, and recalculate skyline
rect.rid = rid
self.rectangles.append(rect)
return rect
def reset(self):
super(Skyline, self).reset()
self._skyline = [HSegment(P(0, 0), self.width)]
self._waste.reset()
class SkylineWMixin(Skyline):
"""Waste managment mixin"""
def __init__(self, width, height, *args, **kwargs):
super(SkylineWMixin, self).__init__(width, height, *args, **kwargs)
self._waste_management = True
class SkylineMwf(Skyline):
"""Implements Min Waste fit heuristic, minimizing the area wasted under the
rectangle.
"""
def _rect_fitness(self, rect, left_index, right_index):
waste = 0
for seg in self._skyline[left_index:right_index+1]:
waste +=\
(min(rect.right, seg.right)-max(rect.left, seg.left)) *\
(rect.bottom-seg.top)
return waste
def _rect_fitnes2s(self, rect, left_index, right_index):
waste = ((min(rect.right, seg.right)-max(rect.left, seg.left)) for seg in self._skyline[left_index:right_index+1])
return sum(waste)
class SkylineMwfl(Skyline):
"""Implements Min Waste fit with low profile heuritic, minimizing the area
wasted below the rectangle, at the same time it tries to keep the height
minimal.
"""
def _rect_fitness(self, rect, left_index, right_index):
waste = 0
for seg in self._skyline[left_index:right_index+1]:
waste +=\
(min(rect.right, seg.right)-max(rect.left, seg.left)) *\
(rect.bottom-seg.top)
return waste*self.width*self.height+rect.top
class SkylineBl(Skyline):
"""Implements Bottom Left heuristic, the best fit option is that which
results in which the top side of the rectangle lies at the bottom-most
position.
"""
def _rect_fitness(self, rect, left_index, right_index):
return rect.top
class SkylineBlWm(SkylineBl, SkylineWMixin):
pass
class SkylineMwfWm(SkylineMwf, SkylineWMixin):
pass
class SkylineMwflWm(SkylineMwfl, SkylineWMixin):
pass

View File

@ -0,0 +1,23 @@
from .guillotine import GuillotineBafMinas
from .geometry import Rectangle
class WasteManager(GuillotineBafMinas):
def __init__(self, rot=True, merge=True, *args, **kwargs):
super(WasteManager, self).__init__(1, 1, rot=rot, merge=merge, *args, **kwargs)
def add_waste(self, x, y, width, height):
"""Add new waste section"""
self._add_section(Rectangle(x, y, width, height))
def _fits_surface(self, width, height):
raise NotImplementedError
def validate_packing(self):
raise NotImplementedError
def reset(self):
super(WasteManager, self).reset()
self._sections = []