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 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