dvdoug /
BoxPacker
| 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
Loading history...
|
|||
| 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 |