Passed
Push — 3.x ( 922c95...5b6501 )
by Doug
12:25 queued 11:05
created

VolumePacker::getCurrentPackedDepth()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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