Completed
Push — refactor_layerpacker ( dfc43c )
by Doug
09:17
created

VolumePacker::isSameDimensions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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