├── tests ├── __init__.py ├── test_waste.py ├── test_decimal.py ├── test_enclose.py ├── test_factory.py ├── test_collisions.py ├── test_stats.py ├── test_guillotine.py ├── test_maxrects.py ├── test_geometry.py └── test_skyline.py ├── setup.cfg ├── docs ├── skyline.png └── maxrects.png ├── .travis.yml ├── rectpack ├── waste.py ├── __init__.py ├── pack_algo.py ├── enclose.py ├── maxrects.py ├── geometry.py ├── skyline.py ├── guillotine.py └── packer.py ├── setup.py ├── .gitignore ├── README.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /docs/skyline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secnot/rectpack/HEAD/docs/skyline.png -------------------------------------------------------------------------------- /docs/maxrects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secnot/rectpack/HEAD/docs/maxrects.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.5" 5 | - "3.6" 6 | - "nightly" 7 | install: 8 | - pip install . 9 | # command to run tests 10 | script: python setup.py test 11 | 12 | -------------------------------------------------------------------------------- /rectpack/waste.py: -------------------------------------------------------------------------------- 1 | from .guillotine import GuillotineBafMinas 2 | from .geometry import Rectangle 3 | 4 | 5 | 6 | class WasteManager(GuillotineBafMinas): 7 | 8 | def __init__(self, rot=True, merge=True, *args, **kwargs): 9 | super(WasteManager, self).__init__(1, 1, rot=rot, merge=merge, *args, **kwargs) 10 | 11 | def add_waste(self, x, y, width, height): 12 | """Add new waste section""" 13 | self._add_section(Rectangle(x, y, width, height)) 14 | 15 | def _fits_surface(self, width, height): 16 | raise NotImplementedError 17 | 18 | def validate_packing(self): 19 | raise NotImplementedError 20 | 21 | def reset(self): 22 | super(WasteManager, self).reset() 23 | self._sections = [] 24 | -------------------------------------------------------------------------------- /rectpack/__init__.py: -------------------------------------------------------------------------------- 1 | from .guillotine import GuillotineBssfSas, GuillotineBssfLas, \ 2 | GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \ 3 | GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \ 4 | GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \ 5 | GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \ 6 | GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \ 7 | GuillotineBafMinas 8 | 9 | from .maxrects import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf 10 | 11 | from .skyline import SkylineMwf, SkylineMwfl, SkylineBl, \ 12 | SkylineBlWm, SkylineMwfWm, SkylineMwflWm 13 | 14 | from .packer import SORT_AREA, SORT_PERI, SORT_DIFF, SORT_SSIDE, \ 15 | SORT_LSIDE, SORT_RATIO, SORT_NONE 16 | 17 | from .packer import PackerBNF, PackerBFF, PackerBBF, PackerOnlineBNF, \ 18 | PackerOnlineBFF, PackerOnlineBBF, PackerGlobal, newPacker, \ 19 | PackingMode, PackingBin, float2dec 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | long_description = """A collection of heuristic algorithms for solving the 2D knapsack problem, 4 | also known as the bin packing problem. In essence packing a set of rectangles into the 5 | smallest number of bins.""" 6 | 7 | setup( 8 | name="rectpack", 9 | version="0.2.2", 10 | description="2D Rectangle packing library", 11 | long_description=long_description, 12 | url="https://github.com/secnot/rectpack/", 13 | author="SecNot", 14 | keywords=["knapsack", "rectangle", "packing 2D", "bin", "binpacking"], 15 | license="Apache-2.0", 16 | classifiers=[ 17 | "Development Status :: 3 - Alpha", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: Apache Software License", 21 | ], 22 | packages=["rectpack"], 23 | zip_safe=False, 24 | test_suite="nose.collector", 25 | tests_require=["nose"], 26 | ) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Vim temp files 60 | *~ 61 | *.swp 62 | *.swo 63 | 64 | -------------------------------------------------------------------------------- /tests/test_waste.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import rectpack.waste as waste 3 | from rectpack.geometry import Rectangle 4 | 5 | class TestWaste(TestCase): 6 | 7 | def test_init(self): 8 | w = waste.WasteManager() 9 | 10 | def test_add_waste(self): 11 | w = waste.WasteManager() 12 | w.add_waste(30, 40, 50, 50) 13 | w.add_waste(5, 5, 20, 20) 14 | w.add_waste(0, 100, 100, 100) 15 | 16 | rect1 = w.add_rect(20, 20) 17 | rect2 = w.add_rect(45, 40) 18 | rect3 = w.add_rect(90, 80) 19 | rect4 = w.add_rect(100, 100) 20 | rect5 = w.add_rect(10, 10) 21 | 22 | self.assertEqual(rect1, Rectangle(5, 5, 20, 20)) 23 | self.assertEqual(rect2, Rectangle(30, 40, 45, 40)) 24 | self.assertEqual(rect3, Rectangle(0, 100, 90, 80)) 25 | self.assertEqual(rect4, None) 26 | self.assertEqual(rect5, Rectangle(30, 80, 10, 10)) 27 | 28 | self.assertEqual(len(w), 4) 29 | 30 | # Test merge is enabled for new waste 31 | w = waste.WasteManager() 32 | w.add_waste(0, 0, 50, 50) 33 | w.add_waste(50, 0, 50, 50) 34 | self.assertEqual(len(w._sections), 1) 35 | self.assertEqual(w.add_rect(100, 50), Rectangle(0, 0, 100, 50)) 36 | 37 | def test_empty(self): 38 | # Test it is empty by default 39 | w = waste.WasteManager() 40 | self.assertFalse(w.add_rect(1, 1), None) 41 | 42 | def test_add_rect(self): 43 | w = waste.WasteManager(rot=False) 44 | w.add_waste(50, 40, 100, 40) 45 | self.assertEqual(w.add_rect(30, 80), None) 46 | 47 | # Test with rotation 48 | w = waste.WasteManager(rot=True) 49 | w.add_waste(50, 40, 100, 40) 50 | self.assertEqual(w.add_rect(30, 80), Rectangle(50, 40, 80, 30)) 51 | 52 | # Test rectangle rid 53 | w = waste.WasteManager(rot=True) 54 | w.add_waste(50, 40, 100, 40) 55 | rect = w.add_rect(30, 80, rid=23) 56 | self.assertEqual(rect.rid, 23) 57 | 58 | 59 | def test_iter(self): 60 | # Iterate through rectangles 61 | w = waste.WasteManager() 62 | w.add_waste(30, 40, 50, 50) 63 | w.add_waste(5, 5, 20, 20) 64 | w.add_waste(0, 100, 100, 100) 65 | 66 | w.add_rect(50, 50) 67 | 68 | for r in w: 69 | self.assertEqual(r, Rectangle(30, 40, 50, 50)) 70 | 71 | self.assertEqual(len(w), 1) 72 | -------------------------------------------------------------------------------- /tests/test_decimal.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import random 3 | import math 4 | import decimal 5 | 6 | from rectpack.guillotine import GuillotineBssfSas 7 | from rectpack.maxrects import MaxRectsBssf 8 | from rectpack.skyline import SkylineMwfWm 9 | from rectpack.packer import PackerBFF, float2dec 10 | 11 | 12 | def random_rectangle(max_side, min_side): 13 | width = decimal.Decimal(str(round(random.uniform(max_side, min_side), 1))) 14 | height = decimal.Decimal(str(round(random.uniform(max_side, min_side), 1))) 15 | 16 | return (width, height) 17 | 18 | 19 | def random_rectangle_generator(num, max_side=30, min_side=8): 20 | """ 21 | Generate a random rectangle list with dimensions within 22 | specified parameters. 23 | 24 | Arguments: 25 | max_dim (number): Max rectangle side length 26 | min_side (number): Min rectangle side length 27 | max_ratio (number): 28 | 29 | Returns: 30 | Rectangle list 31 | """ 32 | return (random_rectangle(max_side, min_side) for i in range(0, num)) 33 | 34 | 35 | class TestDecimal(TestCase): 36 | """ 37 | Test all work when using decimal instead of integers 38 | """ 39 | def setUp(self): 40 | self.rectangles = [r for r in random_rectangle_generator(500)] 41 | self.bins = [(80, 80, 1), (100, 100, 30)] 42 | 43 | def setup_packer(self, packer): 44 | for b in self.bins: 45 | packer.add_bin(b[0], b[1], b[2]) 46 | 47 | for r in self.rectangles: 48 | packer.add_rect(r[0], r[1]) 49 | 50 | def test_maxrects(self): 51 | m = PackerBFF(pack_algo=MaxRectsBssf, rotation=True) 52 | self.setup_packer(m) 53 | m.pack() 54 | m.validate_packing() 55 | self.assertTrue(len(m)>1) 56 | 57 | def test_guillotine(self): 58 | g = PackerBFF(pack_algo=GuillotineBssfSas, rotation=True) 59 | self.setup_packer(g) 60 | g.pack() 61 | g.validate_packing() 62 | self.assertTrue(len(g)>1) 63 | 64 | def test_skyline(self): 65 | s = PackerBFF(pack_algo=SkylineMwfWm, rotation=True) 66 | self.setup_packer(s) 67 | s.pack() 68 | s.validate_packing() 69 | self.assertTrue(len(s)>1) 70 | 71 | 72 | class TestFloat2Dec(TestCase): 73 | 74 | def test_rounding(self): 75 | """Test rounding is allways up""" 76 | d = float2dec(3.141511, 3) 77 | self.assertEqual(decimal.Decimal('3.142'), d) 78 | 79 | d = float2dec(3.444444, 3) 80 | self.assertEqual(decimal.Decimal('3.445'), d) 81 | 82 | d = float2dec(3.243234, 0) 83 | self.assertEqual(decimal.Decimal('4'), d) 84 | 85 | d = float2dec(7.234234, 0) 86 | self.assertEqual(decimal.Decimal('8'), d) 87 | 88 | def test_decimal_places(self): 89 | """Test rounded to correct decimal place""" 90 | d = float2dec(4.2, 3) 91 | self.assertEqual(decimal.Decimal('4.201'), d) 92 | 93 | d = float2dec(5.7, 3) 94 | self.assertEqual(decimal.Decimal('5.701'), d) 95 | 96 | d = float2dec(2.2, 4) 97 | self.assertEqual(decimal.Decimal('2.2001'), d) 98 | 99 | def test_integer(self): 100 | """Test integers are also converted, but not rounded""" 101 | d = float2dec(7, 3) 102 | self.assertEqual(decimal.Decimal('7.000'), d) 103 | 104 | d = float2dec(2, 3) 105 | self.assertEqual(decimal.Decimal('2.000'), d) 106 | 107 | def test_not_rounded(self): 108 | """Test floats are only rounded when needed""" 109 | d = float2dec(3.0, 3) 110 | self.assertEqual(decimal.Decimal('3.000'), d) 111 | -------------------------------------------------------------------------------- /tests/test_enclose.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import rectpack.enclose as enclose 3 | import random 4 | 5 | 6 | 7 | def random_rectangle(max_side, min_side): 8 | width = random.randint(min_side, max_side) 9 | height = random.randint(min_side, max_side) 10 | return (width, height) 11 | 12 | 13 | 14 | def random_rectangle_generator(num, max_side=30, min_side=8): 15 | """ 16 | Generate a random rectangle list with dimensions within 17 | specified parameters. 18 | 19 | Arguments: 20 | max_dim (number): Max rectangle side length 21 | min_side (number): Min rectangle side length 22 | max_ratio (number): 23 | 24 | Returns: 25 | Rectangle list 26 | """ 27 | return (random_rectangle(max_side, min_side) for i in range(0, num)) 28 | 29 | 30 | 31 | class TestEnclose(TestCase): 32 | 33 | def setUp(self): 34 | from math import sqrt, ceil 35 | self.rectangles = [r for r in random_rectangle_generator(100)] 36 | self.area = sum(r[0]*r[1] for r in self.rectangles) 37 | self.max_width = ceil(sqrt(self.area)*1.20) 38 | self.max_height = ceil(sqrt(self.area)*1.80) 39 | 40 | def test_container_candidates(self): 41 | # Test without rotation 42 | en = enclose.Enclose(rotation=False) 43 | en.add_rect(2, 100) 44 | en.add_rect(3, 100) 45 | candidates = en._container_candidates() 46 | 47 | self.assertTrue((5, 200) in candidates) 48 | self.assertTrue((3, 200) in candidates) 49 | self.assertFalse((2, 200) in candidates) 50 | 51 | # Test with rotation 52 | en = enclose.Enclose(rotation=True) 53 | en.add_rect(2, 100) 54 | en.add_rect(3, 100) 55 | candidates = en._container_candidates() 56 | 57 | self.assertTrue((100, 200) in candidates) 58 | self.assertTrue((200, 200) in candidates) 59 | self.assertTrue((105, 200) in candidates) 60 | 61 | # Width limit 62 | en = enclose.Enclose(max_width=180) 63 | en.add_rect(2, 100) 64 | en.add_rect(3, 100) 65 | candidates = en._container_candidates() 66 | 67 | self.assertTrue((100, 200) in candidates) 68 | self.assertFalse((200, 200) in candidates) 69 | 70 | # Height limit 71 | en = enclose.Enclose(max_height=180) 72 | en.add_rect(2, 100) 73 | en.add_rect(3, 100) 74 | candidates = en._container_candidates() 75 | 76 | self.assertTrue((3, 180) in candidates) 77 | self.assertTrue((100, 180) in candidates) 78 | self.assertTrue((200, 180) in candidates) 79 | 80 | def test_containment(self): 81 | """ 82 | Test all rectangles are inside the container area 83 | """ 84 | en = enclose.Enclose(self.rectangles, self.max_width, self.max_height, True) 85 | packer = en.generate() 86 | 87 | # Check all rectangles are inside container 88 | packer.validate_packing() 89 | 90 | def test_add_rect(self): 91 | en = enclose.Enclose([], 100, 100, True) 92 | en.add_rect(10, 10) 93 | en.add_rect(20, 20) 94 | 95 | packer = en.generate() 96 | self.assertEqual(packer.width, 30) 97 | self.assertEqual(packer.height, 20) 98 | 99 | en.add_rect(50, 50) 100 | packer = en.generate() 101 | self.assertEqual(packer.width, 50) 102 | self.assertEqual(packer.height, 70) 103 | 104 | def test_failed_envelope(self): 105 | """ 106 | Test container not found 107 | """ 108 | en = enclose.Enclose(self.rectangles, max_width=50, max_height=50) 109 | packer = en.generate() 110 | self.assertEqual(packer, None) 111 | 112 | en = enclose.Enclose(max_width=50, max_height=50) 113 | packer = en.generate() 114 | self.assertEqual(packer, None) 115 | -------------------------------------------------------------------------------- /rectpack/pack_algo.py: -------------------------------------------------------------------------------- 1 | from .geometry import Rectangle 2 | 3 | 4 | class PackingAlgorithm(object): 5 | """PackingAlgorithm base class""" 6 | 7 | def __init__(self, width, height, rot=True, bid=None, *args, **kwargs): 8 | """ 9 | Initialize packing algorithm 10 | 11 | Arguments: 12 | width (int, float): Packing surface width 13 | height (int, float): Packing surface height 14 | rot (bool): Rectangle rotation enabled or disabled 15 | bid (string|int|...): Packing surface identification 16 | """ 17 | self.width = width 18 | self.height = height 19 | self.rot = rot 20 | self.rectangles = [] 21 | self.bid = bid 22 | self._surface = Rectangle(0, 0, width, height) 23 | self.reset() 24 | 25 | def __len__(self): 26 | return len(self.rectangles) 27 | 28 | def __iter__(self): 29 | return iter(self.rectangles) 30 | 31 | def _fits_surface(self, width, height): 32 | """ 33 | Test surface is big enough to place a rectangle 34 | 35 | Arguments: 36 | width (int, float): Rectangle width 37 | height (int, float): Rectangle height 38 | 39 | Returns: 40 | boolean: True if it could be placed, False otherwise 41 | """ 42 | assert(width > 0 and height > 0) 43 | if self.rot and (width > self.width or height > self.height): 44 | width, height = height, width 45 | 46 | if width > self.width or height > self.height: 47 | return False 48 | else: 49 | return True 50 | 51 | def __getitem__(self, key): 52 | """ 53 | Return rectangle in selected position. 54 | """ 55 | return self.rectangles[key] 56 | 57 | def used_area(self): 58 | """ 59 | Total area of rectangles placed 60 | 61 | Returns: 62 | int, float: Area 63 | """ 64 | return sum(r.area() for r in self) 65 | 66 | def fitness(self, width, height, rot = False): 67 | """ 68 | Metric used to rate how much space is wasted if a rectangle is placed. 69 | Returns a value greater or equal to zero, the smaller the value the more 70 | 'fit' is the rectangle. If the rectangle can't be placed, returns None. 71 | 72 | Arguments: 73 | width (int, float): Rectangle width 74 | height (int, float): Rectangle height 75 | rot (bool): Enable rectangle rotation 76 | 77 | Returns: 78 | int, float: Rectangle fitness 79 | None: Rectangle can't be placed 80 | """ 81 | raise NotImplementedError 82 | 83 | def add_rect(self, width, height, rid=None): 84 | """ 85 | Add rectangle of widthxheight dimensions. 86 | 87 | Arguments: 88 | width (int, float): Rectangle width 89 | height (int, float): Rectangle height 90 | rid: Optional rectangle user id 91 | 92 | Returns: 93 | Rectangle: Rectangle with placemente coordinates 94 | None: If the rectangle couldn be placed. 95 | """ 96 | raise NotImplementedError 97 | 98 | def rect_list(self): 99 | """ 100 | Returns a list with all rectangles placed into the surface. 101 | 102 | Returns: 103 | List: Format [(x, y, width, height, rid), ...] 104 | """ 105 | rectangle_list = [] 106 | for r in self: 107 | rectangle_list.append((r.x, r.y, r.width, r.height, r.rid)) 108 | 109 | return rectangle_list 110 | 111 | def validate_packing(self): 112 | """ 113 | Check for collisions between rectangles, also check all are placed 114 | inside surface. 115 | """ 116 | surface = Rectangle(0, 0, self.width, self.height) 117 | 118 | for r in self: 119 | if not surface.contains(r): 120 | raise Exception("Rectangle placed outside surface") 121 | 122 | 123 | rectangles = [r for r in self] 124 | if len(rectangles) <= 1: 125 | return 126 | 127 | for r1 in range(0, len(rectangles)-2): 128 | for r2 in range(r1+1, len(rectangles)-1): 129 | if rectangles[r1].intersects(rectangles[r2]): 130 | raise Exception("Rectangle collision detected") 131 | 132 | def is_empty(self): 133 | # Returns true if there is no rectangles placed. 134 | return not bool(len(self)) 135 | 136 | def reset(self): 137 | self.rectangles = [] # List of placed Rectangles. 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from rectpack.packer import newPacker, PackingMode, PackingBin 3 | import random 4 | 5 | 6 | class TestFactory(TestCase): 7 | 8 | def setUp(self): 9 | self.rectangles = [(w, h) for w in range(8, 50, 8) for h in range(8, 50, 8)] 10 | 11 | def _common(self, mode, bin_algo, width, height): 12 | # create a new packer 13 | p = newPacker(mode, bin_algo) 14 | # as many bins as rectangles should be enough 15 | p.add_bin(width, height, count=len(self.rectangles)) 16 | # try to add the rectangles 17 | for r in self.rectangles: 18 | p.add_rect(*r) 19 | # pack them, if in offline mode 20 | if mode == PackingMode.Offline: 21 | p.pack() 22 | # provide the results for validation 23 | return p 24 | 25 | def test_Offline_BNF_big_enough(self): 26 | # create bins that are big enough to hold the rectangles 27 | p = self._common(PackingMode.Offline, PackingBin.BNF, 50, 50) 28 | # check that bins were created 29 | self.assertGreater(len(p.bin_list()), 0) 30 | # check that all of the rectangles made it in 31 | self.assertEqual(len(p.rect_list()), len(self.rectangles)) 32 | 33 | def test_Offline_BNF_too_small(self): 34 | # create bins that are too small to hold the rectangles 35 | p = self._common(PackingMode.Offline, PackingBin.BNF, 5, 5) 36 | # check that none of the rectangles made it in 37 | self.assertEqual(len(p.rect_list()), 0) 38 | 39 | def test_Offline_BFF_big_enough(self): 40 | # create bins that are big enough to hold the rectangles 41 | p = self._common(PackingMode.Offline, PackingBin.BFF, 50, 50) 42 | # check that bins were created 43 | self.assertGreater(len(p.bin_list()), 0) 44 | # check that all of the rectangles made it in 45 | self.assertEqual(len(p.rect_list()), len(self.rectangles)) 46 | 47 | def test_Offline_BFF_too_small(self): 48 | # create bins that are too small to hold the rectangles 49 | p = self._common(PackingMode.Offline, PackingBin.BFF, 5, 5) 50 | # check that none of the rectangles made it in 51 | self.assertEqual(len(p.rect_list()), 0) 52 | 53 | def test_Offline_BBF_big_enough(self): 54 | # create bins that are big enough to hold the rectangles 55 | p = self._common(PackingMode.Offline, PackingBin.BBF, 50, 50) 56 | # check that bins were created 57 | self.assertGreater(len(p.bin_list()), 0) 58 | # check that all of the rectangles made it in 59 | self.assertEqual(len(p.rect_list()), len(self.rectangles)) 60 | 61 | def test_Offline_BBF_too_small(self): 62 | # create bins that are too small to hold the rectangles 63 | p = self._common(PackingMode.Offline, PackingBin.BBF, 5, 5) 64 | # check that none of the rectangles made it in 65 | self.assertEqual(len(p.rect_list()), 0) 66 | 67 | def test_Offline_Global_big_enough(self): 68 | # create bins that are big enough to hold the rectangles 69 | p = self._common(PackingMode.Offline, PackingBin.Global, 50, 50) 70 | # check that bins were created 71 | self.assertGreater(len(p.bin_list()), 0) 72 | # check that all of the rectangles made it in 73 | self.assertEqual(len(p.rect_list()), len(self.rectangles)) 74 | 75 | def test_Offline_Global_too_small(self): 76 | # create bins that are too small to hold the rectangles 77 | p = self._common(PackingMode.Offline, PackingBin.Global, 5, 5) 78 | # check that none of the rectangles made it in 79 | self.assertEqual(len(p.rect_list()), 0) 80 | 81 | 82 | def helper(): 83 | """create a bunch of tests to copy and paste into TestFactory""" 84 | for mode in PackingMode: 85 | for bin_algo in PackingBin: 86 | for size, w, h in ('big_enough', 50, 50), ('too_small', 5, 5): 87 | name = '_'.join(('test', mode, bin_algo, size)) 88 | print("""\ 89 | def %s(self): 90 | # create bins that are %s to hold the rectangles 91 | p = self._common(PackingMode.%s, PackingBin.%s, %s, %s)""" % 92 | (name, size.replace('_', ' '), mode, bin_algo, w, h)) 93 | if size == 'big_enough': 94 | print("""\ 95 | # check that bins were created 96 | self.assertGreater(len(p.bin_list()), 0) 97 | # check that all of the rectangles made it in 98 | self.assertEqual(len(p.rect_list()), len(self.rectangles)) 99 | """) 100 | else: 101 | print("""\ 102 | # check that none of the rectangles made it in 103 | self.assertEqual(len(p.rect_list()), 0) 104 | """) 105 | -------------------------------------------------------------------------------- /rectpack/enclose.py: -------------------------------------------------------------------------------- 1 | import heapq # heapq.heappush, heapq.heappop 2 | from .packer import newPacker, PackingMode, PackingBin, SORT_LSIDE 3 | from .skyline import SkylineBlWm 4 | 5 | 6 | 7 | class Enclose(object): 8 | 9 | def __init__(self, rectangles=[], max_width=None, max_height=None, rotation=True): 10 | """ 11 | Arguments: 12 | rectangles (list): Rectangle to be enveloped 13 | [(width1, height1), (width2, height2), ...] 14 | max_width (number|None): Enveloping rectangle max allowed width. 15 | max_height (number|None): Enveloping rectangle max allowed height. 16 | rotation (boolean): Enable/Disable rectangle rotation. 17 | """ 18 | # Enclosing rectangle max width 19 | self._max_width = max_width 20 | 21 | # Encloseing rectangle max height 22 | self._max_height = max_height 23 | 24 | # Enable or disable rectangle rotation 25 | self._rotation = rotation 26 | 27 | # Default packing algorithm 28 | self._pack_algo = SkylineBlWm 29 | 30 | # rectangles to enclose [(width, height), (width, height, ...)] 31 | self._rectangles = [] 32 | for r in rectangles: 33 | self.add_rect(*r) 34 | 35 | def _container_candidates(self): 36 | """Generate container candidate list 37 | 38 | Returns: 39 | tuple list: [(width1, height1), (width2, height2), ...] 40 | """ 41 | if not self._rectangles: 42 | return [] 43 | 44 | if self._rotation: 45 | sides = sorted(side for rect in self._rectangles for side in rect) 46 | max_height = sum(max(r[0], r[1]) for r in self._rectangles) 47 | min_width = max(min(r[0], r[1]) for r in self._rectangles) 48 | max_width = max_height 49 | else: 50 | sides = sorted(r[0] for r in self._rectangles) 51 | max_height = sum(r[1] for r in self._rectangles) 52 | min_width = max(r[0] for r in self._rectangles) 53 | max_width = sum(sides) 54 | 55 | if self._max_width and self._max_width < max_width: 56 | max_width = self._max_width 57 | 58 | if self._max_height and self._max_height < max_height: 59 | max_height = self._max_height 60 | 61 | assert(max_width>min_width) 62 | 63 | # Generate initial container widths 64 | candidates = [max_width, min_width] 65 | 66 | width = 0 67 | for s in reversed(sides): 68 | width += s 69 | candidates.append(width) 70 | 71 | width = 0 72 | for s in sides: 73 | width += s 74 | candidates.append(width) 75 | 76 | candidates.append(max_width) 77 | candidates.append(min_width) 78 | 79 | # Remove duplicates and widths too big or small 80 | seen = set() 81 | seen_add = seen.add 82 | candidates = [x for x in candidates if not(x in seen or seen_add(x))] 83 | candidates = [x for x in candidates if not(x>max_width or x=min_area] 88 | 89 | def _refine_candidate(self, width, height): 90 | """ 91 | Use bottom-left packing algorithm to find a lower height for the 92 | container. 93 | 94 | Arguments: 95 | width 96 | height 97 | 98 | Returns: 99 | tuple (width, height, PackingAlgorithm): 100 | """ 101 | packer = newPacker(PackingMode.Offline, PackingBin.BFF, 102 | pack_algo=self._pack_algo, sort_algo=SORT_LSIDE, 103 | rotation=self._rotation) 104 | packer.add_bin(width, height) 105 | 106 | for r in self._rectangles: 107 | packer.add_rect(*r) 108 | 109 | packer.pack() 110 | 111 | # Check all rectangles where packed 112 | if len(packer[0]) != len(self._rectangles): 113 | return None 114 | 115 | # Find highest rectangle 116 | new_height = max(packer[0], key=lambda x: x.top).top 117 | return(width, new_height, packer) 118 | 119 | def generate(self): 120 | 121 | # Generate initial containers 122 | candidates = self._container_candidates() 123 | if not candidates: 124 | return None 125 | 126 | # Refine candidates and return the one with the smaller area 127 | containers = [self._refine_candidate(*c) for c in candidates] 128 | containers = [c for c in containers if c] 129 | if not containers: 130 | return None 131 | 132 | width, height, packer = min(containers, key=lambda x: x[0]*x[1]) 133 | 134 | packer.width = width 135 | packer.height = height 136 | return packer 137 | 138 | def add_rect(self, width, height): 139 | """ 140 | Add anoter rectangle to be enclosed 141 | 142 | Arguments: 143 | width (number): Rectangle width 144 | height (number): Rectangle height 145 | """ 146 | self._rectangles.append((width, height)) 147 | 148 | 149 | -------------------------------------------------------------------------------- /tests/test_collisions.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import random 3 | 4 | import rectpack.guillotine as guillotine 5 | import rectpack.skyline as skyline 6 | import rectpack.maxrects as maxrects 7 | import rectpack.packer as packer 8 | 9 | 10 | def random_rectangle(max_side, min_side): 11 | width = random.randint(min_side, max_side) 12 | height = random.randint(min_side, max_side) 13 | return (width, height) 14 | 15 | 16 | def random_rectangle_generator(num, max_side=30, min_side=8): 17 | """ 18 | Generate a random rectangle list with dimensions within 19 | specified parameters. 20 | 21 | Arguments: 22 | max_dim (number): Max rectangle side length 23 | min_side (number): Min rectangle side length 24 | max_ratio (number): 25 | 26 | Returns: 27 | Rectangle list 28 | """ 29 | return (random_rectangle(max_side, min_side) for i in range(0, num)) 30 | 31 | 32 | 33 | class TestCollisions(TestCase): 34 | 35 | def setUp(self): 36 | self.rectangles = [r for r in random_rectangle_generator(1000)] 37 | self.bins = [(100, 100, 1), (150, 150, 20)] 38 | 39 | def setup_packer(self, packer): 40 | for b in self.bins: 41 | packer.add_bin(b[0], b[1], b[2]) 42 | 43 | for r in self.rectangles: 44 | packer.add_rect(r[0], r[1]) 45 | 46 | def test_maxrects_bl(self): 47 | m = packer.PackerBBF(pack_algo=maxrects.MaxRectsBl, 48 | sort_algo=packer.SORT_LSIDE, rotation=True) 49 | self.setup_packer(m) 50 | m.pack() 51 | m.validate_packing() 52 | 53 | def test_maxrects_bssf(self): 54 | m = packer.PackerBBF(pack_algo=maxrects.MaxRectsBssf, 55 | sort_algo=packer.SORT_LSIDE, rotation=True) 56 | self.setup_packer(m) 57 | m.pack() 58 | m.validate_packing() 59 | 60 | def test_maxrects_baf(self): 61 | m = packer.PackerBBF(pack_algo=maxrects.MaxRectsBaf, 62 | sort_algo=packer.SORT_LSIDE, rotation=True) 63 | self.setup_packer(m) 64 | m.pack() 65 | m.validate_packing() 66 | 67 | def test_maxrects_blsf(self): 68 | m = packer.PackerBBF(pack_algo=maxrects.MaxRectsBlsf, 69 | sort_algo=packer.SORT_LSIDE, rotation=True) 70 | self.setup_packer(m) 71 | m.pack() 72 | m.validate_packing() 73 | 74 | def test_skyline_bl_wm(self): 75 | s = packer.PackerBBF(pack_algo=skyline.SkylineBlWm, 76 | sort_algo=packer.SORT_LSIDE, rotation=True) 77 | self.setup_packer(s) 78 | s.pack() 79 | s.validate_packing() 80 | 81 | def test_skyline_mwf_wm(self): 82 | s = packer.PackerBBF(pack_algo=skyline.SkylineMwfWm, 83 | sort_algo=packer.SORT_LSIDE, rotation=True) 84 | self.setup_packer(s) 85 | s.pack() 86 | s.validate_packing() 87 | 88 | def test_skyline_mwfl_wm(self): 89 | s = packer.PackerBBF(pack_algo=skyline.SkylineMwflWm, 90 | sort_algo=packer.SORT_LSIDE, rotation=True) 91 | self.setup_packer(s) 92 | s.pack() 93 | s.validate_packing() 94 | 95 | def test_skyline_bl(self): 96 | s = packer.PackerBBF(pack_algo=skyline.SkylineBl, 97 | sort_algo=packer.SORT_LSIDE, rotation=True) 98 | self.setup_packer(s) 99 | s.pack() 100 | s.validate_packing() 101 | 102 | def test_skyline_mwf(self): 103 | s = packer.PackerBBF(pack_algo=skyline.SkylineMwf, 104 | sort_algo=packer.SORT_LSIDE, rotation=True) 105 | self.setup_packer(s) 106 | s.pack() 107 | s.validate_packing() 108 | 109 | def test_skyline_mwfl(self): 110 | s = packer.PackerBBF(pack_algo=skyline.SkylineMwfl, 111 | sort_algo=packer.SORT_LSIDE, rotation=True) 112 | self.setup_packer(s) 113 | s.pack() 114 | s.validate_packing() 115 | 116 | def test_guillotine_bssf_sas(self): 117 | g = packer.PackerBBF(pack_algo=guillotine.GuillotineBssfSas, 118 | sort_algo=packer.SORT_SSIDE, rotation=True) 119 | self.setup_packer(g) 120 | g.pack() 121 | g.validate_packing() 122 | 123 | def test_guillotine_blsf_las(self): 124 | g = packer.PackerBBF(pack_algo=guillotine.GuillotineBlsfLas, 125 | sort_algo=packer.SORT_LSIDE, rotation=True) 126 | self.setup_packer(g) 127 | g.pack() 128 | g.validate_packing() 129 | 130 | def test_guillotine_baf_slas(self): 131 | g = packer.PackerBBF(pack_algo=guillotine.GuillotineBafSlas, 132 | sort_algo=packer.SORT_LSIDE, rotation=True) 133 | self.setup_packer(g) 134 | g.pack() 135 | g.validate_packing() 136 | 137 | def test_guillotine_bssf_llas(self): 138 | g = packer.PackerBBF(pack_algo=guillotine.GuillotineBssfLlas, 139 | sort_algo=packer.SORT_SSIDE, rotation=True) 140 | self.setup_packer(g) 141 | g.pack() 142 | g.validate_packing() 143 | 144 | def test_guillotine_blsf_maxas(self): 145 | g = packer.PackerBBF(pack_algo=guillotine.GuillotineBlsfMaxas, 146 | sort_algo=packer.SORT_LSIDE, rotation=True) 147 | self.setup_packer(g) 148 | g.pack() 149 | g.validate_packing() 150 | 151 | def test_guillotine_baf_minas(self): 152 | g = packer.PackerBBF(pack_algo=guillotine.GuillotineBafMinas, 153 | sort_algo=packer.SORT_LSIDE, rotation=True) 154 | self.setup_packer(g) 155 | g.pack() 156 | g.validate_packing() 157 | 158 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | import random 2 | from unittest import TestCase 3 | from collections import defaultdict 4 | from timeit import default_timer as timer 5 | import rectpack 6 | 7 | 8 | from rectpack import GuillotineBssfSas, GuillotineBssfLas, \ 9 | GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \ 10 | GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \ 11 | GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \ 12 | GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \ 13 | GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \ 14 | GuillotineBafMinas 15 | 16 | from rectpack import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf 17 | 18 | from rectpack import SkylineMwf, SkylineMwfl, SkylineBl, \ 19 | SkylineBlWm, SkylineMwfWm, SkylineMwflWm 20 | 21 | from rectpack import PackingMode, PackingBin 22 | 23 | 24 | # For repeatable rectangle generation 25 | random.seed(33) 26 | 27 | 28 | def coherce_to(maxn, minn, n): 29 | assert maxn >= minn 30 | return max(min(maxn, n), minn) 31 | 32 | 33 | def random_rectangle(max_side, min_side, sigma=0.5, ratio=1.0, coherce=True): 34 | 35 | assert min_side <= max_side 36 | 37 | # 38 | half_side = (max_side-min_side)/2 39 | center = max_side-half_side 40 | width = random.normalvariate(0, sigma)*half_side 41 | height = random.normalvariate(0, sigma)*half_side 42 | 43 | # 44 | if ratio > 1: 45 | height = height/ratio 46 | else: 47 | width = width*ratio 48 | 49 | # Coherce value to max 50 | if coherce: 51 | width = coherce_to(max_side, min_side, width+center) 52 | height = coherce_to(max_side, min_side, height+center) 53 | 54 | return width, height 55 | 56 | 57 | RECTANGLES = [random_rectangle(40, 20, ratio=1.2) for _ in range(50)]\ 58 | +[random_rectangle(20, 15, ratio=1.2) for _ in range(50)] 59 | BINS = [random_rectangle(150, 150, ratio=1.5) for _ in range(3)] 60 | 61 | 62 | class TestWastedSpace(TestCase): 63 | 64 | def setUp(self): 65 | self.rectangles = [(int(w), int(h)) for w, h in RECTANGLES] 66 | self.bins = [(int(w), int(h)) for w, h in BINS] 67 | self.packer = None 68 | self.algos = [ 69 | GuillotineBssfSas, GuillotineBssfLas, GuillotineBssfSlas, \ 70 | GuillotineBssfLlas, GuillotineBssfMaxas, GuillotineBssfMinas, \ 71 | GuillotineBlsfSas, GuillotineBlsfLas, GuillotineBlsfSlas, \ 72 | GuillotineBlsfLlas, GuillotineBlsfMaxas, GuillotineBlsfMinas, \ 73 | GuillotineBafSas, GuillotineBafLas, GuillotineBafSlas, \ 74 | MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf, \ 75 | SkylineBl, SkylineBlWm, SkylineMwfl, SkylineMwf] 76 | 77 | self.bin_algos = [ 78 | (rectpack.PackingBin.BNF, "BNF"), 79 | (rectpack.PackingBin.BFF, "BFF"), 80 | (rectpack.PackingBin.BBF, "BBF"), 81 | (rectpack.PackingBin.Global, "GLOBAL"), 82 | ] 83 | self.bin_algos_online = [ 84 | (rectpack.PackingBin.BNF, "BNF"), 85 | (rectpack.PackingBin.BFF, "BFF"), 86 | (rectpack.PackingBin.BBF, "BBF"), 87 | ] 88 | self.sort_algo = rectpack.SORT_AREA 89 | self.log=False 90 | 91 | @staticmethod 92 | def first_bin_wasted_space(packer): 93 | bin1 = packer[1] 94 | bin_area = bin1.width*bin1.height 95 | rect_area = sum((r.width*r.height for r in bin1)) 96 | return ((bin_area-rect_area)/bin_area)*100 97 | 98 | 99 | @staticmethod 100 | def packing_time(packer): 101 | start = timer() 102 | packer.pack() 103 | end = timer() 104 | return end-start 105 | 106 | @staticmethod 107 | def setup_packer(packer, bins, rectangles): 108 | for b in bins: 109 | packer.add_bin(*b) 110 | for r in rectangles: 111 | packer.add_rect(*r) 112 | return packer 113 | 114 | def test_offline_modes(self): 115 | for bin_algo, algo_name in self.bin_algos: 116 | for algo in self.algos: 117 | packer = rectpack.newPacker(pack_algo=algo, 118 | mode=PackingMode.Offline, 119 | bin_algo=bin_algo, 120 | sort_algo=self.sort_algo) 121 | self.setup_packer(packer, self.bins, self.rectangles) 122 | time = self.packing_time(packer) 123 | self.first_bin_wasted_space(packer) 124 | wasted = self.first_bin_wasted_space(packer) 125 | 126 | # Test wasted spaced threshold 127 | if self.log: 128 | print("Offline {0} {1:<20s} {2:>10.3f}s {3:>10.3f}% {4:>10} bins".format( 129 | algo_name, algo.__name__, time, wasted, len(packer))) 130 | self.assertTrue(wasted<50) 131 | 132 | # Validate rectangle packing 133 | for b in packer: 134 | b.validate_packing() 135 | 136 | def test_online_modes(self): 137 | for bin_algo, algo_name in self.bin_algos_online: 138 | for algo in self.algos: 139 | packer = rectpack.newPacker(pack_algo=algo, 140 | mode=PackingMode.Online, 141 | bin_algo=bin_algo, 142 | sort_algo=self.sort_algo) 143 | start = timer() 144 | self.setup_packer(packer, self.bins, self.rectangles) 145 | end = timer() 146 | time = end-start 147 | self.first_bin_wasted_space(packer) 148 | wasted = self.first_bin_wasted_space(packer) 149 | 150 | # Test wasted spaced threshold 151 | if self.log: 152 | print("Online {0} {1:<20s} {2:>10.3f}s {3:>10.3f}% {4:>10} bins".format( 153 | algo_name, algo.__name__, time, wasted, len(packer))) 154 | self.assertTrue(wasted<90) 155 | 156 | # Validate rectangle packing 157 | for b in packer: 158 | b.validate_packing() 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rectpack [![Build Status](https://travis-ci.org/secnot/rectpack.svg?branch=master)](https://travis-ci.org/secnot/rectpack) 2 | 3 | 4 | Rectpack is a collection of heuristic algorithms for solving the 2D knapsack problem, 5 | also known as the bin packing problem. In essence packing a set of rectangles into the 6 | smallest number of bins. 7 | 8 | ![alt tag](docs/maxrects.png) 9 | 10 | 11 | ## Installation 12 | 13 | Download the package or clone the repository, and then install with: 14 | 15 | ```bash 16 | python setup.py install 17 | ``` 18 | 19 | or use pypi: 20 | 21 | ```bash 22 | pip install rectpack 23 | ``` 24 | 25 | ## Basic Usage 26 | 27 | Packing rectangles into a number of bins is very simple: 28 | 29 | ```python 30 | from rectpack import newPacker 31 | 32 | rectangles = [(100, 30), (40, 60), (30, 30),(70, 70), (100, 50), (30, 30)] 33 | bins = [(300, 450), (80, 40), (200, 150)] 34 | 35 | packer = newPacker() 36 | 37 | # Add the rectangles to packing queue 38 | for r in rectangles: 39 | packer.add_rect(*r) 40 | 41 | # Add the bins where the rectangles will be placed 42 | for b in bins: 43 | packer.add_bin(*b) 44 | 45 | # Start packing 46 | packer.pack() 47 | ``` 48 | 49 | Once the rectangles have been packed the results can be accessed individually 50 | 51 | ```python 52 | # Obtain number of bins used for packing 53 | nbins = len(packer) 54 | 55 | # Index first bin 56 | abin = packer[0] 57 | 58 | # Bin dimmensions (bins can be reordered during packing) 59 | width, height = abin.width, abin.height 60 | 61 | # Number of rectangles packed into first bin 62 | nrect = len(packer[0]) 63 | 64 | # Second bin first rectangle 65 | rect = packer[1][0] 66 | 67 | # rect is a Rectangle object 68 | x = rect.x # rectangle bottom-left x coordinate 69 | y = rect.y # rectangle bottom-left y coordinate 70 | w = rect.width 71 | h = rect.height 72 | ``` 73 | 74 | looping over all of them 75 | 76 | ```python 77 | for abin in packer: 78 | print(abin.bid) # Bin id if it has one 79 | for rect in abin: 80 | print(rect) 81 | ``` 82 | 83 | or using **rect_list()** 84 | 85 | ```python 86 | # Full rectangle list 87 | all_rects = packer.rect_list() 88 | for rect in all_rects: 89 | b, x, y, w, h, rid = rect 90 | 91 | # b - Bin index 92 | # x - Rectangle bottom-left corner x coordinate 93 | # y - Rectangle bottom-left corner y coordinate 94 | # w - Rectangle width 95 | # h - Rectangle height 96 | # rid - User asigned rectangle id or None 97 | ``` 98 | 99 | Lastly all the dimmension (bins and rectangles) must be integers or decimals to avoid 100 | collisions caused by floating point rounding. If your data is floating point use 101 | float2dec to convert float values to decimals (see float below) 102 | 103 | 104 | ## API 105 | 106 | A more detailed description of API calls: 107 | 108 | * class **newPacker**([, mode][, bin_algo][, pack_algo][, sort_algo][, rotation]) 109 | Return a new packer object 110 | * mode: Mode of operations 111 | * PackingMode.Offline: The set of rectangles is known beforehand, packing won't 112 | start until *pack()* is called. 113 | * PackingMode.Online: The rectangles are unknown at the beginning of the job, and 114 | will be packed as soon as they are added. 115 | * bin_algo: Bin selection heuristic 116 | * PackingBin.BNF: (Bin Next Fit) If a rectangle doesn't fit into the current bin, 117 | close it and try next one. 118 | * PackingBin.BFF: (Bin First Fit) Pack rectangle into the first bin it fits (without closing) 119 | * PackingBin.BBF: (Bin Best Fit) Pack rectangle into the bin that gives best fitness. 120 | * PackingBin.Global: For each bin pack the rectangle with the best fitness until it is full, 121 | then continue with next bin. 122 | * pack_algo: One of the supported packing algorithms (see list below) 123 | * sort_algo: Rectangle sort order before packing (only for offline mode) 124 | * SORT_NONE: Rectangles left unsorted. 125 | * SORT_AREA: Sort by descending area. 126 | * SORT_PERI: Sort by descending perimeter. 127 | * SORT_DIFF: Sort by difference of rectangle sides. 128 | * SORT_SSIDE: Sort by shortest side. 129 | * SORT_LSIDE: Sort by longest side. 130 | * SORT_RATIO: Sort by ration between sides. 131 | * rotation: Enable or disable rectangle rotation. 132 | 133 | 134 | * packer.**add_bin**(width, height[, count][, bid]) 135 | Add empty bin or bins to a packer 136 | * width: Bin width 137 | * height: Bin height 138 | * count: Number of bins to add, 1 by default. It's possible to add infinie bins 139 | with *count=float("inf")* 140 | * bid: Optional bin identifier 141 | 142 | 143 | * packer.**add_rect**(width, height[, rid]) 144 | Add rectangle to packing queue 145 | * width: Rectangle width 146 | * height: Rectangle height 147 | * rid: User assigned rectangle id 148 | 149 | 150 | * packer.**pack**(): 151 | Starts packing process (only for offline mode). 152 | 153 | 154 | * packer.**rect_list**(): 155 | Returns the list of packed rectangles, each one represented by the tuple (b, x, y, w, h, rid) where: 156 | * b: Index for the bin the rectangle was packed into 157 | * x: X coordinate for the rectangle bottom-left corner 158 | * y: Y coordinate for the rectangle bottom-left corner 159 | * w: Rectangle width 160 | * h: Rectangle height 161 | * rid: User provided id or None 162 | 163 | 164 | ## Supported Algorithms 165 | 166 | This library implements three of the algorithms described in [1] Skyline, Maxrects, 167 | and Guillotine, with the following variants: 168 | 169 | * MaxRects 170 | * MaxRectsBl 171 | * MaxRectsBssf 172 | * MaxRectsBaf 173 | * MaxRectsBlsf 174 | 175 | 176 | * Skyline 177 | * SkylineBl 178 | * SkylineBlWm 179 | * SkylineMwf 180 | * SkylineMwfl 181 | * SkylineMwfWm 182 | * SkylineMwflWm 183 | 184 | 185 | * Guillotine 186 | * GuillotineBssfSas 187 | * GuillotineBssfLas 188 | * GuillotineBssfSlas 189 | * GuillotineBssfLlas 190 | * GuillotineBssfMaxas 191 | * GuillotineBssfMinas 192 | * GuillotineBlsfSas 193 | * GuillotineBlsfLas 194 | * GuillotineBlsfSlas 195 | * GuillotineBlsfLlas 196 | * GuillotineBlsfMaxas 197 | * GuillotineBlsfMinas 198 | * GuillotineBafSas 199 | * GuillotineBafLas 200 | * GuillotineBafSlas 201 | * GuillotineBafLlas 202 | * GuillotineBafMaxas 203 | * GuillotineBafMinas 204 | 205 | I recommend to use the default algorithm unless the packing is too slow, in that 206 | case switch to one of the Guillotine variants for example *GuillotineBssfSas*. 207 | You can learn more about the algorithms in [1]. 208 | 209 | ## Testing 210 | 211 | Rectpack is thoroughly tested, run the tests with: 212 | 213 | ```bash 214 | python setup.py test 215 | ``` 216 | 217 | or 218 | 219 | ```bash 220 | python -m unittest discover 221 | ``` 222 | 223 | ## Float 224 | 225 | If you need to use floats just convert them to fixed-point using a Decimal type, 226 | be carefull rounding up so the actual rectangle size is always smaller than 227 | the conversion. Rectpack provides helper funcion **float2dec** for this task, 228 | it accepts a number and the number of decimals to round to, and returns 229 | the rounded Decimal. 230 | 231 | ```python 232 | from rectpack import float2dec, newPacker 233 | 234 | float_rects = [...] 235 | dec_rects = [(float2dec(r[0], 3), float2dec(r[1], 3)) for r in float_rects] 236 | 237 | p = newPacker() 238 | ... 239 | ``` 240 | 241 | ## References 242 | 243 | [1] Jukka Jylang - A Thousand Ways to Pack the Bin - A Practical Approach to Two-Dimensional 244 | Rectangle Bin Packing (2010) 245 | 246 | [2] Huang, E. Korf - Optimal Rectangle Packing: An Absolute Placement Approach (2013) 247 | -------------------------------------------------------------------------------- /rectpack/maxrects.py: -------------------------------------------------------------------------------- 1 | from .pack_algo import PackingAlgorithm 2 | from .geometry import Rectangle 3 | import itertools 4 | import collections 5 | import operator 6 | 7 | 8 | first_item = operator.itemgetter(0) 9 | 10 | 11 | 12 | class MaxRects(PackingAlgorithm): 13 | 14 | def __init__(self, width, height, rot=True, *args, **kwargs): 15 | super(MaxRects, self).__init__(width, height, rot, *args, **kwargs) 16 | 17 | def _rect_fitness(self, max_rect, width, height): 18 | """ 19 | Arguments: 20 | max_rect (Rectangle): Destination max_rect 21 | width (int, float): Rectangle width 22 | height (int, float): Rectangle height 23 | 24 | Returns: 25 | None: Rectangle couldn't be placed into max_rect 26 | integer, float: fitness value 27 | """ 28 | if width <= max_rect.width and height <= max_rect.height: 29 | return 0 30 | else: 31 | return None 32 | 33 | def _select_position(self, w, h): 34 | """ 35 | Find max_rect with best fitness for placing a rectangle 36 | of dimentsions w*h 37 | 38 | Arguments: 39 | w (int, float): Rectangle width 40 | h (int, float): Rectangle height 41 | 42 | Returns: 43 | (rect, max_rect) 44 | rect (Rectangle): Placed rectangle or None if was unable. 45 | max_rect (Rectangle): Maximal rectangle were rect was placed 46 | """ 47 | if not self._max_rects: 48 | return None, None 49 | 50 | # Normal rectangle 51 | fitn = ((self._rect_fitness(m, w, h), w, h, m) for m in self._max_rects 52 | if self._rect_fitness(m, w, h) is not None) 53 | 54 | # Rotated rectangle 55 | fitr = ((self._rect_fitness(m, h, w), h, w, m) for m in self._max_rects 56 | if self._rect_fitness(m, h, w) is not None) 57 | 58 | if not self.rot: 59 | fitr = [] 60 | 61 | fit = itertools.chain(fitn, fitr) 62 | 63 | try: 64 | _, w, h, m = min(fit, key=first_item) 65 | except ValueError: 66 | return None, None 67 | 68 | return Rectangle(m.x, m.y, w, h), m 69 | 70 | def _generate_splits(self, m, r): 71 | """ 72 | When a rectangle is placed inside a maximal rectangle, it stops being one 73 | and up to 4 new maximal rectangles may appear depending on the placement. 74 | _generate_splits calculates them. 75 | 76 | Arguments: 77 | m (Rectangle): max_rect rectangle 78 | r (Rectangle): rectangle placed 79 | 80 | Returns: 81 | list : list containing new maximal rectangles or an empty list 82 | """ 83 | new_rects = [] 84 | 85 | if r.left > m.left: 86 | new_rects.append(Rectangle(m.left, m.bottom, r.left-m.left, m.height)) 87 | if r.right < m.right: 88 | new_rects.append(Rectangle(r.right, m.bottom, m.right-r.right, m.height)) 89 | if r.top < m.top: 90 | new_rects.append(Rectangle(m.left, r.top, m.width, m.top-r.top)) 91 | if r.bottom > m.bottom: 92 | new_rects.append(Rectangle(m.left, m.bottom, m.width, r.bottom-m.bottom)) 93 | 94 | return new_rects 95 | 96 | def _split(self, rect): 97 | """ 98 | Split all max_rects intersecting the rectangle rect into up to 99 | 4 new max_rects. 100 | 101 | Arguments: 102 | rect (Rectangle): Rectangle 103 | 104 | Returns: 105 | split (Rectangle list): List of rectangles resulting from the split 106 | """ 107 | max_rects = collections.deque() 108 | 109 | for r in self._max_rects: 110 | if r.intersects(rect): 111 | max_rects.extend(self._generate_splits(r, rect)) 112 | else: 113 | max_rects.append(r) 114 | 115 | # Add newly generated max_rects 116 | self._max_rects = list(max_rects) 117 | 118 | def _remove_duplicates(self): 119 | """ 120 | Remove every maximal rectangle contained by another one. 121 | """ 122 | contained = set() 123 | for m1, m2 in itertools.combinations(self._max_rects, 2): 124 | if m1.contains(m2): 125 | contained.add(m2) 126 | elif m2.contains(m1): 127 | contained.add(m1) 128 | 129 | # Remove from max_rects 130 | self._max_rects = [m for m in self._max_rects if m not in contained] 131 | 132 | def fitness(self, width, height): 133 | """ 134 | Metric used to rate how much space is wasted if a rectangle is placed. 135 | Returns a value greater or equal to zero, the smaller the value the more 136 | 'fit' is the rectangle. If the rectangle can't be placed, returns None. 137 | 138 | Arguments: 139 | width (int, float): Rectangle width 140 | height (int, float): Rectangle height 141 | 142 | Returns: 143 | int, float: Rectangle fitness 144 | None: Rectangle can't be placed 145 | """ 146 | assert(width > 0 and height > 0) 147 | 148 | rect, max_rect = self._select_position(width, height) 149 | if rect is None: 150 | return None 151 | 152 | # Return fitness 153 | return self._rect_fitness(max_rect, rect.width, rect.height) 154 | 155 | def add_rect(self, width, height, rid=None): 156 | """ 157 | Add rectangle of widthxheight dimensions. 158 | 159 | Arguments: 160 | width (int, float): Rectangle width 161 | height (int, float): Rectangle height 162 | rid: Optional rectangle user id 163 | 164 | Returns: 165 | Rectangle: Rectangle with placemente coordinates 166 | None: If the rectangle couldn be placed. 167 | """ 168 | assert(width > 0 and height >0) 169 | 170 | # Search best position and orientation 171 | rect, _ = self._select_position(width, height) 172 | if not rect: 173 | return None 174 | 175 | # Subdivide all the max rectangles intersecting with the selected 176 | # rectangle. 177 | self._split(rect) 178 | 179 | # Remove any max_rect contained by another 180 | self._remove_duplicates() 181 | 182 | # Store and return rectangle position. 183 | rect.rid = rid 184 | self.rectangles.append(rect) 185 | return rect 186 | 187 | def reset(self): 188 | super(MaxRects, self).reset() 189 | self._max_rects = [Rectangle(0, 0, self.width, self.height)] 190 | 191 | 192 | 193 | 194 | class MaxRectsBl(MaxRects): 195 | 196 | def _select_position(self, w, h): 197 | """ 198 | Select the position where the y coordinate of the top of the rectangle 199 | is lower, if there are severtal pick the one with the smallest x 200 | coordinate 201 | """ 202 | fitn = ((m.y+h, m.x, w, h, m) for m in self._max_rects 203 | if self._rect_fitness(m, w, h) is not None) 204 | fitr = ((m.y+w, m.x, h, w, m) for m in self._max_rects 205 | if self._rect_fitness(m, h, w) is not None) 206 | 207 | if not self.rot: 208 | fitr = [] 209 | 210 | fit = itertools.chain(fitn, fitr) 211 | 212 | try: 213 | _, _, w, h, m = min(fit, key=first_item) 214 | except ValueError: 215 | return None, None 216 | 217 | return Rectangle(m.x, m.y, w, h), m 218 | 219 | 220 | class MaxRectsBssf(MaxRects): 221 | """Best Sort Side Fit minimize short leftover side""" 222 | def _rect_fitness(self, max_rect, width, height): 223 | if width > max_rect.width or height > max_rect.height: 224 | return None 225 | 226 | return min(max_rect.width-width, max_rect.height-height) 227 | 228 | class MaxRectsBaf(MaxRects): 229 | """Best Area Fit pick maximal rectangle with smallest area 230 | where the rectangle can be placed""" 231 | def _rect_fitness(self, max_rect, width, height): 232 | if width > max_rect.width or height > max_rect.height: 233 | return None 234 | 235 | return (max_rect.width*max_rect.height)-(width*height) 236 | 237 | 238 | class MaxRectsBlsf(MaxRects): 239 | """Best Long Side Fit minimize long leftover side""" 240 | def _rect_fitness(self, max_rect, width, height): 241 | if width > max_rect.width or height > max_rect.height: 242 | return None 243 | 244 | return max(max_rect.width-width, max_rect.height-height) 245 | -------------------------------------------------------------------------------- /tests/test_guillotine.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import rectpack.guillotine as guillotine 3 | from rectpack.geometry import Rectangle 4 | 5 | 6 | class TestGuillotine(TestCase): 7 | 8 | def test_init(self): 9 | # Test default section 10 | g = guillotine.Guillotine(100, 100) 11 | self.assertEqual(len(g._sections), 1) 12 | self.assertEqual(g._sections[0], Rectangle(0, 0, 100, 100)) 13 | 14 | # Test arguments 15 | g = guillotine.Guillotine(50, 100) 16 | self.assertEqual(g.width, 50) 17 | self.assertEqual(g.height, 100) 18 | 19 | # Test merge optional argument 20 | g = guillotine.Guillotine(50, 100) 21 | self.assertEqual(g._merge, True) 22 | g = guillotine.Guillotine(50, 100, merge=False) 23 | self.assertEqual(g._merge, False) 24 | g = guillotine.Guillotine(50, 100, merge=True) 25 | self.assertEqual(g._merge, True) 26 | 27 | def test_add_rect(self): 28 | # Rectangle too big for surface 29 | g = guillotine.GuillotineBssfMaxas(100, 100, merge=False) 30 | self.assertEqual(g.add_rect(110, 3), None) 31 | 32 | g = guillotine.GuillotineBssfMaxas(100, 100, merge=False) 33 | self.assertEqual(g.add_rect(5, 5), Rectangle(0, 0, 5, 5)) 34 | self.assertEqual(len(g), 1) 35 | self.assertTrue(g.add_rect(10, 10)) 36 | self.assertEqual(len(g), 2) 37 | self.assertEqual(g.add_rect(99, 99), None) 38 | self.assertEqual(len(g), 2) 39 | 40 | # Test new sections created 41 | g = guillotine.GuillotineBssfMaxas(100, 100) 42 | self.assertEqual(len(g._sections), 1) 43 | g.add_rect(5, 5) 44 | self.assertEqual(len(g._sections), 2) 45 | 46 | # Test rectangle id preserved 47 | g = guillotine.GuillotineBssfMaxas(100, 100) 48 | g.add_rect(5, 5, rid=88) 49 | for r in g: 50 | self.assertEqual(r.rid, 88) 51 | 52 | # Test full size rectangle 53 | g = guillotine.GuillotineBssfMaxas(300, 100) 54 | self.assertTrue(g.add_rect(300, 100)) 55 | 56 | # Test rotations 57 | g = guillotine.GuillotineBlsfMaxas(50, 100, rot=True) 58 | rect = g.add_rect(100, 50) 59 | self.assertEqual(rect, Rectangle(0, 0, 50, 100)) 60 | 61 | # Test returned coordinates 62 | g = guillotine.GuillotineBlsfMaxas(100, 100, rot=False) 63 | rect1 = g.add_rect(60, 40) 64 | rect2 = g.add_rect(20, 30) 65 | rect3 = g.add_rect(30, 40) 66 | self.assertEqual(rect1, Rectangle(0, 0, 60, 40)) 67 | self.assertEqual(rect2, Rectangle(0, 40, 20, 30)) 68 | self.assertEqual(rect3, Rectangle(60, 0, 30, 40)) 69 | 70 | def test_fitness(self): 71 | # Test several sections same fitness value 72 | g = guillotine.GuillotineBlsfMaxas(50, 50, rot=False) 73 | fitness1 = g.fitness(10, 10) 74 | fitness2 = g.fitness(20, 20) 75 | fitness3 = g.fitness(30, 30) 76 | fitness4 = g.fitness(40, 40) 77 | fitness5 = g.fitness(45, 45) 78 | self.assertTrue(fitness1>fitness2>fitness3>fitness4>fitness5) 79 | g.add_rect(5, 5) 80 | fitness1 = g.fitness(10, 10) 81 | fitness2 = g.fitness(20, 20) 82 | fitness3 = g.fitness(30, 30) 83 | fitness4 = g.fitness(40, 40) 84 | fitness5 = g.fitness(45, 45) 85 | self.assertTrue(fitness1>fitness2>fitness3>fitness4>fitness5) 86 | 87 | def test_section_fitness(self): 88 | g1 = guillotine.GuillotineBssfSas(100, 50) 89 | g2 = guillotine.GuillotineBlsfSas(100, 50) 90 | g3 = guillotine.GuillotineBafSas(100, 50) 91 | 92 | fit1 = g1._section_fitness(g1._sections[0], 5, 6) 93 | fit2 = g2._section_fitness(g2._sections[0], 5, 6) 94 | fit3 = g3._section_fitness(g3._sections[0], 5, 6) 95 | self.assertNotEqual(fit1, fit2, fit3) 96 | 97 | def test_split_vertical(self): 98 | # Normal split 99 | g = guillotine.Guillotine(100, 100) 100 | 101 | section = g._sections[0] 102 | g._sections = [] 103 | g._split_vertical(section, 50, 50) 104 | 105 | self.assertTrue(Rectangle(50, 0, 50, 100) in g._sections) 106 | self.assertTrue(Rectangle(0, 50, 50, 50) in g._sections) 107 | self.assertEqual(len(g._sections), 2) 108 | 109 | # Full width split 110 | g = guillotine.Guillotine(100, 100) 111 | 112 | section = g._sections[0] 113 | g._sections = [] 114 | g._split_vertical(section, 100, 50) 115 | 116 | self.assertTrue(Rectangle(0, 50, 100, 50) in g._sections) 117 | self.assertEqual(len(g._sections), 1) 118 | 119 | # Full Height split 120 | g = guillotine.Guillotine(100, 100) 121 | 122 | section = g._sections[0] 123 | g._sections = [] 124 | g._split_vertical(section, 50, 100) 125 | 126 | self.assertTrue(Rectangle(50, 0, 50, 100) in g._sections) 127 | self.assertEqual(len(g._sections), 1) 128 | 129 | # Full section split 130 | g = guillotine.Guillotine(100, 100) 131 | 132 | section = g._sections[0] 133 | g._sections = [] 134 | g._split_vertical(section, 100, 100) 135 | 136 | self.assertEqual(len(g._sections), 0) 137 | 138 | def test_split_horizontal(self): 139 | # Normal split 140 | g = guillotine.Guillotine(100, 100) 141 | 142 | section = g._sections[0] 143 | g._sections = [] 144 | g._split_horizontal(section, 50, 50) 145 | 146 | self.assertTrue(Rectangle(0, 50, 100, 50) in g._sections) 147 | self.assertTrue(Rectangle(50, 0, 50, 50) in g._sections) 148 | self.assertEqual(len(g._sections), 2) 149 | 150 | # Full width split 151 | g = guillotine.Guillotine(100, 100) 152 | 153 | section = g._sections[0] 154 | g._sections = [] 155 | g._split_horizontal(section, 100, 50) 156 | 157 | self.assertTrue(Rectangle(0, 50, 100, 50) in g._sections) 158 | self.assertEqual(len(g._sections), 1) 159 | 160 | # Full height split 161 | g = guillotine.Guillotine(100, 100) 162 | 163 | section = g._sections[0] 164 | g._sections = [] 165 | g._split_horizontal(section, 50, 100) 166 | 167 | self.assertTrue(Rectangle(50, 0, 50, 100) in g._sections) 168 | self.assertEqual(len(g._sections), 1) 169 | 170 | # Full section split 171 | g = guillotine.Guillotine(100, 100) 172 | 173 | section = g._sections[0] 174 | g._sections = [] 175 | g._split_horizontal(section, 100, 100) 176 | 177 | self.assertEqual(len(g._sections), 0) 178 | 179 | def test_split(self): 180 | #TODO: Test correct split is called for each subclass 181 | pass 182 | 183 | def test_fits_surface(self): 184 | g = guillotine.Guillotine(100, 200, rot=False) 185 | self.assertTrue(g._fits_surface(10, 10)) 186 | self.assertTrue(g._fits_surface(100, 200)) 187 | self.assertTrue(g._fits_surface(100, 100)) 188 | self.assertFalse(g._fits_surface(101, 200)) 189 | self.assertFalse(g._fits_surface(100, 201)) 190 | 191 | def test_add_section(self): 192 | g = guillotine.Guillotine(100, 100) 193 | g._sections = [Rectangle(0, 50, 50, 50)] 194 | g._add_section(Rectangle(50, 0, 50, 50)) 195 | 196 | self.assertEqual(len(g._sections), 2) 197 | 198 | # Test sections are merged recursively 199 | g = guillotine.Guillotine(100, 100, merge=True) 200 | g._sections = [Rectangle(0, 50, 100, 50), Rectangle(50, 0, 50, 50)] 201 | 202 | g._add_section(Rectangle(0, 0, 50, 50)) 203 | 204 | self.assertEqual(len(g._sections), 1) 205 | self.assertEqual(g._sections[0], Rectangle(0, 0, 100, 100)) 206 | 207 | # Test merge disabled 208 | g = guillotine.Guillotine(100, 100, merge=False) 209 | g._sections = [Rectangle(0, 50, 100, 50), Rectangle(50, 0, 50, 50)] 210 | 211 | g._add_section(Rectangle(0, 0, 50, 50)) 212 | self.assertEqual(len(g._sections), 3) 213 | self.assertTrue(Rectangle(0, 50, 100, 50) in g._sections) 214 | self.assertTrue(Rectangle(50, 0, 50, 50) in g._sections) 215 | self.assertTrue(Rectangle(0, 0, 50, 50) in g._sections) 216 | 217 | def test_getitem(self): 218 | """Test __getitem__ returns requested element or slice""" 219 | g = guillotine.GuillotineBafSas(100, 100) 220 | g.add_rect(10, 10) 221 | g.add_rect(5, 5) 222 | g.add_rect(1, 1) 223 | 224 | self.assertEqual(g[0], Rectangle(0, 0, 10, 10)) 225 | self.assertEqual(g[1], Rectangle(0, 10, 5, 5)) 226 | self.assertEqual(g[2], Rectangle(5, 10, 1, 1)) 227 | 228 | self.assertEqual(g[-1], Rectangle(5, 10, 1, 1)) 229 | self.assertEqual(g[-2], Rectangle(0, 10, 5, 5)) 230 | 231 | self.assertEqual(g[1:], 232 | [Rectangle(0, 10, 5, 5), Rectangle(5, 10, 1, 1)]) 233 | self.assertEqual(g[0:2], 234 | [Rectangle(0, 0, 10, 10), Rectangle(0, 10, 5, 5)]) 235 | -------------------------------------------------------------------------------- /rectpack/geometry.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | 4 | 5 | class Point(object): 6 | 7 | __slots__ = ('x', 'y') 8 | 9 | def __init__(self, x, y): 10 | self.x = x 11 | self.y = y 12 | 13 | def __eq__(self, other): 14 | return (self.x == other.x and self.y == other.y) 15 | 16 | def __repr__(self): 17 | return "P({}, {})".format(self.x, self.y) 18 | 19 | def distance(self, point): 20 | """ 21 | Calculate distance to another point 22 | """ 23 | return sqrt((self.x-point.x)**2+(self.y-point.y)**2) 24 | 25 | def distance_squared(self, point): 26 | return (self.x-point.x)**2+(self.y-point.y)**2 27 | 28 | 29 | class Segment(object): 30 | 31 | __slots__ = ('start', 'end') 32 | 33 | def __init__(self, start, end): 34 | """ 35 | Arguments: 36 | start (Point): Segment start point 37 | end (Point): Segment end point 38 | """ 39 | assert(isinstance(start, Point) and isinstance(end, Point)) 40 | self.start = start 41 | self.end = end 42 | 43 | def __eq__(self, other): 44 | if not isinstance(other, self.__class__): 45 | None 46 | return self.start==other.start and self.end==other.end 47 | 48 | def __repr__(self): 49 | return "S({}, {})".format(self.start, self.end) 50 | 51 | @property 52 | def length_squared(self): 53 | """Faster than length and useful for some comparisons""" 54 | return self.start.distance_squared(self.end) 55 | 56 | @property 57 | def length(self): 58 | return self.start.distance(self.end) 59 | 60 | @property 61 | def top(self): 62 | return max(self.start.y, self.end.y) 63 | 64 | @property 65 | def bottom(self): 66 | return min(self.start.y, self.end.y) 67 | 68 | @property 69 | def right(self): 70 | return max(self.start.x, self.end.x) 71 | 72 | @property 73 | def left(self): 74 | return min(self.start.x, self.end.x) 75 | 76 | 77 | class HSegment(Segment): 78 | """Horizontal Segment""" 79 | 80 | def __init__(self, start, length): 81 | """ 82 | Create an Horizontal segment given its left most end point and its 83 | length. 84 | 85 | Arguments: 86 | - start (Point): Starting Point 87 | - length (number): segment length 88 | """ 89 | assert(isinstance(start, Point) and not isinstance(length, Point)) 90 | super(HSegment, self).__init__(start, Point(start.x+length, start.y)) 91 | 92 | @property 93 | def length(self): 94 | return self.end.x-self.start.x 95 | 96 | 97 | class VSegment(Segment): 98 | """Vertical Segment""" 99 | 100 | def __init__(self, start, length): 101 | """ 102 | Create a Vertical segment given its bottom most end point and its 103 | length. 104 | 105 | Arguments: 106 | - start (Point): Starting Point 107 | - length (number): segment length 108 | """ 109 | assert(isinstance(start, Point) and not isinstance(length, Point)) 110 | super(VSegment, self).__init__(start, Point(start.x, start.y+length)) 111 | 112 | @property 113 | def length(self): 114 | return self.end.y-self.start.y 115 | 116 | 117 | 118 | class Rectangle(object): 119 | """Basic rectangle primitive class. 120 | x, y-> Lower right corner coordinates 121 | width - 122 | height - 123 | """ 124 | __slots__ = ('width', 'height', 'x', 'y', 'rid') 125 | 126 | def __init__(self, x, y, width, height, rid = None): 127 | """ 128 | Args: 129 | x (int, float): 130 | y (int, float): 131 | width (int, float): 132 | height (int, float): 133 | rid (int): 134 | """ 135 | assert(height >=0 and width >=0) 136 | 137 | self.width = width 138 | self.height = height 139 | self.x = x 140 | self.y = y 141 | self.rid = rid 142 | 143 | @property 144 | def bottom(self): 145 | """ 146 | Rectangle bottom edge y coordinate 147 | """ 148 | return self.y 149 | 150 | @property 151 | def top(self): 152 | """ 153 | Rectangle top edge y coordiante 154 | """ 155 | return self.y+self.height 156 | 157 | @property 158 | def left(self): 159 | """ 160 | Rectangle left ednge x coordinate 161 | """ 162 | return self.x 163 | 164 | @property 165 | def right(self): 166 | """ 167 | Rectangle right edge x coordinate 168 | """ 169 | return self.x+self.width 170 | 171 | @property 172 | def corner_top_l(self): 173 | return Point(self.left, self.top) 174 | 175 | @property 176 | def corner_top_r(self): 177 | return Point(self.right, self.top) 178 | 179 | @property 180 | def corner_bot_r(self): 181 | return Point(self.right, self.bottom) 182 | 183 | @property 184 | def corner_bot_l(self): 185 | return Point(self.left, self.bottom) 186 | 187 | def __lt__(self, other): 188 | """ 189 | Compare rectangles by area (used for sorting) 190 | """ 191 | return self.area() < other.area() 192 | 193 | def __eq__(self, other): 194 | """ 195 | Equal rectangles have same area. 196 | """ 197 | if not isinstance(other, self.__class__): 198 | return False 199 | 200 | return (self.width == other.width and \ 201 | self.height == other.height and \ 202 | self.x == other.x and \ 203 | self.y == other.y) 204 | 205 | def __hash__(self): 206 | return hash((self.x, self.y, self.width, self.height)) 207 | 208 | def __iter__(self): 209 | """ 210 | Iterate through rectangle corners 211 | """ 212 | yield self.corner_top_l 213 | yield self.corner_top_r 214 | yield self.corner_bot_r 215 | yield self.corner_bot_l 216 | 217 | def __repr__(self): 218 | return "R({}, {}, {}, {})".format(self.x, self.y, self.width, self.height) 219 | 220 | def area(self): 221 | """ 222 | Rectangle area 223 | """ 224 | return self.width * self.height 225 | 226 | def move(self, x, y): 227 | """ 228 | Move Rectangle to x,y coordinates 229 | 230 | Arguments: 231 | x (int, float): X coordinate 232 | y (int, float): Y coordinate 233 | """ 234 | self.x = x 235 | self.y = y 236 | 237 | def contains(self, rect): 238 | """ 239 | Tests if another rectangle is contained by this one 240 | 241 | Arguments: 242 | rect (Rectangle): The other rectangle 243 | 244 | Returns: 245 | bool: True if it is container, False otherwise 246 | """ 247 | return (rect.y >= self.y and \ 248 | rect.x >= self.x and \ 249 | rect.y+rect.height <= self.y+self.height and \ 250 | rect.x+rect.width <= self.x+self.width) 251 | 252 | def intersects(self, rect, edges=False): 253 | """ 254 | Detect intersections between this and another Rectangle. 255 | 256 | Parameters: 257 | rect (Rectangle): The other rectangle. 258 | edges (bool): True to consider rectangles touching by their 259 | edges or corners to be intersecting. 260 | (Should have been named include_touching) 261 | 262 | Returns: 263 | bool: True if the rectangles intersect, False otherwise 264 | """ 265 | if edges: 266 | if (self.bottom > rect.top or self.top < rect.bottom or\ 267 | self.left > rect.right or self.right < rect.left): 268 | return False 269 | else: 270 | if (self.bottom >= rect.top or self.top <= rect.bottom or 271 | self.left >= rect.right or self.right <= rect.left): 272 | return False 273 | 274 | return True 275 | 276 | def intersection(self, rect, edges=False): 277 | """ 278 | Returns the rectangle resulting of the intersection between this and another 279 | rectangle. If the rectangles are only touching by their edges, and the 280 | argument 'edges' is True the rectangle returned will have an area of 0. 281 | Returns None if there is no intersection. 282 | 283 | Arguments: 284 | rect (Rectangle): The other rectangle. 285 | edges (bool): If True Rectangles touching by their edges are 286 | considered to be intersection. In this case a rectangle of 287 | 0 height or/and width will be returned. 288 | 289 | Returns: 290 | Rectangle: Intersection. 291 | None: There was no intersection. 292 | """ 293 | if not self.intersects(rect, edges=edges): 294 | return None 295 | 296 | bottom = max(self.bottom, rect.bottom) 297 | left = max(self.left, rect.left) 298 | top = min(self.top, rect.top) 299 | right = min(self.right, rect.right) 300 | 301 | return Rectangle(left, bottom, right-left, top-bottom) 302 | 303 | def join(self, other): 304 | """ 305 | Try to join a rectangle to this one, if the result is also a rectangle 306 | and the operation is successful and this rectangle is modified to the union. 307 | 308 | Arguments: 309 | other (Rectangle): Rectangle to join 310 | 311 | Returns: 312 | bool: True when successfully joined, False otherwise 313 | """ 314 | if self.contains(other): 315 | return True 316 | 317 | if other.contains(self): 318 | self.x = other.x 319 | self.y = other.y 320 | self.width = other.width 321 | self.height = other.height 322 | return True 323 | 324 | if not self.intersects(other, edges=True): 325 | return False 326 | 327 | # Other rectangle is Up/Down from this 328 | if self.left == other.left and self.width == other.width: 329 | y_min = min(self.bottom, other.bottom) 330 | y_max = max(self.top, other.top) 331 | self.y = y_min 332 | self.height = y_max-y_min 333 | return True 334 | 335 | # Other rectangle is Right/Left from this 336 | if self.bottom == other.bottom and self.height == other.height: 337 | x_min = min(self.left, other.left) 338 | x_max = max(self.right, other.right) 339 | self.x = x_min 340 | self.width = x_max-x_min 341 | return True 342 | 343 | return False 344 | 345 | -------------------------------------------------------------------------------- /rectpack/skyline.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | import operator 4 | import heapq 5 | import copy 6 | from .pack_algo import PackingAlgorithm 7 | from .geometry import Point as P 8 | from .geometry import HSegment, Rectangle 9 | from .waste import WasteManager 10 | 11 | 12 | class Skyline(PackingAlgorithm): 13 | """ Class implementing Skyline algorithm as described by 14 | Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010) 15 | 16 | _skyline: stores all the segments at the top of the skyline. 17 | _waste: Handles all wasted sections. 18 | """ 19 | 20 | def __init__(self, width, height, rot=True, *args, **kwargs): 21 | """ 22 | _skyline is the list used to store all the skyline segments, each 23 | one is a list with the format [x, y, width] where x is the x 24 | coordinate of the left most point of the segment, y the y coordinate 25 | of the segment, and width the length of the segment. The initial 26 | segment is allways [0, 0, surface_width] 27 | 28 | Arguments: 29 | width (int, float): 30 | height (int, float): 31 | rot (bool): Enable or disable rectangle rotation 32 | """ 33 | self._waste_management = False 34 | self._waste = WasteManager(rot=rot) 35 | super(Skyline, self).__init__(width, height, rot, merge=False, *args, **kwargs) 36 | 37 | def _placement_points_generator(self, skyline, width): 38 | """Returns a generator for the x coordinates of all the placement 39 | points on the skyline for a given rectangle. 40 | 41 | WARNING: In some cases could be duplicated points, but it is faster 42 | to compute them twice than to remove them. 43 | 44 | Arguments: 45 | skyline (list): Skyline HSegment list 46 | width (int, float): Rectangle width 47 | 48 | Returns: 49 | generator 50 | """ 51 | skyline_r = skyline[-1].right 52 | skyline_l = skyline[0].left 53 | 54 | # Placements using skyline segment left point 55 | ppointsl = (s.left for s in skyline if s.left+width <= skyline_r) 56 | 57 | # Placements using skyline segment right point 58 | ppointsr = (s.right-width for s in skyline if s.right-width >= skyline_l) 59 | 60 | # Merge positions 61 | return heapq.merge(ppointsl, ppointsr) 62 | 63 | def _generate_placements(self, width, height): 64 | """ 65 | Generate a list with 66 | 67 | Arguments: 68 | skyline (list): SkylineHSegment list 69 | width (number): 70 | 71 | Returns: 72 | tuple (Rectangle, fitness): 73 | Rectangle: Rectangle in valid position 74 | left_skyline: Index for the skyline under the rectangle left edge. 75 | right_skyline: Index for the skyline under the rectangle right edte. 76 | """ 77 | skyline = self._skyline 78 | 79 | points = collections.deque() 80 | 81 | left_index = right_index = 0 # Left and right side skyline index 82 | support_height = skyline[0].top 83 | support_index = 0 84 | 85 | placements = self._placement_points_generator(skyline, width) 86 | for p in placements: 87 | 88 | # If Rectangle's right side changed segment, find new support 89 | if p+width > skyline[right_index].right: 90 | for right_index in range(right_index+1, len(skyline)): 91 | if skyline[right_index].top >= support_height: 92 | support_index = right_index 93 | support_height = skyline[right_index].top 94 | if p+width <= skyline[right_index].right: 95 | break 96 | 97 | # If left side changed segment. 98 | if p >= skyline[left_index].right: 99 | left_index +=1 100 | 101 | # Find new support if the previous one was shifted out. 102 | if support_index < left_index: 103 | support_index = left_index 104 | support_height = skyline[left_index].top 105 | for i in range(left_index, right_index+1): 106 | if skyline[i].top >= support_height: 107 | support_index = i 108 | support_height = skyline[i].top 109 | 110 | # Add point if there is enought room at the top 111 | if support_height+height <= self.height: 112 | points.append((Rectangle(p, support_height, width, height),\ 113 | left_index, right_index)) 114 | 115 | return points 116 | 117 | def _merge_skyline(self, skylineq, segment): 118 | """ 119 | Arguments: 120 | skylineq (collections.deque): 121 | segment (HSegment): 122 | """ 123 | if len(skylineq) == 0: 124 | skylineq.append(segment) 125 | return 126 | 127 | if skylineq[-1].top == segment.top: 128 | s = skylineq[-1] 129 | skylineq[-1] = HSegment(s.start, s.length+segment.length) 130 | else: 131 | skylineq.append(segment) 132 | 133 | def _add_skyline(self, rect): 134 | """ 135 | Arguments: 136 | seg (Rectangle): 137 | """ 138 | skylineq = collections.deque([]) # Skyline after adding new one 139 | 140 | for sky in self._skyline: 141 | if sky.right <= rect.left or sky.left >= rect.right: 142 | self._merge_skyline(skylineq, sky) 143 | continue 144 | 145 | if sky.left < rect.left and sky.right > rect.left: 146 | # Skyline section partially under segment left 147 | self._merge_skyline(skylineq, 148 | HSegment(sky.start, rect.left-sky.left)) 149 | sky = HSegment(P(rect.left, sky.top), sky.right-rect.left) 150 | 151 | if sky.left < rect.right: 152 | if sky.left == rect.left: 153 | self._merge_skyline(skylineq, 154 | HSegment(P(rect.left, rect.top), rect.width)) 155 | # Skyline section partially under segment right 156 | if sky.right > rect.right: 157 | self._merge_skyline(skylineq, 158 | HSegment(P(rect.right, sky.top), sky.right-rect.right)) 159 | sky = HSegment(sky.start, rect.right-sky.left) 160 | 161 | if sky.left >= rect.left and sky.right <= rect.right: 162 | # Skyline section fully under segment, account for wasted space 163 | if self._waste_management and sky.top < rect.bottom: 164 | self._waste.add_waste(sky.left, sky.top, 165 | sky.length, rect.bottom - sky.top) 166 | else: 167 | # Segment 168 | self._merge_skyline(skylineq, sky) 169 | 170 | # Aaaaand ..... Done 171 | self._skyline = list(skylineq) 172 | 173 | def _rect_fitness(self, rect, left_index, right_index): 174 | return rect.top 175 | 176 | def _select_position(self, width, height): 177 | """ 178 | Search for the placement with the bes fitness for the rectangle. 179 | 180 | Returns: 181 | tuple (Rectangle, fitness) - Rectangle placed in the fittest position 182 | None - Rectangle couldn't be placed 183 | """ 184 | positions = self._generate_placements(width, height) 185 | if self.rot and width != height: 186 | positions += self._generate_placements(height, width) 187 | if not positions: 188 | return None, None 189 | return min(((p[0], self._rect_fitness(*p))for p in positions), 190 | key=operator.itemgetter(1)) 191 | 192 | def fitness(self, width, height): 193 | """Search for the best fitness 194 | """ 195 | assert(width > 0 and height >0) 196 | if width > max(self.width, self.height) or\ 197 | height > max(self.height, self.width): 198 | return None 199 | 200 | # If there is room in wasted space, FREE PACKING!! 201 | if self._waste_management: 202 | if self._waste.fitness(width, height) is not None: 203 | return 0 204 | 205 | # Get best fitness segment, for normal rectangle, and for 206 | # rotated rectangle if rotation is enabled. 207 | rect, fitness = self._select_position(width, height) 208 | return fitness 209 | 210 | def add_rect(self, width, height, rid=None): 211 | """ 212 | Add new rectangle 213 | """ 214 | assert(width > 0 and height > 0) 215 | if width > max(self.width, self.height) or\ 216 | height > max(self.height, self.width): 217 | return None 218 | 219 | rect = None 220 | # If Waste managment is enabled, first try to place the rectangle there 221 | if self._waste_management: 222 | rect = self._waste.add_rect(width, height, rid) 223 | 224 | # Get best possible rectangle position 225 | if not rect: 226 | rect, _ = self._select_position(width, height) 227 | if rect: 228 | self._add_skyline(rect) 229 | 230 | if rect is None: 231 | return None 232 | 233 | # Store rectangle, and recalculate skyline 234 | rect.rid = rid 235 | self.rectangles.append(rect) 236 | return rect 237 | 238 | def reset(self): 239 | super(Skyline, self).reset() 240 | self._skyline = [HSegment(P(0, 0), self.width)] 241 | self._waste.reset() 242 | 243 | 244 | 245 | 246 | class SkylineWMixin(Skyline): 247 | """Waste managment mixin""" 248 | def __init__(self, width, height, *args, **kwargs): 249 | super(SkylineWMixin, self).__init__(width, height, *args, **kwargs) 250 | self._waste_management = True 251 | 252 | 253 | class SkylineMwf(Skyline): 254 | """Implements Min Waste fit heuristic, minimizing the area wasted under the 255 | rectangle. 256 | """ 257 | def _rect_fitness(self, rect, left_index, right_index): 258 | waste = 0 259 | for seg in self._skyline[left_index:right_index+1]: 260 | waste +=\ 261 | (min(rect.right, seg.right)-max(rect.left, seg.left)) *\ 262 | (rect.bottom-seg.top) 263 | 264 | return waste 265 | 266 | def _rect_fitnes2s(self, rect, left_index, right_index): 267 | waste = ((min(rect.right, seg.right)-max(rect.left, seg.left)) for seg in self._skyline[left_index:right_index+1]) 268 | return sum(waste) 269 | 270 | class SkylineMwfl(Skyline): 271 | """Implements Min Waste fit with low profile heuritic, minimizing the area 272 | wasted below the rectangle, at the same time it tries to keep the height 273 | minimal. 274 | """ 275 | def _rect_fitness(self, rect, left_index, right_index): 276 | waste = 0 277 | for seg in self._skyline[left_index:right_index+1]: 278 | waste +=\ 279 | (min(rect.right, seg.right)-max(rect.left, seg.left)) *\ 280 | (rect.bottom-seg.top) 281 | 282 | return waste*self.width*self.height+rect.top 283 | 284 | 285 | class SkylineBl(Skyline): 286 | """Implements Bottom Left heuristic, the best fit option is that which 287 | results in which the top side of the rectangle lies at the bottom-most 288 | position. 289 | """ 290 | def _rect_fitness(self, rect, left_index, right_index): 291 | return rect.top 292 | 293 | 294 | 295 | 296 | class SkylineBlWm(SkylineBl, SkylineWMixin): 297 | pass 298 | 299 | class SkylineMwfWm(SkylineMwf, SkylineWMixin): 300 | pass 301 | 302 | class SkylineMwflWm(SkylineMwfl, SkylineWMixin): 303 | pass 304 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/test_maxrects.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from rectpack.geometry import Rectangle, Point 3 | import rectpack.maxrects as maxrects 4 | 5 | 6 | class TestMaxRects(TestCase): 7 | 8 | def test_init(self): 9 | # Test initial maximal rectangle 10 | m = maxrects.MaxRects(20, 50) 11 | self.assertEqual(m._max_rects[0], Rectangle(0, 0, 20, 50)) 12 | self.assertEqual(m.width, 20) 13 | self.assertEqual(m.height, 50) 14 | 15 | def test_reset(self): 16 | # Test _max_rects and rectangles is initialized 17 | m = maxrects.MaxRects(100, 200) 18 | self.assertTrue(m.add_rect(30, 30)) 19 | self.assertTrue(m.add_rect(50, 50)) 20 | self.assertEqual(len(m), 2) 21 | 22 | m.reset() 23 | self.assertEqual(len(m), 0) 24 | self.assertEqual(len(m._max_rects), 1) 25 | self.assertEqual(len(m.rectangles), 0) 26 | self.assertEqual(m._max_rects[0], Rectangle(0, 0, 100, 200)) 27 | 28 | def test_add_rect(self): 29 | # Basic packing test. 30 | m = maxrects.MaxRects(200, 100) 31 | self.assertEqual(m.add_rect(50, 30), Rectangle(0, 0, 50, 30)) 32 | self.assertEqual(len(m._max_rects), 2) 33 | self.assertEqual(m.add_rect(70, 200), Rectangle(0, 30, 200, 70)) 34 | self.assertEqual(len(m._max_rects), 1) 35 | self.assertEqual(m.add_rect(20, 20), Rectangle(50, 0, 20, 20)) 36 | self.assertEqual(len(m._max_rects), 2) 37 | self.assertEqual(m.add_rect(50, 50), None) 38 | self.assertEqual(m.add_rect(30, 100), Rectangle(70, 0, 100, 30)) 39 | 40 | #Test with rotation disabled 41 | m = maxrects.MaxRects(200, 50, rot=False) 42 | self.assertEqual(m.add_rect(40, 80), None) 43 | 44 | m = maxrects.MaxRects(200, 50, rot=True) 45 | self.assertEqual(m.add_rect(40, 80), Rectangle(0, 0, 80, 40)) 46 | 47 | def test_remove_duplicates(self): 48 | # Test duplicated collisions removed 49 | m = maxrects.MaxRects(100, 100) 50 | rect1 = Rectangle(0, 0, 60, 40) 51 | rect2 = Rectangle(30, 20, 60, 40) 52 | rect3 = Rectangle(35, 25, 10, 10) 53 | rect4 = Rectangle(90, 90, 10, 10) 54 | m._max_rects = [rect1, rect2, rect3, rect4] 55 | 56 | m._remove_duplicates() 57 | self.assertTrue(rect1 in m._max_rects) 58 | self.assertTrue(rect2 in m._max_rects) 59 | self.assertTrue(rect4 in m._max_rects) 60 | self.assertEqual(len(m._max_rects), 3) 61 | 62 | # Test with only one max_rect 63 | m = maxrects.MaxRects(100, 100) 64 | m._remove_duplicates() 65 | self.assertEqual(len(m._max_rects), 1) 66 | 67 | def test_iter(self): 68 | m = maxrects.MaxRects(100, 100) 69 | self.assertTrue(m.add_rect(10, 15)) 70 | self.assertTrue(m.add_rect(40, 40)) 71 | 72 | rectangles = [] 73 | for r in m: 74 | rectangles.append(r) 75 | 76 | self.assertTrue(Rectangle(0, 0, 10, 15) in rectangles) 77 | self.assertTrue(Rectangle(10, 0, 40, 40) in rectangles) 78 | self.assertEqual(len(rectangles), 2) 79 | 80 | def test_fitness(self): 81 | mr = maxrects.MaxRects(100, 200, rot=True) 82 | m = maxrects.MaxRects(100, 200, rot=False) 83 | self.assertEqual(m.fitness(200, 100), None) 84 | self.assertEqual(mr.fitness(200, 100), 0) 85 | self.assertEqual(m.fitness(100, 100), 0) 86 | 87 | def test_split(self): 88 | m = maxrects.MaxRects(100, 100) 89 | m.add_rect(20, 20) 90 | self.assertTrue(Rectangle(20, 0, 80, 100) in m._max_rects) 91 | self.assertTrue(Rectangle(0, 20, 100, 80) in m._max_rects) 92 | self.assertEqual(len(m._max_rects), 2) 93 | 94 | m._split(Rectangle(20, 20, 20, 20)) 95 | self.assertEqual(len(m._max_rects), 6) 96 | m._remove_duplicates() 97 | self.assertEqual(len(m._max_rects), 4) 98 | 99 | def test_generate_splits(self): 100 | m = maxrects.MaxRects(40, 40) 101 | mr = Rectangle(20, 20, 40, 40) 102 | 103 | # The same 104 | rects = m._generate_splits(mr, Rectangle(20, 20, 40, 40)) 105 | self.assertFalse(rects) 106 | 107 | # Contained 108 | rects = m._generate_splits(mr, Rectangle(0, 0, 80, 80)) 109 | self.assertFalse(rects) 110 | 111 | # Center 112 | rects = m._generate_splits(mr, Rectangle(30, 30, 10, 10)) 113 | self.assertTrue(Rectangle(20, 20, 10, 40) in rects) # Left 114 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 115 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 116 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) # Top 117 | self.assertEqual(len(rects), 4) 118 | 119 | # Top - Center 120 | rects = m._generate_splits(mr, Rectangle(30, 30, 10, 30)) 121 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 122 | self.assertTrue(Rectangle(20, 20, 10, 40) in rects) # Left 123 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 124 | self.assertEqual(len(rects), 3) 125 | 126 | rects = m._generate_splits(mr, Rectangle(30, 30, 10, 100)) 127 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 128 | self.assertTrue(Rectangle(20, 20, 10, 40) in rects) # Left 129 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 130 | self.assertEqual(len(rects), 3) 131 | 132 | # Bottom - Center 133 | rects = m._generate_splits(mr, Rectangle(30, 20, 10, 10)) 134 | self.assertTrue(Rectangle(20, 30, 40, 30) in rects) # Top 135 | self.assertTrue(Rectangle(20, 20, 10, 40) in rects) # Left 136 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 137 | self.assertEqual(len(rects), 3) 138 | 139 | rects = m._generate_splits(mr, Rectangle(30, 0, 10, 30)) 140 | self.assertTrue(Rectangle(20, 30, 40, 30) in rects) # Top 141 | self.assertTrue(Rectangle(20, 20, 10, 40) in rects) # Left 142 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 143 | self.assertEqual(len(rects), 3) 144 | 145 | # Left - Center 146 | rects = m._generate_splits(mr, Rectangle(20, 30, 20, 10)) 147 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) # Top 148 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 149 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 150 | self.assertEqual(len(rects), 3) 151 | 152 | rects = m._generate_splits(mr, Rectangle(0, 30, 40, 10)) 153 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) # Top 154 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 155 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 156 | self.assertEqual(len(rects), 3) 157 | 158 | # Right - Center 159 | rects = m._generate_splits(mr, Rectangle(40, 30, 20, 20)) 160 | self.assertTrue(Rectangle(20, 50, 40, 10) in rects) # Top 161 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 162 | self.assertTrue(Rectangle(20, 20, 20, 40) in rects) # Left 163 | self.assertEqual(len(rects), 3) 164 | 165 | rects = m._generate_splits(mr, Rectangle(40, 30, 90, 20)) 166 | self.assertTrue(Rectangle(20, 50, 40, 10) in rects) # Top 167 | self.assertTrue(Rectangle(20, 20, 40, 10) in rects) # Bottom 168 | self.assertTrue(Rectangle(20, 20, 20, 40) in rects) # Left 169 | self.assertEqual(len(rects), 3) 170 | 171 | # Top - Right 172 | rects = m._generate_splits(mr, Rectangle(40, 40, 20, 20)) 173 | self.assertTrue(Rectangle(20, 20, 20, 40) in rects) # Left 174 | self.assertTrue(Rectangle(20, 20, 40, 20) in rects) # Bottom 175 | self.assertEqual(len(rects), 2) 176 | 177 | rects = m._generate_splits(mr, Rectangle(40, 40, 30, 30)) 178 | self.assertTrue(Rectangle(20, 20, 20, 40) in rects) # Left 179 | self.assertTrue(Rectangle(20, 20, 40, 20) in rects) # Bottom 180 | self.assertEqual(len(rects), 2) 181 | 182 | # Bottom - Left 183 | rects = m._generate_splits(mr, Rectangle(20, 20, 20, 20)) 184 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) # Top 185 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 186 | self.assertEqual(len(rects), 2) 187 | 188 | rects = m._generate_splits(mr, Rectangle(10, 10, 30, 30)) 189 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) # Top 190 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) # Right 191 | self.assertEqual(len(rects), 2) 192 | 193 | # Top - Full 194 | rects = m._generate_splits(mr, Rectangle(20, 40, 40, 20)) 195 | self.assertTrue(Rectangle(20, 20, 40, 20) in rects) 196 | self.assertEqual(len(rects), 1) 197 | 198 | rects = m._generate_splits(mr, Rectangle(10, 40, 60, 60)) 199 | self.assertTrue(Rectangle(20, 20, 40, 20) in rects) 200 | self.assertEqual(len(rects), 1) 201 | 202 | # Bottom - Full 203 | rects = m._generate_splits(mr, Rectangle(20, 20, 40, 20)) 204 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) 205 | self.assertEqual(len(rects), 1) 206 | 207 | rects = m._generate_splits(mr, Rectangle(10, 10, 50, 30)) 208 | self.assertTrue(Rectangle(20, 40, 40, 20) in rects) 209 | self.assertEqual(len(rects), 1) 210 | 211 | # Right - Full 212 | rects = m._generate_splits(mr, Rectangle(40, 20, 20, 40)) 213 | self.assertTrue(Rectangle(20, 20, 20, 40) in rects) 214 | self.assertEqual(len(rects), 1) 215 | 216 | rects = m._generate_splits(mr, Rectangle(40, 10, 30, 60)) 217 | self.assertTrue(Rectangle(20, 20, 20, 40) in rects) 218 | self.assertEqual(len(rects), 1) 219 | 220 | # Left - Full 221 | rects = m._generate_splits(mr, Rectangle(20, 20, 20, 40)) 222 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) 223 | self.assertEqual(len(rects), 1) 224 | 225 | rects = m._generate_splits(mr, Rectangle(10, 10, 30, 60)) 226 | self.assertTrue(Rectangle(40, 20, 20, 40) in rects) 227 | self.assertEqual(len(rects), 1) 228 | 229 | def test_getitem(self): 230 | m = maxrects.MaxRectsBl(100, 100, rot=False) 231 | m.add_rect(40, 40) 232 | m.add_rect(20, 20) 233 | m.add_rect(60, 40) 234 | self.assertEqual(m[0], Rectangle(0, 0, 40, 40)) 235 | self.assertEqual(m[1], Rectangle(40, 0, 20, 20)) 236 | self.assertEqual(m[2], Rectangle(40, 20, 60, 40)) 237 | 238 | self.assertEqual(m[-1], Rectangle(40, 20, 60, 40)) 239 | self.assertEqual(m[1:], 240 | [Rectangle(40, 0, 20, 20), Rectangle(40, 20, 60, 40)]) 241 | 242 | 243 | 244 | class TestMaxRectBL(TestCase): 245 | 246 | def test_select_position(self): 247 | m = maxrects.MaxRectsBl(100, 100, rot=False) 248 | self.assertEqual(m.add_rect(40, 40), Rectangle(0, 0, 40, 40)) 249 | self.assertFalse(m.add_rect(100, 100)) 250 | 251 | self.assertEqual(m.add_rect(20, 20), Rectangle(40, 0, 20, 20)) 252 | self.assertEqual(m.add_rect(60, 40), Rectangle(40, 20, 60, 40)) 253 | 254 | 255 | class TestMaxRectBAF(TestCase): 256 | 257 | def test_rect_fitness(self): 258 | m = maxrects.MaxRectsBaf(100, 100, rot=False) 259 | self.assertEqual(m.add_rect(60, 10), Rectangle(0, 0, 60, 10)) 260 | 261 | self.assertTrue(m.fitness(40, 40) < m.fitness(50, 50)) 262 | self.assertTrue(m.fitness(40, 40) < m.fitness(35, 35)) 263 | self.assertEqual(m.add_rect(40, 40), Rectangle(60, 0, 40, 40)) 264 | 265 | class TestMaxRectBLSF(TestCase): 266 | 267 | def test_rect_fitnesss(self): 268 | m = maxrects.MaxRectsBlsf(100, 100, rot=False) 269 | self.assertEqual(m.add_rect(60, 10), Rectangle(0, 0, 60, 10)) 270 | 271 | self.assertTrue(m.fitness(30, 90) < m.fitness(40, 89)) 272 | self.assertTrue(m.fitness(99, 10) < m.fitness(99, 5)) 273 | 274 | 275 | class TestMaxRectBSSF(TestCase): 276 | 277 | def test_rect_fitness(self): 278 | m = maxrects.MaxRectsBssf(100, 100, rot=False) 279 | self.assertEqual(m.add_rect(60, 10), Rectangle(0, 0, 60, 10)) 280 | 281 | self.assertTrue(m.fitness(30, 91) > m.fitness(30, 92)) 282 | self.assertTrue(m.fitness(38, 91) < m.fitness(30, 92)) 283 | self.assertTrue(m.fitness(38, 91) > m.fitness(40, 92)) 284 | -------------------------------------------------------------------------------- /rectpack/guillotine.py: -------------------------------------------------------------------------------- 1 | from .pack_algo import PackingAlgorithm 2 | from .geometry import Rectangle 3 | import itertools 4 | import operator 5 | 6 | 7 | class Guillotine(PackingAlgorithm): 8 | """Implementation of several variants of Guillotine packing algorithm 9 | 10 | For a more detailed explanation of the algorithm used, see: 11 | Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010) 12 | """ 13 | def __init__(self, width, height, rot=True, merge=True, *args, **kwargs): 14 | """ 15 | Arguments: 16 | width (int, float): 17 | height (int, float): 18 | merge (bool): Optional keyword argument 19 | """ 20 | self._merge = merge 21 | super(Guillotine, self).__init__(width, height, rot, *args, **kwargs) 22 | 23 | 24 | def _add_section(self, section): 25 | """Adds a new section to the free section list, but before that and if 26 | section merge is enabled, tries to join the rectangle with all existing 27 | sections, if successful the resulting section is again merged with the 28 | remaining sections until the operation fails. The result is then 29 | appended to the list. 30 | 31 | Arguments: 32 | section (Rectangle): New free section. 33 | """ 34 | section.rid = 0 35 | plen = 0 36 | 37 | while self._merge and self._sections and plen != len(self._sections): 38 | plen = len(self._sections) 39 | self._sections = [s for s in self._sections if not section.join(s)] 40 | self._sections.append(section) 41 | 42 | 43 | def _split_horizontal(self, section, width, height): 44 | """For an horizontal split the rectangle is placed in the lower 45 | left corner of the section (section's xy coordinates), the top 46 | most side of the rectangle and its horizontal continuation, 47 | marks the line of division for the split. 48 | +-----------------+ 49 | | | 50 | | | 51 | | | 52 | | | 53 | +-------+---------+ 54 | |#######| | 55 | |#######| | 56 | |#######| | 57 | +-------+---------+ 58 | If the rectangle width is equal to the the section width, only one 59 | section is created over the rectangle. If the rectangle height is 60 | equal to the section height, only one section to the right of the 61 | rectangle is created. If both width and height are equal, no sections 62 | are created. 63 | """ 64 | # First remove the section we are splitting so it doesn't 65 | # interfere when later we try to merge the resulting split 66 | # rectangles, with the rest of free sections. 67 | #self._sections.remove(section) 68 | 69 | # Creates two new empty sections, and returns the new rectangle. 70 | if height < section.height: 71 | self._add_section(Rectangle(section.x, section.y+height, 72 | section.width, section.height-height)) 73 | 74 | if width < section.width: 75 | self._add_section(Rectangle(section.x+width, section.y, 76 | section.width-width, height)) 77 | 78 | 79 | def _split_vertical(self, section, width, height): 80 | """For a vertical split the rectangle is placed in the lower 81 | left corner of the section (section's xy coordinates), the 82 | right most side of the rectangle and its vertical continuation, 83 | marks the line of division for the split. 84 | +-------+---------+ 85 | | | | 86 | | | | 87 | | | | 88 | | | | 89 | +-------+ | 90 | |#######| | 91 | |#######| | 92 | |#######| | 93 | +-------+---------+ 94 | If the rectangle width is equal to the the section width, only one 95 | section is created over the rectangle. If the rectangle height is 96 | equal to the section height, only one section to the right of the 97 | rectangle is created. If both width and height are equal, no sections 98 | are created. 99 | """ 100 | # When a section is split, depending on the rectangle size 101 | # two, one, or no new sections will be created. 102 | if height < section.height: 103 | self._add_section(Rectangle(section.x, section.y+height, 104 | width, section.height-height)) 105 | 106 | if width < section.width: 107 | self._add_section(Rectangle(section.x+width, section.y, 108 | section.width-width, section.height)) 109 | 110 | 111 | def _split(self, section, width, height): 112 | """ 113 | Selects the best split for a section, given a rectangle of dimmensions 114 | width and height, then calls _split_vertical or _split_horizontal, 115 | to do the dirty work. 116 | 117 | Arguments: 118 | section (Rectangle): Section to split 119 | width (int, float): Rectangle width 120 | height (int, float): Rectangle height 121 | """ 122 | raise NotImplementedError 123 | 124 | 125 | def _section_fitness(self, section, width, height): 126 | """The subclass for each one of the Guillotine selection methods, 127 | BAF, BLSF.... will override this method, this is here only 128 | to asure a valid value return if the worst happens. 129 | """ 130 | raise NotImplementedError 131 | 132 | def _select_fittest_section(self, w, h): 133 | """Calls _section_fitness for each of the sections in free section 134 | list. Returns the section with the minimal fitness value, all the rest 135 | is boilerplate to make the fitness comparison, to rotatate the rectangles, 136 | and to take into account when _section_fitness returns None because 137 | the rectangle couldn't be placed. 138 | 139 | Arguments: 140 | w (int, float): Rectangle width 141 | h (int, float): Rectangle height 142 | 143 | Returns: 144 | (section, was_rotated): Returns the tuple 145 | section (Rectangle): Section with best fitness 146 | was_rotated (bool): The rectangle was rotated 147 | """ 148 | fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections 149 | if self._section_fitness(s, w, h) is not None) 150 | fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections 151 | if self._section_fitness(s, h, w) is not None) 152 | 153 | if not self.rot: 154 | fitr = [] 155 | 156 | fit = itertools.chain(fitn, fitr) 157 | 158 | try: 159 | _, sec, rot = min(fit, key=operator.itemgetter(0)) 160 | except ValueError: 161 | return None, None 162 | 163 | return sec, rot 164 | 165 | 166 | def add_rect(self, width, height, rid=None): 167 | """ 168 | Add rectangle of widthxheight dimensions. 169 | 170 | Arguments: 171 | width (int, float): Rectangle width 172 | height (int, float): Rectangle height 173 | rid: Optional rectangle user id 174 | 175 | Returns: 176 | Rectangle: Rectangle with placemente coordinates 177 | None: If the rectangle couldn be placed. 178 | """ 179 | assert(width > 0 and height >0) 180 | 181 | # Obtain the best section to place the rectangle. 182 | section, rotated = self._select_fittest_section(width, height) 183 | if not section: 184 | return None 185 | 186 | if rotated: 187 | width, height = height, width 188 | 189 | # Remove section, split and store results 190 | self._sections.remove(section) 191 | self._split(section, width, height) 192 | 193 | # Store rectangle in the selected position 194 | rect = Rectangle(section.x, section.y, width, height, rid) 195 | self.rectangles.append(rect) 196 | return rect 197 | 198 | def fitness(self, width, height): 199 | """ 200 | In guillotine algorithm case, returns the min of the fitness of all 201 | free sections, for the given dimension, both normal and rotated 202 | (if rotation enabled.) 203 | """ 204 | assert(width > 0 and height > 0) 205 | 206 | # Get best fitness section. 207 | section, rotated = self._select_fittest_section(width, height) 208 | if not section: 209 | return None 210 | 211 | # Return fitness of returned section, with correct dimmensions if the 212 | # the rectangle was rotated. 213 | if rotated: 214 | return self._section_fitness(section, height, width) 215 | else: 216 | return self._section_fitness(section, width, height) 217 | 218 | def reset(self): 219 | super(Guillotine, self).reset() 220 | self._sections = [] 221 | self._add_section(Rectangle(0, 0, self.width, self.height)) 222 | 223 | 224 | 225 | class GuillotineBaf(Guillotine): 226 | """Implements Best Area Fit (BAF) section selection criteria for 227 | Guillotine algorithm. 228 | """ 229 | def _section_fitness(self, section, width, height): 230 | if width > section.width or height > section.height: 231 | return None 232 | return section.area()-width*height 233 | 234 | 235 | class GuillotineBlsf(Guillotine): 236 | """Implements Best Long Side Fit (BLSF) section selection criteria for 237 | Guillotine algorithm. 238 | """ 239 | def _section_fitness(self, section, width, height): 240 | if width > section.width or height > section.height: 241 | return None 242 | return max(section.width-width, section.height-height) 243 | 244 | 245 | class GuillotineBssf(Guillotine): 246 | """Implements Best Short Side Fit (BSSF) section selection criteria for 247 | Guillotine algorithm. 248 | """ 249 | def _section_fitness(self, section, width, height): 250 | if width > section.width or height > section.height: 251 | return None 252 | return min(section.width-width, section.height-height) 253 | 254 | 255 | class GuillotineSas(Guillotine): 256 | """Implements Short Axis Split (SAS) selection rule for Guillotine 257 | algorithm. 258 | """ 259 | def _split(self, section, width, height): 260 | if section.width < section.height: 261 | return self._split_horizontal(section, width, height) 262 | else: 263 | return self._split_vertical(section, width, height) 264 | 265 | 266 | 267 | class GuillotineLas(Guillotine): 268 | """Implements Long Axis Split (LAS) selection rule for Guillotine 269 | algorithm. 270 | """ 271 | def _split(self, section, width, height): 272 | if section.width >= section.height: 273 | return self._split_horizontal(section, width, height) 274 | else: 275 | return self._split_vertical(section, width, height) 276 | 277 | 278 | 279 | class GuillotineSlas(Guillotine): 280 | """Implements Short Leftover Axis Split (SLAS) selection rule for 281 | Guillotine algorithm. 282 | """ 283 | def _split(self, section, width, height): 284 | if section.width-width < section.height-height: 285 | return self._split_horizontal(section, width, height) 286 | else: 287 | return self._split_vertical(section, width, height) 288 | 289 | 290 | 291 | class GuillotineLlas(Guillotine): 292 | """Implements Long Leftover Axis Split (LLAS) selection rule for 293 | Guillotine algorithm. 294 | """ 295 | def _split(self, section, width, height): 296 | if section.width-width >= section.height-height: 297 | return self._split_horizontal(section, width, height) 298 | else: 299 | return self._split_vertical(section, width, height) 300 | 301 | 302 | 303 | class GuillotineMaxas(Guillotine): 304 | """Implements Max Area Axis Split (MAXAS) selection rule for Guillotine 305 | algorithm. Maximize the larger area == minimize the smaller area. 306 | Tries to make the rectangles more even-sized. 307 | """ 308 | def _split(self, section, width, height): 309 | if width*(section.height-height) <= height*(section.width-width): 310 | return self._split_horizontal(section, width, height) 311 | else: 312 | return self._split_vertical(section, width, height) 313 | 314 | 315 | 316 | class GuillotineMinas(Guillotine): 317 | """Implements Min Area Axis Split (MINAS) selection rule for Guillotine 318 | algorithm. 319 | """ 320 | def _split(self, section, width, height): 321 | if width*(section.height-height) >= height*(section.width-width): 322 | return self._split_horizontal(section, width, height) 323 | else: 324 | return self._split_vertical(section, width, height) 325 | 326 | 327 | 328 | # Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one 329 | # Axis split, and one selection criteria. 330 | class GuillotineBssfSas(GuillotineBssf, GuillotineSas): 331 | pass 332 | class GuillotineBssfLas(GuillotineBssf, GuillotineLas): 333 | pass 334 | class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas): 335 | pass 336 | class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas): 337 | pass 338 | class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas): 339 | pass 340 | class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas): 341 | pass 342 | class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas): 343 | pass 344 | class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas): 345 | pass 346 | class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas): 347 | pass 348 | class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas): 349 | pass 350 | class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas): 351 | pass 352 | class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas): 353 | pass 354 | class GuillotineBafSas(GuillotineBaf, GuillotineSas): 355 | pass 356 | class GuillotineBafLas(GuillotineBaf, GuillotineLas): 357 | pass 358 | class GuillotineBafSlas(GuillotineBaf, GuillotineSlas): 359 | pass 360 | class GuillotineBafLlas(GuillotineBaf, GuillotineLlas): 361 | pass 362 | class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas): 363 | pass 364 | class GuillotineBafMinas(GuillotineBaf, GuillotineMinas): 365 | pass 366 | 367 | 368 | 369 | -------------------------------------------------------------------------------- /tests/test_geometry.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from rectpack.geometry import Point, Segment, VSegment, HSegment, Rectangle 3 | 4 | 5 | 6 | 7 | class TestPoint(TestCase): 8 | 9 | def test_init(self): 10 | p = Point(1, 2) 11 | self.assertEqual(p.x, 1) 12 | self.assertEqual(p.y, 2) 13 | 14 | def test_equality(self): 15 | p1 = Point(3, 4) 16 | p2 = Point(3, 4) 17 | p3 = Point(4, 3) 18 | 19 | self.assertTrue(p1==p2) 20 | self.assertFalse(p1==p3) 21 | self.assertFalse(p2==p3) 22 | 23 | def test_distance(self): 24 | p1 = Point(1, 1) 25 | p2 = Point(2, 1) 26 | 27 | self.assertEqual(p1.distance(p2), 1) 28 | 29 | 30 | class TestSegment(TestCase): 31 | 32 | def test_init(self): 33 | s = Segment(Point(0,0), Point(3, 3)) 34 | self.assertEqual(s.start, Point(0, 0)) 35 | self.assertEqual(s.end, Point(3, 3)) 36 | 37 | with self.assertRaises(AssertionError): 38 | Segment(Point(1, 1), 2) 39 | 40 | with self.assertRaises(AssertionError): 41 | Segment(2, Point(1, 1)) 42 | 43 | def test_length(self): 44 | s = Segment(Point(0, 0), Point(0, 10)) 45 | self.assertEqual(s.length, 10) 46 | 47 | s = Segment(Point(0, 0), Point(0, 3)) 48 | self.assertEqual(s.length, 3) 49 | 50 | def test_maxmin(self): 51 | s = Segment(Point(1, 1), Point(5, 3)) 52 | self.assertEqual(s.top, 3) 53 | self.assertEqual(s.bottom, 1) 54 | self.assertEqual(s.right, 5) 55 | self.assertEqual(s.left, 1) 56 | 57 | def test_eq(self): 58 | s1 = Segment(Point(0,0), Point(0, 10)) 59 | s2 = Segment(Point(1, 1), Point(1, 11)) 60 | s3 = Segment(Point(0, 0), Point(0, 10)) 61 | self.assertEqual(s1, s3) 62 | self.assertNotEqual(s1, s2) 63 | 64 | class TestHSegment(TestCase): 65 | 66 | def test_init(self): 67 | s = HSegment(Point(1, 1), 10) 68 | self.assertEqual(s.start, Point(1, 1)) 69 | self.assertEqual(s.end, Point(11, 1)) 70 | 71 | with self.assertRaises(AssertionError): 72 | HSegment(Point(1, 1), Point(1, 3)) 73 | 74 | with self.assertRaises(AssertionError): 75 | HSegment(1, Point(1, 1)) 76 | 77 | def test_length(self): 78 | s = HSegment(Point(10, 10), 10) 79 | self.assertEqual(s.length, 10) 80 | 81 | 82 | class TestVSegment(TestCase): 83 | 84 | def test_init(self): 85 | s = VSegment(Point(1, 1), 10) 86 | self.assertEqual(s.start, Point(1, 1)) 87 | self.assertEqual(s.end, Point(1, 11)) 88 | 89 | with self.assertRaises(AssertionError): 90 | VSegment(Point(1, 1), Point(1, 3)) 91 | 92 | with self.assertRaises(AssertionError): 93 | VSegment(1, Point(1, 1)) 94 | 95 | def test_length(self): 96 | s = VSegment(Point(1, 1), 10) 97 | self.assertEqual(s.length, 10) 98 | 99 | 100 | class TestRectangle(TestCase): 101 | 102 | def test_initialization(self): 103 | r = Rectangle(1, 2, 3, 4) 104 | self.assertEqual(r.left, 1) 105 | self.assertEqual(r.bottom, 2) 106 | self.assertEqual(r.right, 4) 107 | self.assertEqual(r.top, 6) 108 | self.assertEqual(r.width, 3) 109 | self.assertEqual(r.height, 4) 110 | 111 | self.assertEqual(r.corner_top_l, Point(1, 6)) 112 | self.assertEqual(r.corner_top_r, Point(4, 6)) 113 | self.assertEqual(r.corner_bot_l, Point(1, 2)) 114 | self.assertEqual(r.corner_bot_r, Point(4, 2)) 115 | 116 | def test_bool(self): 117 | """Test rectangles evaluates a True""" 118 | r = Rectangle(1, 2, 3, 4) 119 | self.assertTrue(r) 120 | 121 | def test_move(self): 122 | r = Rectangle(1, 2, 2, 2) 123 | self.assertEqual(r.bottom, 2) 124 | self.assertEqual(r.left, 1) 125 | 126 | r.move(10, 12) 127 | self.assertEqual(r.bottom, 12) 128 | self.assertEqual(r.left, 10) 129 | self.assertEqual(r.top,14) 130 | self.assertEqual(r.right, 12) 131 | self.assertEqual(r.height, 2) 132 | self.assertEqual(r.width, 2) 133 | 134 | self.assertEqual(r.corner_top_l, Point(10, 14)) 135 | self.assertEqual(r.corner_top_r, Point(12, 14)) 136 | self.assertEqual(r.corner_bot_l, Point(10, 12)) 137 | self.assertEqual(r.corner_bot_r, Point(12, 12)) 138 | 139 | def test_area(self): 140 | r1 = Rectangle(0, 0, 1, 2) 141 | r2 = Rectangle(1, 1, 2, 3) 142 | r3 = Rectangle(1, 1, 5, 5) 143 | 144 | self.assertEqual(r1.area(), 2) 145 | self.assertEqual(r2.area(), 6) 146 | 147 | def test_equal(self): 148 | r1 = Rectangle(0, 0, 1, 2) 149 | r2 = Rectangle(0, 0, 1, 1) 150 | r3 = Rectangle(1, 1, 1, 2) 151 | self.assertNotEqual(r1, r3) 152 | self.assertNotEqual(r1, r2) 153 | self.assertNotEqual(r2, r3) 154 | 155 | r4 = Rectangle(0, 0, 1, 2) 156 | self.assertEqual(r1, r4) 157 | 158 | r5 = Rectangle(1, 1, 1, 2) 159 | self.assertEqual(r3, r5) 160 | 161 | def test_lt(self): 162 | r1 = Rectangle(0, 0, 2, 1) 163 | r2 = Rectangle(0, 0, 1, 2) 164 | r3 = Rectangle(0, 0, 1, 3) 165 | 166 | self.assertFalse(r1 < r2) 167 | self.assertTrue(r1 < r3) 168 | self.assertTrue(r2 < r3) 169 | 170 | def test_hash(self): 171 | """Test it is hashable""" 172 | r = Rectangle(1, 1, 1, 1) 173 | d = {r: 43} 174 | 175 | self.assertEqual(d[r], 43) 176 | 177 | def test_intersects(self): 178 | 179 | # No intersections 180 | r = Rectangle(20, 20, 4, 4) 181 | r1 = Rectangle(30, 30, 1, 1) 182 | r2 = Rectangle(20, 40, 2, 2) 183 | r3 = Rectangle(10, 10, 2, 2) 184 | 185 | self.assertFalse(r.intersects(r1)) 186 | self.assertFalse(r.intersects(r2)) 187 | self.assertFalse(r.intersects(r3)) 188 | 189 | # Full contained intersects 190 | r = Rectangle(20, 20, 4, 4) 191 | r1 = Rectangle(21, 21, 2, 2) 192 | r2 = Rectangle(18, 18, 8, 8) 193 | self.assertTrue(r.intersects(r1)) 194 | self.assertTrue(r.intersects(r2)) 195 | 196 | # Area intersects 197 | r = Rectangle(10, 10, 2, 2) 198 | r_top = Rectangle(10, 11, 2 , 2) 199 | r_bottom = Rectangle(10, 9, 2, 2) 200 | r_right = Rectangle(11, 10, 2, 2) 201 | r_left = Rectangle(9, 10, 2, 2) 202 | self.assertTrue(r.intersects(r_top)) 203 | self.assertTrue(r.intersects(r_bottom)) 204 | self.assertTrue(r.intersects(r_right)) 205 | self.assertTrue(r.intersects(r_left)) 206 | 207 | r = Rectangle(10, 10, 2, 2) 208 | r_top_left = Rectangle(9, 11, 2, 2) 209 | r_top_right = Rectangle(11, 11, 2, 2) 210 | r_bottom_left = Rectangle(9, 9, 2, 2) 211 | r_bottom_right = Rectangle(11, 9, 2, 2) 212 | self.assertTrue(r.intersects(r_top_left)) 213 | self.assertTrue(r.intersects(r_top_right)) 214 | self.assertTrue(r.intersects(r_bottom_left)) 215 | self.assertTrue(r.intersects(r_bottom_right)) 216 | 217 | # Edge intersects 218 | r = Rectangle(10, 10, 2, 2) 219 | r_top = Rectangle(10, 12, 2, 2) 220 | r_bottom = Rectangle(10, 8, 2, 2) 221 | r_right = Rectangle(12, 10, 2, 2) 222 | r_left = Rectangle(8, 10, 2, 2) 223 | 224 | self.assertFalse(r.intersects(r_top)) 225 | self.assertFalse(r.intersects(r_bottom)) 226 | self.assertFalse(r.intersects(r_left)) 227 | self.assertFalse(r.intersects(r_right)) 228 | 229 | self.assertTrue(r.intersects(r_top, edges=True)) 230 | self.assertTrue(r.intersects(r_bottom, edges=True)) 231 | self.assertTrue(r.intersects(r_left, edges=True)) 232 | self.assertTrue(r.intersects(r_right, edges=True)) 233 | 234 | # Partial Edge intersects 235 | r = Rectangle(10, 10, 2, 2) 236 | r_top = Rectangle(10, 12, 3, 2) 237 | r_bottom = Rectangle(10, 8, 1, 2) 238 | r_right = Rectangle(12, 10, 2, 1) 239 | r_left = Rectangle(8, 10, 2, 3) 240 | 241 | self.assertFalse(r.intersects(r_top)) 242 | self.assertFalse(r.intersects(r_bottom)) 243 | self.assertFalse(r.intersects(r_left)) 244 | self.assertFalse(r.intersects(r_right)) 245 | 246 | self.assertTrue(r.intersects(r_top, edges=True)) 247 | self.assertTrue(r.intersects(r_bottom, edges=True)) 248 | self.assertTrue(r.intersects(r_left, edges=True)) 249 | self.assertTrue(r.intersects(r_right, edges=True)) 250 | 251 | # Corner intersects 252 | r = Rectangle(10, 10, 2, 2) 253 | r_top_left = Rectangle(8, 12, 2, 2) 254 | r_top_right = Rectangle(12, 12, 2, 2) 255 | r_bottom_left = Rectangle(8, 8, 2, 2) 256 | r_bottom_right = Rectangle(12, 8, 2, 2) 257 | self.assertFalse(r.intersects(r_top_left)) 258 | self.assertFalse(r.intersects(r_top_right)) 259 | self.assertFalse(r.intersects(r_bottom_left)) 260 | self.assertFalse(r.intersects(r_bottom_right)) 261 | 262 | self.assertTrue(r.intersects(r_top_left, edges=True)) 263 | self.assertTrue(r.intersects(r_top_right, edges=True)) 264 | self.assertTrue(r.intersects(r_bottom_left, edges=True)) 265 | self.assertTrue(r.intersects(r_bottom_right, edges=True)) 266 | 267 | 268 | def test_intersection(self): 269 | 270 | # No intersection 271 | r = Rectangle(20, 20, 4, 4) 272 | r1 = Rectangle(30, 30, 1, 1) 273 | r2 = Rectangle(20, 40, 2, 2) 274 | r3 = Rectangle(10, 10, 2, 2) 275 | 276 | self.assertFalse(r.intersection(r1)) 277 | self.assertFalse(r.intersection(r2)) 278 | self.assertFalse(r.intersection(r3)) 279 | 280 | # Full contained intersection 281 | r = Rectangle(20, 20, 4, 4) 282 | r1 = Rectangle(21, 21, 2, 2) 283 | self.assertEqual(r1, r.intersection(r1)) 284 | 285 | # Area intersection 286 | r = Rectangle(10, 10, 2, 2) 287 | r_top = Rectangle(10, 11, 2 , 2) 288 | r_bottom = Rectangle(10, 9, 2, 2) 289 | r_right = Rectangle(11, 10, 2, 2) 290 | r_left = Rectangle(9, 10, 2, 2) 291 | self.assertEqual(r.intersection(r_top), Rectangle(10, 11, 2, 1)) 292 | self.assertEqual(r.intersection(r_bottom), Rectangle(10, 10, 2, 1)) 293 | self.assertEqual(r.intersection(r_right), Rectangle(11, 10, 1, 2)) 294 | self.assertEqual(r.intersection(r_left), Rectangle(10, 10, 1, 2)) 295 | 296 | r = Rectangle(10, 10, 2, 4) 297 | r_top_left = Rectangle(9, 12, 2, 4) 298 | r_top_right = Rectangle(11, 12, 2, 4) 299 | r_bottom_left = Rectangle(9, 9, 2, 4) 300 | r_bottom_right = Rectangle(11, 9, 2, 4) 301 | self.assertEqual(r.intersection(r_top_left), Rectangle(10, 12, 1, 2)) 302 | self.assertEqual(r.intersection(r_top_right), Rectangle(11, 12, 1, 2)) 303 | self.assertEqual(r.intersection(r_bottom_left), Rectangle(10, 10, 1, 3)) 304 | self.assertEqual(r.intersection(r_bottom_right), Rectangle(11, 10, 1, 3)) 305 | 306 | # Edge intersection 307 | r = Rectangle(10, 10, 2, 2) 308 | r_top = Rectangle(10, 12, 3, 2) 309 | r_bottom = Rectangle(10, 8, 1, 2) 310 | r_left = Rectangle(8, 10, 2, 1) 311 | r_right = Rectangle(12, 10, 2, 1) 312 | self.assertEqual(r.intersection(r_top), None) 313 | self.assertEqual(r.intersection(r_bottom), None) 314 | self.assertEqual(r.intersection(r_left), None) 315 | self.assertEqual(r.intersection(r_right), None) 316 | 317 | self.assertEqual(r.intersection(r_top, edges=True), Rectangle(10, 12, 2, 0)) 318 | self.assertEqual(r.intersection(r_bottom, edges=True), Rectangle(10, 10, 1, 0)) 319 | self.assertEqual(r.intersection(r_left, edges=True), Rectangle(10, 10, 0, 1)) 320 | self.assertEqual(r.intersection(r_right, edges=True), Rectangle(12, 10, 0,1)) 321 | 322 | # Corners 323 | r = Rectangle(10, 10, 1, 1) 324 | r_top_r = Rectangle(11, 11, 1, 1) 325 | r_top_l = Rectangle(9, 11, 1, 1) 326 | r_bot_r = Rectangle(11, 9, 1, 1) 327 | r_bot_l = Rectangle(9, 9, 1, 1) 328 | 329 | self.assertEqual(r.intersection(r_top_r), None) 330 | self.assertEqual(r.intersection(r_top_l), None) 331 | self.assertEqual(r.intersection(r_bot_r), None) 332 | self.assertEqual(r.intersection(r_bot_l), None) 333 | 334 | self.assertEqual(r.intersection(r_top_r, edges=True), Rectangle(11, 11, 0, 0)) 335 | self.assertEqual(r.intersection(r_top_l, edges=True), Rectangle(10, 11, 0, 0)) 336 | self.assertEqual(r.intersection(r_bot_r, edges=True), Rectangle(11, 10, 0, 0)) 337 | self.assertEqual(r.intersection(r_bot_l, edges=True), Rectangle(10, 10, 0, 0)) 338 | 339 | def test_contains(self): 340 | 341 | # Outside 342 | r = Rectangle(1, 1, 3, 3) 343 | r1 = Rectangle(10, 10, 1, 2) 344 | self.assertFalse(r.contains(r1)) 345 | 346 | # Part inside 347 | r = Rectangle(1, 1, 2, 2) 348 | r1 = Rectangle(2, 2, 4, 4) 349 | r2 = Rectangle(0, 1, 2, 3) 350 | r3 = Rectangle(0, 0, 4, 4) 351 | self.assertFalse(r.contains(r1)) 352 | self.assertFalse(r.contains(r2)) 353 | self.assertFalse(r.contains(r3)) 354 | 355 | # Same size 356 | r = Rectangle(1, 1, 1, 1) 357 | r1 = Rectangle(1, 1, 1, 1) 358 | self.assertTrue(r.contains(r1)) 359 | 360 | # Inside touching edges 361 | 362 | # Inside 363 | r = Rectangle(1, 1, 4, 4) 364 | r1 = Rectangle(2, 2, 1, 1) 365 | self.assertTrue(r.contains(r1)) 366 | 367 | def test_join(self): 368 | 369 | # Top edge 370 | r = Rectangle(4, 4, 2, 2) 371 | r_top = Rectangle(4, 6, 2, 2) 372 | r_top_big = Rectangle(4, 6, 3, 2) 373 | 374 | self.assertFalse(r.join(r_top_big)) 375 | self.assertTrue(r.join(r_top)) 376 | self.assertEqual(r.corner_top_l, Point(4, 8)) 377 | self.assertEqual(r.corner_top_r, Point(6, 8)) 378 | self.assertEqual(r.corner_bot_l, Point(4, 4)) 379 | self.assertEqual(r.corner_bot_r, Point(6, 4)) 380 | 381 | # Bottom edge 382 | r = Rectangle(4, 4, 2, 2) 383 | r_bottom = Rectangle(4, 2, 2, 2) 384 | r_bottom_big = Rectangle(4, 2, 4, 2) 385 | 386 | self.assertFalse(r.join(r_bottom_big)) 387 | self.assertTrue(r.join(r_bottom)) 388 | self.assertEqual(r.corner_top_l, Point(4, 6)) 389 | self.assertEqual(r.corner_top_r, Point(6, 6)) 390 | self.assertEqual(r.corner_bot_l, Point(4, 2)) 391 | self.assertEqual(r.corner_bot_r, Point(6, 2)) 392 | 393 | # Right edge 394 | r = Rectangle(4, 4, 2, 2) 395 | r_right = Rectangle(6, 4, 3, 2) 396 | r_right_big = Rectangle(6, 4, 2, 4) 397 | 398 | self.assertFalse(r.join(r_right_big)) 399 | self.assertTrue(r.join(r_right)) 400 | self.assertEqual(r.corner_top_l, Point(4, 6)) 401 | self.assertEqual(r.corner_top_r, Point(9, 6)) 402 | self.assertEqual(r.corner_bot_l, Point(4, 4)) 403 | self.assertEqual(r.corner_bot_r, Point(9, 4)) 404 | 405 | # Left edge 406 | r = Rectangle(4, 4, 2, 2) 407 | r_left = Rectangle(2, 4, 2, 2) 408 | r_left_big = Rectangle(2, 4, 2, 4) 409 | 410 | self.assertFalse(r.join(r_left_big)) 411 | self.assertTrue(r.join(r_left)) 412 | self.assertEqual(r.corner_top_l, Point(2, 6)) 413 | self.assertEqual(r.corner_top_r, Point(6, 6)) 414 | self.assertEqual(r.corner_bot_l, Point(2, 4)) 415 | self.assertEqual(r.corner_bot_r, Point(6, 4)) 416 | 417 | # Contains 418 | r = Rectangle(4, 4, 3, 3) 419 | r_inside = Rectangle(4, 4, 1, 1) 420 | 421 | self.assertTrue(r.join(r_inside)) 422 | self.assertEqual(r.corner_top_l, Point(4, 7)) 423 | self.assertEqual(r.corner_top_r, Point(7, 7)) 424 | self.assertEqual(r.corner_bot_l, Point(4, 4)) 425 | self.assertEqual(r.corner_bot_r, Point(7, 4)) 426 | 427 | # Is contained 428 | r = Rectangle(4, 4, 1, 1) 429 | r_inside = Rectangle(4, 4, 3, 3) 430 | 431 | self.assertTrue(r.join(r_inside)) 432 | self.assertEqual(r.corner_top_l, Point(4, 7)) 433 | self.assertEqual(r.corner_top_r, Point(7, 7)) 434 | self.assertEqual(r.corner_bot_l, Point(4, 4)) 435 | self.assertEqual(r.corner_bot_r, Point(7, 4)) 436 | 437 | # The same 438 | r = Rectangle(2, 2, 2, 2) 439 | r_same = Rectangle(2, 2, 2, 2) 440 | 441 | self.assertTrue(r.join(r_same)) 442 | 443 | def test_iter(self): 444 | r = Rectangle(1, 1, 1, 2) 445 | corners = [Point(1, 1), Point(2, 1), Point(2, 3), Point(1,3)] 446 | 447 | for c in r: 448 | self.assertTrue(c in corners) 449 | -------------------------------------------------------------------------------- /rectpack/packer.py: -------------------------------------------------------------------------------- 1 | from .maxrects import MaxRectsBssf 2 | 3 | import operator 4 | import itertools 5 | import collections 6 | 7 | import decimal 8 | 9 | # Float to Decimal helper 10 | def float2dec(ft, decimal_digits): 11 | """ 12 | Convert float (or int) to Decimal (rounding up) with the 13 | requested number of decimal digits. 14 | 15 | Arguments: 16 | ft (float, int): Number to convert 17 | decimal (int): Number of digits after decimal point 18 | 19 | Return: 20 | Decimal: Number converted to decima 21 | """ 22 | with decimal.localcontext() as ctx: 23 | ctx.rounding = decimal.ROUND_UP 24 | places = decimal.Decimal(10)**(-decimal_digits) 25 | return decimal.Decimal.from_float(float(ft)).quantize(places) 26 | 27 | 28 | # Sorting algos for rectangle lists 29 | SORT_AREA = lambda rectlist: sorted(rectlist, reverse=True, 30 | key=lambda r: r[0]*r[1]) # Sort by area 31 | 32 | SORT_PERI = lambda rectlist: sorted(rectlist, reverse=True, 33 | key=lambda r: r[0]+r[1]) # Sort by perimeter 34 | 35 | SORT_DIFF = lambda rectlist: sorted(rectlist, reverse=True, 36 | key=lambda r: abs(r[0]-r[1])) # Sort by Diff 37 | 38 | SORT_SSIDE = lambda rectlist: sorted(rectlist, reverse=True, 39 | key=lambda r: (min(r[0], r[1]), max(r[0], r[1]))) # Sort by short side 40 | 41 | SORT_LSIDE = lambda rectlist: sorted(rectlist, reverse=True, 42 | key=lambda r: (max(r[0], r[1]), min(r[0], r[1]))) # Sort by long side 43 | 44 | SORT_RATIO = lambda rectlist: sorted(rectlist, reverse=True, 45 | key=lambda r: r[0]/r[1]) # Sort by side ratio 46 | 47 | SORT_NONE = lambda rectlist: list(rectlist) # Unsorted 48 | 49 | 50 | 51 | class BinFactory(object): 52 | 53 | def __init__(self, width, height, count, pack_algo, *args, **kwargs): 54 | self._width = width 55 | self._height = height 56 | self._count = count 57 | 58 | self._pack_algo = pack_algo 59 | self._algo_kwargs = kwargs 60 | self._algo_args = args 61 | self._ref_bin = None # Reference bin used to calculate fitness 62 | 63 | self._bid = kwargs.get("bid", None) 64 | 65 | def _create_bin(self): 66 | return self._pack_algo(self._width, self._height, *self._algo_args, **self._algo_kwargs) 67 | 68 | def is_empty(self): 69 | return self._count<1 70 | 71 | def fitness(self, width, height): 72 | if not self._ref_bin: 73 | self._ref_bin = self._create_bin() 74 | 75 | return self._ref_bin.fitness(width, height) 76 | 77 | def fits_inside(self, width, height): 78 | # Determine if rectangle widthxheight will fit into empty bin 79 | if not self._ref_bin: 80 | self._ref_bin = self._create_bin() 81 | 82 | return self._ref_bin._fits_surface(width, height) 83 | 84 | def new_bin(self): 85 | if self._count > 0: 86 | self._count -= 1 87 | return self._create_bin() 88 | else: 89 | return None 90 | 91 | def __eq__(self, other): 92 | return self._width*self._height == other._width*other._height 93 | 94 | def __lt__(self, other): 95 | return self._width*self._height < other._width*other._height 96 | 97 | def __str__(self): 98 | return "Bin: {} {} {}".format(self._width, self._height, self._count) 99 | 100 | 101 | 102 | class PackerBNFMixin(object): 103 | """ 104 | BNF (Bin Next Fit): Only one open bin at a time. If the rectangle 105 | doesn't fit, close the current bin and go to the next. 106 | """ 107 | 108 | def add_rect(self, width, height, rid=None): 109 | while True: 110 | # if there are no open bins, try to open a new one 111 | if len(self._open_bins)==0: 112 | # can we find an unopened bin that will hold this rect? 113 | new_bin = self._new_open_bin(width, height, rid=rid) 114 | if new_bin is None: 115 | return None 116 | 117 | # we have at least one open bin, so check if it can hold this rect 118 | rect = self._open_bins[0].add_rect(width, height, rid=rid) 119 | if rect is not None: 120 | return rect 121 | 122 | # since the rect doesn't fit, close this bin and try again 123 | closed_bin = self._open_bins.popleft() 124 | self._closed_bins.append(closed_bin) 125 | 126 | 127 | class PackerBFFMixin(object): 128 | """ 129 | BFF (Bin First Fit): Pack rectangle in first bin it fits 130 | """ 131 | 132 | def add_rect(self, width, height, rid=None): 133 | # see if this rect will fit in any of the open bins 134 | for b in self._open_bins: 135 | rect = b.add_rect(width, height, rid=rid) 136 | if rect is not None: 137 | return rect 138 | 139 | while True: 140 | # can we find an unopened bin that will hold this rect? 141 | new_bin = self._new_open_bin(width, height, rid=rid) 142 | if new_bin is None: 143 | return None 144 | 145 | # _new_open_bin may return a bin that's too small, 146 | # so we have to double-check 147 | rect = new_bin.add_rect(width, height, rid=rid) 148 | if rect is not None: 149 | return rect 150 | 151 | 152 | class PackerBBFMixin(object): 153 | """ 154 | BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness 155 | """ 156 | 157 | # only create this getter once 158 | first_item = operator.itemgetter(0) 159 | 160 | def add_rect(self, width, height, rid=None): 161 | 162 | # Try packing into open bins 163 | fit = ((b.fitness(width, height), b) for b in self._open_bins) 164 | fit = (b for b in fit if b[0] is not None) 165 | try: 166 | _, best_bin = min(fit, key=self.first_item) 167 | best_bin.add_rect(width, height, rid) 168 | return True 169 | except ValueError: 170 | pass 171 | 172 | # Try packing into one of the empty bins 173 | while True: 174 | # can we find an unopened bin that will hold this rect? 175 | new_bin = self._new_open_bin(width, height, rid=rid) 176 | if new_bin is None: 177 | return False 178 | 179 | # _new_open_bin may return a bin that's too small, 180 | # so we have to double-check 181 | if new_bin.add_rect(width, height, rid): 182 | return True 183 | 184 | 185 | 186 | class PackerOnline(object): 187 | """ 188 | Rectangles are packed as soon are they are added 189 | """ 190 | 191 | def __init__(self, pack_algo=MaxRectsBssf, rotation=True): 192 | """ 193 | Arguments: 194 | pack_algo (PackingAlgorithm): What packing algo to use 195 | rotation (bool): Enable/Disable rectangle rotation 196 | """ 197 | self._rotation = rotation 198 | self._pack_algo = pack_algo 199 | self.reset() 200 | 201 | def __iter__(self): 202 | return itertools.chain(self._closed_bins, self._open_bins) 203 | 204 | def __len__(self): 205 | return len(self._closed_bins)+len(self._open_bins) 206 | 207 | def __getitem__(self, key): 208 | """ 209 | Return bin in selected position. (excluding empty bins) 210 | """ 211 | if not isinstance(key, int): 212 | raise TypeError("Indices must be integers") 213 | 214 | size = len(self) # avoid recalulations 215 | 216 | if key < 0: 217 | key += size 218 | 219 | if not 0 <= key < size: 220 | raise IndexError("Index out of range") 221 | 222 | if key < len(self._closed_bins): 223 | return self._closed_bins[key] 224 | else: 225 | return self._open_bins[key-len(self._closed_bins)] 226 | 227 | def _new_open_bin(self, width=None, height=None, rid=None): 228 | """ 229 | Extract the next empty bin and append it to open bins 230 | 231 | Returns: 232 | PackingAlgorithm: Initialized empty packing bin. 233 | None: No bin big enough for the rectangle was found 234 | """ 235 | factories_to_delete = set() # 236 | new_bin = None 237 | 238 | for key, binfac in self._empty_bins.items(): 239 | 240 | # Only return the new bin if the rect fits. 241 | # (If width or height is None, caller doesn't know the size.) 242 | if not binfac.fits_inside(width, height): 243 | continue 244 | 245 | # Create bin and add to open_bins 246 | new_bin = binfac.new_bin() 247 | if new_bin is None: 248 | continue 249 | self._open_bins.append(new_bin) 250 | 251 | # If the factory was depleted mark for deletion 252 | if binfac.is_empty(): 253 | factories_to_delete.add(key) 254 | 255 | break 256 | 257 | # Delete marked factories 258 | for f in factories_to_delete: 259 | del self._empty_bins[f] 260 | 261 | return new_bin 262 | 263 | def add_bin(self, width, height, count=1, **kwargs): 264 | # accept the same parameters as PackingAlgorithm objects 265 | kwargs['rot'] = self._rotation 266 | bin_factory = BinFactory(width, height, count, self._pack_algo, **kwargs) 267 | self._empty_bins[next(self._bin_count)] = bin_factory 268 | 269 | def rect_list(self): 270 | rectangles = [] 271 | bin_count = 0 272 | 273 | for abin in self: 274 | for rect in abin: 275 | rectangles.append((bin_count, rect.x, rect.y, rect.width, rect.height, rect.rid)) 276 | bin_count += 1 277 | 278 | return rectangles 279 | 280 | def bin_list(self): 281 | """ 282 | Return a list of the dimmensions of the bins in use, that is closed 283 | or open containing at least one rectangle 284 | """ 285 | return [(b.width, b.height) for b in self] 286 | 287 | def validate_packing(self): 288 | for b in self: 289 | b.validate_packing() 290 | 291 | def reset(self): 292 | # Bins fully packed and closed. 293 | self._closed_bins = collections.deque() 294 | 295 | # Bins ready to pack rectangles 296 | self._open_bins = collections.deque() 297 | 298 | # User provided bins not in current use 299 | self._empty_bins = collections.OrderedDict() # O(1) deletion of arbitrary elem 300 | self._bin_count = itertools.count() 301 | 302 | 303 | class Packer(PackerOnline): 304 | """ 305 | Rectangles aren't packed untils pack() is called 306 | """ 307 | 308 | def __init__(self, pack_algo=MaxRectsBssf, sort_algo=SORT_NONE, 309 | rotation=True): 310 | """ 311 | """ 312 | super(Packer, self).__init__(pack_algo=pack_algo, rotation=rotation) 313 | 314 | self._sort_algo = sort_algo 315 | 316 | # User provided bins and Rectangles 317 | self._avail_bins = collections.deque() 318 | self._avail_rect = collections.deque() 319 | 320 | # Aux vars used during packing 321 | self._sorted_rect = [] 322 | 323 | def add_bin(self, width, height, count=1, **kwargs): 324 | self._avail_bins.append((width, height, count, kwargs)) 325 | 326 | def add_rect(self, width, height, rid=None): 327 | self._avail_rect.append((width, height, rid)) 328 | 329 | def _is_everything_ready(self): 330 | return self._avail_rect and self._avail_bins 331 | 332 | def pack(self): 333 | 334 | self.reset() 335 | 336 | if not self._is_everything_ready(): 337 | # maybe we should throw an error here? 338 | return 339 | 340 | # Add available bins to packer 341 | for b in self._avail_bins: 342 | width, height, count, extra_kwargs = b 343 | super(Packer, self).add_bin(width, height, count, **extra_kwargs) 344 | 345 | # If enabled sort rectangles 346 | self._sorted_rect = self._sort_algo(self._avail_rect) 347 | 348 | # Start packing 349 | for r in self._sorted_rect: 350 | super(Packer, self).add_rect(*r) 351 | 352 | 353 | 354 | class PackerBNF(Packer, PackerBNFMixin): 355 | """ 356 | BNF (Bin Next Fit): Only one open bin, if rectangle doesn't fit 357 | go to next bin and close current one. 358 | """ 359 | pass 360 | 361 | class PackerBFF(Packer, PackerBFFMixin): 362 | """ 363 | BFF (Bin First Fit): Pack rectangle in first bin it fits 364 | """ 365 | pass 366 | 367 | class PackerBBF(Packer, PackerBBFMixin): 368 | """ 369 | BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness 370 | """ 371 | pass 372 | 373 | class PackerOnlineBNF(PackerOnline, PackerBNFMixin): 374 | """ 375 | BNF Bin Next Fit Online variant 376 | """ 377 | pass 378 | 379 | class PackerOnlineBFF(PackerOnline, PackerBFFMixin): 380 | """ 381 | BFF Bin First Fit Online variant 382 | """ 383 | pass 384 | 385 | class PackerOnlineBBF(PackerOnline, PackerBBFMixin): 386 | """ 387 | BBF Bin Best Fit Online variant 388 | """ 389 | pass 390 | 391 | 392 | class PackerGlobal(Packer, PackerBNFMixin): 393 | """ 394 | GLOBAL: For each bin pack the rectangle with the best fitness. 395 | """ 396 | first_item = operator.itemgetter(0) 397 | 398 | def __init__(self, pack_algo=MaxRectsBssf, rotation=True): 399 | """ 400 | """ 401 | super(PackerGlobal, self).__init__(pack_algo=pack_algo, 402 | sort_algo=SORT_NONE, rotation=rotation) 403 | 404 | def _find_best_fit(self, pbin): 405 | """ 406 | Return best fitness rectangle from rectangles packing _sorted_rect list 407 | 408 | Arguments: 409 | pbin (PackingAlgorithm): Packing bin 410 | 411 | Returns: 412 | key of the rectangle with best fitness 413 | """ 414 | fit = ((pbin.fitness(r[0], r[1]), k) for k, r in self._sorted_rect.items()) 415 | fit = (f for f in fit if f[0] is not None) 416 | try: 417 | _, rect = min(fit, key=self.first_item) 418 | return rect 419 | except ValueError: 420 | return None 421 | 422 | 423 | def _new_open_bin(self, remaining_rect): 424 | """ 425 | Extract the next bin where at least one of the rectangles in 426 | rem 427 | 428 | Arguments: 429 | remaining_rect (dict): rectangles not placed yet 430 | 431 | Returns: 432 | PackingAlgorithm: Initialized empty packing bin. 433 | None: No bin big enough for the rectangle was found 434 | """ 435 | factories_to_delete = set() # 436 | new_bin = None 437 | 438 | for key, binfac in self._empty_bins.items(): 439 | 440 | # Only return the new bin if at least one of the remaining 441 | # rectangles fit inside. 442 | a_rectangle_fits = False 443 | for _, rect in remaining_rect.items(): 444 | if binfac.fits_inside(rect[0], rect[1]): 445 | a_rectangle_fits = True 446 | break 447 | 448 | if not a_rectangle_fits: 449 | factories_to_delete.add(key) 450 | continue 451 | 452 | # Create bin and add to open_bins 453 | new_bin = binfac.new_bin() 454 | if new_bin is None: 455 | continue 456 | self._open_bins.append(new_bin) 457 | 458 | # If the factory was depleted mark for deletion 459 | if binfac.is_empty(): 460 | factories_to_delete.add(key) 461 | 462 | break 463 | 464 | # Delete marked factories 465 | for f in factories_to_delete: 466 | del self._empty_bins[f] 467 | 468 | return new_bin 469 | 470 | def pack(self): 471 | 472 | self.reset() 473 | 474 | if not self._is_everything_ready(): 475 | return 476 | 477 | # Add available bins to packer 478 | for b in self._avail_bins: 479 | width, height, count, extra_kwargs = b 480 | super(Packer, self).add_bin(width, height, count, **extra_kwargs) 481 | 482 | # Store rectangles into dict for fast deletion 483 | self._sorted_rect = collections.OrderedDict( 484 | enumerate(self._sort_algo(self._avail_rect))) 485 | 486 | # For each bin pack the rectangles with lowest fitness until it is filled or 487 | # the rectangles exhausted, then open the next bin where at least one rectangle 488 | # will fit and repeat the process until there aren't more rectangles or bins 489 | # available. 490 | while len(self._sorted_rect) > 0: 491 | 492 | # Find one bin where at least one of the remaining rectangles fit 493 | pbin = self._new_open_bin(self._sorted_rect) 494 | if pbin is None: 495 | break 496 | 497 | # Pack as many rectangles as possible into the open bin 498 | while True: 499 | 500 | # Find 'fittest' rectangle 501 | best_rect_key = self._find_best_fit(pbin) 502 | if best_rect_key is None: 503 | closed_bin = self._open_bins.popleft() 504 | self._closed_bins.append(closed_bin) 505 | break # None of the remaining rectangles can be packed in this bin 506 | 507 | best_rect = self._sorted_rect[best_rect_key] 508 | del self._sorted_rect[best_rect_key] 509 | 510 | PackerBNFMixin.add_rect(self, *best_rect) 511 | 512 | 513 | 514 | 515 | 516 | # Packer factory 517 | class Enum(tuple): 518 | __getattr__ = tuple.index 519 | 520 | PackingMode = Enum(["Online", "Offline"]) 521 | PackingBin = Enum(["BNF", "BFF", "BBF", "Global"]) 522 | 523 | 524 | def newPacker(mode=PackingMode.Offline, 525 | bin_algo=PackingBin.BBF, 526 | pack_algo=MaxRectsBssf, 527 | sort_algo=SORT_AREA, 528 | rotation=True): 529 | """ 530 | Packer factory helper function 531 | 532 | Arguments: 533 | mode (PackingMode): Packing mode 534 | Online: Rectangles are packed as soon are they are added 535 | Offline: Rectangles aren't packed untils pack() is called 536 | bin_algo (PackingBin): Bin selection heuristic 537 | pack_algo (PackingAlgorithm): Algorithm used 538 | rotation (boolean): Enable or disable rectangle rotation. 539 | 540 | Returns: 541 | Packer: Initialized packer instance. 542 | """ 543 | packer_class = None 544 | 545 | # Online Mode 546 | if mode == PackingMode.Online: 547 | sort_algo=None 548 | if bin_algo == PackingBin.BNF: 549 | packer_class = PackerOnlineBNF 550 | elif bin_algo == PackingBin.BFF: 551 | packer_class = PackerOnlineBFF 552 | elif bin_algo == PackingBin.BBF: 553 | packer_class = PackerOnlineBBF 554 | else: 555 | raise AttributeError("Unsupported bin selection heuristic") 556 | 557 | # Offline Mode 558 | elif mode == PackingMode.Offline: 559 | if bin_algo == PackingBin.BNF: 560 | packer_class = PackerBNF 561 | elif bin_algo == PackingBin.BFF: 562 | packer_class = PackerBFF 563 | elif bin_algo == PackingBin.BBF: 564 | packer_class = PackerBBF 565 | elif bin_algo == PackingBin.Global: 566 | packer_class = PackerGlobal 567 | sort_algo=None 568 | else: 569 | raise AttributeError("Unsupported bin selection heuristic") 570 | 571 | else: 572 | raise AttributeError("Unknown packing mode.") 573 | 574 | if sort_algo: 575 | return packer_class(pack_algo=pack_algo, sort_algo=sort_algo, 576 | rotation=rotation) 577 | else: 578 | return packer_class(pack_algo=pack_algo, rotation=rotation) 579 | 580 | 581 | -------------------------------------------------------------------------------- /tests/test_skyline.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from rectpack.geometry import Rectangle 3 | import rectpack.skyline as skyline 4 | 5 | 6 | class TestSkyline(TestCase): 7 | 8 | def test_init(self): 9 | s = skyline.SkylineBl(100, 100, rot=False) 10 | rect1 = s.add_rect(30, 30) 11 | rect2 = s.add_rect(100, 70) 12 | self.assertEqual(rect1, Rectangle(0, 0, 30, 30)) 13 | self.assertEqual(rect2, Rectangle(0, 30, 100, 70)) 14 | 15 | def test_rotation(self): 16 | # Test rotation is enabled by default 17 | s = skyline.SkylineBl(100, 10) 18 | rect1 = s.add_rect(10, 100) 19 | self.assertEqual(rect1, Rectangle(0, 0, 100, 10)) 20 | 21 | # Test rotation can be disabled 22 | s = skyline.SkylineBl(100, 10, rot=False) 23 | rect1 = s.add_rect(10, 100) 24 | self.assertEqual(rect1, None) 25 | 26 | def test_waste_management(self): 27 | # Generate one wasted section 28 | s = skyline.SkylineBlWm(100, 100, rot=False) 29 | rect1 = s.add_rect(30, 30) 30 | rect2 = s.add_rect(100, 70) 31 | self.assertEqual(rect1, Rectangle(0, 0, 30, 30)) 32 | self.assertEqual(rect2, Rectangle(0, 30, 100, 70)) 33 | self.assertEqual(len(s), 2) 34 | 35 | # Add rectangle that only fits into wasted section 36 | self.assertEqual(s.add_rect(71, 30), None) 37 | self.assertEqual(s.add_rect(70, 31), None) 38 | rect3 = s.add_rect(70, 30) 39 | self.assertEqual(rect3, Rectangle(30, 0, 70, 30)) 40 | self.assertEqual(len(s), 3) 41 | 42 | rect4 = s.add_rect(70, 30) 43 | self.assertEqual(rect4, None) 44 | 45 | # Test the same without waste management 46 | s = skyline.SkylineBl(100, 100) 47 | rect1 = s.add_rect(30, 30) 48 | rect2 = s.add_rect(100, 70) 49 | self.assertEqual(rect1, Rectangle(0, 0, 30, 30)) 50 | self.assertEqual(rect2, Rectangle(0, 30, 100, 70)) 51 | 52 | self.assertEqual(s.add_rect(70, 30), None) 53 | self.assertEqual(len(s), 2) 54 | 55 | # Test waste supports rectangle rotation 56 | s = skyline.SkylineBlWm(100, 100, rot=False) 57 | sr = skyline.SkylineBlWm(100, 100, rot=True) 58 | self.assertEqual(s.add_rect(30, 30), Rectangle(0, 0, 30, 30)) 59 | self.assertEqual(s.add_rect(100, 70), Rectangle(0, 30, 100, 70)) 60 | self.assertEqual(sr.add_rect(30, 30), Rectangle(0, 0, 30, 30)) 61 | self.assertEqual(sr.add_rect(100, 70), Rectangle(0, 30, 100, 70)) 62 | 63 | self.assertEqual(s.add_rect(30, 70), None) 64 | self.assertEqual(sr.add_rect(30, 70), Rectangle(30, 0, 70, 30)) 65 | 66 | # Try with more than one wasted section 67 | s = skyline.SkylineBlWm(100, 100, rot=False) 68 | self.assertEqual(s.add_rect(40, 50), Rectangle(0, 0, 40, 50)) 69 | self.assertEqual(s.add_rect(20, 30), Rectangle(40, 0, 20, 30)) 70 | self.assertEqual(s.add_rect(20, 10), Rectangle(60, 0, 20, 10)) 71 | self.assertEqual(s.add_rect(100, 50), Rectangle(0, 50, 100, 50)) 72 | 73 | # Next ones only fit if waste is working 74 | self.assertEqual(s.add_rect(20, 20), Rectangle(40, 30, 20, 20)) 75 | self.assertEqual(s.add_rect(20, 30), Rectangle(60, 10, 20, 30)) 76 | self.assertEqual(s.add_rect(20, 50), Rectangle(80, 0, 20, 50)) 77 | self.assertEqual(s.add_rect(20, 5), Rectangle(60, 40, 20, 5)) 78 | self.assertEqual(s.add_rect(20, 5), Rectangle(60, 45, 20, 5)) 79 | self.assertEqual(s.add_rect(1, 1), None) 80 | 81 | def test_iter(self): 82 | # Test correctly calculated when waste is enabled 83 | s = skyline.SkylineBlWm(100, 100) 84 | self.assertTrue(s.add_rect(50, 50)) 85 | self.assertTrue(s.add_rect(100, 50)) 86 | self.assertEqual(len([r for r in s]), 2) 87 | self.assertTrue(s.add_rect(40, 40)) 88 | self.assertEqual(len([r for r in s]), 3) 89 | 90 | def test_len(self): 91 | s = skyline.SkylineBlWm(100, 100) 92 | self.assertTrue(s.add_rect(50, 50)) 93 | self.assertTrue(s.add_rect(100, 50)) 94 | self.assertEqual(len(s), 2) 95 | self.assertTrue(s.add_rect(50, 50)) 96 | self.assertEqual(len(s), 3) 97 | 98 | def test_skyline1(self): 99 | """Test skyline for complex positions is generated correctly 100 | +---------------------------+ 101 | | | 102 | +---------------------+ | 103 | | 4 | | 104 | +----------------+----+ | 105 | | 3 | | 106 | +----------+-----+ +-----+ 107 | | | | | 108 | | | | 5 | 109 | | 1 | | | 110 | | | | | 111 | | +----------+-----+ 112 | | | 2 | 113 | +----------+----------------+ 114 | """ 115 | s = skyline.SkylineMwf(100, 100, rot=False) 116 | rect1 = s.add_rect(40, 60) 117 | rect2 = s.add_rect(60, 10) 118 | rect3 = s.add_rect(70, 20) 119 | rect4 = s.add_rect(80, 20) 120 | rect5 = s.add_rect(20, 40) 121 | 122 | self.assertEqual(rect1, Rectangle(0, 0, 40, 60)) 123 | self.assertEqual(rect2, Rectangle(40, 0, 60, 10)) 124 | self.assertEqual(rect3, Rectangle(0, 60, 70, 20)) 125 | self.assertEqual(rect4, Rectangle(0, 80, 80, 20)) 126 | self.assertEqual(rect5, Rectangle(80, 10, 20, 40)) 127 | 128 | def test_skyline2(self): 129 | """ 130 | +---------------------------+ 131 | | | 132 | | | 133 | | +--------------------+ 134 | | | 4 | 135 | | | | 136 | +----+ +-----------+--------+ 137 | | | | | 138 | | | | | 139 | | | | | 140 | | | | | 141 | | 1 | | 3 | 142 | | | | | 143 | | | | | 144 | | +-------------+ | 145 | | | 2 | | 146 | +----+-------------+--------+ 147 | """ 148 | s = skyline.SkylineMwfl(100, 100, rot=False) 149 | rect1 = s.add_rect(20, 60) 150 | rect2 = s.add_rect(50, 10) 151 | rect3 = s.add_rect(30, 60) 152 | rect4 = s.add_rect(70, 20) 153 | 154 | self.assertEqual(rect1, Rectangle(0, 0, 20, 60)) 155 | self.assertEqual(rect2, Rectangle(20, 0, 50, 10)) 156 | self.assertEqual(rect3, Rectangle(70, 0, 30, 60)) 157 | self.assertEqual(rect4, Rectangle(30, 60, 70, 20)) 158 | 159 | def test_skyline3(self): 160 | """ 161 | +-------------------+-------+ 162 | | 10 | | 163 | +----+----+---------+ | 164 | | | | | 9 | 165 | | | w2 | 8 | | 166 | | 6 | | | | 167 | | | +---------+-------+ 168 | | +----+ | 169 | +----+ | 5 | 170 | | | 7 +---+-------------+ 171 | | | |w1 | | 172 | | +--------+ 4 | 173 | | 1 | | | 174 | | | 2 +----------+--+ 175 | | | | 3 | | 176 | +----+--------+----------+--+ 177 | """ 178 | s = skyline.SkylineMwf(100, 100, rot=False) 179 | rect1 = s.add_rect(20, 50) 180 | rect2 = s.add_rect(30, 30) 181 | rect3 = s.add_rect(40, 10) 182 | rect4 = s.add_rect(50, 40) 183 | rect5 = s.add_rect(70, 20) 184 | rect6 = s.add_rect(20, 40) 185 | rect7 = s.add_rect(10, 30) 186 | rect8 = s.add_rect(40, 20) 187 | rect9 = s.add_rect(30, 30) 188 | rect10 = s.add_rect(70, 10) 189 | w1 = s.add_rect(20, 20) 190 | w2 = s.add_rect(10, 30) 191 | 192 | self.assertEqual(rect1, Rectangle(0, 0, 20, 50)) 193 | self.assertEqual(rect2, Rectangle(20, 0, 30, 30)) 194 | self.assertEqual(rect3, Rectangle(50, 0, 40, 10)) 195 | self.assertEqual(rect4, Rectangle(50, 10, 50, 40)) 196 | self.assertEqual(rect5, Rectangle(30, 50, 70, 20)) 197 | self.assertEqual(rect6, Rectangle(0, 50, 20, 40)) 198 | self.assertEqual(rect7, Rectangle(20, 30, 10, 30)) 199 | self.assertEqual(rect8, Rectangle(30, 70, 40, 20)) 200 | self.assertEqual(rect9, Rectangle(70, 70, 30, 30)) 201 | self.assertEqual(rect10, Rectangle(0, 90, 70, 10)) 202 | self.assertEqual(w1, None) 203 | self.assertEqual(w2, None) 204 | 205 | # With Waste management enabled 206 | s = skyline.SkylineMwfWm(100, 100, rot=False) 207 | rect1 = s.add_rect(20, 50) 208 | rect2 = s.add_rect(30, 30) 209 | rect3 = s.add_rect(40, 10) 210 | rect4 = s.add_rect(50, 40) 211 | rect5 = s.add_rect(70, 20) 212 | rect6 = s.add_rect(20, 40) 213 | rect7 = s.add_rect(10, 30) 214 | rect8 = s.add_rect(40, 20) 215 | rect9 = s.add_rect(30, 30) 216 | rect10 = s.add_rect(70, 10) 217 | w1 = s.add_rect(20, 20) 218 | w2 = s.add_rect(10, 30) 219 | 220 | self.assertEqual(rect1, Rectangle(0, 0, 20, 50)) 221 | self.assertEqual(rect2, Rectangle(20, 0, 30, 30)) 222 | self.assertEqual(rect3, Rectangle(50, 0, 40, 10)) 223 | self.assertEqual(rect4, Rectangle(50, 10, 50, 40)) 224 | self.assertEqual(rect5, Rectangle(30, 50, 70, 20)) 225 | self.assertEqual(rect6, Rectangle(0, 50, 20, 40)) 226 | self.assertEqual(rect7, Rectangle(20, 30, 10, 30)) 227 | self.assertEqual(rect8, Rectangle(30, 70, 40, 20)) 228 | self.assertEqual(rect9, Rectangle(70, 70, 30, 30)) 229 | self.assertEqual(rect10, Rectangle(0, 90, 70, 10)) 230 | self.assertEqual(w1, Rectangle(30, 30, 20, 20)) 231 | self.assertEqual(w2, Rectangle(20, 60, 10, 30)) 232 | 233 | def test_skyline4(self): 234 | """ 235 | +---------------------+-----+ 236 | | 4 | 5 | 237 | | | | 238 | +----+----------------------+ 239 | | | | | 240 | | | | | 241 | | | | | 242 | | | w1 | | 243 | | 1 | | 3 | 244 | | | | | 245 | | | | | 246 | | | | | 247 | | +----------------+ | 248 | | | | | 249 | | | 2 | | 250 | | | | | 251 | +----+----------------+-----+ 252 | """ 253 | s = skyline.SkylineMwflWm(100, 100, rot=False) 254 | rect1 = s.add_rect(20, 80) 255 | rect2 = s.add_rect(60, 20) 256 | rect3 = s.add_rect(20, 80) 257 | rect4 = s.add_rect(80, 20) 258 | w1 = s.add_rect(60, 50) 259 | rect5 = s.add_rect(20, 20) 260 | 261 | self.assertEqual(rect1, Rectangle(0, 0, 20, 80)) 262 | self.assertEqual(rect2, Rectangle(20, 0, 60, 20)) 263 | self.assertEqual(rect3, Rectangle(80, 0, 20, 80)) 264 | self.assertEqual(rect4, Rectangle(0, 80, 80, 20)) 265 | self.assertEqual(rect5, Rectangle(80, 80, 20, 20)) 266 | self.assertEqual(w1, Rectangle(20, 20, 60, 50)) 267 | 268 | def test_skyline5(self): 269 | """ 270 | +------+--------------+-----+ 271 | | | | | 272 | | 8 | 5 | | 273 | | | | | 274 | | +--------------------+ 275 | +------+ | | 276 | | | | | 277 | | | 4 | 7 | 278 | | | | | 279 | | | +-----+ 280 | | 1 +---------+----+ | 281 | | | | w1 | | 282 | | | | | 6 | 283 | | | 2 +----+ | 284 | | | | 3 | | 285 | | | | | | 286 | +------+---------+----+-----+ 287 | """ 288 | s = skyline.SkylineMwflWm(100, 100, rot=False) 289 | rect1 = s.add_rect(20, 70) 290 | rect2 = s.add_rect(30, 40) 291 | rect3 = s.add_rect(20, 20) 292 | rect4 = s.add_rect(50, 40) 293 | rect5 = s.add_rect(50, 20) 294 | rect6 = s.add_rect(30, 50) 295 | rect7 = s.add_rect(20, 30) 296 | rect8 = s.add_rect(20, 30) 297 | w1 = s.add_rect(20, 20) 298 | 299 | self.assertEqual(rect1, Rectangle(0, 0, 20, 70)) 300 | self.assertEqual(rect2, Rectangle(20, 0, 30, 40)) 301 | self.assertEqual(rect3, Rectangle(50, 0, 20, 20)) 302 | self.assertEqual(rect4, Rectangle(20, 40, 50, 40)) 303 | self.assertEqual(rect5, Rectangle(20, 80, 50, 20)) 304 | self.assertEqual(rect6, Rectangle(70, 0, 30, 50)) 305 | self.assertEqual(rect7, Rectangle(70, 50, 20, 30)) 306 | self.assertEqual(rect8, Rectangle(0, 70, 20, 30)) 307 | self.assertEqual(w1, Rectangle(50, 20, 20, 20)) 308 | 309 | def test_skyline6(self): 310 | """ 311 | +-------------+-------------+ 312 | | | | 313 | | 4 | | 314 | | +-------------+ 315 | | | | 316 | +-------------* 5 | 317 | | 3 | | 318 | | | | 319 | +-------------+--+----------+ 320 | | | | 321 | | 2 | | 322 | | | | 323 | +----------------+----+ | 324 | | | | 325 | | 1 | | 326 | | | | 327 | +---------------------+-----+ 328 | """ 329 | s = skyline.SkylineMwflWm(100, 100, rot=False) 330 | rect1 = s.add_rect(80, 30) 331 | rect2 = s.add_rect(60, 20) 332 | rect3 = s.add_rect(50, 20) 333 | rect4 = s.add_rect(50, 30) 334 | rect5 = s.add_rect(50, 30) 335 | 336 | self.assertEqual(rect1, Rectangle(0, 0, 80, 30)) 337 | self.assertEqual(rect2, Rectangle(0, 30, 60, 20)) 338 | self.assertEqual(rect3, Rectangle(0, 50, 50, 20)) 339 | self.assertEqual(rect4, Rectangle(0, 70, 50, 30)) 340 | self.assertEqual(rect5, Rectangle(50, 50, 50, 30)) 341 | 342 | def test_skyline7(self): 343 | """ 344 | +-----------------+---------+ 345 | +-----------------+ | 346 | | | | 347 | | 4 | | 348 | | | | 349 | +-----------+-----+ | 350 | | | | 5 | 351 | | | | | 352 | | w1 | | | 353 | | | | | 354 | | | 2 | | 355 | | | | | 356 | +-----------+ +---------+ 357 | | | | | 358 | | 1 | | 3 | 359 | | | | | 360 | +-----------+-----+---------+ 361 | """ 362 | s = skyline.SkylineMwflWm(100, 100, rot=False) 363 | rect1 = s.add_rect(40, 20) 364 | rect2 = s.add_rect(20, 60) 365 | rect3 = s.add_rect(40, 20) 366 | rect4 = s.add_rect(60, 20) 367 | rect5 = s.add_rect(40, 80) 368 | w1 = s.add_rect(40, 40) 369 | 370 | self.assertEqual(rect1, Rectangle(0, 0, 40, 20)) 371 | self.assertEqual(rect2, Rectangle(40, 0, 20, 60)) 372 | self.assertEqual(rect3, Rectangle(60, 0, 40, 20)) 373 | self.assertEqual(rect4, Rectangle(0, 60, 60, 20)) 374 | self.assertEqual(rect5, Rectangle(60, 20, 40, 80)) 375 | self.assertEqual(w1, Rectangle(0, 20, 40, 40)) 376 | 377 | def test_skyline8(self): 378 | """ 379 | +---------------------------+ 380 | | | 381 | +----------------------+ | 382 | | 4 | | 383 | | | | 384 | +-----------+-----+----+ | 385 | | | | | 386 | | | | | 387 | | w1 | | | 388 | | | | | 389 | | | 2 | | 390 | | | | | 391 | +-----------+ | | 392 | | | +---------+ 393 | | 1 | | 3 | 394 | | | | | 395 | +-----------+-----+---------+ 396 | """ 397 | s = skyline.SkylineMwflWm(100, 100, rot=False) 398 | rect1 = s.add_rect(40, 20) 399 | rect2 = s.add_rect(20, 60) 400 | rect3 = s.add_rect(40, 10) 401 | rect4 = s.add_rect(80, 20) 402 | w1 = s.add_rect(40, 40) 403 | 404 | self.assertEqual(rect1, Rectangle(0, 0, 40, 20)) 405 | self.assertEqual(rect2, Rectangle(40, 0, 20, 60)) 406 | self.assertEqual(rect3, Rectangle(60, 0, 40, 10)) 407 | self.assertEqual(rect4, Rectangle(0, 60, 80, 20)) 408 | self.assertEqual(w1, Rectangle(0, 20, 40, 40)) 409 | 410 | def test_skyline9(self): 411 | """ 412 | +---------------------------+ 413 | | | 414 | | +---------------------+ 415 | | | 4 | 416 | | | | 417 | | +-----+-----+---------+ 418 | | | | | 419 | | | | | 420 | | | | w1 | 421 | | | | | 422 | | | 2 | | 423 | | | | | 424 | | | +---------+ 425 | | | | | 426 | +-----------+ | 3 | 427 | | 1 | | | 428 | +-----------+-----+---------+ 429 | """ 430 | s = skyline.SkylineMwflWm(100, 100, rot=False) 431 | rect1 = s.add_rect(40, 20) 432 | rect2 = s.add_rect(20, 60) 433 | rect3 = s.add_rect(40, 30) 434 | rect4 = s.add_rect(80, 20) 435 | w1 = s.add_rect(40, 30) 436 | 437 | self.assertEqual(rect1, Rectangle(0, 0, 40, 20)) 438 | self.assertEqual(rect2, Rectangle(40, 0, 20, 60)) 439 | self.assertEqual(rect3, Rectangle(60, 0, 40, 30)) 440 | self.assertEqual(rect4, Rectangle(20, 60, 80, 20)) 441 | self.assertEqual(w1, Rectangle(60, 30, 40, 30)) 442 | 443 | def test_skyline10(self): 444 | """ 445 | +---------------------------+ 446 | | | 447 | | | 448 | | | 449 | | | 450 | | +----+ 451 | | | | 452 | | | | 453 | | | | 454 | +----------------+ | | 455 | | | | | 456 | | +-----+ 3 | 457 | | 1 | | | 458 | | | 2 | | 459 | | | | | 460 | | | | | 461 | +----------------+-----+----+ 462 | With rotation 463 | """ 464 | s = skyline.SkylineMwfl(100, 100, rot=True) 465 | rect1 = s.add_rect(50, 40) 466 | rect2 = s.add_rect(30, 30) 467 | rect3 = s.add_rect(70, 20) 468 | 469 | self.assertEqual(rect1, Rectangle(0, 0, 50, 40)) 470 | self.assertEqual(rect2, Rectangle(50, 0, 30, 30)) 471 | self.assertEqual(rect3, Rectangle(80, 0, 20, 70)) 472 | 473 | def test_getitem(self): 474 | """ 475 | Test __getitem__ works with all rectangles included waste. 476 | +---------------------------+ 477 | | | 478 | | +---------------------+ 479 | | | 4 | 480 | | | | 481 | | +-----+-----+---------+ 482 | | | | | 483 | | | | | 484 | | | | w1 | 485 | | | | | 486 | | | 2 | | 487 | | | | | 488 | | | +---------+ 489 | | | | | 490 | +-----------+ | 3 | 491 | | 1 | | | 492 | +-----------+-----+---------+ 493 | """ 494 | s = skyline.SkylineMwflWm(100, 100, rot=False) 495 | rect1 = s.add_rect(40, 20) 496 | rect2 = s.add_rect(20, 60) 497 | rect3 = s.add_rect(40, 30) 498 | rect4 = s.add_rect(80, 20) 499 | w1 = s.add_rect(40, 30) 500 | 501 | self.assertEqual(s[0], Rectangle(0, 0, 40, 20)) 502 | self.assertEqual(s[1], Rectangle(40, 0, 20, 60)) 503 | self.assertEqual(s[2], Rectangle(60, 0, 40, 30)) 504 | self.assertEqual(s[3], Rectangle(20, 60, 80, 20)) 505 | self.assertEqual(s[4], Rectangle(60, 30, 40, 30)) 506 | 507 | self.assertEqual(s[-1], Rectangle(60, 30, 40, 30)) 508 | self.assertEqual(s[3:], 509 | [Rectangle(20, 60, 80, 20), Rectangle(60, 30, 40, 30)]) 510 | 511 | 512 | class TestSkylineMwf(TestCase): 513 | 514 | def test_init(self): 515 | """ """ 516 | p = skyline.SkylineMwf(100, 100) 517 | self.assertFalse(p._waste_management) 518 | 519 | def test_fitness(self): 520 | """Test position wasting less space has better fitness""" 521 | p = skyline.SkylineMwf(100, 100, rot=False) 522 | p.add_rect(20, 20) 523 | 524 | self.assertTrue(p.fitness(90, 10) < p.fitness(100, 10)) 525 | 526 | def test_skyline(self): 527 | """ 528 | +---------------------------+ 529 | | | 530 | | | 531 | +----+ +------------------+ 532 | | | | 5 | 533 | | | +---+--+-----------+ 534 | | | | | | 535 | | | | | | 536 | | | | | | 537 | | 1 | | | | 538 | | | | +-----------+ 539 | | +-------+ 3| | 540 | | | | | | 541 | | | | | 4 | 542 | | | 2 | | | 543 | | | | | | 544 | +----+-------+--+-----------+ 545 | """ 546 | s = skyline.SkylineMwf(100, 100, rot=False) 547 | rect1 = s.add_rect(20, 80) 548 | rect2 = s.add_rect(20, 40) 549 | rect3 = s.add_rect(20, 70) 550 | rect4 = s.add_rect(40, 50) 551 | rect5 = s.add_rect(70, 10) 552 | 553 | self.assertEqual(rect1, Rectangle(0, 0, 20, 80)) 554 | self.assertEqual(rect2, Rectangle(20, 0, 20, 40)) 555 | self.assertEqual(rect3, Rectangle(40, 0, 20, 70)) 556 | self.assertEqual(rect4, Rectangle(60, 0, 40, 50)) 557 | self.assertEqual(rect5, Rectangle(30, 70, 70, 10)) 558 | 559 | 560 | 561 | class TestSkylineMwFwm(TestCase): 562 | 563 | def test_init(self): 564 | """Test Waste management is enabled""" 565 | p = skyline.SkylineMwfWm(100, 100) 566 | self.assertTrue(p._waste_management) 567 | 568 | def test_skyline(self): 569 | """ 570 | +---------------------------+ 571 | | | 572 | | | 573 | +----+ +------------------+ 574 | | | | 5 | 575 | | | +---+--+-----------+ 576 | | | | | | 577 | | | | | | 578 | | | | | w1 | 579 | | 1 | | | | 580 | | | | +-----------+ 581 | | +-------+ 3| | 582 | | | | | | 583 | | | | | 4 | 584 | | | 2 | | | 585 | | | | | | 586 | +----+-------+--+-----------+ 587 | """ 588 | s = skyline.SkylineMwfWm(100, 100, rot=False) 589 | rect1 = s.add_rect(20, 80) 590 | rect2 = s.add_rect(20, 40) 591 | rect3 = s.add_rect(20, 70) 592 | rect4 = s.add_rect(40, 50) 593 | rect5 = s.add_rect(70, 10) 594 | w1 = s.add_rect(40, 20) 595 | 596 | self.assertEqual(rect1, Rectangle(0, 0, 20, 80)) 597 | self.assertEqual(rect2, Rectangle(20, 0, 20, 40)) 598 | self.assertEqual(rect3, Rectangle(40, 0, 20, 70)) 599 | self.assertEqual(rect4, Rectangle(60, 0, 40, 50)) 600 | self.assertEqual(rect5, Rectangle(30, 70, 70, 10)) 601 | self.assertEqual(w1, Rectangle(60, 50, 40, 20)) 602 | 603 | 604 | 605 | class TestSkylineMwfl(TestCase): 606 | 607 | def test_init(self): 608 | """ """ 609 | p = skyline.SkylineMwfl(100, 100) 610 | self.assertFalse(p._waste_management) 611 | 612 | def test_fitness(self): 613 | """Test lower one has best fitness""" 614 | p = skyline.SkylineMwfl(100, 100, rot=False) 615 | p.add_rect(20, 20) 616 | 617 | self.assertTrue(p.fitness(90, 10) < p.fitness(90, 20)) 618 | 619 | def test_skyline1(self): 620 | """ 621 | +---------------------------+ 622 | | | 623 | | | 624 | | | 625 | | | 626 | | | 627 | | | 628 | | +--------+ 629 | +------------------+ 3 | 630 | | | | 631 | | +--------+ 632 | | | | 633 | | 1 | | 634 | | | 2 | 635 | | | | 636 | | | | 637 | +------------------+--------+ 638 | """ 639 | s = skyline.SkylineMwfl(100, 100, rot=True) 640 | rect1 = s.add_rect(70, 50) 641 | rect2 = s.add_rect(40, 30) 642 | rect3 = s.add_rect(20, 30) 643 | 644 | self.assertEqual(rect1, Rectangle(0, 0, 70, 50)) 645 | self.assertEqual(rect2, Rectangle(70, 0, 30, 40)) 646 | self.assertEqual(rect3, Rectangle(70, 40, 30, 20)) 647 | 648 | def test_skyline2(self): 649 | """ 650 | +---------------------------+ 651 | | | 652 | | | 653 | | | 654 | | | 655 | | | 656 | | | 657 | | | 658 | | | 659 | | | 660 | +-----------+---------------+ 661 | | | 3 | 662 | | | | 663 | | 1 +---------+-----+ 664 | | | 2 | | 665 | | | | | 666 | +-----------+---------+-----+ 667 | """ 668 | s = skyline.SkylineMwfl(100, 100, rot=False) 669 | rect1 = s.add_rect(40, 40) 670 | rect2 = s.add_rect(40, 20) 671 | rect3 = s.add_rect(60, 20) 672 | 673 | self.assertEqual(rect1, Rectangle(0, 0, 40, 40)) 674 | self.assertEqual(rect2, Rectangle(40, 0, 40, 20)) 675 | self.assertEqual(rect3, Rectangle(40, 20, 60, 20)) 676 | 677 | 678 | 679 | class TestSkylineMwflWm(TestCase): 680 | 681 | def test_init(self): 682 | """Test Waste management is enabled""" 683 | p = skyline.SkylineMwflWm(100, 100) 684 | self.assertTrue(p._waste_management) 685 | 686 | def test_skyline(self): 687 | """ 688 | +---------------------------+ 689 | | | 690 | | | 691 | | | 692 | | | 693 | | | 694 | | | 695 | | | 696 | | | 697 | | | 698 | +-----------+---------------+ 699 | | | 3 | 700 | | | | 701 | | 1 +---------+-----+ 702 | | | 2 | w1 | 703 | | | | | 704 | +-----------+---------+-----+ 705 | """ 706 | s = skyline.SkylineMwflWm(100, 100, rot=False) 707 | rect1 = s.add_rect(40, 40) 708 | rect2 = s.add_rect(40, 20) 709 | rect3 = s.add_rect(60, 20) 710 | w1 = s.add_rect(20, 20) 711 | 712 | self.assertEqual(rect1, Rectangle(0, 0, 40, 40)) 713 | self.assertEqual(rect2, Rectangle(40, 0, 40, 20)) 714 | self.assertEqual(rect3, Rectangle(40, 20, 60, 20)) 715 | self.assertEqual(w1, Rectangle(80, 0, 20, 20)) 716 | 717 | 718 | 719 | class TestSkylineBl(TestCase): 720 | 721 | def test_init(self): 722 | """Test Waste management is disabled""" 723 | p = skyline.SkylineBl(100, 100) 724 | self.assertFalse(p._waste_management) 725 | 726 | def test_fitness(self): 727 | """Test lower is better""" 728 | p = skyline.SkylineBl(100, 100, rot=False) 729 | self.assertEqual(p.fitness(100, 20), p.fitness(10, 20)) 730 | self.assertTrue(p.fitness(100, 10) < p.fitness(100, 11)) 731 | 732 | # The same but with wasted space 733 | p = skyline.SkylineBl(100, 100, rot=False) 734 | p.add_rect(80, 50) 735 | self.assertEqual(p.fitness(100, 10), p.fitness(80, 10)) 736 | self.assertTrue(p.fitness(100, 10) < p.fitness(40, 20)) 737 | 738 | def test_skyline1(self): 739 | """ 740 | +---------------------------+ 741 | | | 742 | | | 743 | | | 744 | | | 745 | | | 746 | | | 747 | +-------------+-------------+ 748 | | 4 | | 749 | | | | 750 | +---------+---+ | 751 | | | | 3 | 752 | | | | | 753 | | 1 +---+ | 754 | | | 2 | | 755 | | | | | 756 | +---------+---+-------------+ 757 | 758 | Test lower positions is better than one not losing space 759 | """ 760 | s = skyline.SkylineBl(100, 100, rot=False) 761 | rect1 = s.add_rect(40, 30) 762 | rect2 = s.add_rect(10, 20) 763 | rect3 = s.add_rect(50, 50) 764 | rect4 = s.add_rect(50, 20) 765 | 766 | self.assertEqual(rect1, Rectangle(0, 0, 40, 30)) 767 | self.assertEqual(rect2, Rectangle(40, 0, 10, 20)) 768 | self.assertEqual(rect3, Rectangle(50, 0, 50, 50)) 769 | self.assertEqual(rect4, Rectangle(0, 30, 50, 20)) 770 | 771 | def test_skyline2(self): 772 | """ 773 | +---------------------------+ 774 | | | 775 | | | 776 | | | 777 | +--------------------+ | 778 | | | | 779 | | 4 +------+ 780 | | | 5 | 781 | | | | 782 | +----------------+---+------+ 783 | | | 3 | 784 | | | | 785 | | 1 +-----+----+ 786 | | | 2 | | 787 | | | | | 788 | +----------------+-----+----+ 789 | """ 790 | s = skyline.SkylineBl(100, 100, rot=False) 791 | rect1 = s.add_rect(50, 40) 792 | rect2 = s.add_rect(30, 20) 793 | rect3 = s.add_rect(50, 20) 794 | rect4 = s.add_rect(70, 30) 795 | rect5 = s.add_rect(20, 20) 796 | 797 | self.assertEqual(rect1, Rectangle(0, 0, 50, 40)) 798 | self.assertEqual(rect2, Rectangle(50, 0, 30, 20)) 799 | self.assertEqual(rect3, Rectangle(50, 20, 50, 20)) 800 | self.assertEqual(rect4, Rectangle(0, 40, 70, 30)) 801 | self.assertEqual(rect5, Rectangle(70, 40, 20, 20)) 802 | 803 | 804 | class TestSkylineBlWm(TestCase): 805 | 806 | def test_init(self): 807 | """Test Waste management is enabled""" 808 | p = skyline.SkylineBlWm(100, 100) 809 | self.assertTrue(p._waste_management) 810 | 811 | def test_skyline1(self): 812 | """ 813 | +---------------------------+ 814 | | | 815 | | | 816 | | | 817 | | | 818 | +--------------------+ | 819 | | | | 820 | | 4 | | 821 | | | | 822 | | | | 823 | +----------------+---+------+ 824 | | | 3 | 825 | | | | 826 | | 1 +-----+----+ 827 | | | 2 | w1 | 828 | | | | | 829 | +----------------+-----+----+ 830 | """ 831 | s = skyline.SkylineBlWm(100, 100, rot=False) 832 | rect1 = s.add_rect(50, 40) 833 | rect2 = s.add_rect(30, 20) 834 | rect3 = s.add_rect(50, 20) 835 | rect4 = s.add_rect(70, 30) 836 | w1 = s.add_rect(20, 20) 837 | 838 | self.assertEqual(rect1, Rectangle(0, 0, 50, 40)) 839 | self.assertEqual(rect2, Rectangle(50, 0, 30, 20)) 840 | self.assertEqual(rect3, Rectangle(50, 20, 50, 20)) 841 | self.assertEqual(rect4, Rectangle(0, 40, 70, 30)) 842 | self.assertEqual(w1, Rectangle(80, 0, 20, 20)) 843 | 844 | 845 | --------------------------------------------------------------------------------