Bin Packing
A Rust-first cut list and bin packing optimization library for one-dimensional
cutting stock (linear bar / pipe stock), two-dimensional rectangular sheet
stock, and three-dimensional box-into-bin packing problems. The crate is
#![forbid(unsafe_code)], integer-math oriented, and serde-friendly so
problems and solutions travel cleanly across process boundaries and language
bindings.
- Core crate: crates/bin-packing — published to
crates.io as
bin-packing. - Node.js bindings: bindings/node — published to npm as
@0xdoublesharp/bin-packing, powered bynapi-rs. - WebAssembly bindings: bindings/wasm — published to npm as
@0xdoublesharp/bin-packing-wasm, powered bywasm-bindgen. Runs in browsers, Node.js, Deno, Bun, and Cloudflare Workers. - Fuzz targets: fuzz/ — libFuzzer harnesses for the 1D, 2D, and 3D entry points.
Features
1D cutting stock
- Multi-stock support. Mix any number of stock dimensions with independent
length,kerf,trim,cost, and optional inventoryavailablecaps. - Kerf and trim modeling. Each layout deducts trim from usable length and charges kerf between adjacent cuts, so results match physical cut lists.
- Inventory-aware procurement. When stock has
availablecaps, the solver returns both the capped solution and a relaxed-inventory estimate so you can see per-stockused_quantity,required_quantity, andadditional_quantity_needed. - Multi-objective ranking. Solutions are compared lexicographically by
(unplaced, stock_count, total_waste, total_cost, !exact)so better heuristic candidates win automatically inAutomode. - Lower bounds. The column-generation backend reports an LP lower bound
and marks the solution
exact = truewhen it matches the incumbent. - Structured errors. Public boundary validation fails fast with typed
BinPackingErrorvariants instead of panics.
3D box packing
- Multi-bin support. Mix any number of bin types with independent
width,height,depth,cost, and optionalquantitycaps. - Per-item rotation control. Each demand has an
allowed_rotationsbitmask over the six axis-permutation orientations of(w, h, d).RotationMask3D::ALLallows all six;RotationMask3D::UPRIGHTpermits only the two orientations that keep the declaredyaxis vertical. - 29-algorithm catalog. Includes Extreme Points (6 scoring variants), Guillotine 3D beam search (7 variants), horizontal layer-building (5 inner-2D backends), Bischoff & Marriott vertical wall-building, column / stack building, Deepest-Bottom-Left and DBLF, volume-sorted FFD/BFD, Multi-start, GRASP, and Local Search meta-strategies, plus a restricted Martello-Pisinger-Vigo branch-and-bound exact backend.
- Same ranking contract. Solutions are compared lexicographically by
(unplaced, bin_count, total_waste_volume, total_cost, !exact), soAutomode always returns the best candidate. - Dimension safety. Per-axis dimensions are capped at
1 << 15(MAX_DIMENSION_3D) so thatw × h × dnever overflowsu64.
2D rectangular packing
- Multi-sheet support. Mix any number of sheet types with independent
width,height,cost, and optionalquantitycaps. - Per-item rotation control. Each demand has its own
can_rotateflag; the solver enumerates the legal orientations per item. - Guillotine mode.
guillotine_required = truerestricts the Auto strategy to guillotine-compatible constructions and setsTwoDSolution.guillotineon the result. - Multistart search. A randomized MaxRects meta-strategy (
multi_start) permutes input orderings under a reproducibleseed. - Integer-safe math. Dimensions are widened to
u64before area calculations;u32 * u32on dimensions is forbidden by the workspace lint policy.
Cross-cutting
serderound-tripping. Every request, option, solution, and metrics type derivesSerialize/Deserialize.- Reproducibility. All randomized strategies accept a
seed; runs with identical seeds and inputs produce identical output. - Metrics. Each solution carries a
metricsblock (iterations,explored_states, etc.) plus free-formnotes, so callers can surface diagnostics without re-running. - Benchmarks. Criterion benches live in crates/bin-packing/benches/solver_benches.rs.
- Fuzzing.
cargo fuzz run solver_inputsexercises both solvers against randomized inputs.
Algorithms
Selected via OneDOptions.algorithm / TwoDOptions.algorithm. All names
below are the exact snake_case strings accepted by serde and the Node
bindings.
3D (ThreeDAlgorithm)
Extreme Points family — maintains a set of extreme points (EPs) and places each item at the EP that scores best under the chosen criterion:
| Name | Description |
|---|---|
extreme_points |
Volume-fit residual scoring (default EP variant). |
extreme_points_residual_space |
Crainic-Perboli-Tadei "RS" — scores by residual space remaining after placement. |
extreme_points_free_volume |
CPT "FV" — scores by free volume in the remaining space. |
extreme_points_bottom_left_back |
Bottom-left-back tiebreaking (place as close to the origin as possible). |
extreme_points_contact_point |
Score by surface contact with already-placed items or bin walls. |
extreme_points_euclidean |
CPT "EU" — scores by Euclidean distance of the EP from the bin origin. |
Guillotine 3D beam search — recursive three-slab partition with configurable split and ranking rules:
| Name | Description |
|---|---|
guillotine_3d |
Beam search with best-volume-fit ranking. |
guillotine_3d_best_short_side_fit |
Ranked by the shortest leftover edge after placement. |
guillotine_3d_best_long_side_fit |
Ranked by the longest leftover edge after placement. |
guillotine_3d_shorter_leftover_axis |
Split along the axis that leaves the shorter leftover slab. |
guillotine_3d_longer_leftover_axis |
Split along the axis that leaves the longer leftover slab. |
guillotine_3d_min_volume_split |
Split to minimise the volume of the new sub-cuboid. |
guillotine_3d_max_volume_split |
Split to maximise the volume of the new sub-cuboid. |
Layer building — packs items into horizontal layers; each layer uses an independent 2D inner backend:
| Name | Description |
|---|---|
layer_building |
Auto inner-2D backend (picks best of MaxRects, Skyline, Guillotine). |
layer_building_max_rects |
MaxRects inner backend. |
layer_building_skyline |
Skyline inner backend. |
layer_building_guillotine |
Guillotine inner backend. |
layer_building_shelf |
Best-fit-decreasing-height shelf inner backend. |
Geometry-based heuristics:
| Name | Description |
|---|---|
wall_building |
Bischoff & Marriott 1990 vertical wall-building: builds planar walls of items from floor to ceiling. |
column_building |
Vertical stack / column building with 2D footprint packing for column placement. |
deepest_bottom_left |
Deepest-Bottom-Left (Karabulut & İnceoğlu): place each item at the deepest, then bottom-most, then leftmost feasible position. |
deepest_bottom_left_fill |
Deepest-Bottom-Left-Fill: adds a fill pass to resolve deadlocks in DBL. |
first_fit_decreasing_volume |
Sort items by volume descending; assign each to the first bin with space. |
best_fit_decreasing_volume |
Sort items by volume descending; assign each to the bin with the least remaining volume that still fits. |
Meta-strategies:
| Name | Description |
|---|---|
multi_start |
Randomized EP meta-strategy. Runs multistart_runs restarts with shuffled item orderings under seed. |
grasp |
Greedy Randomized Adaptive Search: RCL-based randomized construction (top-30% by volume) followed by local search improvement. |
local_search |
Standalone local search seeded from FFD volume. Explores move/rotate/swap neighbourhood with bin-elimination repair. |
branch_and_bound |
Restricted Martello-Pisinger-Vigo exact backend. Computes L0/L1/L2 lower bounds; returns Unsupported for multi-bin-type, capped, or rotation-constrained inputs. |
auto (default) |
Tiered sweep: tier-1 runs ExtremePoints (3 variants), Guillotine3D, LayerBuilding, and FFD; tier-2 (when multistart_runs > 0) adds MultiStart and LocalSearch. Returns the best result. |
Solution ranking for 3D is lexicographic on
(unplaced, bin_count, total_waste_volume, total_cost, !exact).
1D (OneDAlgorithm)
| Name | Description |
|---|---|
auto (default) |
Runs FFD, BFD, and local search, then optionally escalates to column generation when the instance is small enough (auto_exact_max_types, auto_exact_max_quantity, single uncapped stock). Returns the best candidate. |
first_fit_decreasing |
Classic FFD: sort cuts by length descending, place each into the first open bin that fits, open a new bin otherwise. O(n²) deterministic. |
best_fit_decreasing |
BFD: place each sorted cut into the open bin with the tightest fit. Typically uses fewer bins than FFD on mixed sizes. |
local_search |
Multistart local search seeded from FFD/BFD, with bin-elimination repair. multistart_runs restarts × improvement_rounds swap/move rounds, driven by seed. |
column_generation |
Exact backend: generates cutting patterns via price-and-solve, refines with pattern search, and enumerates up to exact_pattern_limit patterns per iteration for column_generation_rounds rounds. Reports an LP lower bound and sets exact = true when the bound is matched. |
2D (TwoDAlgorithm)
MaxRects family — maintains an explicit set of free rectangles, scoring each candidate placement under a different criterion and then splitting / merging free rectangles:
| Name | Description |
|---|---|
max_rects |
Best-area-fit MaxRects (the classic variant). |
max_rects_best_short_side_fit |
Prefer placements that minimize the shorter leftover edge. |
max_rects_best_long_side_fit |
Prefer placements that minimize the longer leftover edge. |
max_rects_bottom_left |
Bottom-left-first tiebreaking. |
max_rects_contact_point |
Score by perimeter contact with already-placed items or the sheet boundary. |
Skyline family — maintains a monotone skyline along one axis:
| Name | Description |
|---|---|
skyline |
Place each item at the lowest feasible skyline segment. |
skyline_min_waste |
Skyline construction with waste-minimizing candidate ranking, which keeps sub-skyline gaps smaller. |
Guillotine beam search — recursive two-stage cuts with a beam-width
search front (beam_width) and configurable split / ranking rules:
| Name | Description |
|---|---|
guillotine |
Beam search with default (best-area-fit) ranking and default split. |
guillotine_best_short_side_fit |
Beam search ranked by shorter leftover edge. |
guillotine_best_long_side_fit |
Beam search ranked by longer leftover edge. |
guillotine_shorter_leftover_axis |
Split along the axis that leaves the shorter leftover strip. |
guillotine_longer_leftover_axis |
Split along the axis that leaves the longer leftover strip. |
guillotine_min_area_split |
Split to minimize the area of the new sub-rectangle. |
guillotine_max_area_split |
Split to maximize the area of the new sub-rectangle. |
Shelf heuristics — simple fast baselines that stack items into horizontal "shelves" sized by the tallest item on each shelf:
| Name | Description |
|---|---|
next_fit_decreasing_height |
NFDH: open a new shelf as soon as the current one overflows. |
first_fit_decreasing_height |
FFDH: place each item on the first shelf that fits. |
best_fit_decreasing_height |
BFDH: place each item on the tightest-fitting shelf. |
Meta-strategies:
| Name | Description |
|---|---|
multi_start |
Randomized MaxRects meta-strategy. Runs multistart_runs restarts with permuted item orderings under seed. |
auto (default) |
Runs MaxRects (best-area, BSSF, BLSF, contact), Skyline and Skyline-min-waste, BFDH, Guillotine BSSF and shorter-leftover-axis, and Multistart, then returns the best candidate. When guillotine_required is set, only the guillotine variants run. |
Solution ranking for 2D is lexicographic on
(unplaced, sheet_count, total_waste_area, total_cost). When
guillotine_required is set, auto narrows its candidate set to the
guillotine variants — the guillotine constraint is enforced by candidate
selection, not by a ranking tie-break, so every candidate auto ranks is
already guillotine-compatible.
Rust usage
Add the crate to your Cargo.toml:
[]
= "0.1"
The dimension modules expose three entry points:
one_d::solve_1d(problem, options) -> Result<OneDSolution>two_d::solve_2d(problem, options) -> Result<TwoDSolution>three_d::solve_3d(problem, options) -> Result<ThreeDSolution>
All problem and solution types implement serde::Serialize /
serde::Deserialize, so the same models are usable from Rust code or
wire-format APIs. The crate root re-exports BinPackingError and Result;
everything else comes from the one_d, two_d, and three_d modules.
1D cutting stock
use ;
Stock1D fields:
length: raw stock length before trim is removed.kerf: material lost to the saw between adjacent cuts.trim: unusable material removed from the stock length before packing.cost: per-unit cost of consuming one piece of this stock type.available: optional inventory cap (number of pieces of this type that may be used).
OneDOptions fields:
algorithm:auto(default),first_fit_decreasing,best_fit_decreasing,local_search, orcolumn_generation.multistart_runs: number of restarts for local search (default16).improvement_rounds: improvement passes per local-search start (default24).column_generation_rounds: outer rounds for the exact backend (default32).exact_pattern_limit: maximum patterns enumerated per round in the exact backend (default25_000).auto_exact_max_types: maximum distinct demand types forAutomode to attempt the exact backend (default14).auto_exact_max_quantity: maximum total demand quantity forAutomode to attempt the exact backend (default96).seed: optional RNG seed for reproducible randomized algorithms.
OneDSolution fields of interest:
algorithm/exact/lower_boundstock_count,total_waste,total_costlayouts: per-pieceStockLayout1Dentries (stock name, used/remaining length, waste, cost, and cut list), sorted by utilization descending.stock_requirements: per-stock procurement summary including any shortage against the declaredavailableinventory. When inventory caps are present this is computed from a relaxed-inventoryAutore-solve.unplaced: cuts the solver could not place (normally empty unless inventory capped the solution).metrics:iterations,generated_patterns,enumerated_patterns,explored_states, and diagnosticnotes.
2D rectangular packing
use ;
Sheet2D fields:
width,height: sheet dimensions (positive, up to1 << 30).cost: per-unit cost of consuming one sheet of this type.quantity: optional cap on the number of sheets of this type that may be used.
RectDemand2D fields:
width,height: rectangle dimensions (positive, up to1 << 30).quantity: number of identical rectangles required.can_rotate: whether the solver may rotate this rectangle 90°.
TwoDOptions fields:
algorithm: see the 2D algorithm table above; defaults toauto.multistart_runs: number of restarts for the multistart meta-strategy.beam_width: beam width for the guillotine beam search backend.guillotine_required: whentrue, forces guillotine-compatible layouts and restrictsautoto guillotine variants.seed: optional RNG seed for reproducible randomized algorithms.
TwoDSolution fields of interest:
algorithm,guillotinesheet_count,total_waste_area,total_costlayouts: per-sheetSheetLayout2Dentries (sheet name, dimensions, placements, used area, waste area).placements: eachPlacement2Dcarriesx,y,width,height, androtated(whether the rectangle was rotated from its declared orientation).unplaced: demands the solver could not place.metrics:iterations,explored_states, and diagnosticnotes.
3D box packing
use ;
Bin3D fields:
width,height,depth: bin dimensions (positive, up to1 << 15).cost: per-unit cost of consuming one bin of this type.quantity: optional cap on the number of bins of this type that may be used.
BoxDemand3D fields:
width,height,depth: declared box dimensions (positive, up to1 << 15).quantity: number of identical boxes required.allowed_rotations: bitmask of the six axis-permutation orientations permitted for this box. UseRotationMask3D::ALL(all six),RotationMask3D::UPRIGHT(onlyXyzandZyx, keeping the y-axis vertical), or construct a custom mask from the per-rotation constants (RotationMask3D::XYZ,XZY,YXZ,YZX,ZXY,ZYX).
ThreeDOptions fields:
algorithm: see the 3D algorithm table above; defaults toauto.multistart_runs: number of restarts for randomized meta-strategies.improvement_rounds: improvement passes per local-search or GRASP start.beam_width: beam width for the Guillotine 3D beam search backend.seed: optional RNG seed for reproducible randomized algorithms.auto_exact_max_types/auto_exact_max_quantity: thresholds controlling whenAutomode may escalate to the exact branch-and-bound backend.branch_and_bound_node_limit: maximum nodes the exact backend may expand.
ThreeDSolution fields of interest:
algorithm,exact,lower_bound,guillotinebin_count,total_waste_volume,total_costlayouts: per-binBinLayout3Dentries (bin name, dimensions, placements,used_volume,waste_volume).placements: eachPlacement3Dcarriesx,y,z,width,height,depth, androtation(theRotation3Dvariant applied).bin_requirements: per-bin procurement summary (populated when at least oneBin3D.quantitycap is set).unplaced: boxes the solver could not place.metrics:iterations,explored_states,extreme_points_generated,branch_and_bound_nodes, and diagnosticnotes.
Errors
BinPackingError is #[non_exhaustive]. Match it with a wildcard arm.
InvalidInput(String)— problem failed boundary validation (empty stock/sheet list, zero dimension, non-finite cost, trim ≥ length, etc.).Infeasible1D { item, length }— a 1D demand does not fit any declared stock.Infeasible2D { item, width, height }— a 2D demand does not fit any declared sheet, even with rotation.Infeasible3D { item, width, height, depth }— a 3D demand does not fit any declared bin in any allowed rotation.Unsupported(String)— the exact backend encountered a configuration it does not currently handle (for example, multi-stock input or capped inventory for column generation), or an internal invariant violation was caught by a defensive check at runtime.
Node.js usage
Build the binding from bindings/node:
Then call the wrapper with plain JavaScript objects. Field names match the
Rust serde representation (snake_case):
const binPacking = require;
const cutList = binPacking.;
console.log;
console.log;
console.log;
const layout = binPacking.;
console.log;
const packing = binPacking.;
console.log;
TypeScript type definitions ship alongside the JavaScript wrapper in
bindings/node/wrapper.d.ts. Both camelCase
(solve1D / solve2D / solve3D) and lowercase (solve1d / solve2d /
solve3d) aliases are exported, plus a version() helper.
Browser / WASM usage
For browsers, Deno, Bun, and Cloudflare Workers (or anywhere else a native
Node addon is inconvenient), the WebAssembly binding at bindings/wasm
publishes to npm as @0xdoublesharp/bin-packing-wasm. It ships combined and
dimension-specific targets through a single package:
// Bundlers (Vite, webpack, esbuild, Next.js, Rollup, Parcel)
import from '@0xdoublesharp/bin-packing-wasm';
// Smaller dimension-specific browser bundles
import from '@0xdoublesharp/bin-packing-wasm/one-d';
import from '@0xdoublesharp/bin-packing-wasm/two-d';
import from '@0xdoublesharp/bin-packing-wasm/three-d';
// Raw ES modules / Deno — explicit init required
import init from '@0xdoublesharp/bin-packing-wasm/web';
await ;
// Node.js / Bun — synchronous load, no init
import from '@0xdoublesharp/bin-packing-wasm/nodejs';
The API is identical to the Node binding: plain JS objects go in, plain JS
objects come out, and errors throw native JavaScript exceptions with the
original BinPackingError message. Full TypeScript types are included. The
combined output is ~550 KB of wasm-opt -Oz-optimized WebAssembly (1D+2D+3D); the
dimension-specific browser outputs are currently ~200 KB for 1D, ~230 KB for 2D, and ~300 KB for 3D. These are approximate — run the build locally for current figures.
Build it locally:
See bindings/wasm/README.md for the full API reference and per-target usage examples.
Repository layout
crates/bin-packing/ # core solver crate
src/one_d/ # 1D model, heuristics, and exact backend
src/two_d/ # 2D model, MaxRects, Skyline, Guillotine, Shelf
src/three_d/ # 3D model, 29-algorithm catalog
benches/solver_benches.rs # Criterion benchmarks (1D + 2D + 3D)
tests/solver_regressions.rs
bindings/node/ # napi-rs Node.js bindings (@0xdoublesharp/bin-packing)
bindings/wasm/ # wasm-bindgen WebAssembly bindings (@0xdoublesharp/bin-packing-wasm)
fuzz/fuzz_targets/ # cargo-fuzz / libFuzzer targets (1D + 2D + 3D)
AGENTS.md # contributor rules
Verification
Core Rust checks (matches CI gate described in AGENTS.md):
Criterion benchmarks:
Node binding smoke test:
WebAssembly binding build and smoke test:
Fuzz target build and execution: