├── .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 | 
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 |
--------------------------------------------------------------------------------