Passed
Pull Request — master (#294)
by
unknown
18:38 queued 14:21
created

VolumePacker::stabiliseLayers()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

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