bin_packing 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []