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

OrientatedItemFactory   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 31
Bugs 3 Features 0
Metric Value
eloc 97
dl 0
loc 241
ccs 98
cts 98
cp 1
rs 10
c 31
b 3
f 0
wmc 30

9 Methods

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