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