Passed
Push — 3.x ( 3ac6d2...8ba8a4 )
by Doug
02:05
created

VolumePacker::correctLayerRotation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

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