diff --git a/.travis.yml b/.travis.yml index 0f704bda..00df9529 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ env: - TEST_SUITE=functional - TEST_SUITE=efficiency php: - - 7.4snapshot + - 7.4 - 7.3 - 7.2 - 7.1 diff --git a/composer.json b/composer.json index 0f5e4c17..539e1e7e 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,7 @@ "description": "An implementation of the 3D (actually 4D) bin packing/knapsack problem (aka creating parcels by putting items into boxes)", "keywords": ["packing","binpacking","bin packing","knapsack","box","boxpacking","parcel","parcelpacking","shipping","packaging","boxes", "container"], "homepage": "http://boxpacker.io/", + "type": "library", "authors": [ { "name": "Doug Wright", diff --git a/src/OrientatedItemFactory.php b/src/OrientatedItemFactory.php index d5765407..4b0a805e 100644 --- a/src/OrientatedItemFactory.php +++ b/src/OrientatedItemFactory.php @@ -28,6 +28,12 @@ class OrientatedItemFactory implements LoggerAwareInterface */ protected static $emptyBoxCache = []; + /** + * @var int[] + */ + protected static $lookaheadCache = []; + + public function __construct(Box $box) { $this->box = $box; @@ -336,27 +342,54 @@ protected function calculateAdditionalItemsPackedWithThisOrientation( $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking); $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow - - $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX); - $tempPacker = new VolumePacker($tempBox, clone $itemsToPack); - $tempPacker->setLookAheadMode(true); - $remainingRowPacked = $tempPacker->pack(); - /** @var PackedItem $packedItem */ - foreach ($remainingRowPacked->getItems() as $packedItem) { - $itemsToPack->remove($packedItem); + $cacheKey = $originalWidthLeft . + '|' . + $originalLengthLeft . + '|' . + $prevItem->getWidth() . + '|' . + $prevItem->getLength() . + '|' . + $currentRowLength . + '|' + . $depthLeft; + + /** @var Item $itemToPack */ + foreach (clone $itemsToPack as $itemToPack) { + $cacheKey .= '|' . + $itemToPack->getWidth() . + '|' . + $itemToPack->getLength() . + '|' . + $itemToPack->getDepth() . + '|' . + $itemToPack->getWeight(); } - $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX); - $tempPacker = new VolumePacker($tempBox, clone $itemsToPack); - $tempPacker->setLookAheadMode(true); - $nextRowsPacked = $tempPacker->pack(); - /** @var PackedItem $packedItem */ - foreach ($nextRowsPacked->getItems() as $packedItem) { - $itemsToPack->remove($packedItem); - } + if (!isset(static::$lookaheadCache[$cacheKey])) { + $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX); + $tempPacker = new VolumePacker($tempBox, clone $itemsToPack); + $tempPacker->setLookAheadMode(true); + $remainingRowPacked = $tempPacker->pack(); + /** @var PackedItem $packedItem */ + foreach ($remainingRowPacked->getItems() as $packedItem) { + $itemsToPack->remove($packedItem); + } - $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]); + $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX); + $tempPacker = new VolumePacker($tempBox, clone $itemsToPack); + $tempPacker->setLookAheadMode(true); + $nextRowsPacked = $tempPacker->pack(); + /** @var PackedItem $packedItem */ + foreach ($nextRowsPacked->getItems() as $packedItem) { + $itemsToPack->remove($packedItem); + } + + $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]); + + static::$lookaheadCache[$cacheKey] = $nextItems->count() - $itemsToPack->count(); + } - return $nextItems->count() - $itemsToPack->count(); + return static::$lookaheadCache[$cacheKey]; } } diff --git a/src/Packer.php b/src/Packer.php index 31bf1c21..fc513ced 100644 --- a/src/Packer.php +++ b/src/Packer.php @@ -156,6 +156,8 @@ public function doVolumePacking() { $packedBoxes = new PackedBoxList(); + $this->sanityPrecheck(); + //Keep going until everything packed while ($this->items->count()) { $boxesToEvaluate = clone $this->boxes; @@ -257,4 +259,21 @@ public function redistributeWeight(PackedBoxList $originalBoxes) return $redistributor->redistributeWeight($originalBoxes); } + + private function sanityPrecheck() + { + /** @var Item $item */ + foreach (clone $this->items as $item) { + $possibleFits = 0; + /** @var Box $box */ + foreach (clone $this->boxes as $box) { + if ($item->getWeight() <= ($box->getMaxWeight() - $box->getEmptyWeight())) { + $possibleFits += count((new OrientatedItemFactory($box))->getPossibleOrientationsInEmptyBox($item)); + } + } + if ($possibleFits === 0) { + throw new ItemTooLargeException('Item ' . $item->getDescription() . ' is too large to fit into any box', $item); + } + } + } } diff --git a/tests/PackerTest.php b/tests/PackerTest.php index e6d31a5a..f514ef89 100644 --- a/tests/PackerTest.php +++ b/tests/PackerTest.php @@ -222,4 +222,26 @@ public function testIssue170() self::assertCount(2, $packedBoxes); } + + + /** + * From issue #182. + */ + public function testIssue182A() + { + $packer = new Packer(); + $packer->addBox(new TestBox('Box', 410, 310, 310, 2000, 410, 310, 310, 60000)); + $packer->addBox(new TestBox('Box', 410, 310, 260, 2000, 410, 310, 260, 60000)); + $packer->addBox(new TestBox('Box', 410, 310, 205, 2000, 410, 310, 205, 60000)); + $packer->addBox(new TestBox('Box', 310, 310, 210, 2000, 310, 310, 210, 60000)); + $packer->addBox(new TestBox('Box', 310, 210, 210, 2000, 310, 210, 210, 60000)); + $packer->addBox(new TestBox('Box', 310, 210, 155, 2000, 310, 210, 155, 60000)); + $packer->addBox(new TestBox('Box', 210, 160, 105, 2000, 210, 160, 105, 60000)); + $packer->addItem(new TestItem('Item', 150, 100, 100, 1, false), 200); + + /** @var PackedBox[] $packedBoxes */ + $packedBoxes = iterator_to_array($packer->pack(), false); + + self::assertCount(9, $packedBoxes); + } }