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 +7 -0
- data/lib/bin_packing.rb +19 -0
- data/lib/bin_packing/bin.rb +180 -0
- data/lib/bin_packing/box.rb +29 -0
- data/lib/bin_packing/error.rb +4 -0
- data/lib/bin_packing/export.rb +15 -0
- data/lib/bin_packing/export_binding.rb +18 -0
- data/lib/bin_packing/free_space_box.rb +12 -0
- data/lib/bin_packing/heuristics/base.rb +38 -0
- data/lib/bin_packing/heuristics/best_area_fit.rb +15 -0
- data/lib/bin_packing/heuristics/best_long_side_fit.rb +13 -0
- data/lib/bin_packing/heuristics/best_short_side_fit.rb +13 -0
- data/lib/bin_packing/heuristics/bottom_left.rb +12 -0
- data/lib/bin_packing/packer.rb +34 -0
- data/lib/bin_packing/resources/export.html.erb +51 -0
- data/lib/bin_packing/score.rb +43 -0
- data/lib/bin_packing/score_board.rb +64 -0
- data/lib/bin_packing/score_board_entry.rb +19 -0
- data/lib/bin_packing/version.rb +3 -0
- metadata +82 -0
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
|
data/lib/bin_packing.rb
ADDED
|
@@ -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,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,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
|
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: []
|