Passed
Push — 3.x ( 05eab4...5fb647 )
by Doug
05:29 queued 03:46
created

VolumePacker::setSinglePassMode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
rs 10
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 82
    public function __construct(Box $box, ItemList $items)
76
    {
77 82
        $this->box = $box;
78 82
        $this->items = clone $items;
79
80 82
        $this->logger = new NullLogger();
81
82 82
        $this->hasConstrainedItems = $items->hasConstrainedItems();
83
84 82
        $this->layerPacker = new LayerPacker($this->box);
85 82
        $this->layerPacker->setLogger($this->logger);
86 82
    }
87
88
    /**
89
     * Sets a logger.
90
     */
91 22
    public function setLogger(LoggerInterface $logger): void
92
    {
93 22
        $this->logger = $logger;
94 22
        $this->layerPacker->setLogger($logger);
95 22
    }
96
97
    public function packAcrossWidthOnly(): void
98
    {
99
        $this->packAcrossWidthOnly = true;
100
    }
101
102
    /**
103
     * @internal
104
     */
105 26
    public function setSinglePassMode(bool $singlePassMode): void
106
    {
107 26
        $this->singlePassMode = $singlePassMode;
108 26
        if ($singlePassMode) {
109 4
            $this->packAcrossWidthOnly = true;
110
        }
111 26
        $this->layerPacker->setSinglePassMode($singlePassMode);
112 26
    }
113
114
    /**
115
     * Pack as many items as possible into specific given box.
116
     *
117
     * @return PackedBox packed box
118
     */
119 82
    public function pack(): PackedBox
120
    {
121 82
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
122
123 82
        $rotationsToTest = [false];
124 82
        if (!$this->packAcrossWidthOnly) {
125 82
            $rotationsToTest[] = true;
126
        }
127
128 82
        $boxPermutations = [];
129 82
        foreach ($rotationsToTest as $rotation) {
130 82
            if ($rotation) {
131 12
                $boxWidth = $this->box->getInnerLength();
132 12
                $boxLength = $this->box->getInnerWidth();
133
            } else {
134 82
                $boxWidth = $this->box->getInnerWidth();
135 82
                $boxLength = $this->box->getInnerLength();
136
            }
137
138 82
            $boxPermutation = $this->packRotation($boxWidth, $boxLength);
139 82
            if ($boxPermutation->getItems()->count() === $this->items->count()) {
140 74
                return $boxPermutation;
141
            }
142
143 14
            $boxPermutations[] = $boxPermutation;
144
        }
145
146 14
        usort($boxPermutations, static function (PackedBox $a, PackedBox $b) {
147 12
            return $b->getVolumeUtilisation() <=> $a->getVolumeUtilisation();
148 14
        });
149
150 14
        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 82
    private function packRotation(int $boxWidth, int $boxLength): PackedBox
159
    {
160 82
        $this->logger->debug("[EVALUATING ROTATION] {$this->box->getReference()}", ['width' => $boxWidth, 'length' => $boxLength]);
161
162
        /** @var PackedLayer[] $layers */
163 82
        $layers = [];
164 82
        $items = clone $this->items;
165
166 82
        while ($items->count() > 0) {
167 82
            $layerStartDepth = static::getCurrentPackedDepth($layers);
168 82
            $packedItemList = $this->getPackedItemList($layers);
169
170
            //do a preliminary layer pack to get the depth used
171 82
            $preliminaryItems = clone $items;
172 82
            $preliminaryLayer = $this->layerPacker->packLayer($preliminaryItems, clone $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, 0, true);
173 82
            if (count($preliminaryLayer->getItems()) === 0) {
174 10
                break;
175
            }
176
177 82
            if ($preliminaryLayer->getDepth() === $preliminaryLayer->getItems()[0]->getDepth()) { // preliminary === final
178 80
                $layers[] = $preliminaryLayer;
179 80
                $items = $preliminaryItems;
180
            } else { //redo with now-known-depth so that we can stack to that height from the first item
181 4
                $layers[] = $this->layerPacker->packLayer($items, $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayer->getDepth(), true);
182
            }
183
        }
184
185 82
        if (!$this->singlePassMode && $layers) {
186 82
            $layers = $this->stabiliseLayers($layers);
187
188
            // having packed layers, there may be tall, narrow gaps at the ends that can be utilised
189 82
            $maxLayerWidth = max(array_map(static function (PackedLayer $layer) {
190 82
                return $layer->getEndX();
191 82
            }, $layers));
192 82
            $layers[] = $this->layerPacker->packLayer($items, $this->getPackedItemList($layers), $maxLayerWidth, 0, 0, $boxWidth, $boxLength, $this->box->getInnerDepth(), $this->box->getInnerDepth(), false);
193
194 82
            $maxLayerLength = max(array_map(static function (PackedLayer $layer) {
195 82
                return $layer->getEndY();
196 82
            }, $layers));
197 82
            $layers[] = $this->layerPacker->packLayer($items, $this->getPackedItemList($layers), 0, $maxLayerLength, 0, $boxWidth, $boxLength, $this->box->getInnerDepth(), $this->box->getInnerDepth(), false);
198
        }
199
200 82
        $layers = $this->correctLayerRotation($layers, $boxWidth);
201
202 82
        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 82
    private function stabiliseLayers(array $oldLayers): array
215
    {
216 82
        if ($this->hasConstrainedItems) { // constraints include position, so cannot change
217
            return $oldLayers;
218
        }
219
220 82
        $stabiliser = new LayerStabiliser();
221
222 82
        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 82
    private function correctLayerRotation(array $oldLayers, int $boxWidth): array
231
    {
232 82
        if ($this->box->getInnerWidth() === $boxWidth) {
233 82
            return $oldLayers;
234
        }
235
236
        $newLayers = [];
237
        foreach ($oldLayers as $originalLayer) {
238
            $newLayer = new PackedLayer();
239
            foreach ($originalLayer->getItems() as $item) {
240
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
241
                $newLayer->insert($packedItem);
242
            }
243
            $newLayers[] = $newLayer;
244
        }
245
246
        return $newLayers;
247
    }
248
249
    /**
250
     * Generate a single list of items packed.
251
     * @param PackedLayer[] $layers
252
     */
253 82
    private function getPackedItemList(array $layers): PackedItemList
254
    {
255 82
        $packedItemList = new PackedItemList();
256 82
        foreach ($layers as $layer) {
257 82
            foreach ($layer->getItems() as $packedItem) {
258 82
                $packedItemList->insert($packedItem);
259
            }
260
        }
261
262 82
        return $packedItemList;
263
    }
264
265
    /**
266
     * Return the current packed depth.
267
     *
268
     * @param PackedLayer[] $layers
269
     */
270 82
    private static function getCurrentPackedDepth(array $layers): int
271
    {
272 82
        $depth = 0;
273 82
        foreach ($layers as $layer) {
274 54
            $depth += $layer->getDepth();
275
        }
276
277 82
        return $depth;
278
    }
279
}
280