Skip to content

Commit

Permalink
Special-case first item, it has an outsize impact on what follows. Fixes
Browse files Browse the repository at this point in the history
 #272, fixes #583
  • Loading branch information
dvdoug committed Mar 9, 2024
1 parent 04e3a71 commit 27bb78e
Show file tree
Hide file tree
Showing 6 changed files with 659 additions and 640 deletions.
13 changes: 9 additions & 4 deletions src/LayerPacker.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function beStrictAboutItemOrdering(bool $beStrict): void
/**
* Pack items into an individual vertical layer.
*/
public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability, ?OrientatedItem $firstItem): PackedLayer
{
$layer = new PackedLayer();
$x = $startX;
Expand All @@ -89,7 +89,12 @@ public function packLayer(ItemList &$items, PackedItemList $packedItemList, int
continue;
}

$orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
if ($firstItem instanceof OrientatedItem && $firstItem->item === $itemToPack) {
$orientatedItem = $firstItem;
$firstItem = null;
} else {
$orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
}

if ($orientatedItem instanceof OrientatedItem) {
$packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
Expand All @@ -103,15 +108,15 @@ public function packLayer(ItemList &$items, PackedItemList $packedItemList, int
// e.g. when we've packed a tall item, and have just put a shorter one next to it.
$stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->depth;
if ($stackableDepth > 0) {
$stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->depth, $x + $packedItem->width, $y + $packedItem->length, $stackableDepth, $stackableDepth, $considerStability);
$stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->depth, $x + $packedItem->width, $y + $packedItem->length, $stackableDepth, $stackableDepth, $considerStability, null);
$layer->merge($stackedLayer);
}

$x += $packedItem->width;
$remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items

// might be space available lengthwise across the width of this item, up to the current layer length
$layer->merge($this->packLayer($items, $packedItemList, $x - $packedItem->width, $y + $packedItem->length, $z, $x, $y + $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
$layer->merge($this->packLayer($items, $packedItemList, $x - $packedItem->width, $y + $packedItem->length, $z, $x, $y + $rowLength, $depthForLayer, $layer->getDepth(), $considerStability, null));

if ($items->count() === 0 && $skippedItems) {
$items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
Expand Down
35 changes: 26 additions & 9 deletions src/VolumePacker.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,19 @@ public function setSinglePassMode(bool $singlePassMode): void
*/
public function pack(): PackedBox
{
$orientatedItemFactory = new OrientatedItemFactory($this->box);
$orientatedItemFactory->setLogger($this->logger);
$this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);

// Sometimes "space available" decisions depend on orientation of the box, so try both ways
$rotationsToTest = [false];
if (!$this->packAcrossWidthOnly && !$this->hasNoRotationItems) {
$rotationsToTest[] = true;
}

// The orientation of the first item can have an outsized effect on the rest of the placement, so special-case
// that and try everything

$boxPermutations = [];
foreach ($rotationsToTest as $rotation) {
if ($rotation) {
Expand All @@ -108,12 +114,19 @@ public function pack(): PackedBox
$boxLength = $this->box->getInnerLength();
}

$boxPermutation = $this->packRotation($boxWidth, $boxLength);
if ($boxPermutation->items->count() === $this->items->count()) {
return $boxPermutation;
$specialFirstItemOrientations = [null];
if (!$this->singlePassMode) {
$specialFirstItemOrientations = $orientatedItemFactory->getPossibleOrientations($this->items->top(), null, $boxWidth, $boxLength, $this->box->getInnerDepth(), 0, 0, 0, new PackedItemList()) ?: [null];
}

$boxPermutations[] = $boxPermutation;
foreach ($specialFirstItemOrientations as $firstItemOrientation) {
$boxPermutation = $this->packRotation($boxWidth, $boxLength, $firstItemOrientation);
if ($boxPermutation->items->count() === $this->items->count()) {
return $boxPermutation;
}

$boxPermutations[] = $boxPermutation;
}
}

usort($boxPermutations, static fn (PackedBox $a, PackedBox $b) => $b->getVolumeUtilisation() <=> $a->getVolumeUtilisation());
Expand All @@ -126,7 +139,7 @@ public function pack(): PackedBox
*
* @return PackedBox packed box
*/
private function packRotation(int $boxWidth, int $boxLength): PackedBox
private function packRotation(int $boxWidth, int $boxLength, ?OrientatedItem $firstItemOrientation): PackedBox
{
$this->logger->debug("[EVALUATING ROTATION] {$this->box->getReference()}", ['width' => $boxWidth, 'length' => $boxLength]);
$this->layerPacker->setBoxIsRotated($this->box->getInnerWidth() !== $boxWidth);
Expand All @@ -138,9 +151,13 @@ private function packRotation(int $boxWidth, int $boxLength): PackedBox
$layerStartDepth = self::getCurrentPackedDepth($layers);
$packedItemList = $this->getPackedItemList($layers);

if ($packedItemList->count() > 0) {
$firstItemOrientation = null;
}

// do a preliminary layer pack to get the depth used
$preliminaryItems = clone $items;
$preliminaryLayer = $this->layerPacker->packLayer($preliminaryItems, clone $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, 0, true);
$preliminaryLayer = $this->layerPacker->packLayer($preliminaryItems, clone $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, 0, true, $firstItemOrientation);
if (count($preliminaryLayer->getItems()) === 0) {
break;
}
Expand All @@ -150,7 +167,7 @@ private function packRotation(int $boxWidth, int $boxLength): PackedBox
$layers[] = $preliminaryLayer;
$items = $preliminaryItems;
} else { // redo with now-known-depth so that we can stack to that height from the first item
$layers[] = $this->layerPacker->packLayer($items, $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayerDepth, true);
$layers[] = $this->layerPacker->packLayer($items, $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayerDepth, true, $firstItemOrientation);
}
}

Expand All @@ -159,10 +176,10 @@ private function packRotation(int $boxWidth, int $boxLength): PackedBox

// having packed layers, there may be tall, narrow gaps at the ends that can be utilised
$maxLayerWidth = max(array_map(static fn (PackedLayer $layer) => $layer->getEndX(), $layers));
$layers[] = $this->layerPacker->packLayer($items, $this->getPackedItemList($layers), $maxLayerWidth, 0, 0, $boxWidth, $boxLength, $this->box->getInnerDepth(), $this->box->getInnerDepth(), false);
$layers[] = $this->layerPacker->packLayer($items, $this->getPackedItemList($layers), $maxLayerWidth, 0, 0, $boxWidth, $boxLength, $this->box->getInnerDepth(), $this->box->getInnerDepth(), false, null);

$maxLayerLength = max(array_map(static fn (PackedLayer $layer) => $layer->getEndY(), $layers));
$layers[] = $this->layerPacker->packLayer($items, $this->getPackedItemList($layers), 0, $maxLayerLength, 0, $boxWidth, $boxLength, $this->box->getInnerDepth(), $this->box->getInnerDepth(), false);
$layers[] = $this->layerPacker->packLayer($items, $this->getPackedItemList($layers), 0, $maxLayerLength, 0, $boxWidth, $boxLength, $this->box->getInnerDepth(), $this->box->getInnerDepth(), false, null);
}

$layers = $this->correctLayerRotation($layers, $boxWidth);
Expand Down
4 changes: 2 additions & 2 deletions tests/PackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ public function testIssue52A(): void
$packedBoxes = $packer->pack();

self::assertCount(1, $packedBoxes);
self::assertEquals(26, $packedBoxes->top()->getUsedWidth());
self::assertEquals(15, $packedBoxes->top()->getUsedLength());
self::assertEquals(30, $packedBoxes->top()->getUsedWidth());
self::assertEquals(13, $packedBoxes->top()->getUsedLength());
self::assertEquals(8, $packedBoxes->top()->getUsedDepth());
}

Expand Down
7 changes: 2 additions & 5 deletions tests/VolumePackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public function testUsedDimensionsCalculatedCorrectly(): void
$packer = new VolumePacker($box, $itemList);
$packedBox = $packer->pack();

self::assertEquals(60, $packedBox->getUsedWidth());
self::assertEquals(14, $packedBox->getUsedLength());
self::assertEquals(70, $packedBox->getUsedWidth());
self::assertEquals(12, $packedBox->getUsedLength());
self::assertEquals(2, $packedBox->getUsedDepth());
}

Expand Down Expand Up @@ -553,7 +553,6 @@ public function testIssue268(): void

public function testIssue272(): void
{
$this->markTestSkipped();
$box = new TestBox('Box', 725, 725, 650, 0, 725, 725, 650, 100000);

$items = new ItemList();
Expand Down Expand Up @@ -656,7 +655,6 @@ public function testIssue465C(): void

public function testIssue583(): void
{
$this->markTestSkipped();
$box = new TestBox('Example box', 380, 380, 140, 0, 380, 380, 140, 0);

$itemList = new ItemList();
Expand All @@ -665,7 +663,6 @@ public function testIssue583(): void

$packer = new VolumePacker($box, $itemList);
$packedBox = $packer->pack();
echo $packedBox->generateVisualisationURL();

self::assertCount(2, $packedBox->items);
}
Expand Down
Loading

0 comments on commit 27bb78e

Please sign in to comment.