Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added random item and container generation buttons. Modal messaging. Formatting. #24

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 12 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,28 @@
## 3D Container Packing in C#

This is a C# library that can be used to find 3D container packing solutions (also known as 3D bin packing). It includes an implementation of the EB-AFIT packing algorithm originally developed as a master's thesis project by Erhan Baltacıoğlu (EB) at the U.S. Air Force Institute of Technology (AFIT) in 2001. This algorithm is also described in The Distributor's Three-Dimensional Pallet-Packing Problem: A Human Intelligence-Based Heuristic Approach, by Erhan Baltacıoğlu, James T. Moore, and Raymond R. Hill Jr., published in the International Journal of Operational Research in 2006 (volume 1, issue 3).
This is a fork of David Chapman's C# library that can be used to find 3D container packing solutions (also known as 3D bin packing). It includes an implementation of the EB-AFIT packing algorithm originally developed as a master's thesis project by Erhan Baltacıoğlu (EB) at the U.S. Air Force Institute of Technology (AFIT) in 2001 [Link to Abstract](http://betterwaysystems.github.io/packer/reference/AirForceBinPacking.pdf). This algorithm is also described in The Distributor's Three-Dimensional Pallet-Packing Problem: A Human Intelligence-Based Heuristic Approach, by Erhan Baltacıoğlu, James T. Moore, and Raymond R. Hill Jr., published in the International Journal of Operational Research in 2006 (volume 1, issue 3).

The EB-AFIT algorithm supports full item rotation and has excellent runtime performance and container utilization.

## Usage

Start by including the ContainerPacking project in your solution.
This extends the original project in the following ways:

Create a list of Container objects, which describes the dimensions of the containers:
* Randomization of packing units and items
* Updates the 3D model to display labels of each item (adds label property)
* Adds gross weight property
* Allows selection of one or more packing units, displaying weight and volume
* Allows selection of stops, so that containers can be loaded according when they need to be removed first

List<Container> containers = new List<Container>();
containers.Add(new Container(id, length, width, height));
...
Future:

Create a list of items to pack:

List<Item> itemsToPack = new List<Item>();
itemsToPack.Add(new Item(id, dim1, dim2, dim3, quantity));
...

Create a list of algorithm IDs corresponding to the algorithms you would like to use. (Currently EB-AFIT is the only algorithm implemented.) Algorithm IDs are listed in the AlgorithmType enum.

List<int> algorithms = new List<int>();
algorithms.Add((int)AlgorithmType.EB_AFIT);
...

Call the Pack method on your container list, item list, and algorithm list:

List<ContainerPackingResult> result = PackingService.Pack(containers, itemsToPack, algorithms);

The list of ContainerPackingResults contains a ContainerPackingResult object for each container. Within each ContainerPackingResult is the container ID and a list of AlgorithmPackingResult objects, one for each algorithm requested. Within each algorithm packing result is the name and ID of the algorithm used, a list of items that were successfully packed, a list of items that could not be packed, and a few other packing metrics. The items in the packed list are in pack order and include x, y, and z coordinates and x, y, and z pack dimensions. This information is useful if you want to attach a visualization tool to display the packed items in their proper pack locations and orientations.

Internally, the Pack() method will try to pack all the containers with all the items using all the requested algorithms in parallel. If you have a list of containers you want to try, but want them to run serially, then you can call Pack() with one container at a time. For example, if you want to run a large set of containers but would like to update the user interface as each one finishes, then you would want to call Pack() multiple times asynchronously and update the UI as each result returns.

## Demo WebAPI Application

This project also includes a demo web application that lets the user specify an arbitrary set of items, an arbitrary set of containers, and the packing algorithms to use. AJAX packing requests are sent to the server and handled by a WebAPI controller. Once returned, each pack solution can be viewed in the WebGL visualization tool by clicking the camera icon.

![Container packing visualization](https://github.com/davidmchapman/3DContainerPacking/blob/master/images/packing-1.gif?raw=true "Container Packing")
* Allow generation of weight heatmap, so loading algorthim favors forward and center loads

## Acknowledgements and Related Projects

This project would not have been possible without the support of Dr. Raymond Hill at the Air Force Institute of Technology. It also leans heavily on the original C code included in Erhan Baltacıoğlu's thesis, which was discovered and resurrected by Bill Knechtel (GitHub user wknechtel), and ported to JavaScript by GitHub user keremdemirer.
This project would not have been possible without the support of Dr. Raymond Hill at the Air Force Institute of Technology. It also leans heavily on the original C code included in Erhan Baltacıoğlu's thesis, which was discovered and resurrected by Bill Knechtel (GitHub user wknechtel), and ported to JavaScript by GitHub user keremdemirer. Also to David Chapman for his work on putting this together in .NET, and letting me carry the torch.

https://github.com/davidmchapman/3DContainerPacking

https://github.com/wknechtel/3d-bin-pack/

Expand Down
27 changes: 24 additions & 3 deletions src/CromulentBisgetti.DemoApp/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@{
@{
Layout = null;
}

Expand Down Expand Up @@ -99,7 +99,10 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title pull-left">Items to Pack</h3>
<div class="pull-right"><button class="btn btn-default btn-sm" data-bind="click: GenerateItemsToPack">Generate</button></div>
<div class="pull-right">
<button class="btn btn-default btn-sm" data-toggle="tooltip" data-placement="top" title="Generate random items" data-bind="click: RandomizeItemsToPack">Randomize</button>
<button class="btn btn-default btn-sm" data-toggle="tooltip" data-placement="top" title="Generate static items" data-bind="click: GenerateItemsToPack">Generate</button>
</div>
<div class="clearfix"></div>
</div>
<div class="panel-body">
Expand Down Expand Up @@ -150,7 +153,8 @@
<div class="panel-heading">
<h3 class="panel-title pull-left">Containers</h3>
<div class="pull-right">
<button class="btn btn-default btn-sm" data-bind="click: GenerateContainers">Generate</button>
<button class="btn btn-default btn-sm" data-toggle="tooltip" data-placement="top" title="Generate random containers" data-bind="click: RandomizeContainers">Randomize</button>
<button class="btn btn-default btn-sm" data-toggle="tooltip" data-placement="top" title="Generate static containers" data-bind="click: GenerateContainers">Generate</button>
<button class="btn btn-primary btn-sm" data-bind="click: PackContainers">Pack Em Up</button>
</div>
<div class="clearfix"></div>
Expand Down Expand Up @@ -255,5 +259,22 @@
</div>
</div>
</div>

<!-- User messages -->
<div class="modal fade" id="messageModal" tabindex="-1" role="dialog" aria-labelledby="modalTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="modalTitle"><span class="glyphicon glyphicon-warning-sign"></span>&nbsp;Warning</h3>
</div>
<div id="messageContent" class="modal-body">

</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</body>
</html>
124 changes: 93 additions & 31 deletions src/CromulentBisgetti.DemoApp/wwwroot/js/container-packing.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,45 @@ function InitializeDrawing() {
var container = $('#drawing-container');

scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 50, window.innerWidth/window.innerHeight, 0.1, 1000 );
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.lookAt(scene.position);

//var axisHelper = new THREE.AxisHelper( 5 );
//scene.add( axisHelper );

// LIGHT
var light = new THREE.PointLight(0xffffff);
light.position.set(0,150,100);
light.position.set(0, 150, 100);
scene.add(light);

// Get the item stuff ready.
itemMaterial = new THREE.MeshNormalMaterial( { transparent: true, opacity: 0.6 } );
itemMaterial = new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.6 });

renderer = new THREE.WebGLRenderer( { antialias: true } ); // WebGLRenderer CanvasRenderer
renderer.setClearColor( 0xf0f0f0 );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth / 2, window.innerHeight / 2);
container.append( renderer.domElement );
renderer = new THREE.WebGLRenderer({ antialias: true }); // WebGLRenderer CanvasRenderer
renderer.setClearColor(0xf0f0f0);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth / 2, window.innerHeight / 2);
container.append(renderer.domElement);

controls = new THREE.OrbitControls( camera, renderer.domElement );
window.addEventListener( 'resize', onWindowResize, false );
controls = new THREE.OrbitControls(camera, renderer.domElement);
window.addEventListener('resize', onWindowResize, false);

animate();
};

function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth / 2, window.innerHeight / 2 );
renderer.setSize(window.innerWidth / 2, window.innerHeight / 2);
}
//
function animate() {
requestAnimationFrame( animate );
requestAnimationFrame(animate);
controls.update();
render();
}
function render() {
renderer.render( scene, camera );
renderer.render(scene, camera);
}

var ViewModel = function () {
Expand Down Expand Up @@ -90,7 +90,7 @@ var ViewModel = function () {
self.ItemsToPack.push(ko.mapping.fromJS({ ID: 1004, Name: 'Item5', Length: 17, Width: 8, Height: 6, Quantity: 1 }));
self.ItemsToPack.push(ko.mapping.fromJS({ ID: 1005, Name: 'Item6', Length: 3, Width: 3, Height: 2, Quantity: 2 }));
};

self.GenerateContainers = function () {
self.Containers([]);
self.Containers.push(ko.mapping.fromJS({ ID: 1000, Name: 'Box1', Length: 15, Width: 13, Height: 9, AlgorithmPackingResults: [] }));
Expand All @@ -108,6 +108,49 @@ var ViewModel = function () {
self.Containers.push(ko.mapping.fromJS({ ID: 1012, Name: 'Box13', Length: 60, Width: 60, Height: 60, AlgorithmPackingResults: [] }));
};

self.RandomizeItemsToPack = function () {
self.ItemsToPack([]);

var itemCount = getRandomInt(1002, 1012)

for (var x = 1000; x < itemCount; x++) {
var length = getRandomInt(1, 14);
var height = getRandomInt(1, 10);
var width = getRandomInt(1, 10);
var quantity = getRandomInt(1, 10);
var name = "({0}) {1}'L X {2}'W X {3}'H Boxes".format(quantity, length, width, height);
self.ItemsToPack.push(ko.mapping.fromJS({ ID: x, Name: name, Length: length, Width: width, Height: height, Quantity: quantity }));
}
};

self.RandomizeContainers = function () {
self.Containers([]);
var containerCount = getRandomInt(1002, 1020)

for (var x = 1000; x < containerCount; x++) {
var length = getRandomInt(10, 53);
var height = getRandomInt(7, 11);
var width = getRandomInt(7, 11);
var quantity = getRandomInt(1, 10);
var name = "One {0}'L X {1}'W X {2}'H Container".format(length, width, height);
self.Containers.push(ko.mapping.fromJS({ ID: x, Name: name, Length: length, Width: width, Height: height, AlgorithmPackingResults: [] }));
}
};

function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}

String.prototype.format = function () {
var args = arguments;
return this.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined'
? args[number]
: match
;
});
};

self.AddAlgorithmToUse = function () {
var algorithmID = $('#algorithm-select option:selected').val();
var algorithmName = $('#algorithm-select option:selected').text();
Expand Down Expand Up @@ -151,7 +194,7 @@ var ViewModel = function () {
self.AlgorithmsToUse().forEach(algorithm => {
algorithmsToUse.push(algorithm.AlgorithmID);
});

var itemsToPack = [];

self.ItemsToPack().forEach(item => {
Expand All @@ -162,10 +205,10 @@ var ViewModel = function () {
Dim3: item.Height(),
Quantity: item.Quantity()
};

itemsToPack.push(itemToPack);
});

var containers = [];

// Send a packing request for each container in the list.
Expand All @@ -179,14 +222,33 @@ var ViewModel = function () {

containers.push(containerToUse);
});


// Some validation before packing.
if (algorithmsToUse.length == 0) {
$("#messageContent").text("Please select an algorithm to use for packing.");
$('#messageModal').modal('show');
return;
}

if (itemsToPack.length == 0) {
$("#messageContent").text("Please add one or more items to pack.");
$('#messageModal').modal('show');
return;
}

if (containers.length == 0) {
$("#messageContent").text("Please add one or more containers to pack items into.");
$('#messageModal').modal('show');
return;
}

// Build container packing request.
var request = {
Containers: containers,
ItemsToPack: itemsToPack,
AlgorithmTypeIDs: algorithmsToUse
};

PackContainers(JSON.stringify(request))
.then(response => {
// Tie this response back to the correct containers.
Expand All @@ -199,17 +261,17 @@ var ViewModel = function () {
});
});
};

self.ShowPackingView = function (algorithmPackingResult) {
var container = this;
var selectedObject = scene.getObjectByName('container');
scene.remove( selectedObject );
scene.remove(selectedObject);

for (var i = 0; i < 1000; i++) {
var selectedObject = scene.getObjectByName('cube' + i);
scene.remove(selectedObject);
}

camera.position.set(container.Length(), container.Length(), container.Length());

self.ItemsToRender(algorithmPackingResult.PackedItems);
Expand All @@ -220,12 +282,12 @@ var ViewModel = function () {
self.ContainerOriginOffset.z = -1 * container.Width() / 2;

var geometry = new THREE.BoxGeometry(container.Length(), container.Height(), container.Width());
var geo = new THREE.EdgesGeometry( geometry ); // or WireframeGeometry( geometry )
var mat = new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 2 } );
var wireframe = new THREE.LineSegments( geo, mat );
var geo = new THREE.EdgesGeometry(geometry); // or WireframeGeometry( geometry )
var mat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 });
var wireframe = new THREE.LineSegments(geo, mat);
wireframe.position.set(0, 0, 0);
wireframe.name = 'container';
scene.add( wireframe );
scene.add(wireframe);
};

self.AreItemsPacked = function () {
Expand Down Expand Up @@ -257,14 +319,14 @@ var ViewModel = function () {
var cube = new THREE.Mesh(itemGeometry, itemMaterial);
cube.position.set(self.ContainerOriginOffset.x + itemOriginOffset.x + self.ItemsToRender()[itemIndex].CoordX, self.ContainerOriginOffset.y + itemOriginOffset.y + self.ItemsToRender()[itemIndex].CoordY, self.ContainerOriginOffset.z + itemOriginOffset.z + self.ItemsToRender()[itemIndex].CoordZ);
cube.name = 'cube' + itemIndex;
scene.add( cube );
scene.add(cube);

self.LastItemRenderedIndex(itemIndex);
};

self.UnpackItemInRender = function () {
var selectedObject = scene.getObjectByName('cube' + self.LastItemRenderedIndex());
scene.remove( selectedObject );
scene.remove(selectedObject);
self.LastItemRenderedIndex(self.LastItemRenderedIndex() - 1);
};
};
Expand All @@ -275,7 +337,7 @@ var ItemToPack = function () {
this.Length = '';
this.Width = '';
this.Height = '',
this.Quantity = '';
this.Quantity = '';
}

var Container = function () {
Expand All @@ -288,7 +350,7 @@ var Container = function () {
}

$(document).ready(() => {
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="tooltip"]').tooltip();
InitializeDrawing();

viewModel = new ViewModel();
Expand Down