box_packer 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MmU3YzNiYTFmYzA1YTM5MzFiYTc0NDA4MzYzODQ4YWQwZGM1OTI5Yg==
5
- data.tar.gz: !binary |-
6
- NjdhYzg2OGM1ZjRhZDBjNTBiOWVlYzkwNjE5NGFlMDRjZmIwNTIzYw==
2
+ SHA1:
3
+ metadata.gz: 9bd4f81789b4f8f8dc272b4893f2ce1aaf5bf174
4
+ data.tar.gz: 2cbf0f36b648dc9d90872f17a118347fe6ad1d9c
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- ZTNkNmJmZWI4ODI5YzE0MjY2ZTcyZjI2OWJkZWE4NjhjY2VmNDg3MDBkOTI4
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
@@ -1,17 +1 @@
1
1
  *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/README.md CHANGED
@@ -1,46 +1,93 @@
1
1
  BoxPacker
2
2
  =========
3
3
 
4
- A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit.
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
- Add this line to your application's Gemfile:
9
+ Install gem:
10
10
 
11
- gem 'box_packer'
11
+ ``` console
12
+ gem install 'box_packer'
13
+ ```
12
14
 
13
- And then execute:
15
+ Or add to gemfile:
14
16
 
15
- $ bundle
17
+ ``` ruby
18
+ gem 'box_packer'
19
+ ```
16
20
 
17
21
  Usage
18
22
  -----
19
23
 
20
- ```Ruby
21
- require "box_packer"
24
+ ``` ruby
25
+ require 'box_packer'
22
26
 
23
- container = BoxPacker::Container.new("MyContainer", [6, 3, 7], 50)
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
- container.items << BoxPacker::Item.new("MyItem01", [1, 5, 3], 10)
26
- container.items << BoxPacker::Item.new("MyItem02", [2, 5, 2], 13)
27
- container.items << BoxPacker::Item.new("MyItem03", [5, 5, 3], 36)
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
- container.pack #=> 2
30
- puts container
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
- ``` console
34
- *** MyContainer - [3, 6, 7] V:126 WL:50 PL:3 ***
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
- MyItem01 - [1, 3, 5] V:15 W:10
37
- MyItem02 - [2, 2, 5] V:20 W:13
38
- MyItem03 - [3, 5, 5] V:75 W:36
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
- Packing 0 RW:1 RV:31
41
- MyItem03 - [3, 5, 5] Pos:[0, 0, 0] V:75 W:36
42
- MyItem02 - [2, 5, 2] Pos:[0, 0, 5] V:20 W:13
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
- Packing 1 RW:40 RV:111
45
- MyItem01 - [1, 3, 5] Pos:[0, 0, 0] V:15 W:10
92
+ end
46
93
  ```
@@ -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 = "box_packer"
6
+ spec.name = 'box_packer'
8
7
  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"
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 = ["lib"]
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
@@ -1,196 +1,3 @@
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
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