Passed
Push — 3.x ( ad39a9...3ac6d2 )
by Doug
10:59 queued 09:14
created

VolumePacker::correctLayerRotation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 10.1554

Importance

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