Skip to content

Commit

Permalink
Backport packing improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
dvdoug committed Jul 26, 2023
1 parent dab2820 commit b351ea0
Show file tree
Hide file tree
Showing 14 changed files with 800 additions and 444 deletions.
18 changes: 13 additions & 5 deletions src/LayerPacker.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class LayerPacker implements LoggerAwareInterface

private bool $beStrictAboutItemOrdering = false;

private bool $isBoxRotated = false;

public function __construct(Box $box)
{
$this->box = $box;
Expand All @@ -58,6 +60,12 @@ public function setSinglePassMode(bool $singlePassMode): void
$this->orientatedItemFactory->setSinglePassMode($singlePassMode);
}

public function setBoxIsRotated(bool $boxIsRotated): void
{
$this->isBoxRotated = $boxIsRotated;
$this->orientatedItemFactory->setBoxIsRotated($boxIsRotated);
}

public function beStrictAboutItemOrdering(bool $beStrict): void
{
$this->beStrictAboutItemOrdering = $beStrict;
Expand Down Expand Up @@ -106,28 +114,28 @@ public function packLayer(ItemList &$items, PackedItemList $packedItemList, int
$x += $packedItem->getWidth();
$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->getWidth(), $y + $packedItem->getLength(), $z, $x, $y + $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));

if ($items->count() === 0 && $skippedItems) {
$items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
$skippedItems = [];
}

continue;
}

if (!$this->beStrictAboutItemOrdering && $items->count() > 0) { // skip for now, move on to the next item
$this->logger->debug("doesn't fit, skipping for now");
$skippedItems[] = $itemToPack;
// abandon here if next item is the same, no point trying to keep going. Last time is not skipped, need that to trigger appropriate reset logic
while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
while ($items->count() > 1 && self::isSameDimensions($itemToPack, $items->top())) {
$skippedItems[] = $items->extract();
}
continue;
}

if ($x > $startX) {
// Having now placed items, there is space *within the same row* along the length. Pack into that.
$this->logger->debug('No more fit in width wise, packing along remaining length');
$layer->merge($this->packLayer($items, $packedItemList, $x, $y + $rowLength, $z, $widthForLayer, $lengthForLayer - $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));

$this->logger->debug('No more fit in width wise, resetting for new row');
$y += $rowLength;
$x = $startX;
Expand Down
2 changes: 1 addition & 1 deletion src/OrientatedItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class OrientatedItem implements JsonSerializable
protected int $surfaceFootprint;

/**
* @var bool[]
* @var array<string, bool>
*/
protected static array $stabilityCache = [];

Expand Down
47 changes: 39 additions & 8 deletions src/OrientatedItemFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace DVDoug\BoxPacker;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

use function array_filter;
Expand All @@ -23,7 +23,7 @@
*/
class OrientatedItemFactory implements LoggerAwareInterface
{
use LoggerAwareTrait;
protected LoggerInterface $logger;

protected Box $box;

Expand All @@ -32,22 +32,34 @@ class OrientatedItemFactory implements LoggerAwareInterface
*/
protected bool $singlePassMode = false;

protected bool $boxIsRotated = false;

/**
* @var bool[]
* @var array<string, bool>
*/
protected static $emptyBoxStableItemOrientationCache = [];
protected static array $emptyBoxStableItemOrientationCache = [];

public function __construct(Box $box)
{
$this->box = $box;
$this->logger = new NullLogger();
}

public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}

public function setSinglePassMode(bool $singlePassMode): void
{
$this->singlePassMode = $singlePassMode;
}

public function setBoxIsRotated(bool $boxIsRotated): void
{
$this->boxIsRotated = $boxIsRotated;
}

/**
* Get the best orientation for an item.
*/
Expand All @@ -74,6 +86,11 @@ public function getBestOrientation(
'lengthLeft' => $lengthLeft,
'depthLeft' => $depthLeft,
],
'position' => [
'x' => $x,
'y' => $y,
'z' => $z,
],
]
);

Expand All @@ -84,8 +101,7 @@ public function getBestOrientation(
return null;
}

$sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList);
$sorter->setLogger($this->logger);
$sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, $this->logger);
usort($usableOrientations, $sorter);

$this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
Expand Down Expand Up @@ -120,8 +136,20 @@ public function getPossibleOrientations(
}

if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
$orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
return $i->getItem()->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
$orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList): bool {
/** @var ConstrainedPlacementItem $constrainedItem */
$constrainedItem = $i->getItem();

if ($this->boxIsRotated) {
$rotatedPrevPackedItemList = new PackedItemList();
foreach ($prevPackedItemList as $prevPackedItem) {
$rotatedPrevPackedItemList->insert(new PackedItem($prevPackedItem->getItem(), $prevPackedItem->getY(), $prevPackedItem->getX(), $prevPackedItem->getZ(), $prevPackedItem->getLength(), $prevPackedItem->getWidth(), $prevPackedItem->getDepth()));
}

return $constrainedItem->canBePacked($this->box, $rotatedPrevPackedItemList, $y, $x, $z, $i->getLength(), $i->getWidth(), $i->getDepth());
} else {
return $constrainedItem->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
}
});
}

Expand Down Expand Up @@ -206,6 +234,9 @@ protected function hasStableOrientationsInEmptyBox(Item $item): bool
return static::$emptyBoxStableItemOrientationCache[$cacheKey];
}

/**
* @return array<array<int>>
*/
private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
{
// Special case items that are the same as what we just packed - keep orientation
Expand Down
14 changes: 7 additions & 7 deletions src/OrientatedItemSorter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

namespace DVDoug\BoxPacker;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;

use function max;
use function min;
Expand All @@ -21,10 +20,8 @@
*
* @internal
*/
class OrientatedItemSorter implements LoggerAwareInterface
class OrientatedItemSorter
{
use LoggerAwareTrait;

/**
* @var int[]
*/
Expand Down Expand Up @@ -52,7 +49,9 @@ class OrientatedItemSorter implements LoggerAwareInterface

private PackedItemList $prevPackedItemList;

public function __construct(OrientatedItemFactory $factory, bool $singlePassMode, int $widthLeft, int $lengthLeft, int $depthLeft, ItemList $nextItems, int $rowLength, int $x, int $y, int $z, PackedItemList $prevPackedItemList)
private LoggerInterface $logger;

public function __construct(OrientatedItemFactory $factory, bool $singlePassMode, int $widthLeft, int $lengthLeft, int $depthLeft, ItemList $nextItems, int $rowLength, int $x, int $y, int $z, PackedItemList $prevPackedItemList, LoggerInterface $logger)
{
$this->orientatedItemFactory = $factory;
$this->singlePassMode = $singlePassMode;
Expand All @@ -65,9 +64,10 @@ public function __construct(OrientatedItemFactory $factory, bool $singlePassMode
$this->y = $y;
$this->z = $z;
$this->prevPackedItemList = $prevPackedItemList;
$this->logger = $logger;
}

public function __invoke(OrientatedItem $a, OrientatedItem $b)
public function __invoke(OrientatedItem $a, OrientatedItem $b): int
{
// Prefer exact fits in width/length/depth order
$orientationAWidthLeft = $this->widthLeft - $a->getWidth();
Expand Down
21 changes: 13 additions & 8 deletions src/Packer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace DVDoug\BoxPacker;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use SplObjectStorage;
Expand All @@ -25,7 +25,7 @@
*/
class Packer implements LoggerAwareInterface
{
use LoggerAwareTrait;
private LoggerInterface $logger;

/**
* Number of boxes at which balancing weight is deemed not worth it.
Expand All @@ -39,7 +39,7 @@ class Packer implements LoggerAwareInterface
/**
* @var SplObjectStorage<Box, int>
*/
protected SplObjectStorage $boxesQtyAvailable;
protected SplObjectStorage $boxQuantitiesAvailable;

protected PackedBoxSorter $packedBoxSorter;

Expand All @@ -49,12 +49,17 @@ public function __construct()
{
$this->items = new ItemList();
$this->boxes = new BoxList();
$this->boxesQtyAvailable = new SplObjectStorage();
$this->boxQuantitiesAvailable = new SplObjectStorage();
$this->packedBoxSorter = new DefaultPackedBoxSorter();

$this->logger = new NullLogger();
}

public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}

/**
* Add item to be packed.
*/
Expand Down Expand Up @@ -106,7 +111,7 @@ public function setBoxes(BoxList $boxList): void
*/
public function setBoxQuantity(Box $box, int $qty): void
{
$this->boxesQtyAvailable[$box] = $qty;
$this->boxQuantitiesAvailable[$box] = $qty;
}

/**
Expand Down Expand Up @@ -144,7 +149,7 @@ public function pack(): PackedBoxList

// If we have multiple boxes, try and optimise/even-out weight distribution
if (!$this->beStrictAboutItemOrdering && $packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) {
$redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxesQtyAvailable);
$redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxQuantitiesAvailable);
$redistributor->setLogger($this->logger);
$packedBoxes = $redistributor->redistributeWeight($packedBoxes);
}
Expand Down Expand Up @@ -197,7 +202,7 @@ public function doVolumePacking(bool $singlePassMode = false, bool $enforceSingl
$this->items->removePackedItems($bestBox->getItems());

$packedBoxes->insert($bestBox);
$this->boxesQtyAvailable[$bestBox->getBox()] = $this->boxesQtyAvailable[$bestBox->getBox()] - 1;
$this->boxQuantitiesAvailable[$bestBox->getBox()] = $this->boxQuantitiesAvailable[$bestBox->getBox()] - 1;
}

return $packedBoxes;
Expand All @@ -218,7 +223,7 @@ protected function getBoxList(bool $enforceSingleBox = false): iterable
$preferredBoxes = [];
$otherBoxes = [];
foreach ($this->boxes as $box) {
if ($this->boxesQtyAvailable[$box] > 0) {
if ($this->boxQuantitiesAvailable[$box] > 0) {
if ($box->getInnerWidth() * $box->getInnerLength() * $box->getInnerDepth() >= $itemVolume) {
$preferredBoxes[] = $box;
} elseif (!$enforceSingleBox) {
Expand Down
8 changes: 6 additions & 2 deletions src/VolumePacker.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public function pack(): PackedBox
private function packRotation(int $boxWidth, int $boxLength): PackedBox
{
$this->logger->debug("[EVALUATING ROTATION] {$this->box->getReference()}", ['width' => $boxWidth, 'length' => $boxLength]);
$this->layerPacker->setBoxIsRotated($this->box->getInnerWidth() !== $boxWidth);

/** @var PackedLayer[] $layers */
$layers = [];
Expand All @@ -160,11 +161,12 @@ private function packRotation(int $boxWidth, int $boxLength): PackedBox
break;
}

if ($preliminaryLayer->getDepth() === $preliminaryLayer->getItems()[0]->getDepth()) { // preliminary === final
$preliminaryLayerDepth = $preliminaryLayer->getDepth();
if ($preliminaryLayerDepth === $preliminaryLayer->getItems()[0]->getDepth()) { // preliminary === final
$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, $preliminaryLayer->getDepth(), true);
$layers[] = $this->layerPacker->packLayer($items, $packedItemList, 0, 0, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayerDepth, true);
}
}

Expand Down Expand Up @@ -208,6 +210,8 @@ private function stabiliseLayers(array $oldLayers): array
* Swap back width/length of the packed items to match orientation of the box if needed.
*
* @param PackedLayer[] $oldLayers
*
* @return PackedLayer[]
*/
private function correctLayerRotation(array $oldLayers, int $boxWidth): array
{
Expand Down
Loading

0 comments on commit b351ea0

Please sign in to comment.