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 ArrayIterator; |
||
13 | use Countable; |
||
14 | use IteratorAggregate; |
||
15 | use Traversable; |
||
16 | |||
17 | use function array_key_last; |
||
18 | use function array_pop; |
||
19 | use function array_reverse; |
||
20 | use function array_slice; |
||
21 | use function count; |
||
22 | use function current; |
||
23 | use function end; |
||
24 | use function key; |
||
25 | use function prev; |
||
26 | use function usort; |
||
27 | |||
28 | /** |
||
29 | * List of items to be packed, ordered by volume. |
||
30 | */ |
||
31 | class ItemList implements Countable, IteratorAggregate |
||
32 | { |
||
33 | /** |
||
34 | * @var Item[] |
||
35 | */ |
||
36 | private array $list = []; |
||
37 | |||
38 | private bool $isSorted = false; |
||
39 | |||
40 | private ?bool $hasConstrainedItems = null; |
||
41 | |||
42 | private ?bool $hasNoRotationItems = null; |
||
43 | 108 | ||
44 | public function __construct(private readonly ItemSorter $sorter = new DefaultItemSorter()) |
||
45 | 108 | { |
|
46 | } |
||
47 | |||
48 | /** |
||
49 | * Do a bulk create. |
||
50 | * |
||
51 | * @param Item[] $items |
||
52 | 94 | */ |
|
53 | public static function fromArray(array $items, bool $preSorted = false): self |
||
54 | 94 | { |
|
55 | 94 | $list = new self(); |
|
56 | 94 | $list->list = array_reverse($items); // internal sort is largest at the end |
|
57 | $list->isSorted = $preSorted; |
||
58 | 94 | ||
59 | return $list; |
||
60 | } |
||
61 | 109 | ||
62 | public function insert(Item $item, int $qty = 1): void |
||
63 | 109 | { |
|
64 | 109 | for ($i = 0; $i < $qty; ++$i) { |
|
65 | $this->list[] = $item; |
||
66 | 109 | } |
|
67 | $this->isSorted = false; |
||
68 | 109 | ||
69 | if (isset($this->hasConstrainedItems)) { // normally lazy evaluated, override if that's already been done |
||
70 | $this->hasConstrainedItems = $this->hasConstrainedItems || $item instanceof ConstrainedPlacementItem; |
||
71 | } |
||
72 | 109 | ||
73 | if (isset($this->hasNoRotationItems)) { // normally lazy evaluated, override if that's already been done |
||
74 | $this->hasNoRotationItems = $this->hasNoRotationItems || $item->getAllowedRotation() === Rotation::Never; |
||
75 | } |
||
76 | } |
||
77 | |||
78 | /** |
||
79 | * Remove item from list. |
||
80 | 1 | */ |
|
81 | public function remove(Item $item): void |
||
82 | 1 | { |
|
83 | 1 | if (!$this->isSorted) { |
|
84 | 1 | usort($this->list, $this->sorter->compare(...)); |
|
85 | 1 | $this->list = array_reverse($this->list); // internal sort is largest at the end |
|
86 | $this->isSorted = true; |
||
87 | } |
||
88 | 1 | ||
89 | end($this->list); |
||
90 | 1 | do { |
|
91 | 1 | if (current($this->list) === $item) { |
|
92 | unset($this->list[key($this->list)]); |
||
93 | 1 | ||
94 | return; |
||
95 | 1 | } |
|
96 | } while (prev($this->list) !== false); |
||
97 | } |
||
98 | 56 | ||
99 | public function removePackedItems(PackedItemList $packedItemList): void |
||
100 | 56 | { |
|
101 | 55 | foreach ($packedItemList as $packedItem) { |
|
102 | end($this->list); |
||
103 | 55 | do { |
|
104 | 55 | if (current($this->list) === $packedItem->item) { |
|
105 | unset($this->list[key($this->list)]); |
||
106 | 55 | ||
107 | break; |
||
108 | 18 | } |
|
109 | } while (prev($this->list) !== false); |
||
110 | } |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * @internal |
||
115 | 104 | */ |
|
116 | public function extract(): Item |
||
117 | 104 | { |
|
118 | 2 | if (!$this->isSorted) { |
|
119 | 2 | usort($this->list, $this->sorter->compare(...)); |
|
120 | 2 | $this->list = array_reverse($this->list); // internal sort is largest at the end |
|
121 | $this->isSorted = true; |
||
122 | } |
||
123 | 104 | ||
124 | return array_pop($this->list); |
||
125 | } |
||
126 | |||
127 | /** |
||
128 | * @internal |
||
129 | 103 | */ |
|
130 | public function top(): Item |
||
131 | 103 | { |
|
132 | 60 | if (!$this->isSorted) { |
|
133 | 60 | usort($this->list, $this->sorter->compare(...)); |
|
134 | 60 | $this->list = array_reverse($this->list); // internal sort is largest at the end |
|
135 | $this->isSorted = true; |
||
136 | } |
||
137 | 103 | ||
138 | return $this->list[array_key_last($this->list)]; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * @internal |
||
143 | 21 | */ |
|
144 | public function topN(int $n): self |
||
145 | 21 | { |
|
146 | 1 | if (!$this->isSorted) { |
|
147 | 1 | usort($this->list, $this->sorter->compare(...)); |
|
148 | 1 | $this->list = array_reverse($this->list); // internal sort is largest at the end |
|
149 | $this->isSorted = true; |
||
150 | } |
||
151 | 21 | ||
152 | 21 | $topNList = new self(); |
|
153 | 21 | $topNList->list = array_slice($this->list, -$n, $n); |
|
154 | $topNList->isSorted = true; |
||
155 | 21 | ||
156 | return $topNList; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * @return Traversable<Item> |
||
161 | 100 | */ |
|
162 | public function getIterator(): Traversable |
||
163 | 100 | { |
|
164 | 45 | if (!$this->isSorted) { |
|
165 | 45 | usort($this->list, $this->sorter->compare(...)); |
|
166 | 45 | $this->list = array_reverse($this->list); // internal sort is largest at the end |
|
167 | $this->isSorted = true; |
||
168 | } |
||
169 | 100 | ||
170 | return new ArrayIterator(array_reverse($this->list)); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Number of items in list. |
||
175 | 107 | */ |
|
176 | public function count(): int |
||
177 | 107 | { |
|
178 | return count($this->list); |
||
179 | } |
||
180 | |||
181 | /** |
||
182 | * Does this list contain items with constrained placement criteria. |
||
183 | 101 | */ |
|
184 | public function hasConstrainedItems(): bool |
||
185 | 101 | { |
|
186 | 101 | if (!isset($this->hasConstrainedItems)) { |
|
187 | 101 | $this->hasConstrainedItems = false; |
|
188 | 101 | foreach ($this->list as $item) { |
|
189 | 3 | if ($item instanceof ConstrainedPlacementItem) { |
|
190 | 3 | $this->hasConstrainedItems = true; |
|
191 | break; |
||
192 | } |
||
193 | } |
||
194 | } |
||
195 | 101 | ||
196 | return $this->hasConstrainedItems; |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
197 | } |
||
198 | |||
199 | /** |
||
200 | * Does this list contain items which cannot be rotated. |
||
201 | 101 | */ |
|
202 | public function hasNoRotationItems(): bool |
||
203 | 101 | { |
|
204 | 101 | if (!isset($this->hasNoRotationItems)) { |
|
205 | 101 | $this->hasNoRotationItems = false; |
|
206 | 101 | foreach ($this->list as $item) { |
|
207 | if ($item->getAllowedRotation() === Rotation::Never) { |
||
208 | $this->hasNoRotationItems = true; |
||
209 | break; |
||
210 | } |
||
211 | } |
||
212 | } |
||
213 | 101 | ||
214 | return $this->hasNoRotationItems; |
||
0 ignored issues
–
show
|
|||
215 | } |
||
216 | } |
||
217 |