369 lines
13 KiB
Python
369 lines
13 KiB
Python
|
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
|
||
|
|
||
|
|
||
|
|