Passed
Push — 3.x ( d2d64a...d739e8 )
by Doug
02:33
created

OrientatedItemFactory::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

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