581 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			581 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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)
 | |
| 
 | |
| 
 |