Passed
Pull Request — master (#314)
by
unknown
05:46 queued 03:57
created

OrientatedItemFactory   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Test Coverage

Coverage 68.83%

Importance

Changes 31
Bugs 3 Features 0
Metric Value
eloc 86
c 31
b 3
f 0
dl 0
loc 216
ccs 53
cts 77
cp 0.6883
rs 10
wmc 27

8 Methods

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