bin-packing 0.1.0

Cut list optimization and bin packing library for 1D, 2D, and 3D stock problems
Documentation

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 by napi-rs.
  • WebAssembly bindings: bindings/wasm — published to npm as @0xdoublesharp/bin-packing-wasm, powered by wasm-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 inventory available caps.
  • 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 available caps, the solver returns both the capped solution and a relaxed-inventory estimate so you can see per-stock used_quantity, required_quantity, and additional_quantity_needed.
  • Multi-objective ranking. Solutions are compared lexicographically by (unplaced, stock_count, total_waste, total_cost, !exact) so better heuristic candidates win automatically in Auto mode.
  • Lower bounds. The column-generation backend reports an LP lower bound and marks the solution exact = true when it matches the incumbent.
  • Structured errors. Public boundary validation fails fast with typed BinPackingError variants instead of panics.

3D box packing

  • Multi-bin support. Mix any number of bin types with independent width, height, depth, cost, and optional quantity caps.
  • Per-item rotation control. Each demand has an allowed_rotations bitmask over the six axis-permutation orientations of (w, h, d). RotationMask3D::ALL allows all six; RotationMask3D::UPRIGHT permits only the two orientations that keep the declared y axis 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), so Auto mode always returns the best candidate.
  • Dimension safety. Per-axis dimensions are capped at 1 << 15 (MAX_DIMENSION_3D) so that w × h × d never overflows u64.

2D rectangular packing

  • Multi-sheet support. Mix any number of sheet types with independent width, height, cost, and optional quantity caps.
  • Per-item rotation control. Each demand has its own can_rotate flag; the solver enumerates the legal orientations per item.
  • Guillotine mode. guillotine_required = true restricts the Auto strategy to guillotine-compatible constructions and sets TwoDSolution.guillotine on the result.
  • Multistart search. A randomized MaxRects meta-strategy (multi_start) permutes input orderings under a reproducible seed.
  • Integer-safe math. Dimensions are widened to u64 before area calculations; u32 * u32 on dimensions is forbidden by the workspace lint policy.

Cross-cutting

  • serde round-tripping. Every request, option, solution, and metrics type derives Serialize / Deserialize.
  • Reproducibility. All randomized strategies accept a seed; runs with identical seeds and inputs produce identical output.
  • Metrics. Each solution carries a metrics block (iterations, explored_states, etc.) plus free-form notes, 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_inputs exercises 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:

[dependencies]
bin-packing = "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 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:

  • algorithm: auto (default), first_fit_decreasing, best_fit_decreasing, local_search, or column_generation.
  • multistart_runs: number of restarts for local search (default 16).
  • improvement_rounds: improvement passes per local-search start (default 24).
  • column_generation_rounds: outer rounds for the exact backend (default 32).
  • exact_pattern_limit: maximum patterns enumerated per round in the exact backend (default 25_000).
  • auto_exact_max_types: maximum distinct demand types for Auto mode to attempt the exact backend (default 14).
  • auto_exact_max_quantity: maximum total demand quantity for Auto mode to attempt the exact backend (default 96).
  • seed: optional RNG seed for reproducible randomized algorithms.

OneDSolution fields of interest:

  • algorithm / exact / lower_bound
  • stock_count, total_waste, total_cost
  • layouts: per-piece StockLayout1D entries (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 declared available inventory. When inventory caps are present this is computed from a relaxed-inventory Auto re-solve.
  • unplaced: cuts the solver could not place (normally empty unless inventory capped the solution).
  • metrics: iterations, generated_patterns, enumerated_patterns, explored_states, and diagnostic notes.

2D rectangular packing

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,
        }],
        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 to 1 << 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 to 1 << 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 to auto.
  • multistart_runs: number of restarts for the multistart meta-strategy.
  • beam_width: beam width for the guillotine beam search backend.
  • guillotine_required: when true, forces guillotine-compatible layouts and restricts auto to guillotine variants.
  • seed: optional RNG seed for reproducible randomized algorithms.

TwoDSolution fields of interest:

  • algorithm, guillotine
  • sheet_count, total_waste_area, total_cost
  • layouts: per-sheet SheetLayout2D entries (sheet name, dimensions, placements, used area, waste area).
  • placements: each Placement2D carries x, y, width, height, and rotated (whether the rectangle was rotated from its declared orientation).
  • unplaced: demands the solver could not place.
  • metrics: iterations, explored_states, and diagnostic notes.

3D box packing

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 to 1 << 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 to 1 << 15).
  • quantity: number of identical boxes required.
  • allowed_rotations: bitmask of the six axis-permutation orientations permitted for this box. Use RotationMask3D::ALL (all six), RotationMask3D::UPRIGHT (only Xyz and Zyx, 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 to auto.
  • 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 when Auto mode 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, guillotine
  • bin_count, total_waste_volume, total_cost
  • layouts: per-bin BinLayout3D entries (bin name, dimensions, placements, used_volume, waste_volume).
  • placements: each Placement3D carries x, y, z, width, height, depth, and rotation (the Rotation3D variant applied).
  • bin_requirements: per-bin procurement summary (populated when at least one Bin3D.quantity cap is set).
  • unplaced: boxes the solver could not place.
  • metrics: iterations, explored_states, extreme_points_generated, branch_and_bound_nodes, and diagnostic notes.

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:

pnpm install
pnpm run build

Then call the wrapper with plain JavaScript objects. Field names match the Rust serde representation (snake_case):

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 }],
    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 }
);

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.

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:

npm install @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 } from '@0xdoublesharp/bin-packing-wasm/web';
await init();

// Node.js / Bun — synchronous load, no init
import { solve1d } 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:

cd bindings/wasm
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
npm run build   # builds combined and dimension-specific targets into dist/
npm test        # runs the Node smoke test against dist/nodejs

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):

cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets

Criterion benchmarks:

cargo bench -p bin-packing --bench solver_benches -- --sample-size 10

Node binding smoke test:

cd bindings/node
pnpm test

WebAssembly binding build and smoke test:

cd bindings/wasm
npm run build
npm test

Fuzz target build and execution:

cargo check --manifest-path fuzz/Cargo.toml
cargo fuzz run solver_inputs