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 28, 2023
1 parent 6d7d9c5 commit 89e209a
Show file tree
Hide file tree
Showing 17 changed files with 939 additions and 535 deletions.
18 changes: 13 additions & 5 deletions src/LayerPacker.php
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
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
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
16 changes: 8 additions & 8 deletions src/OrientatedItemSorter.php
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,12 +20,10 @@
*
* @internal
*/
class OrientatedItemSorter implements LoggerAwareInterface
class OrientatedItemSorter
{
use LoggerAwareTrait;

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

Expand All @@ -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
7 changes: 1 addition & 6 deletions src/PackedItemList.php
Expand Up @@ -23,17 +23,12 @@
class PackedItemList implements Countable, IteratorAggregate
{
/**
* List containing items.
*
* @var PackedItem[]
*/
private array $list = [];

private int $weight = 0;

/**
* Has this list already been sorted?
*/
private bool $isSorted = false;

public function insert(PackedItem $item): void
Expand All @@ -43,7 +38,7 @@ public function insert(PackedItem $item): void
}

/**
* @return Traversable|PackedItem[]
* @return Traversable<PackedItem>
*/
public function getIterator(): Traversable
{
Expand Down

0 comments on commit 89e209a

Please sign in to comment.