Passed
Push — 3.x ( 0d7474...922c95 )
by Doug
15:19 queued 14:00
created

VolumePacker   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 224
Duplicated Lines 0 %

Test Coverage

Coverage 90.12%

Importance

Changes 59
Bugs 2 Features 0
Metric Value
eloc 76
c 59
b 2
f 0
dl 0
loc 224
ccs 73
cts 81
cp 0.9012
rs 10
wmc 24

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setLogger() 0 4 1
A setSinglePassMode() 0 4 1
A packRotation() 0 31 4
A getPackedItemList() 0 10 3
A __construct() 0 11 1
A stabiliseLayers() 0 9 3
A getCurrentPackedDepth() 0 8 2
A pack() 0 32 5
A correctLayerRotation() 0 17 4
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 62
    public function __construct(Box $box, ItemList $items)
67
    {
68 62
        $this->box = $box;
69 62
        $this->items = clone $items;
70
71 62
        $this->logger = new NullLogger();
72
73 62
        $this->hasConstrainedItems = $items->hasConstrainedItems();
74
75 62
        $this->layerPacker = new LayerPacker($this->box);
76 62
        $this->layerPacker->setLogger($this->logger);
77 62
    }
78
79
    /**
80
     * Sets a logger.
81
     */
82 17
    public function setLogger(LoggerInterface $logger): void
83
    {
84 17
        $this->logger = $logger;
85 17
        $this->layerPacker->setLogger($logger);
86 17
    }
87
88
    /**
89
     * @internal
90
     */
91 41
    public function setSinglePassMode(bool $singlePassMode): void
92
    {
93 41
        $this->singlePassMode = $singlePassMode;
94 41
        $this->layerPacker->setSinglePassMode($singlePassMode);
95 41
    }
96
97
    /**
98
     * Pack as many items as possible into specific given box.
99
     *
100
     * @return PackedBox packed box
101
     */
102 62
    public function pack(): PackedBox
103
    {
104 62
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
105
106 62
        $rotationsToTest = [false];
107 62
        if (!$this->singlePassMode) {
108 62
            $rotationsToTest[] = true;
109
        }
110
111 62
        $boxPermutations = [];
112 62
        foreach ($rotationsToTest as $rotation) {
113 62
            if ($rotation) {
114 9
                $boxWidth = $this->box->getInnerLength();
115 9
                $boxLength = $this->box->getInnerWidth();
116
            } else {
117 62
                $boxWidth = $this->box->getInnerWidth();
118 62
                $boxLength = $this->box->getInnerLength();
119
            }
120
121 62
            $boxPermutation = $this->packRotation($boxWidth, $boxLength);
122 62
            if ($boxPermutation->getItems()->count() === $this->items->count()) {
123 56
                return $boxPermutation;
124
            }
125
126 35
            $boxPermutations[] = $boxPermutation;
127
        }
128
129 19
        usort($boxPermutations, static function (PackedBox $a, PackedBox $b) {
130 9
            return $b->getVolumeUtilisation() <=> $a->getVolumeUtilisation();
131 35
        });
132
133 35
        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 62
    private function packRotation(int $boxWidth, int $boxLength): PackedBox
142
    {
143 62
        $this->logger->debug("[EVALUATING ROTATION] {$this->box->getReference()}", ['width' => $boxWidth, 'length' => $boxLength]);
144
145
        /** @var PackedLayer[] $layers */
146 62
        $layers = [];
147 62
        $items = clone $this->items;
148
149 62
        while ($items->count() > 0) {
150 62
            $layerStartDepth = static::getCurrentPackedDepth($layers);
151 62
            $packedItemList = $this->getPackedItemList($layers);
152
153
            //do a preliminary layer pack to get the depth used
154 62
            $preliminaryItems = clone $items;
155 62
            $preliminaryLayer = $this->layerPacker->packLayer($preliminaryItems, clone $packedItemList, $layers, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, 0);
156 62
            if (count($preliminaryLayer->getItems()) === 0) {
157 34
                break;
158
            }
159
160 62
            if ($preliminaryLayer->getDepth() === $preliminaryLayer->getItems()[0]->getDepth()) { // preliminary === final
161 61
                $layers[] = $preliminaryLayer;
162 61
                $items = $preliminaryItems;
163
            } else { //redo with now-known-depth so that we can stack to that height from the first item
164 3
                $layers[] = $this->layerPacker->packLayer($items, $packedItemList, $layers, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayer->getDepth());
165
            }
166
        }
167
168 62
        $layers = $this->correctLayerRotation($layers, $boxWidth);
169 62
        $layers = $this->stabiliseLayers($layers);
170
171 62
        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 62
    private function stabiliseLayers(array $oldLayers): array
184
    {
185 62
        if ($this->singlePassMode || $this->hasConstrainedItems) { // constraints include position, so cannot change
186 34
            return $oldLayers;
187
        }
188
189 62
        $stabiliser = new LayerStabiliser();
190
191 62
        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 62
    private function correctLayerRotation(array $oldLayers, int $boxWidth): array
200
    {
201 62
        if ($this->box->getInnerWidth() === $boxWidth) {
202 62
            return $oldLayers;
203
        }
204
205
        $newLayers = [];
206
        foreach ($oldLayers as $originalLayer) {
207
            $newLayer = new PackedLayer();
208
            foreach ($originalLayer->getItems() as $item) {
209
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
210
                $newLayer->insert($packedItem);
211
            }
212
            $newLayers[] = $newLayer;
213
        }
214
215
        return $newLayers;
216
    }
217
218
    /**
219
     * Generate a single list of items packed.
220
     * @param PackedLayer[] $layers
221
     */
222 62
    private function getPackedItemList(array $layers): PackedItemList
223
    {
224 62
        $packedItemList = new PackedItemList();
225 62
        foreach ($layers as $layer) {
226 62
            foreach ($layer->getItems() as $packedItem) {
227 62
                $packedItemList->insert($packedItem);
228
            }
229
        }
230
231 62
        return $packedItemList;
232
    }
233
234
    /**
235
     * Return the current packed depth.
236
     *
237
     * @param PackedLayer[] $layers
238
     */
239 62
    private static function getCurrentPackedDepth(array $layers): int
240
    {
241 62
        $depth = 0;
242 62
        foreach ($layers as $layer) {
243 46
            $depth += $layer->getDepth();
244
        }
245
246 62
        return $depth;
247
    }
248
}
249