├── .gitignore ├── Makefile ├── README.md ├── _assets └── screenshot.jpg ├── binpacking ├── __init__.py ├── __main__.py ├── app.py ├── node.py └── rect.py ├── setup.py ├── tests ├── test_node.py └── test_rect.py └── tools └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | _pycache_/ 3 | dump/ 4 | build/ 5 | *.egg-info/ 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS), Windows_NT) 2 | RM := del /F /Q 3 | else 4 | RM := rm -rf 5 | endif 6 | 7 | test: 8 | python tools/test.py 9 | 10 | clean: 11 | $(RM) main\__pycache__ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bin packing 2 | 3 | Just for fun. 4 | 5 | ![bin packing](./_assets/screenshot.jpg) 6 | 7 | requires: 8 | python >= 3.0 9 | pytest == 2.7.0 10 | Pillow == 2.7.0 11 | cx-Freeze == 4.3.4 # For creating executable 12 | 13 | any enhance pull request will be accepted 14 | 15 | ## Usage 16 | 17 | Run `python setup.py develop` to make the package locally available then you can run `python binpacking` to run the program or `py.test tests` to do the test. 18 | 19 | Or, you can use `python -m binpacking` to run the program or `python tools/test.py` to do the test. 20 | 21 | --- 22 | 23 | Copyright (c) 2015, Towry Wang 24 | -------------------------------------------------------------------------------- /_assets/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/towry-archived/bin-packing/47d425aca9eb82290ae59566f5d5818305ba878a/_assets/screenshot.jpg -------------------------------------------------------------------------------- /binpacking/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /binpacking/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from binpacking.app import * 5 | 6 | def run(): 7 | root = tk.Tk() 8 | app = Application(master=root) 9 | root.title("Hello World") 10 | root.geometry('{0}x{1}'.format(DEFAULT_WIDTH, DEFAULT_HEIGHT)) 11 | root.resizable(width= False, height= False) 12 | try: 13 | root.mainloop() 14 | except KeyboardInterrupt: 15 | app.exit() 16 | 17 | if __name__ == '__main__': 18 | run() 19 | -------------------------------------------------------------------------------- /binpacking/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | main.app 4 | ~~~~~~~~ 5 | 6 | :copyright: (c) 2015 by Towry Wang. 7 | :license: MIT, see . 8 | 9 | :requires: 10 | python >= 3 11 | """ 12 | 13 | import tkinter as tk 14 | from tkinter import ttk 15 | import re 16 | import random 17 | from datetime import datetime 18 | 19 | import binpacking.node as mnode 20 | 21 | __all__ = ['Application', 'tk', 'DEFAULT_WIDTH', 'DEFAULT_HEIGHT'] 22 | 23 | DEFAULT_WIDTH = 400 24 | DEFAULT_HEIGHT = 500 25 | 26 | class Application(object): 27 | 28 | def __init__(self, master= None): 29 | master.columnconfigure(0, weight= 1) 30 | master.rowconfigure(0, weight= 1) 31 | self.master = master 32 | self.frame = tk.Frame(master, width= DEFAULT_WIDTH, height= DEFAULT_HEIGHT) 33 | self.frame.grid_propagate(False) 34 | self.frame.columnconfigure(0, weight= 1) 35 | self.frame.columnconfigure(1, weight= 1) 36 | self.frame.rowconfigure(3, weight= 1) 37 | self.frame.grid(row= 0, column= 0, sticky=(tk.N, tk.W, tk.E, tk.S), padx= 5) 38 | self._help_text = tk.StringVar(master) 39 | 40 | self._create_widgets() 41 | 42 | w, h = self.get_canvas_winfo() 43 | self._help_text.set("Input in this form:\nwidth*height\nor width*height*number\nCanvas: w{}:h{}".format(w, h)) 44 | 45 | def _create_widgets(self): 46 | # label 47 | label = tk.Label(self.frame, justify= "left", textvariable= self._help_text, font= ("Consolas", 11)) 48 | label.grid(row= 0, column= 0, sticky= tk.W) 49 | 50 | # text 51 | frame_text = tk.Frame(self.frame) 52 | frame_text.grid(row= 1, column= 0, sticky= (tk.N, tk.W, tk.E, tk.S)) 53 | 54 | text = tk.Text(frame_text, height= 5, width= 25) 55 | text.insert(tk.END, "50*60") 56 | text.grid(row= 0, column= 0, pady= (0, 10), sticky= (tk.W, tk.E)) 57 | 58 | text_log = tk.Text(frame_text, height= 5) 59 | text_log.grid(row= 0, column= 1, pady= (0, 10), sticky= (tk.W, tk.E)) 60 | self._log_text_widget = text_log 61 | 62 | #button 63 | frame_button = tk.Frame(self.frame) 64 | frame_button.grid(row= 2, column= 0, pady= (0, 10), sticky= tk.W) 65 | button = tk.Button(frame_button, text= "Update", command= self._btn_click) 66 | button.grid(row= 0, column= 0) 67 | button_clear = tk.Button(frame_button, text= "Clear", command= self._btn_clear) 68 | button_clear.grid(row= 0, column= 1, padx= (10, 0)) 69 | 70 | #canvas 71 | # canvas_sh = tk.Scrollbar(self.master, orient= tk.HORIZONTAL) 72 | # canvas_sv = tk.Scrollbar(self.master, orient= tk.VERTICAL) 73 | # scrollregion=(0, 0, 1000, 1000) 74 | canvas = tk.Canvas(self.frame, bg= "white") 75 | # canvas['yscrollcommand'] = canvas_sv.set 76 | # canvas['xscrollcommand'] = canvas_sh.set 77 | # canvas_sh['command'] = canvas.xview 78 | # canvas_sv['command'] = canvas.yview 79 | # ttk.Sizegrip(self.master).grid(column= 1, row= 3, sticky= (tk.S, tk.E)) 80 | # canvas_sh.grid(column= 0, row= 3, sticky= (tk.W, tk.E)) 81 | # canvas_sv.grid(column= 1, row= 3, sticky= (tk.N, tk.S)) 82 | canvas.grid(row= 3, column= 0, ipadx= 5, pady= (0, 5), sticky= tk.W+tk.N+tk.E+tk.S) 83 | 84 | self.text = text 85 | self.canvas = canvas 86 | 87 | def _btn_click(self): 88 | text = self.text.get(1.0, tk.END) 89 | if text.strip() == "": return 90 | self._process(text) 91 | def _btn_clear(self): 92 | self.canvas.delete('all') 93 | self.text.delete(1.0, tk.END) 94 | self._log_text_widget.delete(1.0, tk.END) 95 | 96 | def _log(self, msg): 97 | now = datetime.now().strftime("%H:%M:%S") 98 | print("{} - {}".format(now, msg)) 99 | self._log_text_widget.insert(tk.END, "{} - {}\n".format(now, msg)) 100 | 101 | def _process(self, text): 102 | text = text.split('\n') 103 | pattern = re.compile('^(\d+)\*(\d+)(?:\*(\d+))?$') 104 | matrix = [] 105 | for t in text: 106 | m = pattern.match(t) 107 | if not m: 108 | continue 109 | else: 110 | group = m.groups() 111 | if group[2] != None: 112 | for i in range(int(group[2])): 113 | matrix.append(tuple(int(x) for x in group[:-1])) 114 | else: 115 | matrix.append(tuple(int(x) for x in group[:-1])) 116 | 117 | if not len(matrix): return 118 | 119 | # clean the canvas 120 | self.canvas.delete('all') 121 | self._draw_all(matrix) 122 | 123 | def _draw_all(self, matrix): 124 | color = None 125 | node = None 126 | cw, ch = self.get_canvas_winfo() 127 | root = mnode.Node(cw, ch) 128 | self.missed = [] 129 | for unit in matrix: 130 | node = root.split(unit) 131 | if node is None: 132 | self._log("missed") 133 | self.missed.append(unit) 134 | else: 135 | if node.occupied(): print(node) 136 | # draw the block 137 | color = self._color() 138 | self.draw_block(node.x, node.y, node.w, node.h, fill= color) 139 | 140 | def exit(self): 141 | self.master.destroy() 142 | 143 | def get_canvas_winfo(self): 144 | return (self.canvas.winfo_reqwidth(), self.canvas.winfo_reqheight()) 145 | 146 | def draw_block(self, x, y, w, h, fill= 'blue', outline= 'white'): 147 | # space for outline 148 | if outline is None: 149 | outline = fill 150 | 151 | self.canvas.create_rectangle(x, y, x + w, y + h, fill= fill, outline= outline) 152 | 153 | @classmethod 154 | def _color(cls): 155 | r = lambda: random.randint(0, 255) 156 | return "#{0:02X}{1:02X}{2:02X}".format(r(), r(), r()) 157 | -------------------------------------------------------------------------------- /binpacking/node.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from binpacking import rect 4 | 5 | class Node(object): 6 | def __init__(self, width= None, height= None, parent=None): 7 | self.left = None 8 | self.right = None 9 | self.parent = parent 10 | if width and height: 11 | self.rect = rect.Rect(width, height) 12 | else: 13 | self.rect = None 14 | 15 | self.size = 0 16 | 17 | def __repr__(self): 18 | return "".format(self.rect if self.rect else "None") 19 | 20 | @property 21 | def x(self): 22 | return self.rect.x 23 | @property 24 | def y(self): 25 | return self.rect.y 26 | @property 27 | def w(self): 28 | return self.rect.w 29 | @property 30 | def h(self): 31 | return self.rect.h 32 | 33 | def occupied(self): 34 | return self.rect and self.rect.occupied 35 | 36 | def occupy(self): 37 | if not self.rect: return 38 | self.rect.occupied = True 39 | 40 | def fit(self, *args): 41 | if len(args) == 1: 42 | if len(args[0]) != 2: raise ValueError() 43 | w, h = args[0] 44 | elif len(args) == 2: 45 | w, h = args 46 | else: 47 | raise ValueError("requires width and height") 48 | 49 | if self.rect.w == w and self.rect.h == h: 50 | return True 51 | else: 52 | return False 53 | 54 | def embrace(self, *args): 55 | if len(args) == 1: 56 | if len(args[0]) != 2: raise ValueError() 57 | w, h = args[0] 58 | elif len(args) == 2: 59 | w, h = args 60 | else: 61 | raise ValueError("requires width and height") 62 | 63 | if not self.rect: return False 64 | elif self.rect.w >= w and self.rect.h >= h: 65 | return True 66 | else: 67 | return False 68 | 69 | def split(self, *args): 70 | if len(args) == 1: 71 | if len(args[0]) != 2: raise ValueError() 72 | w, h = args[0] 73 | elif len(args) == 2: 74 | w, h = args 75 | else: 76 | raise ValueError("requires width and height") 77 | 78 | # sentinel 79 | if self.occupied(): 80 | return None 81 | if not self.embrace(w, h): 82 | return None 83 | 84 | if self.fit(w, h): 85 | self.occupy() 86 | return self 87 | 88 | # if not splited 89 | if self.size == 0: 90 | self.left = Node() 91 | self.right = Node() 92 | self.size += 2 93 | # if self.rect.w - w == self.rect.h - h: 94 | # left/right or top/down is ok 95 | if self.rect.w - w > self.rect.h - h: 96 | # go with left/right 97 | self.left.rect = rect.Rect(w, self.rect.h, x= self.rect.x, y= self.rect.y) 98 | self.right.rect = rect.Rect(self.rect.w - w, self.rect.h, x= self.rect.x + w, y= self.rect.y) 99 | else: 100 | # go with top/down 101 | self.left.rect = rect.Rect(self.rect.w, h, x= self.rect.x, y= self.rect.y) 102 | self.right.rect = rect.Rect(self.rect.w, self.rect.h - h, x= self.rect.x, y= self.rect.y + h) 103 | 104 | # recursively split it 105 | ok = self.left.split(w, h) 106 | if not ok: 107 | return self.right.split(w, h) 108 | else: 109 | return ok 110 | 111 | 112 | """ 113 | node = Node() 114 | node.rect = Rect(500, 500) 115 | 116 | box = (200, 150) 117 | node.split(0, 0, box) 118 | 119 | """ 120 | 121 | def traverse(root): 122 | if root is None: return 123 | 124 | parent = root 125 | yield parent 126 | 127 | for n in traverse(parent.left): 128 | yield n 129 | 130 | for n in traverse(parent.right): 131 | yield n 132 | -------------------------------------------------------------------------------- /binpacking/rect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class Rect(object): 4 | def __init__(self, w, h, x= 0, y= 0): 5 | self.x = x 6 | self.y = y 7 | self.w = w 8 | self.h = h 9 | self.occupied = False 10 | 11 | def __repr__(self): 12 | return "".format(self.x, self.y, self.w, self.h) 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bin packing algorithm 3 | 4 | :copyright: (c) 2015 by Towry Wang 5 | :license: MIT, http://towry.me/mit-license/ 6 | """ 7 | 8 | import sys 9 | from cx_Freeze import setup, Executable 10 | 11 | __author__ = "Towry Wang" 12 | __version__ = "0.1.0" 13 | __contact__ = "towry@foxmail.com" 14 | __url__ = "http://github.com/towry/bin-packing" 15 | __license__ = "MIT" 16 | 17 | base = None 18 | if sys.platform == 'win32': 19 | base = "Win32GUI" 20 | 21 | setup(name= "binpacking", 22 | version= __version__, 23 | description= "bin packing algorithm", 24 | long_description= __doc__, 25 | author= __author__, 26 | author_email= __contact__, 27 | url= __url__, 28 | license= __license__, 29 | packages= ['binpacking'], 30 | zip_safe= False, 31 | include_package_data= True, 32 | platforms= 'any', 33 | setup_requires= [ 34 | "Pillow >= 2.7.0" 35 | ], 36 | executables= [Executable("binpacking/__main__.py", 37 | base= base, 38 | compress= True, 39 | targetName= "bp.exe") 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /tests/test_node.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from binpacking import node 4 | 5 | def test_one(): 6 | assert True 7 | 8 | main = (500, 500) 9 | block1 = (200, 150) 10 | block2 = (100, 60) 11 | block3 = (100, 50) 12 | block4 = (50, 50) 13 | 14 | def test_two(): 15 | root = node.Node(*main) 16 | 17 | root.split(0, 0, block1) 18 | 19 | assert root.size == 2 20 | 21 | def test_two(): 22 | root = node.Node(*main) 23 | 24 | ok = root.split(block1) 25 | assert ok != None 26 | 27 | ok = root.split(block2) 28 | assert ok != None 29 | 30 | ok = root.split(block3) 31 | assert ok != None 32 | 33 | ok = root.split(block4) 34 | assert ok != None 35 | -------------------------------------------------------------------------------- /tests/test_rect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from binpacking import rect, node 5 | 6 | 7 | def test_one(): 8 | r = rect.Rect(30, 40) 9 | 10 | assert r.x == 0 11 | assert r.y == 0 12 | assert r.w == 30 13 | assert r.h == 40 14 | 15 | def test_two(): 16 | r = rect.Rect(20, 30, x= 100, y= 50) 17 | 18 | assert r.x == 100 19 | assert r.y == 50 20 | assert r.w == 20 21 | assert r.h == 30 22 | 23 | def test_three(): 24 | 25 | try: 26 | r = rect.Rect(30, x= 100) 27 | except: 28 | assert True 29 | return 30 | assert False 31 | -------------------------------------------------------------------------------- /tools/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | import subprocess 6 | 7 | cwd = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 8 | sys.path.insert(0, cwd) 9 | 10 | if __name__ == '__main__': 11 | if (os.getcwd() != cwd): 12 | os.chdir(cwd) 13 | os.putenv("PYTHONPATH", ';'.join(str(i) for i in sys.path)) 14 | subprocess.call(['py.test', '-s', 'tests'], shell= True) 15 | --------------------------------------------------------------------------------