Skip to content

Commit

Permalink
Backport packing enhancements from 3.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dvdoug committed Feb 25, 2018
1 parent cc1b762 commit 09423a5
Show file tree
Hide file tree
Showing 25 changed files with 965 additions and 596 deletions.
1 change: 1 addition & 0 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ build:
coverage:
file: 'build/behat.clover'
format: 'clover'
- php-scrutinizer-run
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
group: travis_latest
language: php
sudo: false

Expand Down
10 changes: 8 additions & 2 deletions behat.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
default:
suites:
packer:
paths:
- '%paths.base%/features/common'
contexts: [ PackerContext ]

extensions:
LeanPHP\Behat\CodeCoverage\Extension:
auth: ~
auth: ~
drivers:
- local
filter:
Expand All @@ -15,6 +21,6 @@ default:
'src':
prefix: ''
report:
format: clover
format: clover
options:
target: build/behat.clover
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

# General information about the project.
project = u'BoxPacker'
copyright = u'2017, Doug Wright'
copyright = u'2018, Doug Wright'
author = u'Doug Wright'

# The version info for the project you're documenting, acts as replacement for
Expand Down
1 change: 0 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ BoxPacker is licensed under the `MIT license`_.
You are reading the documentation for BoxPacker v1. Although still supported with occasional backports from newer versions,
all users are recommended to upgrade to v2/v3 which removes the "always keep flat" limitation on items.

.. _NP-hard problem: http://en.wikipedia.org/wiki/Bin_packing_problem
.. _MIT license: https://github.com/dvdoug/BoxPacker/blob/master/license.txt


Expand Down
2 changes: 0 additions & 2 deletions docs/principles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ At a high level, the algorithm works like this:
* Pack largest (by volume) items first
* Pack vertically up the side of the box
* Pack side-by-side where item under consideration fits alongside the previous item
* Only very small overhangs are allowed (10%) to prevent items bending in transit
* The available width/height for each layer will therefore decrease as the stack of items gets taller
* If more than 1 box is needed to accommodate all of the items, then aim for boxes of roughly equal weight
(e.g. 3 medium size/weight boxes are better than 1 small light box and 2 that are large and heavy)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@
/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context
class PackerContext implements Context
{
/** @var Box */
private $box;
protected $box;

/** @var BoxList */
private $boxList;
protected $boxList;

/** @var ItemList */
private $itemList;
protected $itemList;

/** @var PackedBox */
private $packedBox;
protected $packedBox;

/** @var PackedBoxList */
private $packedBoxList;
protected $packedBoxList;

/** @var string */
protected $packerClass = 'DVDoug\BoxPacker\Packer';

/**
* Initializes context.
Expand Down Expand Up @@ -121,7 +124,8 @@ public function thereIsAKeepFlatItem(
*/
public function iDoAPacking()
{
$packer = new Packer();
/** @var Packer $packer */
$packer = new $this->packerClass();
$packer->setBoxes($this->boxList);
$packer->setItems($this->itemList);
$this->packedBoxList = $packer->pack();
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion license.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (C) 2012-2017 Doug Wright
Copyright (C) 2012-2018 Doug Wright

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion src/ItemList.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function compare($itemA, $itemB)
/**
* Get copy of this list as a standard PHP array.
*
* @return array
* @return Item[]
*/
public function asArray()
{
Expand Down
4 changes: 3 additions & 1 deletion src/ItemTooLargeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

namespace DVDoug\BoxPacker;

use RuntimeException;

/**
* Class ItemTooLargeException
* Exception used when an item is too large to pack.
*/
class ItemTooLargeException extends \RuntimeException
class ItemTooLargeException extends RuntimeException
{
/** @var Item */
public $item;
Expand Down
70 changes: 70 additions & 0 deletions src/LayerStabiliser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
/**
* Box packing (3D bin packing, knapsack problem).
*
* @author Doug Wright
*/

namespace DVDoug\BoxPacker;

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

/**
* Applies load stability to generated result.
*
* @author Doug Wright
*/
class LayerStabiliser implements LoggerAwareInterface
{
use LoggerAwareTrait;

/**
* Constructor.
*/
public function __construct()
{
$this->logger = new NullLogger();
}

/**
* @param PackedLayer[] $packedLayers
*
* @return PackedLayer[]
*/
public function stabilise(array $packedLayers)
{
// first re-order according to footprint
$stabilisedLayers = [];
usort($packedLayers, [$this, 'compare']);

// then for each item in the layer, re-calculate each item's z position
$currentZ = 0;
foreach ($packedLayers as $oldZLayer) {
$oldZStart = $oldZLayer->getStartDepth();
$newZLayer = new PackedLayer();
foreach ($oldZLayer->getItems() as $oldZItem) {
$newZ = $oldZItem->getZ() - $oldZStart + $currentZ;
$newZItem = new PackedItem($oldZItem->getItem(), $oldZItem->getX(), $oldZItem->getY(), $newZ, $oldZItem->getWidth(), $oldZItem->getLength(), $oldZItem->getDepth());
$newZLayer->insert($newZItem);
}

$stabilisedLayers[] = $newZLayer;
$currentZ += $newZLayer->getDepth();
}

return $stabilisedLayers;
}

/**
* @param PackedLayer $layerA
* @param PackedLayer $layerB
*
* @return int
*/
private function compare(PackedLayer $layerA, PackedLayer $layerB)
{
return ($layerB->getFootprint() - $layerA->getFootprint()) ?: ($layerB->getDepth() - $layerA->getDepth());
}
}
96 changes: 51 additions & 45 deletions src/OrientatedItemFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,26 @@ class OrientatedItemFactory implements LoggerAwareInterface
{
use LoggerAwareTrait;

/** @var Item */
protected $item;

/** @var Box */
protected $box;

/**
* @var OrientatedItem[]
*/
protected static $emptyBoxCache = [];

public function __construct(Item $item, Box $box)
{
$this->item = $item;
$this->box = $box;
}

/**
* Get the best orientation for an item.
*
* @param Box $box
* @param Item $item
* @param OrientatedItem|null $prevItem
* @param Item|null $nextItem
* @param bool $isLastItem
Expand All @@ -39,17 +49,15 @@ class OrientatedItemFactory implements LoggerAwareInterface
* @return OrientatedItem|null
*/
public function getBestOrientation(
Box $box,
Item $item,
$prevItem,
$nextItem,
OrientatedItem $prevItem = null,
Item $nextItem = null,
$isLastItem,
$widthLeft,
$lengthLeft,
$depthLeft
) {
$possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
$usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
$possibleOrientations = $this->getPossibleOrientations($this->item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
$usableOrientations = $this->getUsableOrientations($possibleOrientations, $isLastItem);

if (empty($usableOrientations)) {
return;
Expand All @@ -72,9 +80,9 @@ public function getBestOrientation(
if ($nextItem) {
$nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
$nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
if ($nextItem && $nextItemFitA && !$nextItemFitB) {
if ($nextItemFitA && !$nextItemFitB) {
return -1;
} elseif ($nextItem && $nextItemFitB && !$nextItemFitA) {
} elseif ($nextItemFitB && !$nextItemFitA) {
return 1;
}
}
Expand Down Expand Up @@ -102,7 +110,7 @@ public function getBestOrientation(
*/
public function getPossibleOrientations(
Item $item,
$prevItem,
OrientatedItem $prevItem = null,
$widthLeft,
$lengthLeft,
$depthLeft
Expand All @@ -126,34 +134,31 @@ public function getPossibleOrientations(
}

/**
* @param Item $item
* @param Box $box
*
* @return OrientatedItem[]
*/
public function getPossibleOrientationsInEmptyBox(Item $item, Box $box)
public function getPossibleOrientationsInEmptyBox()
{
$cacheKey = $item->getWidth().
$cacheKey = $this->item->getWidth().
'|'.
$item->getLength().
$this->item->getLength().
'|'.
$item->getDepth().
$this->item->getDepth().
'|'.
$box->getInnerWidth().
$this->box->getInnerWidth().
'|'.
$box->getInnerLength().
$this->box->getInnerLength().
'|'.
$box->getInnerDepth();
$this->box->getInnerDepth();

if (isset(static::$emptyBoxCache[$cacheKey])) {
$orientations = static::$emptyBoxCache[$cacheKey];
} else {
$orientations = $this->getPossibleOrientations(
$item,
$this->item,
null,
$box->getInnerWidth(),
$box->getInnerLength(),
$box->getInnerDepth()
$this->box->getInnerWidth(),
$this->box->getInnerLength(),
$this->box->getInnerDepth()
);
static::$emptyBoxCache[$cacheKey] = $orientations;
}
Expand All @@ -163,34 +168,25 @@ public function getPossibleOrientationsInEmptyBox(Item $item, Box $box)

/**
* @param OrientatedItem[] $possibleOrientations
* @param Box $box
* @param Item $item
* @param bool $isLastItem
*
* @return OrientatedItem[]
*/
protected function getUsableOrientations(
$possibleOrientations,
Box $box,
Item $item,
$isLastItem
) {
/*
* Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
*/
$stableOrientations = [];
$unstableOrientations = [];
$orientationsToUse = $stableOrientations = $unstableOrientations = [];

foreach ($possibleOrientations as $o => $orientation) {
// Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
foreach ($possibleOrientations as $orientation) {
if ($orientation->isStable()) {
$stableOrientations[] = $orientation;
} else {
$unstableOrientations[] = $orientation;
}
}

$orientationsToUse = [];

/*
* We prefer to use stable orientations only, but allow unstable ones if either
* the item is the last one left to pack OR
Expand All @@ -199,14 +195,7 @@ protected function getUsableOrientations(
if (count($stableOrientations) > 0) {
$orientationsToUse = $stableOrientations;
} elseif (count($unstableOrientations) > 0) {
$orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);

$stableOrientationsInEmptyBox = array_filter(
$orientationsInEmptyBox,
function (OrientatedItem $orientation) {
return $orientation->isStable();
}
);
$stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox();

if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
$orientationsToUse = $unstableOrientations;
Expand All @@ -215,4 +204,21 @@ function (OrientatedItem $orientation) {

return $orientationsToUse;
}

/**
* Return the orientations for this item if it were to be placed into the box with nothing else.
*
* @return array
*/
protected function getStableOrientationsInEmptyBox()
{
$orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox();

return array_filter(
$orientationsInEmptyBox,
function (OrientatedItem $orientation) {
return $orientation->isStable();
}
);
}
}
Loading

0 comments on commit 09423a5

Please sign in to comment.