Passed
Push — 1.x ( 457fa0...308477 )
by Doug
03:11
created

VolumePacker::pack()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6.0038

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 2
b 0
f 0
nc 10
nop 0
dl 0
loc 35
ccs 20
cts 21
cp 0.9524
crap 6.0038
rs 8.9617
1
<?php
2
/**
3
 * Box packing (3D bin packing, knapsack problem).
4
 *
5
 * @author Doug Wright
6
 */
7
namespace DVDoug\BoxPacker;
8
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerInterface;
11
use Psr\Log\NullLogger;
12
13
/**
14
 * Actual packer.
15
 *
16
 * @author Doug Wright
17
 */
18
class VolumePacker implements LoggerAwareInterface
19
{
20
    /**
21
     * The logger instance.
22
     *
23
     * @var LoggerInterface
24
     */
25
    protected $logger;
26
27
    /**
28
     * Box to pack items into.
29
     *
30
     * @var Box
31
     */
32
    protected $box;
33
34
    /**
35
     * List of items to be packed.
36
     *
37
     * @var ItemList
38
     */
39
    protected $items;
40
41
    /**
42
     * Whether the packer is in single-pass mode.
43
     *
44
     * @var bool
45
     */
46
    protected $singlePassMode = false;
47
48
    /**
49
     * @var LayerPacker
50
     */
51
    private $layerPacker;
52
53
    /**
54
     * @var bool
55
     */
56
    private $hasConstrainedItems;
57
58
    /**
59
     * Constructor.
60
     */
61 42
    public function __construct(Box $box, ItemList $items)
62
    {
63 42
        $this->box = $box;
64 42
        $this->items = clone $items;
65
66 42
        $this->logger = new NullLogger();
67
68 42
        $this->hasConstrainedItems = $items->hasConstrainedItems();
69
70 42
        $this->layerPacker = new LayerPacker($this->box);
71 42
        $this->layerPacker->setLogger($this->logger);
72 42
    }
73
74
    /**
75
     * Sets a logger.
76
     */
77 18
    public function setLogger(LoggerInterface $logger)
78
    {
79 18
        $this->logger = $logger;
80 18
        $this->layerPacker->setLogger($logger);
81 18
    }
82
83
    /**
84
     * @internal
85
     */
86 37
    public function setSinglePassMode($singlePassMode)
87
    {
88 37
        $this->singlePassMode = $singlePassMode;
89 37
        $this->layerPacker->setSinglePassMode($singlePassMode);
90 37
    }
91
92
    /**
93
     * Pack as many items as possible into specific given box.
94
     *
95
     * @return PackedBox packed box
96
     */
97 42
    public function pack()
98
    {
99 42
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
100
101 42
        $rotationsToTest = [false];
102 42
        if (!$this->singlePassMode) {
103 42
            $rotationsToTest[] = true;
104
        }
105
106 42
        $boxPermutations = [];
107 42
        foreach ($rotationsToTest as $rotation) {
108 42
            if ($rotation) {
109 11
                $boxWidth = $this->box->getInnerLength();
110 11
                $boxLength = $this->box->getInnerWidth();
111
            } else {
112 42
                $boxWidth = $this->box->getInnerWidth();
113 42
                $boxLength = $this->box->getInnerLength();
114
            }
115
116 42
            $boxPermutation = $this->packRotation($boxWidth, $boxLength);
117 42
            if ($boxPermutation->getItems()->count() === $this->items->count()) {
118 40
                return $boxPermutation;
119
            }
120
121 28
            $boxPermutations[] = $boxPermutation;
122
        }
123
124
        usort($boxPermutations, static function (PackedBox $a, PackedBox $b) {
125 10
            if ($a->getVolumeUtilisation() < $b->getVolumeUtilisation()) {
126
                return 1;
127
            }
128 10
            return -1;
129 28
        });
130
131 28
        return reset($boxPermutations);
132
    }
133
134
    /**
135
     * Pack as many items as possible into specific given box.
136
     *
137
     * @return PackedBox packed box
138
     */
139 42
    private function packRotation($boxWidth, $boxLength)
140
    {
141 42
        $this->logger->debug("[EVALUATING ROTATION] {$this->box->getReference()}", ['width' => $boxWidth, 'length' => $boxLength]);
142
143
        /** @var PackedLayer[] $layers */
144 42
        $layers = [];
145 42
        $items = clone $this->items;
146
147 42
        while ($items->count() > 0) {
148 42
            $layerStartDepth = static::getCurrentPackedDepth($layers);
149 42
            $packedItemList = $this->getPackedItemList($layers);
150
151
            //do a preliminary layer pack to get the depth used
152 42
            $preliminaryItems = clone $items;
153 42
            $preliminaryLayer = $this->layerPacker->packLayer($preliminaryItems, clone $packedItemList, $layers, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, 0);
154 42
            if (count($preliminaryLayer->getItems()) === 0) {
155 27
                break;
156
            }
157
158 42
            if ($preliminaryLayer->getDepth() === $preliminaryLayer->getItems()[0]->getDepth()) { // preliminary === final
159 23
                $layers[] = $preliminaryLayer;
160 23
                $items = $preliminaryItems;
161
            } else { //redo with now-known-depth so that we can stack to that height from the first item
162 21
                $layers[] = $this->layerPacker->packLayer($items, $packedItemList, $layers, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayer->getDepth());
163
            }
164
        }
165
166 42
        $layers = $this->correctLayerRotation($layers, $boxWidth);
167 42
        $layers = $this->stabiliseLayers($layers);
168
169 42
        return PackedBox::fromPackedItemList($this->box, $this->getPackedItemList($layers));
170
    }
171
172
    /**
173
     * During packing, it is quite possible that layers have been created that aren't physically stable
174
     * i.e. they overhang the ones below.
175
     *
176
     * This function reorders them so that the ones with the greatest surface area are placed at the bottom
177
     *
178
     * @param  PackedLayer[] $oldLayers
179
     * @return PackedLayer[]
180
     */
181 42
    private function stabiliseLayers(array $oldLayers)
182
    {
183 42
        if ($this->singlePassMode || $this->hasConstrainedItems) { // constraints include position, so cannot change
184 32
            return $oldLayers;
185
        }
186
187 42
        $stabiliser = new LayerStabiliser();
188
189 42
        return $stabiliser->stabilise($oldLayers);
190
    }
191
192
    /**
193
     * Swap back width/length of the packed items to match orientation of the box if needed.
194
     *
195
     * @param PackedLayer[] $oldLayers
196
     */
197 42
    private function correctLayerRotation(array $oldLayers, $boxWidth)
198
    {
199 42
        if ($this->box->getInnerWidth() === $boxWidth) {
200 42
            return $oldLayers;
201
        }
202
203 4
        $newLayers = [];
204 4
        foreach ($oldLayers as $originalLayer) {
205 4
            $newLayer = new PackedLayer();
206 4
            foreach ($originalLayer->getItems() as $item) {
207 4
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
208 4
                $newLayer->insert($packedItem);
209
            }
210 4
            $newLayers[] = $newLayer;
211
        }
212
213 4
        return $newLayers;
214
    }
215
216
    /**
217
     * Generate a single list of items packed.
218
     * @param PackedLayer[] $layers
219
     */
220 42
    private function getPackedItemList(array $layers)
221
    {
222 42
        $packedItemList = new PackedItemList();
223 42
        foreach ($layers as $layer) {
224 42
            foreach ($layer->getItems() as $packedItem) {
225 42
                $packedItemList->insert($packedItem);
226
            }
227
        }
228
229 42
        return $packedItemList;
230
    }
231
232
    /**
233
     * Return the current packed depth.
234
     *
235
     * @param PackedLayer[] $layers
236
     */
237 42
    private static function getCurrentPackedDepth(array $layers)
238
    {
239 42
        $depth = 0;
240 42
        foreach ($layers as $layer) {
241 34
            $depth += $layer->getDepth();
242
        }
243
244 42
        return $depth;
245
    }
246
}
247