Passed
Push — master ( 73ddcb...629397 )
by Doug
02:34
created

VolumePacker   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 59
Bugs 2 Features 0
Metric Value
wmc 25
eloc 78
c 59
b 2
f 0
dl 0
loc 230
ccs 82
cts 82
cp 1
rs 10

9 Methods

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