Rectangle Packaging Problem / Efficient 2D Packing
Summary
Automatically fit 2D rectangular parts onto standard-sized CNC panels to reduce waste and manual layout work. This post outlines a Python-based approach using the rectpack library and OpenSCAD for visualization and tool path.
Using rectpack
for Automated Layout
There’s a Python library called rectpack
that implements several efficient algorithms for this task:
https://github.com/secnot/rectpack
Background
I need to produce a bunch of 2D parts out of the same material. The parts are roughly rectangular and I need to produce an undefined large number (test jig stands for TheJigsApp.com). There are a ton of available panel sizes but the CNC I’m using can fit panels of 25x36" max. So our problem is to fit different size rectangles into a given rectangle [Wikipedia: Rectangle Packing].
One interesting paper on the topic is available here. The author’s original study code is up on Github here.
rectpack Library & Code Design
There’s a python library available that implements a few of the more efficient algorithms available [here].
Inputs
- Panel Size
- List of rectangle sizes
Outputs
- Optimal number of panels of a given size to minimize waste
- Total wasted area
- Percentage of unused area
- Number of sets packed
- Spreadsheet with rectangle orientation/placement data
After packing, I visually verify placement using OpenSCAD.
Workflow Overview
- A CSV file defines part dimensions.
- Python uses
rectpack
to iteratively try panel configurations. - The best packing (highest % used) is chosen.
- Outputs include:
- Excel summary
- OpenSCAD model for visual confirmation
- Results are rendered to review placement and panel usage.
Code
from rectpack import newPacker
import pandas
import jinja2
import numpy as np
import pprint
import click
def main(rects, width, length, bins=1):
packer = newPacker()
for rect in rects:
packer.add_rect(*rect)
for i in range(bins):
packer.add_bin(width, length)
packer.pack()
all_rects = packer.rect_list()
if len(all_rects) != len(rects):
raise UserWarning("Cant fit rectangles %d/%d", len(all_rects), len(rects))
dfout = {"bin": [], "x": [], "y":[], "w":[], "h":[], "area":[], "rotation":[]}
for rect, transform in zip(rects, all_rects):
b, x, y, w, h, rid = transform
dfout["bin"].append(b)
dfout["x"].append(x)
dfout["y"].append(y)
dfout["w"].append(w)
dfout["h"].append(h)
dfout["area"].append(h*w)
for rect in rects:
if (rect[0] == w and rect[1] == h):
dfout["rotation"].append(0)
break
elif (rect[0] == h and rect[1] == w):
dfout["rotation"].append(90)
break
else:
print("error ", rect, transform)
assert(0, "part not in expected order")
return dfout
def to_scad(df):
'''
Return scad as a string
'''
template = '''function get_shapes() = [
{% for _, l in df.iterrows() -%}
[{{l['bin']}}, {{l['x']}}, {{l['y']}}, {{l['w']}}, {{l['h']}}],
{% endfor %}
];
'''
return jinja2.Template(template).render(df=df)
def optimize(panels, max_bins):
config = {"nbins": [], "panel width": [], "panel length": [], "packing": [], "% used": []}
dfset = pandas.read_csv(f"rectangles.csv")
rect_set = [[line["width"], line["length"], line["name"]] for _, line in dfset.iterrows()]
for panel in panels:
for nbin in range(1, max_bins+1):
nsets = 1
while True:
try:
d = main(list(rect_set)*nsets, width=panel[0], length=panel[1], bins=nbin)
df = pandas.DataFrame(d)
config["packing"].append(df)
config["nbins"].append(nbin)
config["panel width"].append(panel[0])
config["panel length"].append(panel[1])
config["% used"].append(sum(df["w"]*df["h"])/(nbin*panel[0]*panel[1]))
nsets+=1
except UserWarning:
break
return pandas.DataFrame(config)
@click.command()
@click.option("--bins", "-n", type=int, default=1)
@click.option("--design", required=True)
@click.option("--output-scad", default="shapes.scad")
@click.option("--output-xlsx", default="shapes.xlsx")
def click_main(bins, design, output_scad, output_xlsx):
panels = [
[11.5, 35.5],
[23.5, 35.5],
]
config_df = optimize(panels=panels, max_bins=bins)
max_percent = np.max(config_df["% used"])
i = list(config_df["% used"]).index(max_percent)
df = config_df.iloc[i]
df_shapes = df["packing"]
df_shapes.to_excel(output_xlsx)
with open(output_scad, "w") as f:
f.write(to_scad(df_shapes))
pprint.pprint(list(config_df["% used"]))
pprint.pprint(config_df)
print(f"Min Error: {max_percent}\n Panel Size: {df['panel width']}x{df['panel length']}. nbins: {df['nbins']}")
click_main()
Our design file:
name | width | length |
---|---|---|
upright 1 | 1.5 | 13.5 |
upright 2 | 1.5 | 13.5 |
short side 1 | 2 | 8 |
short side 2 | 2 | 8 |
long side 1 | 2 | 9 |
long side 2 | 2 | 9 |
top | 7.5 | 9 |
button | 1.5 | 9 |
cross | 4.5 | 8.5 |
OpenSCAD Visualization
After optimization, I used OpenSCAD to visually verify the layout and spacing. The code below imports the results and displays all the parts across multiple panels (bins). Each rectangle is shown with a small gap to highlight spacing.
use <./shapes.scad>;
/*
# b - Bin index
# x - Rectangle bottom-left corner x coordinate
# y - Rectangle bottom-left corner y coordinate
# w - Rectangle width
# h - Rectangle height
# rid - User assigned rectangle id or None
*/
bins = 8;
width = 24;
gap=0.5;
length=36;
space = 25;
difference() {
for (i=[0:bins-1]) {
translate([(width+space)*i, 0])square([width,length]);
};
union() {
for (shape=get_shapes()) {
translate([shape[1]+(width+space)*shape[0]+gap/2, shape[2]+gap/2])square([shape[3], shape[4]], center=false);
};
};
};
echo(len(get_shapes()));
Example Execution
python3 pack.py --design rectangles.csv --bins 8
Example result:
Min Error: 0.9099990010987913
Panel Size: 23.5x35.5. nbins: 3
In this case, the best solution used three 23.5x35.5" panels with ~9% material waste.
Visual Results
We’re left with the following wasted outline from panels of 24"x36" (including a 1/4" keep out around the panel).
Negative space (waste) after optimal packing:
Positive space (used areas) showing placement over 3 panels:
Expanding the space around the parts a bit shows how the packing is done:
Final Thoughts
This approach gives the arrangement of the rectangles so I will need to go and match the size of each object to a suitable slot. You can do this in a cad program of choice or with OpenSCAD as long as you’re careful with the import/export formats. DXF works well in both directions but you need to worry about document size and line thickness if you’re using dxf. Seems to work well and this definitely speeds up the process!