box_packer 1.2.3 → 2.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 +4 -4
- data/LICENSE.txt +22 -22
- data/README.md +35 -100
- data/box_packer.rb +127 -0
- metadata +6 -30
- data/lib/box_packer.rb +0 -2
- data/lib/box_packer/box.rb +0 -39
- data/lib/box_packer/builder.rb +0 -18
- data/lib/box_packer/container.rb +0 -102
- data/lib/box_packer/dimensions.rb +0 -26
- data/lib/box_packer/item.rb +0 -34
- data/lib/box_packer/packer.rb +0 -63
- data/lib/box_packer/packing.rb +0 -39
- data/lib/box_packer/position.rb +0 -9
- data/lib/box_packer/svg_exporter.rb +0 -117
- data/lib/box_packer/vector.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b379d4407ae68ae0444cb2b5c2718f4ecdda9e5a
|
4
|
+
data.tar.gz: 883feddbe4f741e7d2f6cba5547635d467f54d80
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1711debc5d423788042c3b9ec7c9cde8e66431b4ddc18b1a31c85ee7bff0392787b097e2f225ab8924a5b5013a101a5229c1fdeae40e74d6c391dabd88e8c766
|
7
|
+
data.tar.gz: cd1147664c5a9d1091fa958a466f3f5d11a2c0daad15cf5f5aae186c140dc4ecfc683c2ecde4ac8c0ca7a218b5b165a9b42e4d9b2b5a2e639f86c3a11074eb78
|
data/LICENSE.txt
CHANGED
@@ -1,22 +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.
|
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
CHANGED
@@ -1,115 +1,50 @@
|
|
1
|
-
BoxPacker
|
2
|
-
=========
|
1
|
+
# BoxPacker
|
3
2
|
|
4
|
-
|
3
|
+
First fit heuristic algorithm for 3D bin-packing with weight limit.
|
5
4
|
|
6
|
-
|
5
|
+
## Version 2
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
For version 2 this entire solution has been rewritten with an emphasis on making a much simpler
|
8
|
+
and easier to understand implementation. However, I haven't gone to the trouble of reproducing
|
9
|
+
all the previous functionality, so if you still need that, it can be found here:
|
10
|
+
[1.2.4](https://github.com/mushishi78/box_packer/tree/1.2.4)
|
10
11
|
|
11
|
-
|
12
|
+
## Installation
|
12
13
|
|
13
|
-
|
14
|
-
gem install 'box_packer'
|
15
|
-
```
|
16
|
-
|
17
|
-
Or add to gemfile:
|
14
|
+
Add to gemfile:
|
18
15
|
|
19
16
|
``` ruby
|
20
17
|
gem 'box_packer'
|
21
18
|
```
|
22
19
|
|
23
|
-
Usage
|
24
|
-
-----
|
20
|
+
## Usage
|
25
21
|
|
26
22
|
``` ruby
|
27
23
|
require 'box_packer'
|
28
24
|
|
29
|
-
BoxPacker.
|
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
|
-
add_item [3, 3, 1], label: 'Watch', weight: 24
|
55
|
-
add_item [1, 1, 4], label: 'Bag', weight: 7
|
56
|
-
pack! # 3
|
57
|
-
|
58
|
-
puts self # |Container| Parcel 20x15x13 Weight Limit:50 Packings Limit:3
|
59
|
-
# | Packing| Remaining Volume:3870 Remaining Weight:3
|
60
|
-
# | Item| Shoes 5x3x2 (0,0,0) Volume:30 Weight:47
|
61
|
-
# | Packing| Remaining Volume:3870 Remaining Weight:3
|
62
|
-
# | Item| Shoes 5x3x2 (0,0,0) Volume:30 Weight:47
|
63
|
-
# | Packing| Remaining Volume:3887 Remaining Weight:19
|
64
|
-
# | Item| Watch 3x3x1 (0,0,0) Volume:9 Weight:24
|
65
|
-
# | Item| Bag 4x1x1 (3,0,0) Volume:4 Weight:7
|
66
|
-
end
|
67
|
-
```
|
68
|
-
|
69
|
-
Alternative builder API:
|
70
|
-
|
71
|
-
``` ruby
|
72
|
-
BoxPacker.builder do |b|
|
73
|
-
c1 = b.container [10,5,11]
|
74
|
-
c2 = b.container [17,23,14]
|
75
|
-
|
76
|
-
c1.items = [b.item([1,1,4]), b.item([4,6,7]), b.item([5,8,10])]
|
77
|
-
c2.items = c1.items
|
78
|
-
|
79
|
-
c1.pack! # 2
|
80
|
-
c2.pack! # 1
|
81
|
-
|
82
|
-
puts c1 # |Container| 11x10x5
|
83
|
-
# | Packing| Remaining Volume:146
|
84
|
-
# | Item| 10x8x5 (0,0,0) Volume:400
|
85
|
-
# | Item| 4x1x1 (0,8,0) Volume:4
|
86
|
-
# | Packing| Remaining Volume:382
|
87
|
-
# | Item| 7x6x4 (10,0,0) Volume:168
|
88
|
-
|
89
|
-
puts c2 # |Container| 23x17x14
|
90
|
-
# | Packing| Remaining Volume:4902
|
91
|
-
# | Item| 10x8x5 (0,0,0) Volume:400
|
92
|
-
# | Item| 7x6x4 (10,0,0) Volume:168
|
93
|
-
# | Item| 4x1x1 (17,0,0) Volume:4
|
94
|
-
|
95
|
-
end
|
25
|
+
packings = BoxPacker.pack(
|
26
|
+
container: { dimensions: [15, 20, 13], weight_limit: 50 },
|
27
|
+
items: [
|
28
|
+
{ dimensions: [2, 3, 5], weight: 47 },
|
29
|
+
{ dimensions: [2, 3, 5], weight: 47 },
|
30
|
+
{ dimensions: [3, 3, 1], weight: 24 },
|
31
|
+
{ dimensions: [1, 1, 4], weight: 7 },
|
32
|
+
]
|
33
|
+
)
|
34
|
+
|
35
|
+
packings.length # 3
|
36
|
+
packings[0][:weight] # 47
|
37
|
+
packings[0][:placements].length # 1
|
38
|
+
packings[0][:placements][0][:dimensions] # [5, 3, 2]
|
39
|
+
packings[0][:placements][0][:position] # [0, 0, 0]
|
40
|
+
packings[1][:weight] # 47
|
41
|
+
packings[1][:placements].length # 1
|
42
|
+
packings[1][:placements][0][:dimensions] # [5, 3, 2]
|
43
|
+
packings[1][:placements][0][:position] # [0, 0, 0]
|
44
|
+
packings[2][:weight] # 31
|
45
|
+
packings[2][:placements].length # 2
|
46
|
+
packings[2][:placements][0][:dimensions] # [3, 3, 1]
|
47
|
+
packings[2][:placements][0][:position] # [0, 0, 0]
|
48
|
+
packings[2][:placements][1][:dimensions] # [4, 1, 1]
|
49
|
+
packings[2][:placements][1][:position] # [3, 0, 0]
|
96
50
|
```
|
97
|
-
|
98
|
-
Export SVG
|
99
|
-
----------
|
100
|
-
|
101
|
-
``` ruby
|
102
|
-
BoxPacker.container [3, 4, 2] do
|
103
|
-
add_item [1,3,2], label: 'Bag', colour: 'red'
|
104
|
-
add_item [3,3,1], label: 'Hat', colour: 'blue'
|
105
|
-
add_item [1,2,2], label: 'Shoes', colour: 'green'
|
106
|
-
add_item [3,1,1], label: 'Slipper', colour: 'purple'
|
107
|
-
add_item [2,1,1], label: 'Dragon', colour: 'orange'
|
108
|
-
pack!
|
109
|
-
draw!('examples/example', scale_longest_side_to: 500, margin: 15)
|
110
|
-
end
|
111
|
-
```
|
112
|
-
|
113
|
-
Output:
|
114
|
-
|
115
|
-

|
data/box_packer.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
module BoxPacker
|
2
|
+
class << self
|
3
|
+
def pack(container:, items:)
|
4
|
+
packings = []
|
5
|
+
|
6
|
+
items.each do |item|
|
7
|
+
# If the item is just too big for the container lets give up on this
|
8
|
+
raise 'Item is too heavy for container' if item[:weight] > container[:weight_limit]
|
9
|
+
|
10
|
+
# Need a bool so we can break out nested loops once it's been packed
|
11
|
+
item_has_been_packed = false
|
12
|
+
|
13
|
+
packings.each do |packing|
|
14
|
+
# If this packings going to be too big with this
|
15
|
+
# item as well then skip on to the next packing
|
16
|
+
next if packing[:weight] + item[:weight] > container[:weight_limit]
|
17
|
+
|
18
|
+
packing[:spaces].each do |space|
|
19
|
+
# Try placing the item in this space,
|
20
|
+
# if it doesn't fit skip on the next space
|
21
|
+
next unless placement = place(item, space)
|
22
|
+
|
23
|
+
# Add the item to the packing and
|
24
|
+
# break up the surrounding spaces
|
25
|
+
packing[:placements] += [placement]
|
26
|
+
packing[:weight] += item[:weight]
|
27
|
+
packing[:spaces] -= [space]
|
28
|
+
packing[:spaces] += break_up_space(space, placement)
|
29
|
+
item_has_been_packed = true
|
30
|
+
break
|
31
|
+
end
|
32
|
+
break if item_has_been_packed
|
33
|
+
end
|
34
|
+
break if item_has_been_packed
|
35
|
+
|
36
|
+
# Can't fit in any of the spaces for the current packings
|
37
|
+
# so lets try a new space the size of the container
|
38
|
+
space = {
|
39
|
+
dimensions: container[:dimensions].sort.reverse,
|
40
|
+
position: [0, 0, 0]
|
41
|
+
}
|
42
|
+
placement = place(item, space)
|
43
|
+
|
44
|
+
# If it can't be placed in this space, then it's just
|
45
|
+
# too big for the container and we should abandon hope
|
46
|
+
raise 'Item cannot be placed in container' unless placement
|
47
|
+
|
48
|
+
# Otherwise lets put the item in a new packing
|
49
|
+
# and break up the remaing free space around it
|
50
|
+
packings += [{
|
51
|
+
placements: [placement],
|
52
|
+
weight: item[:weight],
|
53
|
+
spaces: break_up_space(space, placement)
|
54
|
+
}]
|
55
|
+
end
|
56
|
+
|
57
|
+
packings
|
58
|
+
end
|
59
|
+
|
60
|
+
def place(item, space)
|
61
|
+
item_width, item_height, item_length = item[:dimensions].sort.reverse
|
62
|
+
|
63
|
+
permutations = [
|
64
|
+
[item_width, item_height, item_length],
|
65
|
+
[item_width, item_length, item_height],
|
66
|
+
[item_height, item_width, item_length],
|
67
|
+
[item_height, item_length, item_width],
|
68
|
+
[item_length, item_width, item_height],
|
69
|
+
[item_length, item_height, item_width]
|
70
|
+
]
|
71
|
+
|
72
|
+
permutations.each do |perm|
|
73
|
+
# Skip if the item does not fit with this orientation
|
74
|
+
next unless perm[0] <= space[:dimensions][0] &&
|
75
|
+
perm[1] <= space[:dimensions][1] &&
|
76
|
+
perm[2] <= space[:dimensions][2]
|
77
|
+
|
78
|
+
return {
|
79
|
+
dimensions: perm,
|
80
|
+
position: space[:position],
|
81
|
+
weight: item[:weight]
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def break_up_space(space, placement)
|
87
|
+
[
|
88
|
+
{
|
89
|
+
dimensions: [
|
90
|
+
space[:dimensions][0],
|
91
|
+
space[:dimensions][1],
|
92
|
+
space[:dimensions][2] - placement[:dimensions][0]
|
93
|
+
],
|
94
|
+
position: [
|
95
|
+
space[:position][0] + placement[:dimensions][0],
|
96
|
+
space[:position][1],
|
97
|
+
space[:position][2]
|
98
|
+
]
|
99
|
+
},
|
100
|
+
{
|
101
|
+
dimensions: [
|
102
|
+
placement[:dimensions][0],
|
103
|
+
space[:dimensions][1],
|
104
|
+
space[:dimensions][2] - placement[:dimensions][1]
|
105
|
+
],
|
106
|
+
position: [
|
107
|
+
space[:position][0],
|
108
|
+
space[:position][1] + placement[:dimensions][1],
|
109
|
+
space[:position][2]
|
110
|
+
]
|
111
|
+
},
|
112
|
+
{
|
113
|
+
dimensions: [
|
114
|
+
placement[:dimensions][0],
|
115
|
+
placement[:dimensions][1],
|
116
|
+
space[:dimensions][2] - placement[:dimensions][2]
|
117
|
+
],
|
118
|
+
position: [
|
119
|
+
space[:position][0],
|
120
|
+
space[:position][1],
|
121
|
+
space[:position][2] + placement[:dimensions][2]
|
122
|
+
]
|
123
|
+
}
|
124
|
+
]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: box_packer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-11-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: rasem
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0.6'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0.6'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rspec
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,17 +38,7 @@ extra_rdoc_files: []
|
|
52
38
|
files:
|
53
39
|
- LICENSE.txt
|
54
40
|
- README.md
|
55
|
-
-
|
56
|
-
- lib/box_packer/box.rb
|
57
|
-
- lib/box_packer/builder.rb
|
58
|
-
- lib/box_packer/container.rb
|
59
|
-
- lib/box_packer/dimensions.rb
|
60
|
-
- lib/box_packer/item.rb
|
61
|
-
- lib/box_packer/packer.rb
|
62
|
-
- lib/box_packer/packing.rb
|
63
|
-
- lib/box_packer/position.rb
|
64
|
-
- lib/box_packer/svg_exporter.rb
|
65
|
-
- lib/box_packer/vector.rb
|
41
|
+
- box_packer.rb
|
66
42
|
homepage: https://github.com/mushishi78/box_packer
|
67
43
|
licenses:
|
68
44
|
- MIT
|
@@ -70,7 +46,7 @@ metadata: {}
|
|
70
46
|
post_install_message:
|
71
47
|
rdoc_options: []
|
72
48
|
require_paths:
|
73
|
-
-
|
49
|
+
- "."
|
74
50
|
required_ruby_version: !ruby/object:Gem::Requirement
|
75
51
|
requirements:
|
76
52
|
- - ">="
|
@@ -83,8 +59,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
59
|
version: '0'
|
84
60
|
requirements: []
|
85
61
|
rubyforge_project:
|
86
|
-
rubygems_version: 2.
|
62
|
+
rubygems_version: 2.6.0
|
87
63
|
signing_key:
|
88
64
|
specification_version: 4
|
89
|
-
summary: 3D bin-packing
|
65
|
+
summary: First fit heuristic algorithm for 3D bin-packing with weight limit.
|
90
66
|
test_files: []
|
data/lib/box_packer.rb
DELETED
data/lib/box_packer/box.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
require 'forwardable'
|
2
|
-
require_relative 'position'
|
3
|
-
require_relative 'dimensions'
|
4
|
-
|
5
|
-
module BoxPacker
|
6
|
-
class Box
|
7
|
-
extend Forwardable
|
8
|
-
|
9
|
-
def initialize(dimensions, opts = {})
|
10
|
-
@dimensions = dimensions
|
11
|
-
@position = opts[:position] || Position[0, 0, 0]
|
12
|
-
end
|
13
|
-
|
14
|
-
def_delegators :dimensions, :volume, :each_rotation, :width, :height, :depth
|
15
|
-
attr_accessor :dimensions, :position
|
16
|
-
|
17
|
-
def orient!
|
18
|
-
@dimensions = Dimensions[*dimensions.to_a.sort!.reverse!]
|
19
|
-
end
|
20
|
-
|
21
|
-
def >=(other)
|
22
|
-
dimensions >= other.dimensions
|
23
|
-
end
|
24
|
-
|
25
|
-
def sub_boxes(item)
|
26
|
-
sub_boxes = sub_boxes_args(item).select { |(d, _)| d.volume > 0 }
|
27
|
-
sub_boxes.map! { |args| Box.new(*args) }
|
28
|
-
sub_boxes.sort_by!(&:volume).reverse!
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def sub_boxes_args(item)
|
34
|
-
[[width + height + depth - item.width, position: position + item.width],
|
35
|
-
[item.width + height + depth - item.height, position: position + item.height],
|
36
|
-
[item.width + item.height + depth - item.depth, position: position + item.depth]]
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
data/lib/box_packer/builder.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require_relative 'container'
|
2
|
-
require_relative 'item'
|
3
|
-
|
4
|
-
module BoxPacker
|
5
|
-
def self.builder(&b)
|
6
|
-
b.call(Builder.new) if block_given?
|
7
|
-
end
|
8
|
-
|
9
|
-
class Builder
|
10
|
-
def container(*args, &b)
|
11
|
-
Container.new(*args, &b)
|
12
|
-
end
|
13
|
-
|
14
|
-
def item(*args)
|
15
|
-
Item.new(*args)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
data/lib/box_packer/container.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
require_relative 'dimensions'
|
2
|
-
require_relative 'item'
|
3
|
-
require_relative 'packer'
|
4
|
-
require_relative 'packing'
|
5
|
-
require_relative 'svg_exporter'
|
6
|
-
require_relative 'box'
|
7
|
-
|
8
|
-
module BoxPacker
|
9
|
-
def self.container(*args, &b)
|
10
|
-
Container.new(*args, &b)
|
11
|
-
end
|
12
|
-
|
13
|
-
class Container < Box
|
14
|
-
attr_accessor :label, :weight_limit, :packings_limit
|
15
|
-
attr_reader :items, :packing, :packings, :packed_successfully
|
16
|
-
|
17
|
-
def initialize(dimensions, opts = {}, &b)
|
18
|
-
super(Dimensions[*dimensions])
|
19
|
-
@label = opts[:label]
|
20
|
-
@weight_limit = opts[:weight_limit]
|
21
|
-
@packings_limit = opts[:packings_limit]
|
22
|
-
@items = opts[:items] || []
|
23
|
-
orient!
|
24
|
-
instance_exec(&b) if b
|
25
|
-
end
|
26
|
-
|
27
|
-
def add_item(dimensions, opts = {})
|
28
|
-
quantity = opts.delete(:quantity) || 1
|
29
|
-
quantity.times do
|
30
|
-
items << Item.new(dimensions, opts)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def <<(item)
|
35
|
-
items << item.dup
|
36
|
-
end
|
37
|
-
|
38
|
-
def items=(new_items)
|
39
|
-
@items = new_items.map(&:dup)
|
40
|
-
end
|
41
|
-
|
42
|
-
def pack!
|
43
|
-
prepare_to_pack!
|
44
|
-
return unless packable?
|
45
|
-
if @packed_successfully = Packer.pack(self)
|
46
|
-
packings.count
|
47
|
-
else
|
48
|
-
@packings = []
|
49
|
-
false
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def new_packing!
|
54
|
-
@packing = Packing.new(volume, weight_limit)
|
55
|
-
@packings << @packing
|
56
|
-
end
|
57
|
-
|
58
|
-
def to_s
|
59
|
-
s = "\n|Container|"
|
60
|
-
s << " #{label}" if label
|
61
|
-
s << " #{dimensions}"
|
62
|
-
s << " Weight Limit:#{weight_limit}" if weight_limit
|
63
|
-
s << " Packings Limit:#{packings_limit}" if packings_limit
|
64
|
-
s << "\n"
|
65
|
-
s << (packed_successfully ? packings.map(&:to_s).join : '| | Did Not Pack!')
|
66
|
-
end
|
67
|
-
|
68
|
-
def draw!(filename, opts = {})
|
69
|
-
exporter = SVGExporter.new(self, opts)
|
70
|
-
exporter.draw
|
71
|
-
exporter.save(filename)
|
72
|
-
end
|
73
|
-
|
74
|
-
private
|
75
|
-
|
76
|
-
def packable?
|
77
|
-
return false if items.empty?
|
78
|
-
total_weight = 0
|
79
|
-
|
80
|
-
items.each do |item|
|
81
|
-
if weight_limit && item.weight
|
82
|
-
return false if item.weight > weight_limit
|
83
|
-
total_weight += item.weight
|
84
|
-
end
|
85
|
-
|
86
|
-
return false unless self >= item
|
87
|
-
end
|
88
|
-
|
89
|
-
if weight_limit && packings_limit
|
90
|
-
return total_weight <= weight_limit * packings_limit
|
91
|
-
end
|
92
|
-
|
93
|
-
true
|
94
|
-
end
|
95
|
-
|
96
|
-
def prepare_to_pack!
|
97
|
-
items.each(&:orient!)
|
98
|
-
@packings = []
|
99
|
-
@packed_successfully = false
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require_relative 'vector'
|
2
|
-
|
3
|
-
module BoxPacker
|
4
|
-
class Dimensions < Vector
|
5
|
-
def volume
|
6
|
-
@volume ||= to_a.reduce(&:*)
|
7
|
-
end
|
8
|
-
|
9
|
-
def >=(other)
|
10
|
-
zip_map(other, :>=).reduce(&:&)
|
11
|
-
end
|
12
|
-
|
13
|
-
def each_rotation
|
14
|
-
yield Dimensions[x, y, z]
|
15
|
-
yield Dimensions[x, z, y]
|
16
|
-
yield Dimensions[z, x, y]
|
17
|
-
yield Dimensions[z, y, x]
|
18
|
-
yield Dimensions[y, x, z]
|
19
|
-
yield Dimensions[y, z, x]
|
20
|
-
end
|
21
|
-
|
22
|
-
def to_s
|
23
|
-
to_a.join('x')
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
data/lib/box_packer/item.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
require_relative 'box'
|
2
|
-
require_relative 'dimensions'
|
3
|
-
|
4
|
-
module BoxPacker
|
5
|
-
class Item < Box
|
6
|
-
attr_accessor :label, :weight
|
7
|
-
attr_reader :colour
|
8
|
-
|
9
|
-
def initialize(dimensions, opts = {})
|
10
|
-
super(Dimensions[*dimensions])
|
11
|
-
@label = opts[:label].to_s
|
12
|
-
@weight = opts[:weight]
|
13
|
-
@colour = opts[:colour] || '%06x' % (rand * 0xffffff)
|
14
|
-
end
|
15
|
-
|
16
|
-
def rotate_to_fit_into(box)
|
17
|
-
each_rotation do |rotation|
|
18
|
-
if box.dimensions >= rotation
|
19
|
-
@dimensions = rotation
|
20
|
-
return true
|
21
|
-
end
|
22
|
-
end
|
23
|
-
false
|
24
|
-
end
|
25
|
-
|
26
|
-
def to_s
|
27
|
-
s = '| Item|'
|
28
|
-
s << " #{label}" if label
|
29
|
-
s << " #{dimensions} #{position} Volume:#{volume}"
|
30
|
-
s << " Weight:#{weight}" if weight
|
31
|
-
s << "\n"
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/lib/box_packer/packer.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
module BoxPacker
|
2
|
-
class Packer
|
3
|
-
extend Forwardable
|
4
|
-
|
5
|
-
def self.pack(container)
|
6
|
-
new(container).pack
|
7
|
-
end
|
8
|
-
|
9
|
-
def initialize(container)
|
10
|
-
@container = container
|
11
|
-
end
|
12
|
-
|
13
|
-
def_delegators :container, :new_packing!, :packings_limit, :packings, :packing
|
14
|
-
|
15
|
-
def pack
|
16
|
-
@items = container.items.sort_by!(&:volume).reverse!
|
17
|
-
|
18
|
-
until too_many_packings?
|
19
|
-
new_packing!
|
20
|
-
pack_box(@items, container)
|
21
|
-
return true if @items.empty?
|
22
|
-
end
|
23
|
-
false
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
attr_reader :container
|
29
|
-
|
30
|
-
def too_many_packings?
|
31
|
-
packings.count >= packings_limit if packings_limit
|
32
|
-
end
|
33
|
-
|
34
|
-
def pack_box(possible_items, box)
|
35
|
-
possible_items = possible_items.dup
|
36
|
-
until possible_items.empty?
|
37
|
-
item = possible_items.shift
|
38
|
-
next unless item.rotate_to_fit_into(box)
|
39
|
-
|
40
|
-
pack_item!(item, possible_items, box)
|
41
|
-
break if possible_items.empty?
|
42
|
-
|
43
|
-
box.sub_boxes(item).each do |sub_box|
|
44
|
-
purge!(possible_items)
|
45
|
-
pack_box(possible_items, sub_box)
|
46
|
-
end
|
47
|
-
break
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def pack_item!(item, possible_items, box)
|
52
|
-
item.position = box.position
|
53
|
-
possible_items.delete(item)
|
54
|
-
packing << @items.delete(item)
|
55
|
-
end
|
56
|
-
|
57
|
-
def purge!(possible_items)
|
58
|
-
possible_items.keep_if do |item|
|
59
|
-
packing.fit?(item)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
data/lib/box_packer/packing.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
require 'delegate'
|
2
|
-
|
3
|
-
module BoxPacker
|
4
|
-
class Packing < SimpleDelegator
|
5
|
-
attr_reader :remaining_weight, :remaining_volume
|
6
|
-
|
7
|
-
def initialize(total_volume, total_weight)
|
8
|
-
super([])
|
9
|
-
@remaining_volume = total_volume
|
10
|
-
@remaining_weight = total_weight
|
11
|
-
end
|
12
|
-
|
13
|
-
def <<(item)
|
14
|
-
@remaining_volume -= item.volume
|
15
|
-
@remaining_weight -= item.weight if weight?(item)
|
16
|
-
super
|
17
|
-
end
|
18
|
-
|
19
|
-
def fit?(item)
|
20
|
-
return false if include?(item)
|
21
|
-
return false if remaining_volume < item.volume
|
22
|
-
return false if weight?(item) && remaining_weight < item.weight
|
23
|
-
true
|
24
|
-
end
|
25
|
-
|
26
|
-
def to_s
|
27
|
-
s = "| Packing| Remaining Volume:#{remaining_volume}"
|
28
|
-
s << " Remaining Weight:#{remaining_weight}" if remaining_weight
|
29
|
-
s << "\n"
|
30
|
-
s << map(&:to_s).join
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def weight?(item)
|
36
|
-
remaining_weight && item.weight
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
data/lib/box_packer/position.rb
DELETED
@@ -1,117 +0,0 @@
|
|
1
|
-
require 'rasem'
|
2
|
-
|
3
|
-
module BoxPacker
|
4
|
-
class SVGExporter
|
5
|
-
def initialize(container, opts = {})
|
6
|
-
@container = container
|
7
|
-
@images = []
|
8
|
-
@margin = opts[:margin] || 10
|
9
|
-
|
10
|
-
dimensions = container.dimensions.to_a
|
11
|
-
longest_side = dimensions.max
|
12
|
-
sides_total = dimensions.reduce(&:+)
|
13
|
-
scale_longest_side_to = opts[:scale_longest_side_to] || 400
|
14
|
-
|
15
|
-
@scale = scale_longest_side_to / longest_side.to_f
|
16
|
-
@image_width = (longest_side * scale * 2) + (margin * 3)
|
17
|
-
@image_height = (sides_total * scale) + (margin * 4)
|
18
|
-
end
|
19
|
-
|
20
|
-
def save(filename)
|
21
|
-
images.each_with_index do |image, i|
|
22
|
-
image.close
|
23
|
-
|
24
|
-
File.open("#{filename}#{i + 1}.svg", 'w') do |f|
|
25
|
-
f << image.output
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def draw
|
31
|
-
container.packings.each do |packing|
|
32
|
-
Face.reset(margin, scale, container.dimensions)
|
33
|
-
new_image
|
34
|
-
6.times do
|
35
|
-
face = Face.new(packing)
|
36
|
-
image.rectangle(*face.outline, stroke: 'black', stroke_width: 1, fill: 'white')
|
37
|
-
face.rectangles_and_labels.each do |h|
|
38
|
-
image.rectangle(*h[:rectangle])
|
39
|
-
image.text(*h[:label])
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
attr_reader :container, :scale, :margin, :images, :image, :image_width, :image_height
|
48
|
-
|
49
|
-
def new_image
|
50
|
-
@image = Rasem::SVGImage.new(image_width, image_height)
|
51
|
-
images << image
|
52
|
-
end
|
53
|
-
|
54
|
-
class Face
|
55
|
-
attr_reader :width, :height, :axes
|
56
|
-
|
57
|
-
def self.reset(margin, scale, container_dimensions)
|
58
|
-
@@coords_mapping = [0, 1, 2]
|
59
|
-
@@front = true
|
60
|
-
@@margin = margin
|
61
|
-
@@axes = [margin, margin]
|
62
|
-
@@scale = scale
|
63
|
-
@@container_dimensions = container_dimensions
|
64
|
-
end
|
65
|
-
|
66
|
-
def iterate_class_variables
|
67
|
-
if front
|
68
|
-
@@axes[0] = width + @@margin * 2
|
69
|
-
else
|
70
|
-
@@coords_mapping.rotate!
|
71
|
-
@@axes[0] = @@margin
|
72
|
-
@@axes[1] += height + @@margin
|
73
|
-
end
|
74
|
-
@@front = !@@front
|
75
|
-
end
|
76
|
-
|
77
|
-
def initialize(packing)
|
78
|
-
@i, @j, @k = @@coords_mapping.dup
|
79
|
-
@front = @@front
|
80
|
-
@axes = @@axes.dup
|
81
|
-
@width = @@container_dimensions[i] * @@scale
|
82
|
-
@height = @@container_dimensions[j] * @@scale
|
83
|
-
iterate_class_variables
|
84
|
-
@items = sorted_items(packing)
|
85
|
-
end
|
86
|
-
|
87
|
-
def outline
|
88
|
-
@axes + [width, height]
|
89
|
-
end
|
90
|
-
|
91
|
-
def rectangles_and_labels
|
92
|
-
items.map do |item|
|
93
|
-
x = axes[0] + item.position[i] * @@scale
|
94
|
-
y = axes[1] + item.position[j] * @@scale
|
95
|
-
width = item.dimensions[i] * @@scale
|
96
|
-
height = item.dimensions[j] * @@scale
|
97
|
-
label_x = x + width / 2 - item.label.length
|
98
|
-
label_y = y + height / 2
|
99
|
-
{
|
100
|
-
rectangle: [x, y, width, height, fill: item.colour],
|
101
|
-
label: [label_x, label_y, item.label]
|
102
|
-
}
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
private
|
107
|
-
|
108
|
-
attr_reader :packing, :i, :j, :k, :front, :items
|
109
|
-
|
110
|
-
def sorted_items(packing)
|
111
|
-
items = packing.sort_by { |i| i.position[k] }
|
112
|
-
items.reverse! unless front
|
113
|
-
items
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
data/lib/box_packer/vector.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
module BoxPacker
|
2
|
-
class Vector
|
3
|
-
def self.[](*args)
|
4
|
-
new(*args)
|
5
|
-
end
|
6
|
-
|
7
|
-
def initialize(x, y, z)
|
8
|
-
@x, @y, @z = x, y, z
|
9
|
-
end
|
10
|
-
|
11
|
-
attr_reader :x, :y, :z
|
12
|
-
|
13
|
-
def +(other)
|
14
|
-
self.class.new(*zip_map(other, :+))
|
15
|
-
end
|
16
|
-
|
17
|
-
def -(other)
|
18
|
-
self.class.new(*zip_map(other, :-))
|
19
|
-
end
|
20
|
-
|
21
|
-
def width
|
22
|
-
self.class.new(x, 0, 0)
|
23
|
-
end
|
24
|
-
|
25
|
-
def height
|
26
|
-
self.class.new(0, y, 0)
|
27
|
-
end
|
28
|
-
|
29
|
-
def depth
|
30
|
-
self.class.new(0, 0, z)
|
31
|
-
end
|
32
|
-
|
33
|
-
def to_a
|
34
|
-
[x, y, z]
|
35
|
-
end
|
36
|
-
|
37
|
-
def ==(other)
|
38
|
-
zip_map(other, :==).reduce(&:&)
|
39
|
-
end
|
40
|
-
|
41
|
-
def eql?(other)
|
42
|
-
zip_map(other, :eql?).reduce(&:&)
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def zip_map(other, method)
|
48
|
-
to_a.zip(other.to_a).map { |a, b| a.send(method, b) }
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|