box_packer 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 +15 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +1 -0
- data/box_packer.gemspec +23 -0
- data/lib/box_packer/version.rb +3 -0
- data/lib/box_packer.rb +196 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NzdjZDhlOTBiNmZmYjI3Zjc2ZDA2MTE2YWVlNWJiYWE0NmIwMjExMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
N2E1MDhjOTY1ZmQ5YjQ4M2M0MjUzMmY4ZTE4NzI5ZTNlOWEyYTY0OA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ODMxODdhNTE5OWQ4ZTNkNzFkZGJlYzY2ODJlYzU3OWMyNDQzNTBmNDAzYTlm
|
10
|
+
MjJkMWZhOGVmMjkyM2MxNzc3ZDJhZGQzODJkNjFmMTVlMDc0ZTVkNjI2YjE1
|
11
|
+
YzYwMjZhY2FkMGQ0YTZlZDU5Y2ZiYmU3MzVmOGYxNmU2OTRhZGQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NmZkNTgzYzA3ZWYyZTI2YjdjMGJjZjhkM2Y0YTYyMmRmYmNkZjI4ZGUwNGE1
|
14
|
+
YTFmMzhhYjcwZTZjNTBlZDVlMWZmNzliOThlNGM2OTlhN2ZiZjU5MjU4ZWE1
|
15
|
+
NmMxNTM3NmVhNjIwYzQ4NzkyOWM5ZDczZTBmZWJlZmNmNTBiMjc=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Max White
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
BoxPacker
|
2
|
+
=========
|
3
|
+
|
4
|
+
A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit.
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'box_packer'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Usage
|
18
|
+
-----
|
19
|
+
|
20
|
+
```Ruby
|
21
|
+
container = BoxPacker::Container.new("MyContainer", [6, 3, 7], 50)
|
22
|
+
|
23
|
+
container.items << BoxPacker::Item.new("MyItem01", [1, 5, 3], 10)
|
24
|
+
container.items << BoxPacker::Item.new("MyItem02", [2, 5, 2], 13)
|
25
|
+
container.items << BoxPacker::Item.new("MyItem03", [5, 5, 3], 36)
|
26
|
+
|
27
|
+
container.pack #=> 2
|
28
|
+
puts container
|
29
|
+
```
|
30
|
+
|
31
|
+
``` console
|
32
|
+
*** MyContainer - [3, 6, 7] V:126 WL:50 PL:3 ***
|
33
|
+
|
34
|
+
MyItem01 - [1, 3, 5] V:15 W:10
|
35
|
+
MyItem02 - [2, 2, 5] V:20 W:13
|
36
|
+
MyItem03 - [3, 5, 5] V:75 W:36
|
37
|
+
|
38
|
+
Packing 0 RW:1 RV:31
|
39
|
+
MyItem03 - [3, 5, 5] Pos:[0, 0, 0] V:75 W:36
|
40
|
+
MyItem02 - [2, 5, 2] Pos:[0, 0, 5] V:20 W:13
|
41
|
+
|
42
|
+
Packing 1 RW:40 RV:111
|
43
|
+
MyItem01 - [1, 3, 5] Pos:[0, 0, 0] V:15 W:10
|
44
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/box_packer.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'box_packer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "box_packer"
|
8
|
+
spec.version = BoxPacker::VERSION
|
9
|
+
spec.authors = ["Max White"]
|
10
|
+
spec.email = ["mushishi78@gmail.com"]
|
11
|
+
spec.description = %q{A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit}
|
12
|
+
spec.summary = %q{A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/box_packer.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
require_relative "box_packer/version"
|
2
|
+
require "matrix"
|
3
|
+
require "forwardable"
|
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
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: box_packer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Max White
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit
|
42
|
+
email:
|
43
|
+
- mushishi78@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- box_packer.gemspec
|
54
|
+
- lib/box_packer.rb
|
55
|
+
- lib/box_packer/version.rb
|
56
|
+
homepage: ''
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata: {}
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 2.2.1
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit
|
80
|
+
test_files: []
|