box_packer 0.0.2 → 1.0.0
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 +5 -13
- data/.gitignore +0 -16
- data/.rspec +2 -0
- data/README.md +70 -23
- data/box_packer.gemspec +14 -11
- data/lib/box_packer.rb +3 -196
- data/lib/box_packer/box.rb +39 -0
- data/lib/box_packer/builder.rb +22 -0
- data/lib/box_packer/container.rb +90 -0
- data/lib/box_packer/dimensions.rb +37 -0
- data/lib/box_packer/item.rb +33 -0
- data/lib/box_packer/packer.rb +58 -0
- data/lib/box_packer/packing.rb +40 -0
- data/lib/box_packer/position.rb +11 -0
- data/lib/box_packer/version.rb +1 -1
- data/spec/lib/box_spec.rb +53 -0
- data/spec/lib/container_spec.rb +88 -0
- data/spec/lib/dimensions_spec.rb +52 -0
- data/spec/lib/item_spec.rb +30 -0
- data/spec/lib/packer_spec.rb +81 -0
- data/spec/lib/packing_spec.rb +73 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/matchers/box_packer.rb +40 -0
- metadata +61 -9
- data/lib/tc_box_packer.rb +0 -171
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
NjdhYzg2OGM1ZjRhZDBjNTBiOWVlYzkwNjE5NGFlMDRjZmIwNTIzYw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9bd4f81789b4f8f8dc272b4893f2ce1aaf5bf174
|
4
|
+
data.tar.gz: 2cbf0f36b648dc9d90872f17a118347fe6ad1d9c
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
YjQ1MDEwY2UwZDhlYjA1NjI0OWJhODhkNGQ4YmQ4MjNkZDZiZTkzZmVhNmVk
|
11
|
-
NzE3MWM0MTA2M2VmYTg0YTk0NzQxYTc2NWFjNmYyNTMzNTkxNDY=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
OThjYjRlOWRhMDQ5OTMyNzNjNDI0M2QxNTRmYzE0NGM4ODYyMTM2YzA2MTVj
|
14
|
-
YTE0ZDdlMmM5MGI4MDU0MTRmYWI3Nzc5NjU0YTRiYjllNWRiNTM0NDFiMGFm
|
15
|
-
OGZmMTljZmJmZTFhYjA4YTgzMDU1MzQwNDkxODVhNDhlZTY1NzU=
|
6
|
+
metadata.gz: 69dfe26e637c5f2fbd885af2949978cc5e7af0451c182a2ed7825d291c95e3780f81a7beb79ecb1f2f1d5aef2e273db51e8c4fdaf0260926504786166da79e90
|
7
|
+
data.tar.gz: ed7bf4a3b8f79fabbc84d1ea23ecbd583a6885c65aa2877cb721bfc811f0c0702244b27a4cde5c671b45d7eaa9c81cea3cbb513e727d933edc3ecf2e661d2b9b
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/README.md
CHANGED
@@ -1,46 +1,93 @@
|
|
1
1
|
BoxPacker
|
2
2
|
=========
|
3
3
|
|
4
|
-
A
|
4
|
+
A heuristic first-fit 3D bin packing algorithm with optional weight and bin limits.
|
5
5
|
|
6
6
|
Installation
|
7
7
|
------------
|
8
8
|
|
9
|
-
|
9
|
+
Install gem:
|
10
10
|
|
11
|
-
|
11
|
+
``` console
|
12
|
+
gem install 'box_packer'
|
13
|
+
```
|
12
14
|
|
13
|
-
|
15
|
+
Or add to gemfile:
|
14
16
|
|
15
|
-
|
17
|
+
``` ruby
|
18
|
+
gem 'box_packer'
|
19
|
+
```
|
16
20
|
|
17
21
|
Usage
|
18
22
|
-----
|
19
23
|
|
20
|
-
```
|
21
|
-
require
|
24
|
+
``` ruby
|
25
|
+
require 'box_packer'
|
22
26
|
|
23
|
-
|
27
|
+
BoxPacker.container [3, 6, 7] do
|
28
|
+
add_item [1,3,5]
|
29
|
+
add_item [4,3,5]
|
30
|
+
add_item [3,5,5]
|
31
|
+
pack! # returns 2
|
24
32
|
|
25
|
-
|
26
|
-
|
27
|
-
|
33
|
+
puts packed_successfully # true
|
34
|
+
puts packings.count # 2
|
35
|
+
puts packings[0].include? items[1] # false
|
36
|
+
puts packings[0][1].position # (5,0,0)
|
28
37
|
|
29
|
-
|
30
|
-
|
38
|
+
puts self # |Container| 7x6x3
|
39
|
+
# | Packing| Remaining Volume:36
|
40
|
+
# | Item| 5x5x3 (0,0,0) Volume:75
|
41
|
+
# | Item| 1x5x3 (5,0,0) Volume:15
|
42
|
+
# | Packing| Remaining Volume:66
|
43
|
+
# | Item| 5x4x3 (0,0,0) Volume:60
|
44
|
+
|
45
|
+
end
|
31
46
|
```
|
32
47
|
|
33
|
-
|
34
|
-
|
48
|
+
With optional labels, weights and packings limit:
|
49
|
+
|
50
|
+
``` ruby
|
51
|
+
BoxPacker.container [15, 20, 13], label: 'Parcel', weight_limit: 50, packings_limit: 3 do
|
52
|
+
add_item [2, 3, 5], label: 'Shoes', weight: 47
|
53
|
+
add_item [3, 3, 1], label: 'Watch', weight: 24
|
54
|
+
add_item [1, 1, 4], label: 'Bag', weight: 7
|
55
|
+
pack! # returns 2
|
56
|
+
|
57
|
+
puts self # |Container| Parcel 20x15x13 Weight Limit:50 Packings Limit:3
|
58
|
+
# | Packing| Remaining Volume:3870 Remaining Weight:3
|
59
|
+
# | Item| Shoes 5x3x2 (0,0,0) Volume:30 Weight:47
|
60
|
+
# | Packing| Remaining Volume:3887 Remaining Weight:19
|
61
|
+
# | Item| Watch 3x3x1 (0,0,0) Volume:9 Weight:24
|
62
|
+
# | Item| Bag 4x1x1 (3,0,0) Volume:4 Weight:7
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Alternative builder API:
|
67
|
+
|
68
|
+
``` ruby
|
69
|
+
BoxPacker.builder do |b|
|
70
|
+
c1 = b.container [10,5,11]
|
71
|
+
c2 = b.container [17,23,14]
|
72
|
+
|
73
|
+
c1.items = [b.item([1,1,4]), b.item([4,6,7]), b.item([5,8,10])]
|
74
|
+
c2.items = c1.items
|
75
|
+
|
76
|
+
c1.pack! # 2
|
77
|
+
c2.pack! # 1
|
35
78
|
|
36
|
-
|
37
|
-
|
38
|
-
|
79
|
+
puts c1 # |Container| 11x10x5
|
80
|
+
# | Packing| Remaining Volume:146
|
81
|
+
# | Item| 10x8x5 (0,0,0) Volume:400
|
82
|
+
# | Item| 4x1x1 (0,8,0) Volume:4
|
83
|
+
# | Packing| Remaining Volume:382
|
84
|
+
# | Item| 7x6x4 (10,0,0) Volume:168
|
39
85
|
|
40
|
-
|
41
|
-
|
42
|
-
|
86
|
+
puts c2 # |Container| 23x17x14
|
87
|
+
# | Packing| Remaining Volume:4902
|
88
|
+
# | Item| 10x8x5 (0,0,0) Volume:400
|
89
|
+
# | Item| 7x6x4 (10,0,0) Volume:168
|
90
|
+
# | Item| 4x1x1 (17,0,0) Volume:4
|
43
91
|
|
44
|
-
|
45
|
-
MyItem01 - [1, 3, 5] Pos:[0, 0, 0] V:15 W:10
|
92
|
+
end
|
46
93
|
```
|
data/box_packer.gemspec
CHANGED
@@ -1,23 +1,26 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
lib = File.expand_path('../lib', __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
3
|
require 'box_packer/version'
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
6
|
+
spec.name = 'box_packer'
|
8
7
|
spec.version = BoxPacker::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.description =
|
12
|
-
spec.summary =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
8
|
+
spec.authors = ['Max White']
|
9
|
+
spec.email = ['mushishi78@gmail.com']
|
10
|
+
spec.description = 'A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit'
|
11
|
+
spec.summary = 'A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit'
|
12
|
+
spec.homepage = ''
|
13
|
+
spec.license = 'MIT'
|
15
14
|
|
16
15
|
spec.files = `git ls-files`.split($/)
|
17
16
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
21
|
+
spec.add_development_dependency 'rake'
|
22
|
+
spec.add_development_dependency 'rspec'
|
23
|
+
|
24
|
+
spec.add_dependency 'attr_extras'
|
20
25
|
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
-
spec.add_development_dependency "rake"
|
23
26
|
end
|
data/lib/box_packer.rb
CHANGED
@@ -1,196 +1,3 @@
|
|
1
|
-
require_relative
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
module BoxPacker
|
6
|
-
|
7
|
-
class Box
|
8
|
-
attr_reader :volume
|
9
|
-
attr_accessor :dimensions, :position
|
10
|
-
|
11
|
-
def initialize(dimensions, position=[0,0,0])
|
12
|
-
@dimensions, @position = Vector.elements(dimensions), Vector.elements(position)
|
13
|
-
@volume = @dimensions.reduce(:*)
|
14
|
-
end
|
15
|
-
|
16
|
-
def fit?(box)
|
17
|
-
(0..2).all?{ |i| box.dimensions[i] <= @dimensions[i] }
|
18
|
-
end
|
19
|
-
|
20
|
-
def fit_with_rotation?(box)
|
21
|
-
rotations = [0,0,1]
|
22
|
-
rotations = rotations.permutation.to_a.uniq.permutation.to_a.reverse
|
23
|
-
original_orientation = box.dimensions
|
24
|
-
|
25
|
-
rotations.each do |rotation|
|
26
|
-
box.dimensions = Matrix.rows(rotation) * box.dimensions
|
27
|
-
return true if fit?(box)
|
28
|
-
end
|
29
|
-
|
30
|
-
box.dimensions = original_orientation
|
31
|
-
return false
|
32
|
-
end
|
33
|
-
|
34
|
-
def break_up_remaining_space(box)
|
35
|
-
x_offset = Vector[box.position[0], 0, 0]
|
36
|
-
|
37
|
-
sub_box_x = Box.new((Matrix.diagonal(1,1,1) * @dimensions) +(Matrix.diagonal(-1,0,0) * box.dimensions),
|
38
|
-
(Matrix.diagonal(0,1,1) * @position) +(Matrix.diagonal(1,0,0) * box.dimensions) + x_offset)
|
39
|
-
|
40
|
-
sub_box_y = Box.new((Matrix.diagonal(0,1,1) * @dimensions) +(Matrix.diagonal(1,-1,0) * box.dimensions),
|
41
|
-
(Matrix.diagonal(0,1,1) * @position) +(Matrix.diagonal(0,1,0) * box.dimensions) + x_offset)
|
42
|
-
|
43
|
-
sub_box_z = Box.new((Matrix.diagonal(0,0,1) * @dimensions) +(Matrix.diagonal(1,1,-1) * box.dimensions),
|
44
|
-
(Matrix.diagonal(0,1,1) * @position) +(Matrix.diagonal(0,0,1) * box.dimensions) + x_offset)
|
45
|
-
|
46
|
-
return [sub_box_x, sub_box_y, sub_box_z]
|
47
|
-
end
|
48
|
-
|
49
|
-
def to_s
|
50
|
-
"Box - #{@dimensions.to_a} Pos:#{@position.to_a} V:#{@volume}"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
class Item < Box
|
55
|
-
attr_accessor :id, :weight
|
56
|
-
|
57
|
-
def initialize(id, dimensions, weight)
|
58
|
-
super(dimensions.sort)
|
59
|
-
@id, @weight = id, weight
|
60
|
-
end
|
61
|
-
|
62
|
-
def deep_copy
|
63
|
-
cloned_item = self.clone
|
64
|
-
cloned_item.dimensions = @dimensions.clone
|
65
|
-
cloned_item.position = @position.clone
|
66
|
-
return cloned_item
|
67
|
-
end
|
68
|
-
|
69
|
-
def to_s
|
70
|
-
"#{@id} - #{@dimensions.to_a} V:#{@volume} W:#{@weight}"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class Packing
|
75
|
-
extend Forwardable
|
76
|
-
attr_reader :remaining_weight, :remaining_volume, :items
|
77
|
-
|
78
|
-
def_delegators :@items, :include?, :each, :map, :count
|
79
|
-
|
80
|
-
def initialize(weight_limit, container_volume)
|
81
|
-
@items = []
|
82
|
-
@remaining_weight, @remaining_volume = weight_limit, container_volume
|
83
|
-
end
|
84
|
-
|
85
|
-
def <<(item)
|
86
|
-
@items << item
|
87
|
-
@remaining_volume -= item.volume
|
88
|
-
@remaining_weight -= item.weight
|
89
|
-
end
|
90
|
-
|
91
|
-
def to_s
|
92
|
-
s = @items.map do |item|
|
93
|
-
"#{item.id} - #{item.dimensions.to_a} Pos:#{item.position.to_a} V:#{item.volume} W:#{item.weight}"
|
94
|
-
end
|
95
|
-
return s.join("\n")
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
class Container < Box
|
100
|
-
attr_accessor :id, :weight_limit, :packings_limit, :items
|
101
|
-
attr_reader :packings
|
102
|
-
|
103
|
-
def initialize(id, dimensions, weight_limit, packings_limit=3)
|
104
|
-
super(dimensions.sort)
|
105
|
-
@id, @weight_limit, @packings_limit = id, weight_limit, packings_limit
|
106
|
-
@items, @packings = [], []
|
107
|
-
end
|
108
|
-
|
109
|
-
def pack(sorting_method = :sort_by_volume_into_approx_packings)
|
110
|
-
return if @items.empty? || !items_all_fit? || !items_all_light_enough?
|
111
|
-
|
112
|
-
@items_to_pack = self.send(sorting_method, @items).map(&:deep_copy)
|
113
|
-
@packings = []
|
114
|
-
|
115
|
-
until @items_to_pack.empty? || @packings.count >= @packings_limit do
|
116
|
-
@current_packing = Packing.new(@weight_limit, @volume)
|
117
|
-
|
118
|
-
pack_box(@items_to_pack.clone, self)
|
119
|
-
@packings << @current_packing
|
120
|
-
end
|
121
|
-
|
122
|
-
@items_to_pack.empty? ? @packings.count : nil
|
123
|
-
end
|
124
|
-
|
125
|
-
def to_s
|
126
|
-
s = "\n*** #{@id} - #{@dimensions.to_a} V:#{@volume} WL:#{@weight_limit} PL:#{@packings_limit} ***\n\n"
|
127
|
-
s += @items.map(&:to_s).join("\n")
|
128
|
-
@packings.each_with_index { |packing, i| s += "\n\nPacking #{i} RW:#{packing.remaining_weight} RV:#{packing.remaining_volume}\n#{packing.to_s}"}
|
129
|
-
s += "\n\n"
|
130
|
-
end
|
131
|
-
|
132
|
-
private
|
133
|
-
|
134
|
-
def items_all_fit?
|
135
|
-
@items.all? { |item| fit?(item) }
|
136
|
-
end
|
137
|
-
|
138
|
-
def items_all_light_enough?
|
139
|
-
@items.all? { |item| item.weight <= @weight_limit }
|
140
|
-
end
|
141
|
-
|
142
|
-
def pack_box(current_items, box_to_pack)
|
143
|
-
until current_items.empty?
|
144
|
-
item = current_items.pop
|
145
|
-
|
146
|
-
if box_to_pack.fit_with_rotation?(item)
|
147
|
-
add_item_to_packing(box_to_pack, item)
|
148
|
-
break if (current_items - [item]).empty?
|
149
|
-
|
150
|
-
sub_boxes = box_to_pack.break_up_remaining_space(item)
|
151
|
-
sub_boxes.sort_by!(&:volume).reverse!
|
152
|
-
sub_boxes.each do |box|
|
153
|
-
break if box.volume == 0
|
154
|
-
purge_items!(current_items)
|
155
|
-
pack_box(current_items.clone, box)
|
156
|
-
end
|
157
|
-
break
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def add_item_to_packing(box_to_pack, item_to_add)
|
163
|
-
item_to_add.position = box_to_pack.position
|
164
|
-
@items_to_pack.delete_if { |item| item == item_to_add }
|
165
|
-
@current_packing << item_to_add
|
166
|
-
end
|
167
|
-
|
168
|
-
def purge_items!(current_items)
|
169
|
-
current_items.delete_if { |item| @current_packing.include?(item) \
|
170
|
-
|| item.weight > @current_packing.remaining_weight \
|
171
|
-
|| item.volume > @current_packing.remaining_volume}
|
172
|
-
end
|
173
|
-
|
174
|
-
def sort_by_volume(items_to_sort)
|
175
|
-
items_to_sort.sort_by(&:volume)
|
176
|
-
end
|
177
|
-
|
178
|
-
def sort_by_shuffle(items_to_sort)
|
179
|
-
items_to_sort.shuffle
|
180
|
-
end
|
181
|
-
|
182
|
-
def sort_by_volume_into_approx_packings(items_to_sort)
|
183
|
-
split_into_approx_packings(items_to_sort.sort_by(&:volume))
|
184
|
-
end
|
185
|
-
|
186
|
-
def split_into_approx_packings(items_to_sort)
|
187
|
-
total_volume_of_items = items_to_sort.map(&:volume).reduce(:+)
|
188
|
-
approx_number_of_packings = (total_volume_of_items.to_f / @volume).ceil
|
189
|
-
approx_packing_size = (items_to_sort.count.to_f / approx_number_of_packings).ceil
|
190
|
-
indexs = (0..items_to_sort.count-1).each_slice(approx_number_of_packings).reduce(:zip).flatten.compact
|
191
|
-
return indexs.map{ |i| items_to_sort[i] }
|
192
|
-
end
|
193
|
-
|
194
|
-
end
|
195
|
-
|
196
|
-
end
|
1
|
+
require_relative 'box_packer/version'
|
2
|
+
require_relative 'box_packer/builder'
|
3
|
+
require_relative 'box_packer/container'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'attr_extras'
|
2
|
+
require 'forwardable'
|
3
|
+
require_relative 'position'
|
4
|
+
|
5
|
+
module BoxPacker
|
6
|
+
class Box
|
7
|
+
extend Forwardable
|
8
|
+
attr_initialize :dimensions, [:position]
|
9
|
+
def_delegators :dimensions, :volume, :each_rotation, :width, :height, :depth
|
10
|
+
attr_accessor :dimensions, :position
|
11
|
+
|
12
|
+
def position
|
13
|
+
@position ||= Position[0, 0, 0]
|
14
|
+
end
|
15
|
+
|
16
|
+
def orient!
|
17
|
+
@dimensions = Dimensions[*dimensions.to_a.sort!.reverse!]
|
18
|
+
end
|
19
|
+
|
20
|
+
def >=(other_box)
|
21
|
+
dimensions >= other_box.dimensions
|
22
|
+
end
|
23
|
+
|
24
|
+
def sub_boxes(item)
|
25
|
+
sub_boxes = sub_boxes_args(item).select{ |(d, p)| d.volume > 0 }
|
26
|
+
sub_boxes.map!{ |args| Box.new(*args) }
|
27
|
+
sub_boxes.sort_by!(&:volume).reverse!
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def sub_boxes_args(item)
|
33
|
+
[[ width + height + depth - item.width, position: position + item.width ],
|
34
|
+
[ item.width + height + depth - item.height, position: position + item.height ],
|
35
|
+
[ item.width + item.height + depth - item.depth, position: position + item.depth ]]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'container'
|
2
|
+
require_relative 'item'
|
3
|
+
|
4
|
+
module BoxPacker
|
5
|
+
|
6
|
+
def self.builder(&b)
|
7
|
+
b.call(Builder.new) if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
class Builder
|
11
|
+
|
12
|
+
def container(*args, &b)
|
13
|
+
Container.new(*args, &b)
|
14
|
+
end
|
15
|
+
|
16
|
+
def item(*args)
|
17
|
+
Item.new(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|