OrientatedItemSorter::lookAheadDecider()   B
last analyzed

Complexity

Conditions 7
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 11
c 2
b 0
f 0
nc 4
nop 4
dl 0
loc 20
ccs 11
cts 11
cp 1
crap 7
rs 8.8333
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\LoggerInterface;
13
14
use function max;
15
use function min;
16
17
use const PHP_INT_MAX;
18
19
/**
20
 * Figure out best choice of orientations for an item and a given context.
21
 * @internal
22
 */
23
class OrientatedItemSorter
24
{
25
    /**
26
     * @var array<string, int>
27
     */
28
    protected static array $lookaheadCache = [];
29 86
30
    public function __construct(
31
        private readonly OrientatedItemFactory $orientatedItemFactory,
32
        private readonly bool $singlePassMode,
33
        private readonly int $widthLeft,
34
        private readonly int $lengthLeft,
35
        private readonly int $depthLeft,
36
        private readonly ItemList $nextItems,
37
        private readonly int $rowLength,
38
        private readonly int $x,
39
        private readonly int $y,
40
        private readonly int $z,
41
        private readonly PackedItemList $prevPackedItemList,
42
        private readonly LoggerInterface $logger
43 86
    ) {
44
    }
45 36
46
    public function __invoke(OrientatedItem $a, OrientatedItem $b): int
47
    {
48 36
        // Prefer exact fits in width/length/depth order
49 36
        $orientationAWidthLeft = $this->widthLeft - $a->width;
50 36
        $orientationBWidthLeft = $this->widthLeft - $b->width;
51 36
        $widthDecider = $this->exactFitDecider($orientationAWidthLeft, $orientationBWidthLeft);
52 13
        if ($widthDecider !== 0) {
53
            return $widthDecider;
54
        }
55 33
56 33
        $orientationALengthLeft = $this->lengthLeft - $a->length;
57 33
        $orientationBLengthLeft = $this->lengthLeft - $b->length;
58 33
        $lengthDecider = $this->exactFitDecider($orientationALengthLeft, $orientationBLengthLeft);
59 21
        if ($lengthDecider !== 0) {
60
            return $lengthDecider;
61
        }
62 30
63 30
        $orientationADepthLeft = $this->depthLeft - $a->depth;
64 30
        $orientationBDepthLeft = $this->depthLeft - $b->depth;
65 30
        $depthDecider = $this->exactFitDecider($orientationADepthLeft, $orientationBDepthLeft);
66 4
        if ($depthDecider !== 0) {
67
            return $depthDecider;
68
        }
69
70 30
        // prefer leaving room for next item(s)
71 30
        $followingItemDecider = $this->lookAheadDecider($a, $b, $orientationAWidthLeft, $orientationBWidthLeft);
72 24
        if ($followingItemDecider !== 0) {
73
            return $followingItemDecider;
74
        }
75
76 24
        // otherwise prefer leaving minimum possible gap, or the greatest footprint
77 24
        $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
78
        $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
79 24
80
        return $orientationAMinGap <=> $orientationBMinGap ?: $a->surfaceFootprint <=> $b->surfaceFootprint;
81
    }
82 30
83
    private function lookAheadDecider(OrientatedItem $a, OrientatedItem $b, int $orientationAWidthLeft, int $orientationBWidthLeft): int
84 30
    {
85 18
        if ($this->nextItems->count() === 0) {
86
            return 0;
87
        }
88 26
89 26
        $nextItemFitA = $this->orientatedItemFactory->getPossibleOrientations($this->nextItems->top(), $a, $orientationAWidthLeft, $this->lengthLeft, $this->depthLeft, $this->x, $this->y, $this->z, $this->prevPackedItemList);
90 26
        $nextItemFitB = $this->orientatedItemFactory->getPossibleOrientations($this->nextItems->top(), $b, $orientationBWidthLeft, $this->lengthLeft, $this->depthLeft, $this->x, $this->y, $this->z, $this->prevPackedItemList);
91 10
        if ($nextItemFitA && !$nextItemFitB) {
92
            return -1;
93 25
        }
94 19
        if ($nextItemFitB && !$nextItemFitA) {
95
            return 1;
96
        }
97
98 20
        // if not an easy either/or, do a partial lookahead
99 20
        $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a);
100
        $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b);
101 20
102
        return $additionalPackedB <=> $additionalPackedA ?: 0;
103
    }
104
105
    /**
106
     * Approximation of a forward-looking packing.
107
     *
108
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
109
     * purely on fit.
110 20
     */
111
    protected function calculateAdditionalItemsPackedWithThisOrientation(
112
        OrientatedItem $prevItem
113 20
    ): int {
114 14
        if ($this->singlePassMode) {
115
            return 0;
116
        }
117 20
118
        $currentRowLength = max($prevItem->length, $this->rowLength);
119 20
120
        $itemsToPack = $this->nextItems->topN(8); // cap lookahead as this gets recursive and slow
121 20
122 20
        $cacheKey = $this->widthLeft .
123 20
            '|' .
124 20
            $this->lengthLeft .
125 20
            '|' .
126 20
            $prevItem->width .
127 20
            '|' .
128 20
            $prevItem->length .
129 20
            '|' .
130 20
            $currentRowLength .
131 20
            '|'
132
            . $this->depthLeft;
133 20
134 20
        foreach ($itemsToPack as $itemToPack) {
135 20
            $cacheKey .= '|' .
136 20
                $itemToPack->getWidth() .
137 20
                '|' .
138 20
                $itemToPack->getLength() .
139 20
                '|' .
140 20
                $itemToPack->getDepth() .
141 20
                '|' .
142 20
                $itemToPack->getWeight() .
143 20
                '|' .
144
                $itemToPack->getAllowedRotation()->name;
145
        }
146 20
147 19
        if (!isset(static::$lookaheadCache[$cacheKey])) {
148 19
            $tempBox = new WorkingVolume($this->widthLeft - $prevItem->width, $currentRowLength, $this->depthLeft, PHP_INT_MAX);
149 19
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
150 19
            $tempPacker->setSinglePassMode(true);
151
            $remainingRowPacked = $tempPacker->pack();
152 19
153
            $itemsToPack->removePackedItems($remainingRowPacked->items);
154 19
155 19
            $tempBox = new WorkingVolume($this->widthLeft, $this->lengthLeft - $currentRowLength, $this->depthLeft, PHP_INT_MAX);
156 19
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
157 19
            $tempPacker->setSinglePassMode(true);
158
            $nextRowsPacked = $tempPacker->pack();
159 19
160
            $itemsToPack->removePackedItems($nextRowsPacked->items);
161 19
162 19
            $packedCount = $this->nextItems->count() - $itemsToPack->count();
163
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
164 19
165
            static::$lookaheadCache[$cacheKey] = $packedCount;
166
        }
167 20
168
        return static::$lookaheadCache[$cacheKey];
169
    }
170 36
171
    private function exactFitDecider(int $dimensionALeft, int $dimensionBLeft): int
172 36
    {
173 18
        if ($dimensionALeft === 0 && $dimensionBLeft > 0) {
174
            return -1;
175
        }
176 34
177 18
        if ($dimensionALeft > 0 && $dimensionBLeft === 0) {
178
            return 1;
179
        }
180 33
181
        return 0;
182
    }
183
}
184