A Rust-first cut list and bin packing optimization library for one-dimensional
cutting stock (linear bar / pipe stock), two-dimensional rectangular sheet
stock, three-dimensional box packing, and irregular nesting problems. The crate
uses #![forbid(unsafe_code)], widened integer dimensions for regular packing,
and explicit floating-point tolerances for irregular geometry. It is 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. Targets browsers and Node.js; other WASM hosts may need loader configuration. - Fuzz targets: fuzz/ — libFuzzer harnesses for the 1D, 2D, and 3D entry points.
Version 0.6.1 refreshes dependency security fixes, native package READMEs, and release tooling. The solver APIs and packing models are unchanged.
See the v0.6.1 release notes for changes and compatibility notes. The testing guide explains validation and coverage.
- Irregular nesting: concave polygons, holes, circular arcs, rigid components, irregular bins/defects, rotations, reflection and spacing.
- Staged cutting: physical cut trees, stage limits, cut modes, cut spacing/counts, defects, trims, kerf and production groups.
- Stack loading: compatible upright columns, nesting, stack/weight/density limits, payload, exact axle reactions and unloading groups.
- Explicit objectives: material cost, profit/knapsack, feasibility and open dimensions across regular and physical models.
These APIs are native Rust and available through Node.js and WebAssembly. The
optional irregular feature uses safe Rust geometry. PackingSolver supplies
reference algorithms and benchmark workloads; no C++ backend is required.
Each model documents its supported bounds, geometric approximation and search
limits. Independent validators check source identity and physical results.
- 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 a certified LP or conservative length bound
and marks the solution
exact = truewhen full pattern enumeration and integer search establish the optimum. - Structured errors. Public boundary validation fails fast with typed
BinPackingErrorvariants instead of panics.
- 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), soAutomode always returns the best candidate. - Dimension safety. Per-axis dimensions are capped at
1 << 15(MAX_DIMENSION_3D) so thatw × h × dnever overflowsu64.
- 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. - Kerf-aware packing. Each sheet type accepts an optional
kerfgap (in the same units as the sheet dimensions). The solver enforces the gap between every pair of adjacent placements (factory edges are free; only internal cuts consume kerf). The solution reportskerf_areaper layout andtotal_kerf_areaacross all sheets. - Edge kerf relief. Set
Sheet2D.edge_kerf_relief = trueto allow a trailing placement to extend up to onekerfpast the sheet boundary, modeling a blade that exits the stock. Parts must still fit within the declared sheet dimensions. - Multistart search. A randomized MaxRects meta-strategy (
multi_start) permutes input orderings under a reproducibleseed. - Rotation search. Exhaustive (or sampled) rotation assignment search
(
rotation_search) enumerates all 2^k rotation assignments for k rotatable demand types, finding globally better rotation choices than greedy per-placement rotation. - Usable-drop consolidation. Set
TwoDOptions.min_usable_sideto filter out narrow leftover strips that are too small to reuse. The solver reportsSheetLayout2D.largest_usable_drop_area(largest maximal free rectangle meeting the threshold on that sheet),SheetLayout2D.sum_sq_usable_drop_areas(sum of squares of all qualifying drops on that sheet), and aggregates across the solution asTwoDSolution.max_usable_drop_area(the MAX over all layouts, not the sum) andTwoDSolution.total_sum_sq_usable_drop_areas(saturating sum across layouts). Candidates that are equal on waste and cost are tiebroken in favour of larger / more consolidated drops; this tiebreaker sits AFTERtotal_waste_areaandtotal_costso waste and cost always dominate. - Integer-safe math. Dimensions are widened to
u64before area calculations;u32 * u32on dimensions is forbidden by the workspace lint policy.
After solving, you can generate an ordered cut plan for any OneDSolution or
TwoDSolution. The cut planner is a pure post-processor — it reads a finished
layout and emits a per-bar or per-sheet sequence of CutSteps optimized for a
chosen shop cost model. The solver is not re-run.
Entry points:
bin_packing::one_d::cut_plan::plan_cuts(&problem, &solution, &options) -> Result<CutPlanSolution1D, CutPlanError>bin_packing::two_d::cut_plan::plan_cuts(&solution, &options) -> Result<CutPlanSolution2D, CutPlanError>
Presets: ChopSaw in 1D; TableSaw, PanelSaw and CncRouter in 2D.
The generated preset costs are resolved
by the actual planning APIs and checked before publication.
Any cost field on the options struct overrides the preset default. Pass
..CutPlanOptions2D::default() to accept all preset defaults.
Output:
CutPlanSolution1D— containsbar_plans: Vec<BarCutPlan1D>and a solution-leveltotal_cost. EachBarCutPlan1Dcarries:steps: Vec<CutStep1D>— orderedCutandFenceResetsteps.total_cost,num_cuts,num_fence_resets.
CutPlanSolution2D— containssheet_plans: Vec<SheetCutPlan2D>and a solution-leveltotal_cost. EachSheetCutPlan2Dcarries:steps: Vec<CutStep2D>— orderedCut,LineCut,Rotate,FenceReset,ToolUp,ToolDown, andTravelsteps.total_cost,num_cuts,num_rotations,num_fence_resets,num_tool_ups,travel_distance.
Total cost is the linear sum:
num_cuts * cut_cost + num_rotations * rotate_cost + num_fence_resets * fence_reset_cost + num_tool_ups * tool_up_down_cost + travel_distance * travel_cost.
Errors (CutPlanError, #[non_exhaustive]):
NonGuillotineNotCuttable { sheet_name }— the layout cannot be expressed as a sequence of full guillotine cuts, and theTableSaworPanelSawpreset was requested. Switch toCncRouteror re-solve withguillotine_required = true.InvalidOptions(String)— a cost field is negative,NaN, or infinite.
Quick example (Rust):
use bin_packing::two_d::{
RectDemand2D, Sheet2D, TwoDAlgorithm, TwoDOptions, TwoDProblem, solve_2d,
cut_plan::{CutPlanOptions2D, CutPlanPreset2D, plan_cuts},
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let problem = TwoDProblem {
sheets: vec![Sheet2D {
name: "plywood".into(),
width: 96,
height: 48,
cost: 1.0,
quantity: None,
kerf: 2,
edge_kerf_relief: false,
}],
demands: vec![
RectDemand2D { name: "panel-a".into(), width: 24, height: 18, quantity: 4, can_rotate: true },
RectDemand2D { name: "panel-b".into(), width: 12, height: 12, quantity: 6, can_rotate: true },
],
};
let solution = solve_2d(problem, TwoDOptions { algorithm: TwoDAlgorithm::Auto, guillotine_required: true, ..Default::default() })?;
let cut_plan = plan_cuts(
&solution,
&CutPlanOptions2D { preset: CutPlanPreset2D::TableSaw, ..Default::default() },
)?;
println!("total cut plan cost: {}", cut_plan.total_cost);
for sheet_plan in &cut_plan.sheet_plans {
println!(" {}: {} steps, cost {:.2}", sheet_plan.sheet_name, sheet_plan.steps.len(), sheet_plan.total_cost);
}
Ok(())
}-
Cost-first linear stock.
one_d::solve_1d_min_cost(JavaScriptsolve1dMinCost) can use more bars when doing so reduces material cost. It retains the existing bounded pattern search, inventory and kerf/trim behavior. See cost-first cutting stock. -
Native constrained packing. Additive
two_d::constrainedandthree_d::constrainedAPIs support physical sheet trims/rectangular defects, carton payload limits, explicit source indices, independent validation, and count-first or cost-first construction. JavaScript exposessolve2dConstrainedandsolve3dConstrainedin Node and dimension-specific WebAssembly builds. See 2D constraints and 3D constraints for limits and semantics. -
Count-bound-aware search.
two_d::adaptive::solve_2d_adaptive(JavaScriptsolve2dAdaptive) can stop once a complete packing meets a conservative geometric lower bound. It certifies sheet count only; ordinary Auto and its secondary ranking remain unchanged. See adaptive search. -
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 the 1D, 2D, and 3D solvers against randomized inputs.
Selected via OneDOptions.algorithm, TwoDOptions.algorithm, or
ThreeDOptions.algorithm. All names below are the exact snake_case strings
accepted by serde and the Node / WebAssembly bindings.
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 conservative-scale lower bounds; returns Unsupported for multi-bin-type, capped, or rotatable (non-XYZ) 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).
| 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 runs one capped enumeration pass after at most column_generation_rounds LP/pricing rounds. Reports a certified LP or conservative volume lower bound; exact = true requires complete pattern enumeration and integer search. |
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. |
rotation_search |
Exhaustive (or sampled) rotation assignment search: enumerates all 2^k rotation assignments for k rotatable demand types (or samples multistart_runs random assignments when k exceeds auto_rotation_search_max_types). Each assignment fixes rotations and packs via MaxRects best-area-fit. |
auto (default) |
Runs MaxRects (best-area, BSSF, BLSF, contact), Skyline and Skyline-min-waste, BFDH, Guillotine BSSF and shorter-leftover-axis, Multistart, and RotationSearch, 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, max_usable_drop_area↑, total_sum_sq_usable_drop_areas↑).
The two consolidation keys (↑ = prefer larger) only reorder candidates that
are already equal on every primary key; waste and cost always win. 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.
Add the crate to your Cargo.toml:
[dependencies]
bin-packing = "0.6.1"The regular dimension modules expose these primary 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;
the regular models come from one_d, two_d, and three_d. The irregular
module adds shape nesting; optimization supplies shared objective controls.
See the generated reference for features, defaults and
binding exports, and the API guide for the added models.
use bin_packing::one_d::{
CutDemand1D, OneDAlgorithm, OneDOptions, OneDProblem, Stock1D, solve_1d,
};
fn main() -> bin_packing::Result<()> {
let problem = OneDProblem {
stock: vec![Stock1D {
name: "96in bar".into(),
length: 96,
kerf: 1,
trim: 0,
cost: 1.0,
available: None,
}],
demands: vec![
CutDemand1D { name: "rail".into(), length: 45, quantity: 2 },
CutDemand1D { name: "brace".into(), length: 30, quantity: 2 },
],
};
let solution = solve_1d(
problem,
OneDOptions {
algorithm: OneDAlgorithm::Auto,
..Default::default()
},
)?;
println!("algorithm: {}", solution.algorithm);
println!("stock used: {}", solution.stock_count);
println!("waste: {}", solution.total_waste);
println!("exact: {}", solution.exact);
if let Some(bound) = solution.lower_bound {
println!("lower bound: {bound}");
}
for layout in &solution.layouts {
println!(
"{}: used {} / {}",
layout.stock_name, layout.used_length, layout.stock_length
);
}
for requirement in &solution.stock_requirements {
println!(
"{}: used {}, required {}, additional {}",
requirement.stock_name,
requirement.used_quantity,
requirement.required_quantity,
requirement.additional_quantity_needed,
);
}
Ok(())
}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 (effective defaults are in the
generated reference):
algorithm:auto(default),first_fit_decreasing,best_fit_decreasing,local_search, orcolumn_generation.multistart_runs: number of restarts for local search.improvement_rounds: improvement passes per local-search start.column_generation_rounds: LP/pricing rounds before one pattern-enumeration pass.exact_pattern_limit: cap on the single exhaustive enumeration pass in the exact backend; hitting it discards that batch and leaves the result non-exact.auto_exact_max_types: maximum distinct demand types forAutomode to attempt the exact backend.auto_exact_max_quantity: maximum total demand quantity forAutomode to attempt the exact backend.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.
use bin_packing::two_d::{
RectDemand2D, Sheet2D, TwoDAlgorithm, TwoDOptions, TwoDProblem, solve_2d,
};
fn main() -> bin_packing::Result<()> {
let problem = TwoDProblem {
sheets: vec![Sheet2D {
name: "plywood".into(),
width: 96,
height: 48,
cost: 1.0,
quantity: None,
kerf: 2,
edge_kerf_relief: false,
}],
demands: vec![
RectDemand2D {
name: "panel-a".into(),
width: 24,
height: 18,
quantity: 4,
can_rotate: true,
},
RectDemand2D {
name: "panel-b".into(),
width: 12,
height: 12,
quantity: 6,
can_rotate: true,
},
],
};
let solution = solve_2d(
problem,
TwoDOptions {
algorithm: TwoDAlgorithm::Auto,
seed: Some(42),
..Default::default()
},
)?;
println!("algorithm: {}", solution.algorithm);
println!("sheets used: {}", solution.sheet_count);
println!("waste area: {}", solution.total_waste_area);
println!("guillotine: {}", solution.guillotine);
for layout in &solution.layouts {
println!(
"{}: {} placements, {} waste area",
layout.sheet_name,
layout.placements.len(),
layout.waste_area
);
for placement in &layout.placements {
println!(
" {} @ ({}, {}) {}x{}{}",
placement.name,
placement.x,
placement.y,
placement.width,
placement.height,
if placement.rotated { " rotated" } else { "" },
);
}
}
Ok(())
}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.kerf: optional saw-blade kerf width. The solver enforces this gap between every pair of adjacent placements on the sheet; the gap between a placement and the sheet edge is free (factory edge rule).edge_kerf_relief: whentrue, the trailing placement on the sheet may extend up to onekerfpast the sheet's right and bottom edges, modeling a cut that exits the stock. Individual parts must still fit within the sheet's declared dimensions.
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 (effective defaults are in the
generated reference):
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: filtersautoto guillotine variants. It does not override an explicitly selected algorithm.min_usable_side: minimum side length (in the same units as sheet dimensions) for a leftover drop to count as usable. A zero threshold counts all nonzero drops. Raise this to ignore narrow strips that are too small to reuse; the consolidation tiebreakers then favour solutions that leave fewer but larger reusable offcuts.auto_rotation_search_max_types: maximum number of rotatable demand types for which rotation search uses exhaustive enumeration, subject to a hard ceiling of 16. Above the smaller of this threshold and 16, rotation search switches to samplingmultistart_runsrandom assignments.seed: optional RNG seed for reproducible randomized algorithms.
TwoDSolution fields of interest:
algorithm,guillotinesheet_count,total_waste_area,total_kerf_area,total_costmax_usable_drop_area: the largest single usable drop area across all layouts (MAX over layouts, not a sum). A drop qualifies when both its sides are at leastmin_usable_side.total_sum_sq_usable_drop_areas: saturating sum ofsum_sq_usable_drop_areasacross all layouts. Higher values indicate better drop consolidation (fewer but larger reusable offcuts).layouts: per-sheetSheetLayout2Dentries (sheet name, dimensions, placements, used area, waste area,kerf_area— the portion ofwaste_areaattributable to kerf gaps —largest_usable_drop_area, andsum_sq_usable_drop_areas).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.
use bin_packing::three_d::{
Bin3D, BoxDemand3D, RotationMask3D, ThreeDAlgorithm, ThreeDOptions,
ThreeDProblem, solve_3d,
};
fn main() -> bin_packing::Result<()> {
let problem = ThreeDProblem {
bins: vec![Bin3D {
name: "pallet".into(),
width: 120,
height: 100,
depth: 80,
cost: 1.0,
quantity: None,
}],
demands: vec![
BoxDemand3D {
name: "carton-a".into(),
width: 30,
height: 25,
depth: 20,
quantity: 6,
allowed_rotations: RotationMask3D::ALL,
},
BoxDemand3D {
name: "carton-b".into(),
width: 20,
height: 15,
depth: 10,
quantity: 8,
allowed_rotations: RotationMask3D::UPRIGHT,
},
],
};
let solution = solve_3d(
problem,
ThreeDOptions {
algorithm: ThreeDAlgorithm::Auto,
seed: Some(42),
..Default::default()
},
)?;
println!("algorithm: {}", solution.algorithm);
println!("bins used: {}", solution.bin_count);
println!("waste volume: {}", solution.total_waste_volume);
for layout in &solution.layouts {
println!(
"{}: {} placements, {} waste volume",
layout.bin_name,
layout.placements.len(),
layout.waste_volume
);
for placement in &layout.placements {
println!(
" {} @ ({}, {}, {}) {}x{}x{} rotation={:?}",
placement.name,
placement.x, placement.y, placement.z,
placement.width, placement.height, placement.depth,
placement.rotation,
);
}
}
Ok(())
}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 (effective defaults are in the
generated reference):
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: reserved for compatibility; 3D Auto does not use these fields. Selectbranch_and_boundexplicitly to request the restricted exact backend.branch_and_bound_node_limit: maximum nodes the exact backend may expand.guillotine_required: filters Auto to its guillotine-compatible ensemble; it does not override an explicitly selected algorithm.
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.
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.InvalidConstraint { field, reason }— a physical/objective request or its controls exceed a supported rule or bound.InvalidSolution { reason }— independent readback found inconsistent output.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.
Build the binding from bindings/node:
pnpm install
pnpm run buildThen call the wrapper with plain JavaScript objects. Field names match the
Rust serde representation (snake_case):
const assert = require('node:assert/strict');
const binPacking = require('@0xdoublesharp/bin-packing');
const cutList = binPacking.solve1d(
{
stock: [{ name: 'bar', length: 100, kerf: 1, available: 5 }],
demands: [
{ name: 'A', length: 45, quantity: 2 },
{ name: 'B', length: 30, quantity: 2 }
]
},
{ algorithm: 'auto', seed: 7 }
);
console.log(cutList.stock_count);
console.log(cutList.stock_requirements);
console.log(cutList.unplaced);
const layout = binPacking.solve2d(
{
sheets: [{ name: 'plywood', width: 96, height: 48, kerf: 2 }],
demands: [
{ name: 'panel', width: 24, height: 18, quantity: 4, can_rotate: true }
]
},
{ algorithm: 'auto', guillotine_required: true, beam_width: 12 }
);
console.log(layout.sheet_count, layout.total_waste_area, layout.guillotine);
const packing = binPacking.solve3d(
{
bins: [{ name: 'pallet', width: 120, height: 100, depth: 80 }],
demands: [
{ name: 'carton-a', width: 30, height: 25, depth: 20, quantity: 6 },
{ name: 'carton-b', width: 20, height: 15, depth: 10, quantity: 8 }
]
},
{ algorithm: 'auto', seed: 42 }
);
assert.equal(cutList.unplaced.length, 0);
assert.equal(layout.unplaced.length, 0);
assert.equal(layout.guillotine, true);
assert.equal(packing.unplaced.length, 0);
assert.equal(packing.layouts.flatMap(bin => bin.placements).length, 14);
console.log(packing.bin_count, packing.total_waste_volume);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.
For browsers and Node.js environments where a native 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:
pnpm add @0xdoublesharp/bin-packing-wasm// Bundlers (Vite, webpack, esbuild, Next.js, Rollup, Parcel)
import { solve1d, solve2d } from '@0xdoublesharp/bin-packing-wasm';
// Smaller dimension-specific browser bundles
import { solve1d as solveCutList } from '@0xdoublesharp/bin-packing-wasm/one-d';
import { solve2d as solveSheets } from '@0xdoublesharp/bin-packing-wasm/two-d';
import { solve3d as packBoxes } from '@0xdoublesharp/bin-packing-wasm/three-d';
// Raw ES modules / Deno — explicit init required
import init, { solve2d as solveWebSheets } from '@0xdoublesharp/bin-packing-wasm/web';
await init();
// Node.js / Bun — synchronous load, no init
import { solve1d as solveNodeCuts } 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. Combined builds include irregular nesting.
Use a dimension-specific import to reduce payload size. The
dated expansion report
records measured sizes; a fresh build is required for current sizes.
Build it locally:
cd bindings/wasm
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
pnpm run build # builds combined and dimension-specific targets into dist/
pnpm test # runs the Node smoke test against dist/nodejsSee bindings/wasm/README.md for the full API reference and per-target usage examples.
crates/bin-packing/ # core solver crate
src/one_d/ # 1D model, heuristics, and exact backend
src/two_d/ # 2D model, MaxRects, Skyline, Guillotine, Shelf, RotationSearch
src/three_d/ # 3D model, loading and objective solvers
src/irregular/ # polygon/curve nesting and independent validation
src/optimization.rs # public shared objective controls
src/optimization/ # shared bounded objective search
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
One-time install for the repo-wide hygiene tools:
cargo install taplo-cli typos-cli --lockedFull Rust/doc/text/TOML quality gate (matches CI):
./scripts/check-quality.shSee correctness checks and production coverage for independent oracle tests and the repeatable line/branch coverage command.
The gate also checks generated docs, local documentation links and runnable
Rust examples. Refresh source-derived facts with python3 -B scripts/check-docs.py --write; see documentation maintenance.
Individual Rust/text/TOML commands (the script also runs Python and docs checks):
cargo fmt --all --check
taplo fmt --check
typos
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-targets --all-features --locked
cargo test -p bin-packing --doc --all-features --locked
cargo doc --manifest-path crates/bin-packing/Cargo.toml --locked --no-deps --document-private-itemsCriterion benchmarks:
cargo bench -p bin-packing --bench solver_benches -- --sample-size 10Node binding smoke test:
cd bindings/node
pnpm testWebAssembly binding build and smoke test:
cd bindings/wasm
pnpm run build
pnpm testFuzz target build and execution:
cargo check --manifest-path fuzz/Cargo.toml
cargo fuzz run solver_inputsSee workspace verification for the feature matrix,
all binding entry points, isolated npm archive checks, browser/type checks and the
seeded counterexample finder. scripts/check-bindings.sh rebuilds and checks both
packages; scripts/check-features.sh includes the separate fuzz project.
For local builds on a separate disposable volume, see Scratch build storage.
See the release runbook for version synchronization, documentation checks, package verification, tag publication and readback.
See CONTRIBUTING.md for development setup, verification, bug reports, and pull requests. Project participation follows the code of conduct. Report vulnerabilities privately using the security policy.
Licensed under the MIT License.
