Passed
Pull Request — master (#335)
by
unknown
05:51 queued 04:02
created

OrientatedItemFactory::setSinglePassMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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