Issues (30)

Branch: master

src/OrientatedItemFactory.php (1 issue)

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