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