boxify 0.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 +7 -0
- data/lib/boxify.rb +8 -0
- data/lib/boxify/box.rb +90 -0
- data/lib/boxify/container.rb +21 -0
- data/lib/boxify/dimensionable.rb +17 -0
- data/lib/boxify/eligible_box.rb +51 -0
- data/lib/boxify/pack.rb +99 -0
- data/lib/boxify/placed_box.rb +31 -0
- data/lib/boxify/space.rb +41 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: cbe525b48eb75a67ace2f93c09a24cb5b3e35065
|
|
4
|
+
data.tar.gz: 9392c2eb883e591352cecce4abe5a44ab217f366
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 032807b6118f66d235f55adbed4b26bdb47c04ef1d982a8af34e8b931c9fef961e9df1de9ddefeef68e3f899cdcdd96273c7617dca0a1218b6ab01b283efad72
|
|
7
|
+
data.tar.gz: 6a3a8c7f69e04b7d081ee76baf0ecf262d73553cce602b7ce4526330f5c9ec9827dffbe71dae399242092b4a0ab79db304013a1df5adb9d7bb2806934a02d997
|
data/lib/boxify.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
require_relative 'boxify/dimensionable'
|
|
3
|
+
require_relative 'boxify/container'
|
|
4
|
+
require_relative 'boxify/box'
|
|
5
|
+
require_relative 'boxify/space'
|
|
6
|
+
require_relative 'boxify/eligible_box'
|
|
7
|
+
require_relative 'boxify/placed_box'
|
|
8
|
+
require_relative 'boxify/pack'
|
data/lib/boxify/box.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
class Box
|
|
3
|
+
include Dimensionable
|
|
4
|
+
|
|
5
|
+
attr_accessor :total_count
|
|
6
|
+
|
|
7
|
+
def initialize(width:, depth:, height:, total_count: 1)
|
|
8
|
+
@width = width
|
|
9
|
+
@depth = depth
|
|
10
|
+
@height = height
|
|
11
|
+
@total_count = total_count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rotated
|
|
15
|
+
@rotated ||= Box.new(width: height, depth: depth, height: width)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rotate
|
|
19
|
+
temp_width = width
|
|
20
|
+
@width = height
|
|
21
|
+
@height = temp_width
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class BoxCollection
|
|
26
|
+
|
|
27
|
+
attr_reader :boxes
|
|
28
|
+
|
|
29
|
+
def initialize(boxes: boxes)
|
|
30
|
+
@boxes = boxes
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unplaced
|
|
34
|
+
boxes.select{ |b| b.total_count > 0 }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete(box)
|
|
38
|
+
return if box.total_count == 0
|
|
39
|
+
box.total_count -= 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def total_count
|
|
43
|
+
boxes.map(&:total_count).inject(:+)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def flattened_dimensions
|
|
47
|
+
@flattened_dimensions ||= boxes.map { |b| [b.width, b.height, b.depth]}.flatten.uniq.sort
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def longest_edge
|
|
51
|
+
@longest_edge ||= flattened_dimensions.max
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def second_longest_edge
|
|
55
|
+
@second_longest_edge ||= flattened_dimensions[-2]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def box_with_widest_surface_area
|
|
59
|
+
boxes_sorted_by_surface_area.last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def box_with_minimum_height
|
|
63
|
+
boxes_sorted_by_height.first
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def more_than_one_box_with_widest_surface_area?
|
|
67
|
+
number_of_boxes = unplaced.select {|b| b.surface_area == box_with_widest_surface_area.surface_area }.count
|
|
68
|
+
number_of_boxes > 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def find_eligible_boxes(space)
|
|
72
|
+
unplaced.select{ |b| (b.width <= space.width) && (b.height <= space.height) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Find biggest (widest surface) box with minimum height
|
|
76
|
+
def find_biggest_box_with_minimum_height
|
|
77
|
+
more_than_one_box_with_widest_surface_area? ? box_with_minimum_height : box_with_widest_surface_area
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def boxes_sorted_by_surface_area
|
|
83
|
+
unplaced.sort { |x,y| x.surface_area <=> y.surface_area }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def boxes_sorted_by_height
|
|
87
|
+
unplaced.sort { |x,y| x.height <=> y.height }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
class Container
|
|
3
|
+
include Dimensionable
|
|
4
|
+
|
|
5
|
+
attr_accessor :placed_boxes
|
|
6
|
+
|
|
7
|
+
def initialize(width:, depth:, height:)
|
|
8
|
+
@width = width
|
|
9
|
+
@depth = depth
|
|
10
|
+
@height = height
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def volume_of_placed_boxes
|
|
14
|
+
placed_boxes.volume
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def wasted_space_percentage
|
|
18
|
+
(volume - volume_of_placed_boxes).to_f / volume * 100
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
module Dimensionable
|
|
3
|
+
attr_accessor :width, :depth, :height
|
|
4
|
+
|
|
5
|
+
def area
|
|
6
|
+
width * depth
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def volume
|
|
10
|
+
width * height * depth
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def surface_area
|
|
14
|
+
(2 * height * width) + (2 * height * depth) + (2 * width * depth)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
class EligibleBox
|
|
3
|
+
|
|
4
|
+
attr_reader :boxes, :space
|
|
5
|
+
|
|
6
|
+
def initialize(boxes:, space:)
|
|
7
|
+
@boxes = boxes
|
|
8
|
+
@space = space
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.find_all(boxes:, space:)
|
|
12
|
+
new(boxes: boxes, space: space).eligible_boxes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.find_best_fit(boxes:, space:)
|
|
16
|
+
new(boxes: boxes, space: space).eligible_box_with_largest_volume
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def eligible_boxes
|
|
20
|
+
@eligible_boxes ||= find_eligible_boxes
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def eligible_box_with_largest_volume
|
|
24
|
+
@eligible_box_with_largest_volume ||= find_box_with_largest_volume(eligible_boxes)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def find_eligible_boxes
|
|
30
|
+
da_boxes = boxes.select do |box|
|
|
31
|
+
true if (box.width <= space.width) && (box.height <= space.height)
|
|
32
|
+
|
|
33
|
+
rotated_copy = box.rotated
|
|
34
|
+
if (rotated_copy.width <= space.width) && (rotated_copy.height <= space.height)
|
|
35
|
+
box.rotate
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
da_boxes
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_box_with_largest_volume(boxes)
|
|
43
|
+
if boxes.size > 1
|
|
44
|
+
# Return box with largest volume
|
|
45
|
+
boxes.sort{ |b| b.volume }.first
|
|
46
|
+
else
|
|
47
|
+
boxes.first
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/boxify/pack.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
class Pack
|
|
3
|
+
extend Forwardable
|
|
4
|
+
|
|
5
|
+
STARTING_LEVEL = 0
|
|
6
|
+
|
|
7
|
+
attr_reader :boxes, :placed_boxes, :level, :container, :volume_of_placed_boxes
|
|
8
|
+
|
|
9
|
+
def_delegators :@container, :volume_of_placed_boxes, :wasted_space_percentage
|
|
10
|
+
def_delegator :@container, :height, :container_height
|
|
11
|
+
def_delegator :@container, :depth, :container_depth
|
|
12
|
+
def_delegator :@container, :width, :container_width
|
|
13
|
+
def_delegator :@container, :volume, :container_volume
|
|
14
|
+
|
|
15
|
+
def initialize(boxes:)
|
|
16
|
+
@level = STARTING_LEVEL
|
|
17
|
+
@boxes = boxes
|
|
18
|
+
@placed_boxes = PlacedBoxCollection.new
|
|
19
|
+
@container = Container.new(width: boxes.longest_edge, depth: boxes.second_longest_edge, height: 0)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def pack
|
|
23
|
+
pack_level
|
|
24
|
+
container.placed_boxes = placed_boxes
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pack_level
|
|
29
|
+
|
|
30
|
+
# Increment level of box
|
|
31
|
+
increment_level
|
|
32
|
+
|
|
33
|
+
# Get biggest box as object
|
|
34
|
+
box = boxes.find_biggest_box_with_minimum_height
|
|
35
|
+
|
|
36
|
+
# Set container height (ck = ck + ci)
|
|
37
|
+
increment_height(box.height)
|
|
38
|
+
|
|
39
|
+
# Remove box from array (ki = ki - 1)
|
|
40
|
+
pack_box(box)
|
|
41
|
+
|
|
42
|
+
# Terminate if all boxes have been packed
|
|
43
|
+
return true if boxes.total_count == 0
|
|
44
|
+
|
|
45
|
+
# No space left (not even when rotated / length and width swapped)
|
|
46
|
+
if container.area - box.area <= 0
|
|
47
|
+
pack_level
|
|
48
|
+
else # Space left, check if a package fits in
|
|
49
|
+
space = Space.new(width: container.width, depth: container.depth, height: container.height)
|
|
50
|
+
spaces = SpaceCollection.find_spaces_within_space(space: space, box: box)
|
|
51
|
+
|
|
52
|
+
# Fill each space with boxes
|
|
53
|
+
spaces.each do |space|
|
|
54
|
+
fill_space(space)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
pack_level if boxes.total_count > 0
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Fills space with boxes recursively
|
|
63
|
+
def fill_space(space)
|
|
64
|
+
# Find box that fits into this space
|
|
65
|
+
eligible_box = find_eligible_box(space)
|
|
66
|
+
|
|
67
|
+
if eligible_box
|
|
68
|
+
pack_box(eligible_box)
|
|
69
|
+
|
|
70
|
+
spaces = SpaceCollection.find_spaces_within_space(space: space, box: eligible_box)
|
|
71
|
+
|
|
72
|
+
# Fill each space with boxes
|
|
73
|
+
spaces.each do |space|
|
|
74
|
+
fill_space(space)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Set container height (ck = ck + ci)
|
|
82
|
+
def increment_height(height)
|
|
83
|
+
@container.height += height
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def increment_level
|
|
87
|
+
@level += 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def pack_box(box)
|
|
91
|
+
placed_boxes.add(box: box, level: level)
|
|
92
|
+
boxes.delete(box)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def find_eligible_box(space)
|
|
96
|
+
EligibleBox.find_best_fit(boxes: boxes.unplaced, space: space)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
class PlacedBox
|
|
3
|
+
extend Forwardable
|
|
4
|
+
|
|
5
|
+
attr_reader :box, :level
|
|
6
|
+
|
|
7
|
+
def_delegators :@box, :volume
|
|
8
|
+
|
|
9
|
+
def initialize(box:, level:)
|
|
10
|
+
@box = box
|
|
11
|
+
@level = level
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class PlacedBoxCollection
|
|
16
|
+
|
|
17
|
+
attr_accessor :placed_boxes
|
|
18
|
+
|
|
19
|
+
def initialize(placed_boxes: [])
|
|
20
|
+
@placed_boxes = placed_boxes
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add(box:, level:)
|
|
24
|
+
@placed_boxes.push(PlacedBox.new(box: box, level: level))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def volume
|
|
28
|
+
placed_boxes.map(&:volume).inject(:+)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/boxify/space.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Boxify
|
|
2
|
+
class Space
|
|
3
|
+
include Dimensionable
|
|
4
|
+
|
|
5
|
+
def initialize(width:, depth:, height:)
|
|
6
|
+
@width = width
|
|
7
|
+
@depth = depth
|
|
8
|
+
@height = height
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class SpaceCollection
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
15
|
+
attr_reader :spaces
|
|
16
|
+
|
|
17
|
+
def_delegators :@spaces, :each
|
|
18
|
+
|
|
19
|
+
def initialize(spaces: spaces)
|
|
20
|
+
@spaces = spaces
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.find_spaces_within_space(space:, box:)
|
|
24
|
+
spaces = []
|
|
25
|
+
|
|
26
|
+
if space.depth - box.depth > 0
|
|
27
|
+
spaces.push(Space.new(depth: space.depth - box.depth,
|
|
28
|
+
width: space.width,
|
|
29
|
+
height: box.height))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if space.width - box.width > 0
|
|
33
|
+
spaces.push(Space.new(depth: box.depth,
|
|
34
|
+
width: space.width - box.width,
|
|
35
|
+
height: box.height))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
new(spaces: spaces)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: boxify
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Don Pottinger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2014-12-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby implementation of a Largest Area Fit First (LAFF) for 3D Rectangular
|
|
14
|
+
Box Packing algorithm
|
|
15
|
+
email: don.pottinger@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/boxify.rb
|
|
21
|
+
- lib/boxify/box.rb
|
|
22
|
+
- lib/boxify/container.rb
|
|
23
|
+
- lib/boxify/dimensionable.rb
|
|
24
|
+
- lib/boxify/eligible_box.rb
|
|
25
|
+
- lib/boxify/pack.rb
|
|
26
|
+
- lib/boxify/placed_box.rb
|
|
27
|
+
- lib/boxify/space.rb
|
|
28
|
+
homepage: https://github.com/strukturedkaos/boxify
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata: {}
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubyforge_project:
|
|
48
|
+
rubygems_version: 2.2.2
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Ruby implementation of a Largest Area Fit First (LAFF) for 3D Rectangular
|
|
52
|
+
Box Packing algorithm
|
|
53
|
+
test_files: []
|