bin_packing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 729c8a0a3ebeb449568d02a4ec00f7a4ecc4a574
4
+ data.tar.gz: 7c53d882f799e197c921245ac9135c2b3ccc7b9b
5
+ SHA512:
6
+ metadata.gz: 5efb09d2832044bbd32fa250945eb7d7c0108df2c992a56c62ea85312b9dd3d76a262afc5c4570dc53dd5fe7fa47804da34aea47a1d8e69c8f33b4ba642e37c5
7
+ data.tar.gz: 3d708805897abef9f009edb147d383f6b71654d9ed59d24abe20caab6632cbf6ac17c26dcc559e3218d2a297eba2dc40f5294e421b3ae754432f7f51281f7807
@@ -0,0 +1,19 @@
1
+ require 'bin_packing/error'
2
+ require 'bin_packing/box'
3
+ require 'bin_packing/free_space_box'
4
+ require 'bin_packing/bin'
5
+ require 'bin_packing/score'
6
+ require 'bin_packing/score_board_entry'
7
+ require 'bin_packing/score_board'
8
+ require 'bin_packing/heuristics/base'
9
+ require 'bin_packing/heuristics/best_area_fit'
10
+ require 'bin_packing/heuristics/best_long_side_fit'
11
+ require 'bin_packing/heuristics/best_short_side_fit'
12
+ require 'bin_packing/heuristics/bottom_left'
13
+ require 'bin_packing/packer'
14
+ require 'bin_packing/export_binding'
15
+ require 'bin_packing/export'
16
+ require 'bin_packing/version'
17
+
18
+ module BinPacking
19
+ end
@@ -0,0 +1,180 @@
1
+ module BinPacking
2
+ class Bin
3
+ attr_reader :width, :height, :boxes, :heuristic
4
+
5
+ def initialize(width, height, heuristic = nil)
6
+ @width = width
7
+ @height = height
8
+
9
+ @boxes = []
10
+ box = BinPacking::FreeSpaceBox.new(width, height)
11
+ @free_rectangles = [box]
12
+
13
+ @heuristic = heuristic || BinPacking::Heuristics::BestShortSideFit.new
14
+ end
15
+
16
+ def efficiency
17
+ boxes_area = 0
18
+ @boxes.each { |box| boxes_area += box.area }
19
+ boxes_area * 100 / area
20
+ end
21
+
22
+ def insert(box)
23
+ return false if box.packed?
24
+
25
+ @heuristic.find_position_for_new_node!(box, @free_rectangles)
26
+ return false unless box.packed?
27
+
28
+ num_rectangles_to_process = @free_rectangles.size
29
+ i = 0
30
+ while i < num_rectangles_to_process
31
+ if split_free_node(@free_rectangles[i], box)
32
+ @free_rectangles.delete_at(i)
33
+ num_rectangles_to_process -= 1
34
+ else
35
+ i += 1
36
+ end
37
+ end
38
+
39
+ prune_free_list
40
+
41
+ @boxes << box
42
+ true
43
+ end
44
+
45
+ def insert!(box)
46
+ unless insert(box)
47
+ raise ArgumentError, "Could not insert box #{box.inspect} "\
48
+ "into bin #{inspect}."
49
+ end
50
+ self
51
+ end
52
+
53
+ def score_for(box)
54
+ @heuristic.find_position_for_new_node!(box.clone, @free_rectangles)
55
+ end
56
+
57
+ def is_larger_than?(box)
58
+ (@width >= box.width && @height >= box.height) ||
59
+ (@height >= box.width && @width >= box.height)
60
+ end
61
+
62
+ def label
63
+ "#{@width}x#{@height} #{efficiency}%"
64
+ end
65
+
66
+ private
67
+
68
+ def area
69
+ @width * @height
70
+ end
71
+
72
+ def place_rect(node)
73
+ num_rectangles_to_process = @free_rectangles.size
74
+ i = 0
75
+ while i < num_rectangles_to_process
76
+ if split_free_node(@free_rectangles[i], node)
77
+ @free_rectangles.delete_at(i)
78
+ num_rectangles_to_process -= 1
79
+ else
80
+ i += 1
81
+ end
82
+ end
83
+
84
+ prune_free_list
85
+
86
+ @boxes << node
87
+ end
88
+
89
+ def split_free_node(free_node, used_node)
90
+ # Test with SAT if the rectangles even intersect.
91
+ if used_node.x >= free_node.x + free_node.width ||
92
+ used_node.x + used_node.width <= free_node.x ||
93
+ used_node.y >= free_node.y + free_node.height ||
94
+ used_node.y + used_node.height <= free_node.y
95
+ return false
96
+ end
97
+
98
+ try_split_free_node_vertically(free_node, used_node)
99
+
100
+ try_split_free_node_horizontally(free_node, used_node)
101
+
102
+ true
103
+ end
104
+
105
+ def try_split_free_node_vertically(free_node, used_node)
106
+ if used_node.x < free_node.x + free_node.width && used_node.x + used_node.width > free_node.x
107
+ try_leave_free_space_at_top(free_node, used_node)
108
+ try_leave_free_space_at_bottom(free_node, used_node)
109
+ end
110
+ end
111
+
112
+ def try_leave_free_space_at_top(free_node, used_node)
113
+ if used_node.y > free_node.y && used_node.y < free_node.y + free_node.height
114
+ new_node = free_node.clone
115
+ new_node.height = used_node.y - new_node.y
116
+ @free_rectangles << new_node
117
+ end
118
+ end
119
+
120
+ def try_leave_free_space_at_bottom(free_node, used_node)
121
+ if used_node.y + used_node.height < free_node.y + free_node.height
122
+ new_node = free_node.clone
123
+ new_node.y = used_node.y + used_node.height
124
+ new_node.height = free_node.y + free_node.height - (used_node.y + used_node.height)
125
+ @free_rectangles << new_node
126
+ end
127
+ end
128
+
129
+ def try_split_free_node_horizontally(free_node, used_node)
130
+ if used_node.y < free_node.y + free_node.height && used_node.y + used_node.height > free_node.y
131
+ try_leave_free_space_on_left(free_node, used_node)
132
+ try_leave_free_space_on_right(free_node, used_node)
133
+ end
134
+ end
135
+
136
+ def try_leave_free_space_on_left(free_node, used_node)
137
+ if used_node.x > free_node.x && used_node.x < free_node.x + free_node.width
138
+ new_node = free_node.clone
139
+ new_node.width = used_node.x - new_node.x
140
+ @free_rectangles << new_node
141
+ end
142
+ end
143
+
144
+ def try_leave_free_space_on_right(free_node, used_node)
145
+ if used_node.x + used_node.width < free_node.x + free_node.width
146
+ new_node = free_node.clone
147
+ new_node.x = used_node.x + used_node.width
148
+ new_node.width = free_node.x + free_node.width - (used_node.x + used_node.width)
149
+ @free_rectangles << new_node
150
+ end
151
+ end
152
+
153
+ # Goes through the free rectangle list and removes any redundant entries.
154
+ def prune_free_list
155
+ i = 0
156
+ while i < @free_rectangles.size
157
+ j = i + 1
158
+ while j < @free_rectangles.size
159
+ if is_contained_in?(@free_rectangles[i], @free_rectangles[j])
160
+ @free_rectangles.delete_at(i)
161
+ i -= 1
162
+ break
163
+ end
164
+ if is_contained_in?(@free_rectangles[j], @free_rectangles[i])
165
+ @free_rectangles.delete_at(j)
166
+ else
167
+ j += 1
168
+ end
169
+ end
170
+ i += 1
171
+ end
172
+ end
173
+
174
+ def is_contained_in?(rect_a, rect_b)
175
+ return rect_a.x >= rect_b.x && rect_a.y >= rect_b.y &&
176
+ rect_a.x+rect_a.width <= rect_b.x+rect_b.width &&
177
+ rect_a.y+rect_a.height <= rect_b.y+rect_b.height
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,29 @@
1
+ module BinPacking
2
+ class Box
3
+ attr_accessor :width, :height, :x, :y, :packed
4
+
5
+ def initialize(width, height)
6
+ @width = width
7
+ @height = height
8
+ @x = 0
9
+ @y = 0
10
+ @packed = false
11
+ end
12
+
13
+ def area
14
+ @area ||= @width * @height
15
+ end
16
+
17
+ def rotate
18
+ @width, @height = [@height, @width]
19
+ end
20
+
21
+ def packed?
22
+ @packed
23
+ end
24
+
25
+ def label
26
+ "#{@width}x#{@height} at [#{@x},#{@y}]"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ module BinPacking
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module BinPacking
2
+ class Export
3
+ def initialize(*bins)
4
+ @bins = Array(bins).flatten
5
+ end
6
+
7
+ def to_html(options = {})
8
+ template_path = File.expand_path('../resources/export.html.erb', __FILE__)
9
+ template = File.read(template_path)
10
+ binding = ExportBinding.new(@bins, options[:zoom] || 1)
11
+ html = ERB.new(template).result(binding.get_binding)
12
+ html
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module BinPacking
2
+ class ExportBinding
3
+ attr_reader :bins
4
+
5
+ def initialize(bins, zoom)
6
+ @bins = bins
7
+ @zoom = zoom
8
+ end
9
+
10
+ def zoom(value)
11
+ (value * @zoom).to_i
12
+ end
13
+
14
+ def get_binding
15
+ binding
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ module BinPacking
2
+ class FreeSpaceBox
3
+ attr_accessor :x, :y, :width, :height
4
+
5
+ def initialize(width, height)
6
+ @width = width
7
+ @height = height
8
+ @x = 0
9
+ @y = 0
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module BinPacking
2
+ module Heuristics
3
+ class Base
4
+ def find_position_for_new_node!(box, free_rectangles)
5
+ best_score = BinPacking::Score.new
6
+ width = box.width
7
+ height = box.height
8
+
9
+ free_rectangles.each do |free_rect|
10
+ try_place_rect_in(free_rect, box, width, height, best_score)
11
+ try_place_rect_in(free_rect, box, height, width, best_score)
12
+ end
13
+
14
+ best_score
15
+ end
16
+
17
+ private
18
+
19
+ def try_place_rect_in(free_rect, box, rect_width, rect_height, best_score)
20
+ if free_rect.width >= rect_width && free_rect.height >= rect_height
21
+ score = calculate_score(free_rect, rect_width, rect_height)
22
+ if score > best_score
23
+ box.x = free_rect.x
24
+ box.y = free_rect.y
25
+ box.width = rect_width
26
+ box.height = rect_height
27
+ box.packed = true
28
+ best_score.assign(score)
29
+ end
30
+ end
31
+ end
32
+
33
+ def calculate_score(free_rect, rect_width, rect_height)
34
+ raise NotImplementedError
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ module BinPacking
2
+ module Heuristics
3
+ class BestAreaFit < BinPacking::Heuristics::Base
4
+ private
5
+
6
+ def calculate_score(free_rect, rect_width, rect_height)
7
+ area_fit = free_rect.width * free_rect.height - rect_width * rect_height
8
+ leftover_horiz = (free_rect.width - rect_width).abs
9
+ leftover_vert = (free_rect.height - rect_height).abs
10
+ short_side_fit = [leftover_horiz, leftover_vert].min
11
+ BinPacking::Score.new(area_fit, short_side_fit)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module BinPacking
2
+ module Heuristics
3
+ class BestLongSideFit < BinPacking::Heuristics::Base
4
+ private
5
+
6
+ def calculate_score(free_rect, rect_width, rect_height)
7
+ leftover_horiz = (free_rect.width - rect_width).abs
8
+ leftover_vert = (free_rect.height - rect_height).abs
9
+ BinPacking::Score.new(*[leftover_horiz, leftover_vert].sort.reverse)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module BinPacking
2
+ module Heuristics
3
+ class BestShortSideFit < BinPacking::Heuristics::Base
4
+ private
5
+
6
+ def calculate_score(free_rect, rect_width, rect_height)
7
+ leftover_horiz = (free_rect.width - rect_width).abs
8
+ leftover_vert = (free_rect.height - rect_height).abs
9
+ BinPacking::Score.new(*[leftover_horiz, leftover_vert].sort)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module BinPacking
2
+ module Heuristics
3
+ class BottomLeft < BinPacking::Heuristics::Base
4
+ private
5
+
6
+ def calculate_score(free_rect, rect_width, rect_height)
7
+ top_side_y = free_rect.y + rect_height
8
+ BinPacking::Score.new(top_side_y, free_rect.x)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module BinPacking
2
+ class Packer
3
+ def initialize(bins)
4
+ @bins = bins
5
+ @unpacked_boxes = []
6
+ end
7
+
8
+ def pack(boxes, options = {})
9
+ packed_boxes = []
10
+ boxes = boxes.reject(&:packed?)
11
+ return packed_boxes if boxes.none?
12
+
13
+ limit = options[:limit] || BinPacking::Score::MAX_INT
14
+ board = BinPacking::ScoreBoard.new(@bins, boxes)
15
+ while entry = board.best_fit
16
+ entry.bin.insert!(entry.box)
17
+ board.remove_box(entry.box)
18
+ board.recalculate_bin(entry.bin)
19
+ packed_boxes << entry.box
20
+ break if packed_boxes.size >= limit
21
+ end
22
+
23
+ packed_boxes
24
+ end
25
+
26
+ def pack!(boxes)
27
+ packed_boxes = pack(boxes)
28
+ if packed_boxes.size != boxes.size
29
+ raise ArgumentError, "#{boxes.size - packed_boxes.size} boxes not "\
30
+ "packed into #{@bins.size} bins!"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>
5
+ body {
6
+ margin: 0;
7
+ }
8
+
9
+ .bin {
10
+ position: relative;
11
+ margin: 20px;
12
+ background: #eee;
13
+ }
14
+
15
+ .bin .label {
16
+ position: absolute;
17
+ bottom: 5px;
18
+ right: 5px;
19
+ }
20
+
21
+ .box {
22
+ position: absolute;
23
+ border: 1px solid white;
24
+ background: #0a0;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ .box .box-label {
29
+ position: absolute;
30
+ top: 5px;
31
+ left: 5px;
32
+ color: #fff;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div>
38
+ <% bins.each do |bin| %>
39
+ <div class="bin" style="width: <%= zoom(bin.width) %>px; height: <%= zoom(bin.height) %>px;">
40
+ <div>
41
+ <% bin.boxes.each do |box| %>
42
+ <div class="box" style="width: <%= zoom(box.width) %>px; height: <%= zoom(box.height) %>px; left: <%= zoom(box.x) %>px; top: <%= zoom(box.y) %>px;">
43
+ <span class="box-label"><%= box.label %></span>
44
+ </div>
45
+ <% end %>
46
+ </div>
47
+ <span class="label"><%= bin.label %></span>
48
+ </div>
49
+ <% end %>
50
+ </body>
51
+ </html>
@@ -0,0 +1,43 @@
1
+ module BinPacking
2
+ class Score
3
+ include Comparable
4
+
5
+ MAX_INT = (2**(0.size * 8 -2) -1)
6
+
7
+ attr_reader :score_1, :score_2
8
+
9
+ def self.new_blank
10
+ new
11
+ end
12
+
13
+ def initialize(score_1 = nil, score_2 = nil)
14
+ @score_1 = score_1 || MAX_INT
15
+ @score_2 = score_2 || MAX_INT
16
+ end
17
+
18
+ # Smaller number is greater (used by original algorithm).
19
+ def <=>(other)
20
+ if self.score_1 > other.score_1 || (self.score_1 == other.score_1 && self.score_2 > other.score_2)
21
+ -1
22
+ elsif self.score_1 < other.score_1 || (self.score_1 == other.score_1 && self.score_2 < other.score_2)
23
+ 1
24
+ else
25
+ 0
26
+ end
27
+ end
28
+
29
+ def assign(other)
30
+ @score_1 = other.score_1
31
+ @score_2 = other.score_2
32
+ end
33
+
34
+ def is_blank?
35
+ @score_1 == MAX_INT
36
+ end
37
+
38
+ def decrease_by(delta)
39
+ @score_1 += delta
40
+ @score_2 += delta
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,64 @@
1
+ # box_1 box_2 box_3 ...
2
+ # bin_1 100 200 0
3
+ # bin_2 0 5 0
4
+ # bin_3 9 100 0
5
+ # ...
6
+ module BinPacking
7
+ class ScoreBoard
8
+ def initialize(bins, boxes)
9
+ @entries = []
10
+ bins.each do |bin|
11
+ add_bin_entries(bin, boxes)
12
+ end
13
+ end
14
+
15
+ def any?
16
+ @entries.any?
17
+ end
18
+
19
+ def largest_not_fiting_box
20
+ unfit = nil
21
+ fitting_boxes = Set.new(@entries.select(&:fit?).map(&:box))
22
+ @entries.each do |entry|
23
+ next if fitting_boxes.include?(entry.box)
24
+ unfit = entry if unfit.nil? || unfit.box.area < entry.box.area
25
+ end
26
+ unfit.try(:box)
27
+ end
28
+
29
+ def best_fit
30
+ best = nil
31
+ @entries.each do |entry|
32
+ next unless entry.fit?
33
+ best = entry if best.nil? || best.score < entry.score
34
+ end
35
+ best
36
+ end
37
+
38
+ def remove_box(box)
39
+ @entries.delete_if { |e| e.box == box }
40
+ end
41
+
42
+ def add_bin(bin)
43
+ add_bin_entries(bin, current_boxes)
44
+ end
45
+
46
+ def recalculate_bin(bin)
47
+ @entries.select { |e| e.bin == bin }.each(&:calculate)
48
+ end
49
+
50
+ private
51
+
52
+ def add_bin_entries(bin, boxes)
53
+ boxes.each do |box|
54
+ entry = BinPacking::ScoreBoardEntry.new(bin, box)
55
+ entry.calculate
56
+ @entries << entry
57
+ end
58
+ end
59
+
60
+ def current_boxes
61
+ @entries.map(&:box).uniq
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
1
+ module BinPacking
2
+ class ScoreBoardEntry
3
+ attr_reader :bin, :box, :score
4
+
5
+ def initialize(bin, box)
6
+ @bin = bin
7
+ @box = box
8
+ @score = nil
9
+ end
10
+
11
+ def calculate
12
+ @score = @bin.score_for(@box)
13
+ end
14
+
15
+ def fit?
16
+ !@score.is_blank?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module BinPacking
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bin_packing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - MAK IT
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.0
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ description: |
34
+ Provides algorithm for placing rectangles (box-es) in one or multiple rectangular areas (bins) with reasonable allocation efficiency.
35
+ email: info@makit.lv
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - lib/bin_packing.rb
41
+ - lib/bin_packing/bin.rb
42
+ - lib/bin_packing/box.rb
43
+ - lib/bin_packing/error.rb
44
+ - lib/bin_packing/export.rb
45
+ - lib/bin_packing/export_binding.rb
46
+ - lib/bin_packing/free_space_box.rb
47
+ - lib/bin_packing/heuristics/base.rb
48
+ - lib/bin_packing/heuristics/best_area_fit.rb
49
+ - lib/bin_packing/heuristics/best_long_side_fit.rb
50
+ - lib/bin_packing/heuristics/best_short_side_fit.rb
51
+ - lib/bin_packing/heuristics/bottom_left.rb
52
+ - lib/bin_packing/packer.rb
53
+ - lib/bin_packing/resources/export.html.erb
54
+ - lib/bin_packing/score.rb
55
+ - lib/bin_packing/score_board.rb
56
+ - lib/bin_packing/score_board_entry.rb
57
+ - lib/bin_packing/version.rb
58
+ homepage: http://rubygems.org/gems/bin_packing
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 2.2.2
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: 2D bin packing algorithm
82
+ test_files: []