Skip to main content
← API reference

True-shape nesting

Arbitrary polygons packed onto fixed sheets.

True-shape nesting

POST /v1/optimize/nest

{
  "parts": [
    {
      "name": "bracket",
      "polygon": [[0, 0], [300, 0], [300, 100], [100, 100], [100, 300], [0, 300]],
      "qty": 4,
      "allowedRotations": [0, 90, 180, 270]
    },
    {
      "name": "gusset",
      "polygon": [[0, 0], [280, 0], [0, 280]],
      "qty": 4,
      "allowedRotations": [0, 90, 180, 270]
    }
  ],
  "stock": [
    {
      "w": 2440,
      "h": 1220,
      "price": 12.5,
      "exclusions": [{ "polygon": [[0, 0], [300, 0], [0, 300]], "quality": 0 }]
    }
  ]
}

The three modes above pack rectangles. POST /v1/optimize/nest packs ARBITRARY POLYGONS: a part is an outline (polygon, with optional interior holes), not a width×height, so parts interlock into each other’s concave pockets and the notch-air a bounding box wastes is reclaimed — on a representative job, 6 sheets where the same parts by bounding box need 9.

It is a different class of algorithm (a geometric collision engine, not the guillotine packer), for laser, plasma and waterjet cutting. Two things the rectangular API cannot express come with it: per-sheet exclusion zones (stock[].exclusions — a defect, a clamp footprint, a pre-printed area; a quality-0 zone is a no-go region for any part) and true-shape holes. material partition and the meta passthrough work as everywhere else.

Spacing is set twice: options.minSeparation between parts, and options.edgeClearance at the sheet edge (it defaults to minSeparation). With several stock sizes for one material, both engines compare them and keep the best plan — by sheet area, or by price with minimizeCost — and may mix sizes (a half-empty last sheet moves to a smaller size), so the result does not depend on the order you list them in.

The default engine, lbf, answers instantly; engine "max" runs the same job asynchronously and searches for a layout with fewer sheets (see Async jobs). The example below is one real captured call — eight parts on a single sheet with a damaged corner excluded.

Every format, engine, limit and code sample for developers: CutOptim nesting API

POST /v1/optimize/nest — request (top level)

FieldTypeDescription
partsNestPart[] · requiredOne or more parts to nest, one NestPart per row.
stockNestStock[] · requiredOne or more sheet types, one NestStock per row.
optionsNestOptionsSolve options. All optional.
engine"lbf" | "max" | "sparrow" · default "lbf"lbf (default) is single-pass, synchronous and instant. max is ASYNCHRONOUS: 202 + poll GET /v1/jobs/{id}; it runs lbf first, then a time-budgeted search that fills sheets one at a time looking for FEWER sheets, and never returns more than lbf, nor a plan that ranks worse (price with minimizeCost, otherwise sheet area). It accepts several sheet sizes per material and respectStock; a polygon sheet or exclusion zones are served by lbf inside the job, with a warning. sparrow is a deprecated alias of max.
include("svg" | "dxf")[]Additive inline export of the achieved layout. svg returns a self-contained, styled <svg> string (one titled band per sheet, parts drawn as filled polygons with holes, exclusion zones hatched); dxf returns an R12 document (closed POLYLINE/VERTEX outlines on STOCK/PARTS/HOLES/ZONES layers, part names on LABELS). Both are unit-agnostic, exactly like the request coordinates. The geometry in sheets[] is returned either way.

parts[] (NestPart)

A part. Give its outline as polygon coordinates OR as a source file — exactly one of the two (both, or neither, is a 400).

FieldTypeDescription
namestringOptional label, echoed on every placement. Defaults to source.filename without its extension when a file was given, otherwise Part <1-based row index>.
polygonnumber[][]An ordered ring of ≥3 vertices. May be given closed (first == last) or open; the DTO layer normalises. Winding order is not required — it is oriented internally.
sourceNestSourceRead this part's outline out of an SVG or DXF FILE instead of listing coordinates. The same parser the CutOptim app uses for its Nesting mode runs server-side, so a part library that already exists as CAD files needs no path/arc flattener of your own.

The file replaces ONLY the geometry. qty, material, allowedRotations, minQuality, priority and meta behave exactly as they do on a polygon part.

Exactly ONE outline per source. Holes are folded in automatically (a ring contained in another becomes a hole), so a typical single-part drawing — outline plus bolt holes — is one outline. A file describing several separate parts is a 400 that names POST /v1/import/nest, which splits it.

Nothing is stored. The bytes exist only as this request body, are parsed in memory and are gone when the response is written: no disk, no database, no temp file, no log line. There is nothing to delete and nothing retained.

Units are reported, never applied. A DXF may declare $INSUNITS; it comes back in imported.units and several disagreeing files produce a warning, but coordinates pass through verbatim — this API converts nothing (see the unit rule on /v1/optimize/nest).
holesnumber[][][]Optional interior holes — a part with a cut-out. NOT accepted together with source: the file already carries its own holes.
qtyinteger · default 1Copies to place.
allowedRotationsnumber[] | "continuous"Allowed rotations in DEGREES (e.g. [0,90,180,270]). Omit or "continuous" for free rotation.
minQualityinteger · default 1The part may only be placed where sheet quality ≥ this. Default 1 → it avoids every quality-0 exclusion zone. Raise it to keep the part off inferior-but-not-forbidden zones too.
prioritybooleanMust-cut. With options.respectStock (a capped supply, where not every part may fit) these parts are placed first, so a plain part never takes the sheet space a must-cut part needs, and every plan comparison ranks the plan that keeps more of them ahead. Honoured by both engines. Without respectStock every part that fits is placed anyway, so the flag changes nothing. A must-cut part that fits no sheet is still listed in unplaced.
materialstringA part of material X nests only on material-X sheets; the job is partitioned by material.
metaobjectOpaque JSON (your ERP ids), echoed verbatim on every placed copy. Never affects the layout.

stock[] (NestStock)

A stock sheet type: either a rectangle (w & h) OR an arbitrary polygon outline (an off-cut remnant). Exactly one of {w & h} | polygon.

FieldTypeDescription
w, hnumberRectangular sheet size. Give w & h OR polygon, not both.
polygonnumber[][]Arbitrary sheet outline (an off-cut remnant, a non-rectangular board) instead of w & h.
qtyinteger · default 1A HARD cap only when options.respectStock is true.
pricenumberPer-sheet price. Summed into totalPrice / stockUsage[].totalPrice, and the ranking key of options.minimizeCost.
costintegerInteger relative per-sheet cost, used by options.minimizeCost for a row that has no price (e.g. when price is not money).
materialstringThis sheet serves only material-matching parts.
exclusionsNestZone[]Per-sheet excluded / inferior zones: a defect, a clamp footprint, a pre-printed area — what the rectangular API cannot express.
exclusions[].polygonnumber[][] · requiredAn ordered ring of ≥3 vertices. May be given closed (first == last) or open; the DTO layer normalises. Winding order is not required — it is oriented internally.
exclusions[].qualityinteger · default 0A zone of quality Q is a hazard for any part whose minQuality exceeds Q. Default 0 = hard-forbidden for any part with the default minQuality of 1.
metaobjectEchoed on every sheet cut from this stock row.

options (NestOptions)

FieldTypeDescription
minSeparationnumberMinimum clearance between parts and between a part and any hazard (sheet edge / zone). Use for kerf / beam / torch width. Default 0. The sheet-edge part can be set separately with edgeClearance.
edgeClearancenumberMinimum distance between any part and the SHEET EDGE, independent of minSeparation (e.g. 5 between parts, 0 or 10 at the edge). Omit it and the edge keeps minSeparation (the previous behaviour, unchanged). Both engines. Rectangular sheets only — with a polygon sheet it is a 400, as is a value that leaves no room on a sheet (the sheet minus twice edgeClearance − minSeparation must stay positive). Exclusion zones keep minSeparation, like parts.
timeBudgetMsintegerSearch budget for engine max, in milliseconds (default 60000; clamped to 10000–180000, and a clamp is reported in warnings). Ignored by lbf (no search budget). The job finishes about 1–2 s after the budget, plus any time spent queued; for a job of a few dozen parts 15000 is usually enough. Poll GET /v1/jobs/{id} every 2–3 s — polling costs no quota.
seedintegerDeterminism: fixed seed → reproducible layout. Omit and the server pins a fixed default, so the response stays reproducible.
minimizeCostbooleanHow engine lbf chooses the sheet size when a material has two or more stock rows. It always tries the rows in your order, each row on its own and a cheapest-first order, and keeps the best, so the result does not depend on the order you list them in. Ranking: most must-cut (parts[].priority) parts placed → most parts placed → (with minimizeCost) lowest total price → least total sheet area → fewest sheets. minimizeCost ranks by price (or cost when a row has no price) only when EVERY row of that material carries one; otherwise it falls back to sheet area and says so in warnings. Since 2026-09-22 (F3) the plan may also MIX sizes within a material: every sheet, least-filled first, is re-nested onto the cheaper or smaller rows and replaced when the whole plan ranks better — typically the half-empty last sheet moves to a smaller size. Engine max uses the same ranking to decide whether its search result replaces the lbf plan.
respectStockbooleanTreat each stock qty as a hard cap (both engines; the mixed-size re-nest never uses more sheets of a row than its qty).
simplifyTolerancenumberPolygon simplification tolerance (max area deviation as a fraction). Speeds up dense DXF outlines with hundreds of vertices. 0 disables.
compactboolean · default trueCompact the least-filled sheet of each material (both engines, default on). After the plan is final, that sheet's parts are re-nested on the same sheet with rotation SUBSETS of what each part allows — {0,180}, {0,90,180,270} and a single orientation, intersected with a discrete allowedRotations, never a new angle — and the variant with the smallest used area (sheets[].usedWidth × usedHeight) is kept. With engine max the sheet (when it holds two or more parts) is then also re-packed by the strip search itself — along the sheet's height and, on a one-sheet plan, also along its width; a few seconds on top of the search budget, under a hard time limit — and the result replaces the sheet only when it is strictly smaller under compactFor and keeps the spacing and the edge clearance. Sheet count, sizes, price and placed parts are unchanged by construction. Runs within a work budget (a very full last sheet may be left as it is). false returns the uncompacted layout.
compactFor"box" | "horizontal" | "vertical" · default "box"Which used measure the compaction minimises — match it to how you charge a partial sheet. box (default): the smallest used bounding area (usedArea). horizontal: the lowest usedHeight, for charging a strip across the full sheet width. vertical: the smallest usedWidth, for charging a strip over the full sheet height. Ties fall back to the used area; the strip modes also try every copy turned a quarter (90° or 270°, where allowedRotations permits it). It only changes which compacted layout is kept: sheet count, sizes, price and placed parts never change, and the result is never worse than the uncompacted layout on that measure. Ignored with compact: false.

Nest — response

{
  "engine": "lbf",
  "engineVersion": "1.0.0+nest-d4046d5-3780cda5",
  "contractVersion": "1",
  "deterministic": true,
  "sheets": [
    {
      "w": 2440,
      "h": 1220,
      "stock": 0,
      "price": 12.5,
      "density": 0.1217,
      "cutLength": 8623.919,
      "pierces": 8,
      "usedWidth": 580.204,
      "usedHeight": 1102.332,
      "usedArea": 639577.44,
      "parts": [
        { "name": "bracket", "sheet": 0, "x": 300.003, "y": 400.226, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 300.133, "y": 700.617, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 300.061, "y": 1000.969, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 400.095, "y": 1102.332, "rotation": -180 },
        { "name": "gusset", "sheet": 0, "x": 530.102, "y": 50.205, "rotation": 90 },
        { "name": "gusset", "sheet": 0, "x": 300.058, "y": 380.171, "rotation": -90 },
        { "name": "gusset", "sheet": 0, "x": 580.15, "y": 380.193, "rotation": 90 },
        { "name": "gusset", "sheet": 0, "x": 300.204, "y": 660.982, "rotation": -90 }
      ],
      "exclusions": [{ "polygon": [[0, 0], [300, 0], [0, 300]], "quality": 0 }]
    }
  ],
  "metrics": {
    "sheetCount": 1,
    "density": 0.1217,
    "placed": 8,
    "total": 8,
    "totalPrice": 12.5,
    "cutLength": 8623.919,
    "pierces": 8
  },
  "stockUsage": [
    {
      "stock": 0,
      "w": 2440,
      "h": 1220,
      "sheetCount": 1,
      "totalPrice": 12.5,
      "density": 0.1217,
      "usedArea": 639577.44
    }
  ],
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 26.34 }
}

Each entry in sheets is one used sheet; a placed part carries the rigid transform (rotation in degrees, then x/y translation), NOT a re-emitted polygon — rotate your input outline by rotation about its origin and add (x, y) to reconstruct the placement exactly. rotation may be negative; the reconstruction is exact regardless of sign.

⚠️ density is the placed POLYGON area over used sheet area — the honest fill, with concave pockets counted as empty — and is NOT comparable to a rectangular packer’s yieldPct (which counts each bounding box as solid, so it reads higher for a worse result); the cross-comparable metric across the two is sheetCount on the same parts.

The layout is deterministic: set options.seed to reproduce it. exclusions is echoed on each sheet for rendering. Every sheet carries stock — the index of the stock row it was cut from — and stockUsage totals the sheets, price and density per stock row, so a quote can be priced per sheet size. Every sheet also carries cutLength and pierces (metrics totals them): the summed perimeter of its placed parts’ outlines and holes, and one pierce per closed contour — geometric, with no common-line cutting or lead-ins.

Every sheet also carries usedWidth and usedHeight — the farthest point its placed parts reach from the sheet origin, the used bounding box for partial-sheet charging — and usedArea (stockUsage totals it per row).

Both engines compact the least-filled sheet toward that corner by default, with rotation subsets of what each part allows (options.compact: false turns it off; options.compactFor picks the measure to minimise — "box" (default) the used area, "horizontal" the used height for a full-width strip charge, "vertical" the used width for a full-height strip; sheet count and price never change).

Ask for include:["svg","dxf"] and the response also carries a self-contained SVG drawing and an R12 DXF of the achieved nest, inline.

POST /v1/optimize/nest — top level

FieldTypeDescription
engine"lbf" | "max"The engine that served the request: lbf for a synchronous nest, max for the result of an async max job (read from GET /v1/jobs/{id}).
engineVersionstringAlgorithm version = package version + pinned jagua-rs commit + a hash of the mapping layer. Versions independently of the rectangular engines.
contractVersionstringNest contract version, currently "1". Versions independently of the /v1/ rectangular contract — it is a different path and engine family.
deterministicbooleantrue for lbf (a pinned seed makes the layout reproducible) — unless a comparison of alternative layouts (several stock rows, right angles) hit its time limit on the server (then false, with a warning). For max it is false whenever a time-limited search ran — the search for fewer sheets, or the re-packing of the least-filled sheet (options.compact) — and true only when the lbf pass alone decided the layout (a single-part sheet, compact: false).
sheetsNestSheet[]One entry per used sheet.
metricsNestMetricsJob totals.
stockUsageNestStockUsage[]One entry per USED request stock row, sorted by stock: sheets used, total price and density. Always present.
unplacedUnplacedNest[]Demand that could not be placed — a plan-plus-warning, not an error.
warningsstring[]e.g. unplaced parts, a material with no matching stock, or — for max — how the result was reached (for instance that the lbf layout was kept because no layout with fewer sheets was found in the time budget).
materialsNestMaterialSummary[]Per-material rollup, present only for a material-aware request.
unmatchedMaterialsUnmatchedMaterialNest[]Parts whose material has no matching stock. Present only when it happens.
importedNestImportSummaryWhat the request's source files contributed. ABSENT when every part carried polygon coordinates, which is what keeps such a request byte-identical to before the feature existed.
svgstringAdditive — a self-contained SVG of the achieved layout, present only when include has "svg".
dxfstringAdditive — an R12 DXF of the achieved layout, present only when include has "dxf".
timing{ solveMs }The solve time; environment-dependent.
timing.solveMsnumberMilliseconds spent solving.

sheets[] (nest)

FieldTypeDescription
w, hnumberPresent for rectangular sheets. Always the real sheet size (also with edgeClearance).
polygonnumber[][]Present for arbitrary-outline sheets instead of w/h.
stockintegerIndex into the request stock[] this sheet was cut from — the key an ERP uses to tie the sheet back to its stock row.
pricenumber | nullThe stock row's price, or null.
densitynumberThis sheet's fill = placed polygon area / sheet area.
cutLengthnumberΣ perimeter of the placed parts' outer outlines AND holes on this sheet, 3 decimals, in your unit — the contour length a laser / plasma / waterjet head follows. Geometric: no common-line cutting, lead-in/out or micro-joints (your CAM adds those).
piercesintegerPierce points on this sheet: one per closed contour (each placed part's outline + one per hole).
usedWidthnumberThe farthest x any placed part reaches, measured from the sheet origin (0,0), 3 decimals, in your unit — real sheet coordinates (with edgeClearance it includes the edge gap). With usedHeight: the used bounding box from the origin corner, for partial-sheet charging (full sheet / horizontal cut = usedHeight × w / vertical cut = usedWidth × h / boundary box — the charging rule is yours). A polygon sheet: the same, in the polygon's own coordinate frame.
usedHeightnumberThe farthest y any placed part reaches, from the sheet origin (see usedWidth).
usedAreanumberusedWidth × usedHeight (rounded to 2 decimals).
partsPlacedNestPart[]Placements on this sheet.
exclusionsNestZone[]The zones that applied to this sheet, echoed for rendering.
materialstringPresent when the sheet carried a material.
metaobjectEchoed from the stock row's meta.

sheets[].parts[] (nest — placed)

A placed copy. Carries the rigid transform (rotation THEN translation), not a re-emitted polygon: rotate the input outline by rotation about its origin, then add (x, y).

FieldTypeDescription
namestringThe requested name, or the generated default.
sheetinteger0-based index into sheets.
xnumberTranslation x (applied after rotation).
ynumberTranslation y.
rotationnumberDegrees, applied about the part's own origin first. May be negative (jagua's own value); the reconstruction is exact regardless of sign.
materialstringThe material this copy nested from.
metaobjectThe part's opaque passthrough.

metrics (nest)

FieldTypeDescription
sheetCountintegerSheets used. Equals sheets.length. ⚠️ The honest, cross-comparable metric between nesting and the rectangular modes.
densitynumber⚠️ Placed POLYGON area / used sheet area — the honest fill (concave pockets count as empty). NOT comparable to a rectangular packer's yieldPct, which counts each bounding box as solid and so reads higher for a worse result; the cross-comparable metric is sheetCount.
placedintegerPart copies placed.
totalintegerPart copies requested.
totalPricenumberSum of used sheet prices.
cutLengthnumberΣ sheets[].cutLength — placed parts only (outlines + holes), in your unit.
piercesintegerΣ sheets[].pierces.

stockUsage[] (nest)

Sheets used per request stock ROW ("quantity per sheet size"). The unit is the row, not the size: two rows with the same w × h but a different price or meta are two entries.

FieldTypeDescription
stockintegerIndex into the request stock[].
w, hnumberPresent for rectangular rows.
materialstringPresent when the row carries a material.
sheetCountintegerSheets cut from this row.
totalPricenumberΣ sheet price (0 for a row without a price, the same rule as metrics.totalPrice).
densitynumberPlaced polygon area / total area of this row's sheets.
usedAreanumberΣ sheets[].usedArea of this row's sheets — the used bounding area to total per sheet size.

unplaced[] (nest)

FieldTypeDescription
namestringThe part name.
qtyintegerHow many copies could not be placed.