Passed
Push — 3.x ( d739e8...b351ea )
by Doug
03:11
created

LayerPacker   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 164
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 79
dl 0
loc 164
ccs 70
cts 70
cp 1
rs 10
c 8
b 0
f 0
wmc 24

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A checkNonDimensionalConstraints() 0 8 4
A setBoxIsRotated() 0 4 1
C packLayer() 0 82 13
A beStrictAboutItemOrdering() 0 3 1
A setLogger() 0 4 1
A setSinglePassMode() 0 4 1
A isSameDimensions() 0 11 2
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
 *
23
 * @internal
24
 */
25
class LayerPacker implements LoggerAwareInterface
26
{
27
    private LoggerInterface $logger;
28
29
    private Box $box;
30
31
    private bool $singlePassMode = false;
32
33
    private OrientatedItemFactory $orientatedItemFactory;
34
35
    private bool $beStrictAboutItemOrdering = false;
36
37
    private bool $isBoxRotated = false;
38
39
    public function __construct(Box $box)
40
    {
41
        $this->box = $box;
42
        $this->logger = new NullLogger();
43
44
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
45
        $this->orientatedItemFactory->setLogger($this->logger);
46
    }
47
48
    /**
49
     * Sets a logger.
50
     */
51
    public function setLogger(LoggerInterface $logger): void
52
    {
53
        $this->logger = $logger;
54
        $this->orientatedItemFactory->setLogger($logger);
55
    }
56
57
    public function setSinglePassMode(bool $singlePassMode): void
58
    {
59
        $this->singlePassMode = $singlePassMode;
60
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
61
    }
62 84
63
    public function setBoxIsRotated(bool $boxIsRotated): void
64 84
    {
65 84
        $this->isBoxRotated = $boxIsRotated;
66
        $this->orientatedItemFactory->setBoxIsRotated($boxIsRotated);
67 84
    }
68 84
69
    public function beStrictAboutItemOrdering(bool $beStrict): void
70
    {
71
        $this->beStrictAboutItemOrdering = $beStrict;
72
    }
73
74 84
    /**
75
     * Pack items into an individual vertical layer.
76 84
     */
77 84
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
78
    {
79
        $layer = new PackedLayer();
80 51
        $x = $startX;
81
        $y = $startY;
82 51
        $z = $startZ;
83 51
        $rowLength = 0;
84
        $prevItem = null;
85
        $skippedItems = [];
86 38
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
87
88 38
        while ($items->count() > 0) {
89
            $itemToPack = $items->extract();
90
91
            // skip items that will never fit e.g. too heavy
92
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
93
                continue;
94 84
            }
95
96 84
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
97 84
98 84
            if ($orientatedItem instanceof OrientatedItem) {
99 84
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
100 84
                $layer->insert($packedItem);
101 84
                $packedItemList->insert($packedItem);
102 84
103 84
                $rowLength = max($rowLength, $packedItem->getLength());
104
                $prevItem = $orientatedItem;
105 84
106 84
                // Figure out if we can stack items on top of this rather than side by side
107
                // e.g. when we've packed a tall item, and have just put a shorter one next to it.
108
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
109 84
                if ($stackableDepth > 0) {
110 11
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->getDepth(), $x + $packedItem->getWidth(), $y + $packedItem->getLength(), $stackableDepth, $stackableDepth, $considerStability);
111
                    $layer->merge($stackedLayer);
112
                }
113 83
114
                $x += $packedItem->getWidth();
115 83
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
116 83
117 83
                // might be space available lengthwise across the width of this item, up to the current layer length
118 83
                $layer->merge($this->packLayer($items, $packedItemList, $x - $packedItem->getWidth(), $y + $packedItem->getLength(), $z, $x, $y + $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
119
120 83
                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...
121 83
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
122
                    $skippedItems = [];
123
                }
124
125 83
                continue;
126 83
            }
127 29
128 29
            if (!$this->beStrictAboutItemOrdering && $items->count() > 0) { // skip for now, move on to the next item
129
                $this->logger->debug("doesn't fit, skipping for now");
130
                $skippedItems[] = $itemToPack;
131 83
                // 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
132 83
                while ($items->count() > 1 && self::isSameDimensions($itemToPack, $items->top())) {
133
                    $skippedItems[] = $items->extract();
134 83
                }
135 11
                continue;
136 11
            }
137
138 83
            if ($x > $startX) {
139
                $this->logger->debug('No more fit in width wise, resetting for new row');
140
                $y += $rowLength;
141 74
                $x = $startX;
142 62
                $rowLength = 0;
143 62
                $skippedItems[] = $itemToPack;
144
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
145 62
                $skippedItems = [];
146 30
                $prevItem = null;
147
                continue;
148 62
            }
149
150
            $this->logger->debug('no items fit, so starting next vertical layer');
151 74
            $skippedItems[] = $itemToPack;
152
153 74
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
154 74
155
            return $layer;
156 74
        }
157 74
158 74
        return $layer;
159 74
    }
160 74
161 74
    /**
162 74
     * As well as purely dimensional constraints, there are other constraints that need to be met
163 74
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
164 74
     */
165
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
166
    {
167 64
        $customConstraintsOK = true;
168 64
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
169
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
170 64
        }
171
172 64
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
173
    }
174
175 84
    /**
176
     * Compare two items to see if they have same dimensions.
177
     */
178
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
179
    {
180
        if ($itemA === $itemB) {
181
            return true;
182 84
        }
183
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
184 84
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
185 84
        sort($itemADimensions);
186 1
        sort($itemBDimensions);
187
188
        return $itemADimensions === $itemBDimensions;
189 84
    }
190
}
191