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
@@ -1,45 +1,43 @@
|
|
1
1
|
require 'matrix'
|
2
2
|
|
3
3
|
module BoxPacker
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
end
|
4
|
+
class Dimensions < Vector
|
5
|
+
def +(other)
|
6
|
+
Dimensions[*super]
|
7
|
+
end
|
8
|
+
|
9
|
+
def -(other)
|
10
|
+
Dimensions[*super]
|
11
|
+
end
|
12
|
+
|
13
|
+
def width
|
14
|
+
Dimensions[self[0], 0, 0]
|
15
|
+
end
|
16
|
+
|
17
|
+
def height
|
18
|
+
Dimensions[0, self[1], 0]
|
19
|
+
end
|
20
|
+
|
21
|
+
def depth
|
22
|
+
Dimensions[0, 0, self[2]]
|
23
|
+
end
|
24
|
+
|
25
|
+
def volume
|
26
|
+
@volume ||= self[0] * self[1] * self[2]
|
27
|
+
end
|
28
|
+
|
29
|
+
def >=(other)
|
30
|
+
map2(other) { |v1, v2| v1 >= v2 }.reduce(&:&)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each_rotation
|
34
|
+
to_a.permutation.each do |perm|
|
35
|
+
yield Dimensions[*perm]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{self[0]}x#{self[1]}x#{self[2]}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/box_packer/item.rb
CHANGED
@@ -1,35 +1,31 @@
|
|
1
|
-
require_relative 'box'
|
2
|
-
require_relative 'dimensions'
|
3
|
-
|
4
1
|
module BoxPacker
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
class Item < Box
|
3
|
+
attr_accessor :label, :weight
|
4
|
+
attr_reader :colour
|
8
5
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
def initialize(dimensions, opts = {})
|
7
|
+
super(Dimensions[*dimensions])
|
8
|
+
@label = opts[:label].to_s
|
9
|
+
@weight = opts[:weight]
|
10
|
+
@colour = opts[:colour] || '%06x' % (rand * 0xffffff)
|
11
|
+
end
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
def fit_into?(box)
|
14
|
+
each_rotation do |rotation|
|
15
|
+
if box.dimensions >= rotation
|
16
|
+
@dimensions = rotation
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
false
|
21
|
+
end
|
25
22
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
23
|
+
def to_s
|
24
|
+
s = '| Item|'
|
25
|
+
s << " #{label}" if label
|
26
|
+
s << " #{dimensions} #{position} Volume:#{volume}"
|
27
|
+
s << " Weight:#{weight}" if weight
|
28
|
+
s << "\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/box_packer/packer.rb
CHANGED
@@ -1,58 +1,53 @@
|
|
1
|
-
require 'attr_extras'
|
2
|
-
require 'forwardable'
|
3
|
-
|
4
1
|
module BoxPacker
|
5
|
-
|
6
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
58
|
-
end
|
2
|
+
class Packer
|
3
|
+
extend Forwardable
|
4
|
+
attr_method :pack, :container
|
5
|
+
def_delegators :container, :new_packing!, :packings_limit, :packings, :packing
|
6
|
+
|
7
|
+
def pack
|
8
|
+
@items = container.items.sort_by!(&:volume).reverse!
|
9
|
+
|
10
|
+
until too_many_packings?
|
11
|
+
new_packing!
|
12
|
+
pack_box(@items, container)
|
13
|
+
return true if @items.empty?
|
14
|
+
end
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def too_many_packings?
|
21
|
+
packings.count >= packings_limit if packings_limit
|
22
|
+
end
|
23
|
+
|
24
|
+
def pack_box(possible_items, box)
|
25
|
+
possible_items = possible_items.dup
|
26
|
+
until possible_items.empty?
|
27
|
+
item = possible_items.shift
|
28
|
+
next unless item.fit_into?(box)
|
29
|
+
|
30
|
+
pack_item!(item, possible_items, box)
|
31
|
+
break if possible_items.empty?
|
32
|
+
|
33
|
+
box.sub_boxes(item).each do |sub_box|
|
34
|
+
purge!(possible_items)
|
35
|
+
pack_box(possible_items, sub_box)
|
36
|
+
end
|
37
|
+
break
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def pack_item!(item, possible_items, box)
|
42
|
+
item.position = box.position
|
43
|
+
possible_items.delete(item)
|
44
|
+
packing << @items.delete(item)
|
45
|
+
end
|
46
|
+
|
47
|
+
def purge!(possible_items)
|
48
|
+
possible_items.keep_if do |item|
|
49
|
+
packing.fit?(item)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/box_packer/packing.rb
CHANGED
@@ -1,40 +1,39 @@
|
|
1
1
|
require 'delegate'
|
2
2
|
|
3
3
|
module BoxPacker
|
4
|
-
|
5
|
-
|
4
|
+
class Packing < SimpleDelegator
|
5
|
+
attr_reader :remaining_weight, :remaining_volume
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
def initialize(total_volume, total_weight)
|
8
|
+
super([])
|
9
|
+
@remaining_volume = total_volume
|
10
|
+
@remaining_weight = total_weight
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
def <<(item)
|
14
|
+
@remaining_volume -= item.volume
|
15
|
+
@remaining_weight -= item.weight if weight?(item)
|
16
|
+
super
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
private
|
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
|
34
32
|
|
35
|
-
|
36
|
-
remaining_weight && item.weight
|
37
|
-
end
|
33
|
+
private
|
38
34
|
|
39
|
-
|
40
|
-
|
35
|
+
def weight?(item)
|
36
|
+
remaining_weight && item.weight
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/box_packer/position.rb
CHANGED
@@ -1,122 +1,118 @@
|
|
1
|
-
require 'attr_extras'
|
2
1
|
require 'rasem'
|
3
2
|
|
4
3
|
module BoxPacker
|
5
|
-
|
6
|
-
|
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
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
122
|
-
end
|
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
|
+
attr_query :front?
|
57
|
+
|
58
|
+
def self.reset(margin, scale, container_dimensions)
|
59
|
+
@@coords_mapping = [0, 1, 2]
|
60
|
+
@@front = true
|
61
|
+
@@margin = margin
|
62
|
+
@@axes = [margin, margin]
|
63
|
+
@@scale = scale
|
64
|
+
@@container_dimensions = container_dimensions
|
65
|
+
end
|
66
|
+
|
67
|
+
def iterate_class_variables
|
68
|
+
if front?
|
69
|
+
@@axes[0] = width + @@margin * 2
|
70
|
+
else
|
71
|
+
@@coords_mapping.rotate!
|
72
|
+
@@axes[0] = @@margin
|
73
|
+
@@axes[1] += height + @@margin
|
74
|
+
end
|
75
|
+
@@front = !@@front
|
76
|
+
end
|
77
|
+
|
78
|
+
def initialize(packing)
|
79
|
+
@i, @j, @k = @@coords_mapping.dup
|
80
|
+
@front = @@front
|
81
|
+
@axes = @@axes.dup
|
82
|
+
@width = @@container_dimensions[i] * @@scale
|
83
|
+
@height = @@container_dimensions[j] * @@scale
|
84
|
+
iterate_class_variables
|
85
|
+
@items = sorted_items(packing)
|
86
|
+
end
|
87
|
+
|
88
|
+
def outline
|
89
|
+
@axes + [width, height]
|
90
|
+
end
|
91
|
+
|
92
|
+
def rectangles_and_labels
|
93
|
+
items.map do |item|
|
94
|
+
x = axes[0] + item.position[i] * @@scale
|
95
|
+
y = axes[1] + item.position[j] * @@scale
|
96
|
+
width = item.dimensions[i] * @@scale
|
97
|
+
height = item.dimensions[j] * @@scale
|
98
|
+
label_x = x + width / 2 - item.label.length
|
99
|
+
label_y = y + height / 2
|
100
|
+
{
|
101
|
+
rectangle: [x, y, width, height, fill: item.colour],
|
102
|
+
label: [label_x, label_y, item.label]
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
attr_reader :packing, :i, :j, :k, :front, :items
|
110
|
+
|
111
|
+
def sorted_items(packing)
|
112
|
+
items = packing.sort_by { |i| i.position[k] }
|
113
|
+
items.reverse! unless front?
|
114
|
+
items
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|