box_packer 1.1.2 → 1.2.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 +4 -4
- data/.gitignore +14 -1
- data/.rubocop.yml +4 -0
- data/README.md +57 -58
- data/Rakefile +1 -1
- data/box_packer.gemspec +9 -12
- data/lib/box_packer.rb +13 -3
- data/lib/box_packer/box.rb +29 -39
- data/lib/box_packer/builder.rb +14 -21
- data/lib/box_packer/container.rb +94 -107
- data/lib/box_packer/dimensions.rb +40 -42
- data/lib/box_packer/item.rb +27 -31
- data/lib/box_packer/packer.rb +52 -57
- data/lib/box_packer/packing.rb +30 -31
- data/lib/box_packer/position.rb +6 -8
- data/lib/box_packer/svg_exporter.rb +115 -119
- data/lib/box_packer/version.rb +1 -1
- data/spec/lib/box_spec.rb +51 -52
- data/spec/lib/container_spec.rb +88 -87
- data/spec/lib/dimensions_spec.rb +53 -51
- data/spec/lib/item_spec.rb +23 -26
- data/spec/lib/packer_spec.rb +69 -70
- data/spec/lib/packing_spec.rb +63 -60
- data/spec/spec_helper.rb +1 -14
- data/spec/support/matchers/yield_each_once.rb +38 -0
- metadata +27 -53
- data/spec/support/matchers/box_packer.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb419643a33b63ce8d279f9baafa0c2e4c010300
|
4
|
+
data.tar.gz: a5174fcc00bc4ccb58542182ac40a7aceef95f14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa82429a9a668dafdc268f9bc85d46261b9b9742a31099c87cb37870ce8cc6af3b8c358cc5babd5ebfd8e39549caa16344aef181737fdb33a1616b1696423cf4
|
7
|
+
data.tar.gz: 82af4730e2b2b13dc9868398a65b76f46561663ba0e158b606e8502aa3a4c569b9b42b7e22f83399420cbae3ad19368908c8fd0d01c4e3c036db594ed20e64f4
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
BoxPacker
|
2
2
|
=========
|
3
3
|
|
4
|
-
A heuristic first-fit 3D bin packing algorithm with optional weight and bin limits.
|
4
|
+
A heuristic first-fit 3D bin packing algorithm with optional weight and bin limits.
|
5
5
|
|
6
6
|
Installation
|
7
7
|
------------
|
@@ -24,44 +24,43 @@ Usage
|
|
24
24
|
``` ruby
|
25
25
|
require 'box_packer'
|
26
26
|
|
27
|
-
BoxPacker.container [3, 6, 7] do
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
32
|
+
|
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)
|
37
|
+
|
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
|
45
44
|
end
|
46
45
|
```
|
47
46
|
|
48
47
|
With optional labels, weights, quantity and packings limit:
|
49
48
|
|
50
49
|
``` ruby
|
51
|
-
BoxPacker.container [15, 20, 13], label: 'Parcel', weight_limit: 50, packings_limit: 3 do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
50
|
+
BoxPacker.container [15, 20, 13], label: 'Parcel', weight_limit: 50, packings_limit: 3 do
|
51
|
+
add_item [2, 3, 5], label: 'Shoes', weight: 47, quantity: 2
|
52
|
+
add_item [3, 3, 1], label: 'Watch', weight: 24
|
53
|
+
add_item [1, 1, 4], label: 'Bag', weight: 7
|
54
|
+
pack! # returns 2
|
55
|
+
|
56
|
+
puts self # |Container| Parcel 20x15x13 Weight Limit:50 Packings Limit:3
|
57
|
+
# | Packing| Remaining Volume:3870 Remaining Weight:3
|
58
|
+
# | Item| Shoes 5x3x2 (0,0,0) Volume:30 Weight:47
|
59
|
+
# | Packing| Remaining Volume:3870 Remaining Weight:3
|
60
|
+
# | Item| Shoes 5x3x2 (0,0,0) Volume:30 Weight:47
|
61
|
+
# | Packing| Remaining Volume:3887 Remaining Weight:19
|
62
|
+
# | Item| Watch 3x3x1 (0,0,0) Volume:9 Weight:24
|
63
|
+
# | Item| Bag 4x1x1 (3,0,0) Volume:4 Weight:7
|
65
64
|
end
|
66
65
|
```
|
67
66
|
|
@@ -69,27 +68,27 @@ Alternative builder API:
|
|
69
68
|
|
70
69
|
``` ruby
|
71
70
|
BoxPacker.builder do |b|
|
72
|
-
|
73
|
-
|
71
|
+
c1 = b.container [10,5,11]
|
72
|
+
c2 = b.container [17,23,14]
|
74
73
|
|
75
|
-
|
76
|
-
|
74
|
+
c1.items = [b.item([1,1,4]), b.item([4,6,7]), b.item([5,8,10])]
|
75
|
+
c2.items = c1.items
|
77
76
|
|
78
|
-
|
79
|
-
|
77
|
+
c1.pack! # 2
|
78
|
+
c2.pack! # 1
|
80
79
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
80
|
+
puts c1 # |Container| 11x10x5
|
81
|
+
# | Packing| Remaining Volume:146
|
82
|
+
# | Item| 10x8x5 (0,0,0) Volume:400
|
83
|
+
# | Item| 4x1x1 (0,8,0) Volume:4
|
84
|
+
# | Packing| Remaining Volume:382
|
85
|
+
# | Item| 7x6x4 (10,0,0) Volume:168
|
87
86
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
87
|
+
puts c2 # |Container| 23x17x14
|
88
|
+
# | Packing| Remaining Volume:4902
|
89
|
+
# | Item| 10x8x5 (0,0,0) Volume:400
|
90
|
+
# | Item| 7x6x4 (10,0,0) Volume:168
|
91
|
+
# | Item| 4x1x1 (17,0,0) Volume:4
|
93
92
|
|
94
93
|
end
|
95
94
|
```
|
@@ -98,14 +97,14 @@ Export SVG
|
|
98
97
|
----------
|
99
98
|
|
100
99
|
``` ruby
|
101
|
-
BoxPacker.container [3, 4, 2] do
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
100
|
+
BoxPacker.container [3, 4, 2] do
|
101
|
+
add_item [1,3,2], label: 'Bag', colour: 'red'
|
102
|
+
add_item [3,3,1], label: 'Hat', colour: 'blue'
|
103
|
+
add_item [1,2,2], label: 'Shoes', colour: 'green'
|
104
|
+
add_item [3,1,1], label: 'Slipper', colour: 'purple'
|
105
|
+
add_item [2,1,1], label: 'Dragon', colour: 'orange'
|
106
|
+
pack!
|
107
|
+
draw!('examples/example', scale_longest_side_to: 500, margin: 15)
|
109
108
|
end
|
110
109
|
```
|
111
110
|
|
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
data/box_packer.gemspec
CHANGED
@@ -7,21 +7,18 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.version = BoxPacker::VERSION
|
8
8
|
spec.authors = ['Max White']
|
9
9
|
spec.email = ['mushishi78@gmail.com']
|
10
|
-
spec.
|
11
|
-
|
12
|
-
spec.homepage = ''
|
10
|
+
spec.summary = 'Heuristic first-fit 3D bin-packing algorithm' \
|
11
|
+
'with optional weight and bin limits.'
|
12
|
+
spec.homepage = 'https://github.com/mushishi78/box_packer'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
|
15
|
-
spec.files = `git ls-files`.split(
|
16
|
-
spec.executables = spec.files.grep(
|
17
|
-
spec.test_files = spec.files.grep(
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)/)
|
18
18
|
spec.require_paths = ['lib']
|
19
19
|
|
20
|
-
spec.add_development_dependency '
|
21
|
-
spec.add_development_dependency 'rake'
|
22
|
-
spec.add_development_dependency 'rspec'
|
23
|
-
|
24
|
-
spec.add_dependency 'attr_extras', '~> 3.1.0'
|
25
|
-
spec.add_dependency 'rasem', '~> 0.6.0'
|
20
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
26
21
|
|
22
|
+
spec.add_dependency 'attire', '~> 0'
|
23
|
+
spec.add_dependency 'rasem', '~> 0.6'
|
27
24
|
end
|
data/lib/box_packer.rb
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'attire'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require 'box_packer/position'
|
5
|
+
require 'box_packer/dimensions'
|
6
|
+
require 'box_packer/box'
|
7
|
+
require 'box_packer/item'
|
8
|
+
require 'box_packer/packer'
|
9
|
+
require 'box_packer/packing'
|
10
|
+
require 'box_packer/svg_exporter'
|
11
|
+
require 'box_packer/container'
|
12
|
+
require 'box_packer/builder'
|
13
|
+
require 'box_packer/version'
|
data/lib/box_packer/box.rb
CHANGED
@@ -1,40 +1,30 @@
|
|
1
|
-
require 'attr_extras'
|
2
|
-
require 'forwardable'
|
3
|
-
require_relative 'position'
|
4
|
-
require_relative 'dimensions'
|
5
|
-
|
6
1
|
module BoxPacker
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
[ item.width + item.height + depth - item.depth, position: position + item.depth ]]
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
40
|
-
end
|
2
|
+
class Box
|
3
|
+
extend Forwardable
|
4
|
+
attr_init :dimensions, position: Position[0, 0, 0]
|
5
|
+
def_delegators :dimensions, :volume, :each_rotation, :width, :height, :depth
|
6
|
+
attr_accessor :dimensions, :position
|
7
|
+
|
8
|
+
def orient!
|
9
|
+
@dimensions = Dimensions[*dimensions.to_a.sort!.reverse!]
|
10
|
+
end
|
11
|
+
|
12
|
+
def >=(other)
|
13
|
+
dimensions >= other.dimensions
|
14
|
+
end
|
15
|
+
|
16
|
+
def sub_boxes(item)
|
17
|
+
sub_boxes = sub_boxes_args(item).select { |(d, _)| d.volume > 0 }
|
18
|
+
sub_boxes.map! { |args| Box.new(*args) }
|
19
|
+
sub_boxes.sort_by!(&:volume).reverse!
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def sub_boxes_args(item)
|
25
|
+
[[width + height + depth - item.width, position: position + item.width],
|
26
|
+
[item.width + height + depth - item.height, position: position + item.height],
|
27
|
+
[item.width + item.height + depth - item.depth, position: position + item.depth]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/box_packer/builder.rb
CHANGED
@@ -1,22 +1,15 @@
|
|
1
|
-
require_relative 'container'
|
2
|
-
require_relative 'item'
|
3
|
-
|
4
1
|
module BoxPacker
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
end
|
2
|
+
def self.builder(&b)
|
3
|
+
b.call(Builder.new) if block_given?
|
4
|
+
end
|
5
|
+
|
6
|
+
class Builder
|
7
|
+
def container(*args, &b)
|
8
|
+
Container.new(*args, &b)
|
9
|
+
end
|
10
|
+
|
11
|
+
def item(*args)
|
12
|
+
Item.new(*args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/box_packer/container.rb
CHANGED
@@ -1,108 +1,95 @@
|
|
1
|
-
require_relative 'box'
|
2
|
-
require_relative 'packing'
|
3
|
-
require_relative 'dimensions'
|
4
|
-
require_relative 'item'
|
5
|
-
require_relative 'packer'
|
6
|
-
require_relative 'svg_exporter'
|
7
|
-
|
8
1
|
module BoxPacker
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
@packings = []
|
104
|
-
@packed_successfully = false
|
105
|
-
end
|
106
|
-
|
107
|
-
end
|
108
|
-
end
|
2
|
+
def self.container(*args, &b)
|
3
|
+
Container.new(*args, &b)
|
4
|
+
end
|
5
|
+
|
6
|
+
class Container < Box
|
7
|
+
attr_accessor :label, :weight_limit, :packings_limit
|
8
|
+
attr_reader :items, :packing, :packings, :packed_successfully
|
9
|
+
|
10
|
+
def initialize(dimensions, opts = {}, &b)
|
11
|
+
super(Dimensions[*dimensions])
|
12
|
+
@label = opts[:label]
|
13
|
+
@weight_limit = opts[:weight_limit]
|
14
|
+
@packings_limit = opts[:packings_limit]
|
15
|
+
@items = opts[:items] || []
|
16
|
+
orient!
|
17
|
+
instance_exec(&b) if b
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_item(dimensions, opts = {})
|
21
|
+
quantity = opts.delete(:quantity) || 1
|
22
|
+
quantity.times do
|
23
|
+
items << Item.new(dimensions, opts)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def <<(item)
|
28
|
+
items << item.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
def items=(new_items)
|
32
|
+
@items = new_items.map(&:dup)
|
33
|
+
end
|
34
|
+
|
35
|
+
def pack!
|
36
|
+
prepare_to_pack!
|
37
|
+
return unless packable?
|
38
|
+
if @packed_successfully = Packer.pack(self)
|
39
|
+
packings.count
|
40
|
+
else
|
41
|
+
@packings = []
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def new_packing!
|
47
|
+
@packing = Packing.new(volume, weight_limit)
|
48
|
+
@packings << @packing
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
s = "\n|Container|"
|
53
|
+
s << " #{label}" if label
|
54
|
+
s << " #{dimensions}"
|
55
|
+
s << " Weight Limit:#{weight_limit}" if weight_limit
|
56
|
+
s << " Packings Limit:#{packings_limit}" if packings_limit
|
57
|
+
s << "\n"
|
58
|
+
s << packed_successfully ? packings.map(&:to_s).join : '| | Did Not Pack!'
|
59
|
+
end
|
60
|
+
|
61
|
+
def draw!(filename, opts = {})
|
62
|
+
exporter = SVGExporter.new(self, opts)
|
63
|
+
exporter.draw
|
64
|
+
exporter.save(filename)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def packable?
|
70
|
+
return false if items.empty?
|
71
|
+
total_weight = 0
|
72
|
+
|
73
|
+
items.each do |item|
|
74
|
+
if weight_limit && item.weight
|
75
|
+
return false if item.weight > weight_limit
|
76
|
+
total_weight += item.weight
|
77
|
+
end
|
78
|
+
|
79
|
+
return false unless self >= item
|
80
|
+
end
|
81
|
+
|
82
|
+
if weight_limit && packings_limit
|
83
|
+
return total_weight <= weight_limit * packings_limit
|
84
|
+
end
|
85
|
+
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
def prepare_to_pack!
|
90
|
+
items.each(&:orient!)
|
91
|
+
@packings = []
|
92
|
+
@packed_successfully = false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|