Passed
Push — master ( 2e0db4...02b4d3 )
by Doug
02:32
created

LayerPacker::packLayer()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 82
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 13

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 13
eloc 52
c 5
b 0
f 0
nc 10
nop 10
dl 0
loc 82
ccs 53
cts 53
cp 1
crap 13
rs 6.6166

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\NullLogger;
14
15
use function array_merge;
16
use function iterator_to_array;
17
use function max;
18
use function sort;
19
20
/**
21
 * Layer packer.
22
 * @internal
23
 */
24
class LayerPacker implements LoggerAwareInterface
25
{
26
    private LoggerInterface $logger;
27
28
    private bool $singlePassMode = false;
29
30
    private readonly OrientatedItemFactory $orientatedItemFactory;
31
32
    private bool $beStrictAboutItemOrdering = false;
33
34
    private bool $isBoxRotated = false;
35
36 92
    public function __construct(private readonly Box $box)
37
    {
38 92
        $this->logger = new NullLogger();
39
40 92
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
0 ignored issues
show
Bug introduced by
The property orientatedItemFactory is declared read-only in DVDoug\BoxPacker\LayerPacker.
Loading history...
41 92
        $this->orientatedItemFactory->setLogger($this->logger);
42
    }
43
44
    /**
45
     * Sets a logger.
46
     */
47 92
    public function setLogger(LoggerInterface $logger): void
48
    {
49 92
        $this->logger = $logger;
50 92
        $this->orientatedItemFactory->setLogger($logger);
51
    }
52
53 22
    public function setSinglePassMode(bool $singlePassMode): void
54
    {
55 22
        $this->singlePassMode = $singlePassMode;
56 22
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
57
    }
58
59 92
    public function setBoxIsRotated(bool $boxIsRotated): void
60
    {
61 92
        $this->isBoxRotated = $boxIsRotated;
62 92
        $this->orientatedItemFactory->setBoxIsRotated($boxIsRotated);
63
    }
64
65 39
    public function beStrictAboutItemOrdering(bool $beStrict): void
66
    {
67 39
        $this->beStrictAboutItemOrdering = $beStrict;
68
    }
69
70
    /**
71
     * Pack items into an individual vertical layer.
72
     */
73 92
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
0 ignored issues
show
Bug introduced by
The type DVDoug\BoxPacker\PackedItemList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
74
    {
75 92
        $layer = new PackedLayer();
76 92
        $x = $startX;
77 92
        $y = $startY;
78 92
        $z = $startZ;
79 92
        $rowLength = 0;
80 92
        $prevItem = null;
81 92
        $skippedItems = [];
82 92
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
83
84 92
        while ($items->count() > 0) {
85 92
            $itemToPack = $items->extract();
86
87
            // skip items that will never fit e.g. too heavy
88 92
            if ($itemToPack->getWeight() > $remainingWeightAllowed) {
89 18
                continue;
90
            }
91
92 90
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
93
94 90
            if ($orientatedItem instanceof OrientatedItem) {
95 89
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
96 89
                $layer->insert($packedItem);
97 89
                $packedItemList->insert($packedItem);
98
99 89
                $rowLength = max($rowLength, $packedItem->getLength());
100 89
                $prevItem = $orientatedItem;
101
102
                // Figure out if we can stack items on top of this rather than side by side
103
                // e.g. when we've packed a tall item, and have just put a shorter one next to it.
104 89
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
105 89
                if ($stackableDepth > 0) {
106 30
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->getDepth(), $x + $packedItem->getWidth(), $y + $packedItem->getLength(), $stackableDepth, $stackableDepth, $considerStability);
107 30
                    $layer->merge($stackedLayer);
108
                }
109
110 89
                $x += $packedItem->getWidth();
111 89
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
112
113 89
                if ($items->count() === 0 && $skippedItems) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $skippedItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
114 16
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
115 16
                    $skippedItems = [];
116
                }
117 89
                continue;
118
            }
119
120 81
            if (!$this->beStrictAboutItemOrdering && $items->count() > 0) { // skip for now, move on to the next item
121 70
                $this->logger->debug("doesn't fit, skipping for now");
122 70
                $skippedItems[] = $itemToPack;
123
                // abandon here if next item is the same, no point trying to keep going. Last time is not skipped, need that to trigger appropriate reset logic
124 70
                while ($items->count() > 1 && self::isSameDimensions($itemToPack, $items->top())) {
125 32
                    $skippedItems[] = $items->extract();
126
                }
127 70
                continue;
128
            }
129
130 81
            if ($x > $startX) {
131
                // Having now placed items, there is space *within the same row* along the length. Pack into that.
132 80
                $this->logger->debug('No more fit in width wise, packing along remaining length');
133 80
                $layer->merge($this->packLayer($items, $packedItemList, $x, $y + $rowLength, $z, $widthForLayer, $lengthForLayer - $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
134
135 80
                $this->logger->debug('No more fit in width wise, resetting for new row');
136 80
                $y += $rowLength;
137 80
                $x = $startX;
138 80
                $rowLength = 0;
139 80
                $skippedItems[] = $itemToPack;
140 80
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
141 80
                $skippedItems = [];
142 80
                $prevItem = null;
143 80
                continue;
144
            }
145
146 71
            $this->logger->debug('no items fit, so starting next vertical layer');
147 71
            $skippedItems[] = $itemToPack;
148
149 71
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
150
151 71
            return $layer;
152
        }
153
154 91
        return $layer;
155
    }
156
157
    /**
158
     * Compare two items to see if they have same dimensions.
159
     */
160 42
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
161
    {
162 42
        if ($itemA === $itemB) {
163 29
            return true;
164
        }
165 21
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
166 21
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
167 21
        sort($itemADimensions);
168 21
        sort($itemBDimensions);
169
170 21
        return $itemADimensions === $itemBDimensions;
171
    }
172
}
173