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)
 |