Passed
Push — master ( 73ddcb...629397 )
by Doug
02:34
created

OrientatedItemFactory   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 25
Bugs 3 Features 0
Metric Value
wmc 26
eloc 88
c 25
b 3
f 0
dl 0
loc 222
ccs 95
cts 95
cp 1
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getBestOrientation() 0 39 2
A setSinglePassMode() 0 3 1
B getUsableOrientations() 0 30 7
B getPossibleOrientations() 0 28 7
A getStableOrientationsInEmptyBox() 0 8 1
A generatePermutations() 0 23 5
A getPossibleOrientationsInEmptyBox() 0 34 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 function array_filter;
12
use function count;
13
use Psr\Log\LoggerAwareInterface;
14
use Psr\Log\LoggerAwareTrait;
15
use Psr\Log\NullLogger;
16
use function usort;
17
18
/**
19
 * Figure out orientations for an item and a given set of dimensions.
20
 *
21
 * @author Doug Wright
22
 * @internal
23
 */
24
class OrientatedItemFactory implements LoggerAwareInterface
25
{
26
    use LoggerAwareTrait;
27
28
    /** @var Box */
29
    protected $box;
30
31
    /**
32
     * Whether the packer is in single-pass mode.
33
     *
34
     * @var bool
35
     */
36
    protected $singlePassMode = false;
37
38
    /**
39
     * @var OrientatedItem[]
40
     */
41
    protected static $emptyBoxCache = [];
42
43 78
    public function __construct(Box $box)
44
    {
45 78
        $this->box = $box;
46 78
        $this->logger = new NullLogger();
47 78
    }
48
49 53
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 53
        $this->singlePassMode = $singlePassMode;
52 53
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 73
    public function getBestOrientation(
58
        Item $item,
59
        ?OrientatedItem $prevItem,
60
        ItemList $nextItems,
61
        int $widthLeft,
62
        int $lengthLeft,
63
        int $depthLeft,
64
        int $rowLength,
65
        int $x,
66
        int $y,
67
        int $z,
68
        PackedItemList $prevPackedItemList
69
    ): ?OrientatedItem {
70 73
        $this->logger->debug(
71 73
            "evaluating item {$item->getDescription()} for fit",
72
            [
73 73
                'item' => $item,
74
                'space' => [
75 73
                    'widthLeft' => $widthLeft,
76 73
                    'lengthLeft' => $lengthLeft,
77 73
                    'depthLeft' => $depthLeft,
78
                ],
79
            ]
80
        );
81
82 73
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
83 73
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations);
84
85 73
        if (empty($usableOrientations)) {
86 66
            return null;
87
        }
88
89 73
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList);
90 73
        $sorter->setLogger($this->logger);
91 73
        usort($usableOrientations, $sorter);
92
93 73
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
94
95 73
        return $usableOrientations[0];
96
    }
97
98
    /**
99
     * Find all possible orientations for an item.
100
     *
101
     * @return OrientatedItem[]
102
     */
103 76
    public function getPossibleOrientations(
104
        Item $item,
105
        ?OrientatedItem $prevItem,
106
        int $widthLeft,
107
        int $lengthLeft,
108
        int $depthLeft,
109
        int $x,
110
        int $y,
111
        int $z,
112
        PackedItemList $prevPackedItemList
113
    ): array {
114 76
        $permutations = $this->generatePermutations($item, $prevItem);
115
116
        //remove any that simply don't fit
117 76
        $orientations = [];
118 76
        foreach ($permutations as $dimensions) {
119 76
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
120 76
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
121
            }
122
        }
123
124 76
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
125 3
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
126 3
                return $i->getItem()->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
0 ignored issues
show
Bug introduced by
The method canBePacked() does not exist on DVDoug\BoxPacker\Item. It seems like you code against a sub-type of said class. However, the method does not exist in DVDoug\BoxPacker\Test\TestItem. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

126
                return $i->getItem()->/** @scrutinizer ignore-call */ canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
Loading history...
127 3
            });
128
        }
129
130 76
        return $orientations;
131
    }
132
133
    /**
134
     * @return OrientatedItem[]
135
     */
136 37
    public function getPossibleOrientationsInEmptyBox(Item $item): array
137
    {
138 37
        $cacheKey = $item->getWidth() .
139 37
            '|' .
140 37
            $item->getLength() .
141 37
            '|' .
142 37
            $item->getDepth() .
143 37
            '|' .
144 37
            $item->getAllowedRotations() .
145 37
            '|' .
146 37
            $this->box->getInnerWidth() .
147 37
            '|' .
148 37
            $this->box->getInnerLength() .
149 37
            '|' .
150 37
            $this->box->getInnerDepth();
151
152 37
        if (isset(static::$emptyBoxCache[$cacheKey])) {
153 28
            $orientations = static::$emptyBoxCache[$cacheKey];
154
        } else {
155 28
            $orientations = $this->getPossibleOrientations(
156 28
                $item,
157 28
                null,
158 28
                $this->box->getInnerWidth(),
159 28
                $this->box->getInnerLength(),
160 28
                $this->box->getInnerDepth(),
161 28
                0,
162 28
                0,
163 28
                0,
164 28
                new PackedItemList()
165
            );
166 28
            static::$emptyBoxCache[$cacheKey] = $orientations;
167
        }
168
169 37
        return $orientations;
170
    }
171
172
    /**
173
     * @param  OrientatedItem[] $possibleOrientations
174
     * @return OrientatedItem[]
175
     */
176 73
    protected function getUsableOrientations(
177
        Item $item,
178
        array $possibleOrientations
179
    ): array {
180 73
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
181
182
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
183 73
        foreach ($possibleOrientations as $orientation) {
184 73
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
185 71
                $stableOrientations[] = $orientation;
186
            } else {
187 7
                $unstableOrientations[] = $orientation;
188
            }
189
        }
190
191
        /*
192
         * We prefer to use stable orientations only, but allow unstable ones if
193
         * the item doesn't fit in the box any other way
194
         */
195 73
        if (count($stableOrientations) > 0) {
196 71
            $orientationsToUse = $stableOrientations;
197 66
        } elseif (count($unstableOrientations) > 0) {
198 7
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
199
200 7
            if (count($stableOrientationsInEmptyBox) === 0) {
201 6
                $orientationsToUse = $unstableOrientations;
202
            }
203
        }
204
205 73
        return $orientationsToUse;
206
    }
207
208
    /**
209
     * Return the orientations for this item if it were to be placed into the box with nothing else.
210
     */
211 7
    protected function getStableOrientationsInEmptyBox(Item $item): array
212
    {
213 7
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
214
215 7
        return array_filter(
216 7
            $orientationsInEmptyBox,
217 7
            function (OrientatedItem $orientation) {
218 7
                return $orientation->isStable();
219 7
            }
220
        );
221
    }
222
223 76
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
224
    {
225 76
        $permutations = [];
226
227
        //Special case items that are the same as what we just packed - keep orientation
228 76
        if ($prevItem && $prevItem->isSameDimensions($item)) {
229 59
            $permutations[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
230
        } else {
231 76
            $permutations[] = [$item->getWidth(), $item->getLength(), $item->getDepth()];
232
233 76
            if ($item->getAllowedRotations() > 1) { //simple 2D rotation
234 75
                $permutations[] = [$item->getLength(), $item->getWidth(), $item->getDepth()];
235
            }
236
237 76
            if ($item->getAllowedRotations() === Item::ROTATION_BEST_FIT) { //add 3D rotation if we're allowed
238 49
                $permutations[] = [$item->getWidth(), $item->getDepth(), $item->getLength()];
239 49
                $permutations[] = [$item->getLength(), $item->getDepth(), $item->getWidth()];
240 49
                $permutations[] = [$item->getDepth(), $item->getWidth(), $item->getLength()];
241 49
                $permutations[] = [$item->getDepth(), $item->getLength(), $item->getWidth()];
242
            }
243
        }
244
245 76
        return $permutations;
246
    }
247
}
248