npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

linearcutting

v1.1.0

Published

1D cut list optimiser — cut steel, timber, tube and extrusion with the least waste, fewest bars, or fewest saw setups.

Downloads

270

Readme

linearcutting

1D cut list optimiser for JavaScript and TypeScript. Work out how to cut a list of pieces from stock lengths with the least waste, the fewest bars, or the fewest saw setups — steel, timber, tube, extrusion, pipe. Any material that is long and gets cut across.

npm install linearcutting
import { optimise } from "linearcutting";

const plan = await optimise({ parts: "2400x5, 1800x3", stock: 6000 });

console.log(plan.barsUsed);   // 4
console.log(plan.optimal);    // true — proved, not guessed
console.log(plan.summary());

That runs as written. No API key, no account, no signup. The free public tier allows 30 requests an hour and 250 a calendar month per IP, because you should be able to evaluate a thing before you pay for it.

4 bars, 6600mm waste (72.5% yield) — provably optimal, no better plan exists
3 distinct layouts, 4 saw setups

  6000 (stock): 2400 + 1800 + 1800  [waste 0]
  6000 (stock): 2400 + 2400  [waste 1200] ×2
  6000 (stock): 1800  [waste 4200]

Zero dependencies, fully typed. fetch is native — nothing to install but this.


Why not just sort longest-first?

Because that is provably not optimal, and often not close. Cutting a list of pieces from stock is the 1D cutting stock problem, and no greedy rule (longest-first, best-fit, first-fit-decreasing) reliably finds the best plan.

LinearCutting runs a branch-and-bound search against a mathematical lower bound. That gives you something a greedy heuristic never can:

plan.optimal          // true: no better plan EXISTS. Not "none was found".
plan.barsLowerBound   // a relaxation bound — see the warning below

When a job is too big to prove in the time budget, optimal is False. That means "not proved", not "not good" — it does not pretend either way.

optimal and barsLowerBound disagree on purpose. The bound is roughly total length ÷ bar length, and it is often not achievable. Five 2400s and three 1800s is 17,400mm — 2.9 bars of 6000 — so the bound says 3. You cannot cut those pieces from 3 bars; 4 is the true minimum, and the solver proves it. So barsUsed: 4 with barsLowerBound: 3 and optimal: true is not a contradiction: the bound was loose, not the plan. Trust optimal. Treat the bound as a sanity check, never as a target.

Every plan is also verified before it is returned — re-audited against your inputs, so a plan that does not actually cut your list cannot reach you.

Mind the kerf

The blade destroys material. A 6000mm bar does not hold 2400 + 2400 + 1200 once you account for a 3mm blade, and a plan that ignores that fails at the saw.

const plan = await optimise({ parts: "2400x2, 1200", stock: 6000, kerf: 3.2 });

Get this number right. It is the single most common way a cut list goes wrong.

Optimise for the saw, not just the steel

Most optimisers minimise waste. Waste is not the only cost: every distinct layout is a fence reset, and on a manual saw the setups can cost more than the offcut ever did.

const plan = await optimise({ parts: cutList, stock: 6000, method: "fewest_setups" });
plan.setups;   // the number of measurements the operator dials in

On a real 960-piece job this took the setups from 150 down to 97 — a 35% cut in fence resets — for three extra bars. Whether that trade is worth taking depends on whether your bottleneck is steel or setup time, which is exactly why it is a choice and not a default.

| Method | What it minimises | |---|---| | balanced | Fewest bars, then least waste. The default, and right for most jobs. | | least_waste | Material consumed. | | offcuts_first | Buys new stock only after the remnant rack is used up. | | fewest_setups | Saw setups — the times the operator moves the stop. |

Use the offcuts on your rack

const plan = await optimise({
  parts: "2400x5, 1800x3",
  stock: 6000,
  offcuts: [3200, 1900, [1200, 2]],   // [length, quantity]
  method: "offcuts_first",
});

for (const layout of plan.layouts) {
  if (layout.fromOffcut) console.log("from the rack:", layout);
}

plan.unusedOffcuts;    // what is still on the rack afterwards
plan.usableRemnants;   // new offcuts this job creates, worth keeping

Writing a cut list

All of these mean the same thing. Use whichever suits the code you are in:

parts = "2400x5, 1800x3";                     // a string
parts = ["2400x5", "1800x3"];                 // an array of strings
parts = [[2400, 5], [1800, 3]];               // [length, quantity]
parts = [[2400, 5, "rail"], [1800, 3, "stile"]];   // ...with labels
parts = [{ length: 2400, quantity: 5 }];      // the long form

Stock is the same, and a third element is a price rather than a label:

stock = 6000;                    // this length, buy as many as needed
stock = [6000, 4000];            // choose between lengths
stock = [[6000, 20, 45.50]];     // [length, how many you have, price each]
const plan = await client.optimise({ parts, stock, method: "cheapest" });

Prices only take effect with `method="cheapest"`, which minimises **money**
instead of millimetres — a different plan, because a 6000 bar is rarely 1.5× the
price of a 4000. Every other method ignores prices (and reports no cost), so
`balanced` can never quietly turn into a money optimiser behind your back.

```js
plan.totalCost;
plan.costLowerBound;
for (const buy of plan.purchases) {
  console.log(`${buy.count} × ${buy.length}mm = ${buy.cost}`);
}

With an API key

import { Client } from "linearcutting";

const client = new Client({ apiKey: "lk_live_..." });
const plan = await client.optimise({ parts: "2400x5", stock: 6000 });

A key lifts both free-tier limits (30/hour, 250/month), raises the size caps, and gives you your own concurrency, so a busy afternoon on the public tier cannot slow you down. Plans start at $5/month for 500 requests and run to $99/month for 30,000 — about $3.30 per 1,000 requests. See linearcutting.com/pricing.

Cutting sheet as a PDF

import { writeFile } from "node:fs/promises";

const sheet = await client.pdf({
  parts: "2400x5", stock: 6000, jobName: "Henderson balustrade",
});
await writeFile("cutting-sheet.pdf", sheet);

Errors

Error messages are written for people. Print them.

import { ValidationError, RateLimited } from "linearcutting";

try {
  const plan = await optimise({ parts: "7000", stock: 6000 });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(error.message);   // says what was wrong and what was expected
  } else if (error instanceof RateLimited) {
    await sleep(error.retryAfter * 1000);
  }
}

ValidationError, AuthError, RateLimited (with .retryAfter) and ServerError all extend LinearCuttingError. 429s and 503s are retried automatically, honouring the server's Retry-After.

Fully typed — Plan, Layout, Method and the rest are exported.

What this is not

1D only. Linear material, cut across its length. It does not do 2D sheet or panel nesting, plate, glass, or DXF and beam-saw export. If your material is flat and you cut shapes out of it, this is the wrong tool and you should use something else.

Benchmarks

Three real jobs, every method, with the input cut lists published so you can re-run them: linearcutting.com/benchmarks · raw dataset

A benchmark you cannot reproduce is marketing.

Links

MIT licensed.