Passed
Pull Request — master (#471)
by
unknown
05:23 queued 03:37
created

OrientatedItemFactory   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 215
Duplicated Lines 0 %

Test Coverage

Coverage 60.2%

Importance

Changes 30
Bugs 3 Features 0
Metric Value
eloc 84
c 30
b 3
f 0
dl 0
loc 215
ccs 59
cts 98
cp 0.602
rs 10
wmc 27

8 Methods

Rating   Name   Duplication   Size   Complexity  
A setSinglePassMode() 0 3 1
A generatePermutations() 0 26 5
A setLogger() 0 3 1
A __construct() 0 3 1
A getBestOrientation() 0 39 3
B getPossibleOrientations() 0 31 7
A hasStableOrientationsInEmptyBox() 0 39 2
B getUsableOrientations() 0 28 7
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_filter;
16
use function count;
17
use function usort;
18
19
/**
20
 * Figure out orientations for an item and a given set of dimensions.
21
 * @internal
22
 */
23
class OrientatedItemFactory implements LoggerAwareInterface
24
{
25
    protected LoggerInterface $logger;
26
27
    protected bool $singlePassMode = false;
28
29
    /**
30
     * @var array<string, bool>
31
     */
32
    protected static array $emptyBoxStableItemOrientationCache = [];
33
34 84
    public function __construct(protected Box $box)
35
    {
36 84
        $this->logger = new NullLogger();
37
    }
38
39 84
    public function setLogger(LoggerInterface $logger): void
40
    {
41 84
        $this->logger = $logger;
42
    }
43
44 4
    public function setSinglePassMode(bool $singlePassMode): void
45
    {
46 4
        $this->singlePassMode = $singlePassMode;
47
    }
48
49
    /**
50
     * Get the best orientation for an item.
51
     */
52 84
    public function getBestOrientation(
53
        Item $item,
54
        ?OrientatedItem $prevItem,
55
        ItemList $nextItems,
56
        int $widthLeft,
57
        int $lengthLeft,
58
        int $depthLeft,
59
        int $rowLength,
60
        int $x,
61
        int $y,
62
        int $z,
63
        PackedItemList $prevPackedItemList,
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...
64
        bool $considerStability
65
    ): ?OrientatedItem {
66 84
        $this->logger->debug(
67 84
            "evaluating item {$item->getDescription()} for fit",
68 84
            [
69 84
                'item' => $item,
70 84
                'space' => [
71 84
                    'widthLeft' => $widthLeft,
72 84
                    'lengthLeft' => $lengthLeft,
73 84
                    'depthLeft' => $depthLeft,
74 84
                ],
75 84
            ]
76 84
        );
77
78 84
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
79 84
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
80
81 84
        if (empty($usableOrientations)) {
82 76
            return null;
83
        }
84
85 82
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, $this->logger);
86 82
        usort($usableOrientations, $sorter);
87
88 82
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
89
90 82
        return $usableOrientations[0];
91
    }
92
93
    /**
94
     * Find all possible orientations for an item.
95
     *
96
     * @return OrientatedItem[]
97
     */
98 84
    public function getPossibleOrientations(
99
        Item $item,
100
        ?OrientatedItem $prevItem,
101
        int $widthLeft,
102
        int $lengthLeft,
103
        int $depthLeft,
104
        int $x,
105
        int $y,
106
        int $z,
107
        PackedItemList $prevPackedItemList
108
    ): array {
109 84
        $permutations = $this->generatePermutations($item, $prevItem);
110
111
        // remove any that simply don't fit
112 84
        $orientations = [];
113 84
        foreach ($permutations as $dimensions) {
114 84
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
115 82
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
116
            }
117
        }
118
119 84
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
120
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList): bool {
121
                /** @var ConstrainedPlacementItem $constrainedItem */
122
                $constrainedItem = $i->getItem();
123
124
                return $constrainedItem->canBePacked(new PackedBox($this->box, $prevPackedItemList), $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
125
            });
126
        }
127
128 84
        return $orientations;
129
    }
130
131
    /**
132
     * @param  OrientatedItem[] $possibleOrientations
133
     * @return OrientatedItem[]
134
     */
135 84
    protected function getUsableOrientations(
136
        Item $item,
137
        array $possibleOrientations
138
    ): array {
139 84
        $stableOrientations = $unstableOrientations = [];
140
141
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
142 84
        foreach ($possibleOrientations as $orientation) {
143 82
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
144 82
                $stableOrientations[] = $orientation;
145
            } else {
146
                $unstableOrientations[] = $orientation;
147
            }
148
        }
149
150
        /*
151
         * We prefer to use stable orientations only, but allow unstable ones if
152
         * the item doesn't fit in the box any other way
153
         */
154 84
        if (count($stableOrientations) > 0) {
155 82
            return $stableOrientations;
156
        }
157
158 76
        if ((count($unstableOrientations) > 0) && !$this->hasStableOrientationsInEmptyBox($item)) {
159
            return $unstableOrientations;
160
        }
161
162 76
        return [];
163
    }
164
165
    /**
166
     * Return the orientations for this item if it were to be placed into the box with nothing else.
167
     */
168
    protected function hasStableOrientationsInEmptyBox(Item $item): bool
169
    {
170
        $cacheKey = $item->getWidth() .
171
            '|' .
172
            $item->getLength() .
173
            '|' .
174
            $item->getDepth() .
175
            '|' .
176
            $item->getAllowedRotation()->name .
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on DVDoug\BoxPacker\Rotation.
Loading history...
177
            '|' .
178
            $this->box->getInnerWidth() .
179
            '|' .
180
            $this->box->getInnerLength() .
181
            '|' .
182
            $this->box->getInnerDepth();
183
184
        if (isset(static::$emptyBoxStableItemOrientationCache[$cacheKey])) {
185
            return static::$emptyBoxStableItemOrientationCache[$cacheKey];
186
        }
187
188
        $orientations = $this->getPossibleOrientations(
189
            $item,
190
            null,
191
            $this->box->getInnerWidth(),
192
            $this->box->getInnerLength(),
193
            $this->box->getInnerDepth(),
194
            0,
195
            0,
196
            0,
197
            new PackedItemList()
198
        );
199
200
        $stableOrientations = array_filter(
201
            $orientations,
202
            static fn (OrientatedItem $orientation) => $orientation->isStable()
203
        );
204
        static::$emptyBoxStableItemOrientationCache[$cacheKey] = count($stableOrientations) > 0;
205
206
        return static::$emptyBoxStableItemOrientationCache[$cacheKey];
207
    }
208
209
    /**
210
     * @return array<array<int>>
211
     */
212 84
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
213
    {
214
        // Special case items that are the same as what we just packed - keep orientation
215 84
        if ($prevItem && $prevItem->isSameDimensions($item)) {
216 62
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
217
        }
218
219 84
        $permutations = [];
220 84
        $w = $item->getWidth();
221 84
        $l = $item->getLength();
222 84
        $d = $item->getDepth();
223
224 84
        $permutations[$w . '|' . $l . '|' . $d] = [$w, $l, $d];
225
226 84
        if ($item->getAllowedRotation() !== Rotation::Never) { // simple 2D rotation
227 84
            $permutations[$l . '|' . $w . '|' . $d] = [$l, $w, $d];
228
        }
229
230 84
        if ($item->getAllowedRotation() === Rotation::BestFit) { // add 3D rotation if we're allowed
231 52
            $permutations[$w . '|' . $d . '|' . $l] = [$w, $d, $l];
232 52
            $permutations[$l . '|' . $d . '|' . $w] = [$l, $d, $w];
233 52
            $permutations[$d . '|' . $w . '|' . $l] = [$d, $w, $l];
234 52
            $permutations[$d . '|' . $l . '|' . $w] = [$d, $l, $w];
235
        }
236
237 84
        return $permutations;
238
    }
239
}
240