2D sheet optimization
Rectangular parts nested into sheets: the request, the response and every field of it.
2D — request
POST /v1/optimize/2d
Authorization: Bearer co_live_…
Content-Type: application/json
{
"parts": [{ "name": "Door", "w": 600, "h": 400, "qty": 4, "rotatable": true }],
"stock": [{ "w": 2440, "h": 1220, "qty": 10, "price": 42 }],
"options": {
"kerf": 3,
"trim": { "left": 10, "top": 10 },
"minimizeCost": true,
"effort": "balanced"
},
"engine": "heuristic",
"include": ["cutPlan", "offcuts"]
}Part fields
| Field | Description |
|---|---|
| w, h | required — part dimensions |
| qty | default 1 — expanded server-side; counts toward the 2,000 cap |
| name | optional label, echoed on every placement |
| rotatable | default true — may the part be turned 90° |
| grain | grain direction: 'w' or 'h', the side of the part the grain runs along. On a sheet whose stock row has a grain, the part is turned so its grain runs along the sheet's (rotatable is then ignored). No grained stock → no effect, with a warning |
| grainGroup | deprecated — still accepted and working exactly as before: members of a group are kept on one sheet, but not next to each other, so the grain does not run on across them. For that, use matchedSets (grain matching, below) |
| priority | must-cut: wins board space when stock is capped (with respectStock) |
| edgeBanding | per-edge banding: name a type reference on any of top / right / bottom / left (a free string, your own code) — the response totals the metres by reference. Metadata only, it never moves a part. 2D only |
| material | material tag (a free string, your own code): parts and stock of the same material are packed only together. Unlike edgeBanding it changes the layout. Absent = one unspecified pool. Works on every mode |
Stock fields
w and h are required. qty defaults to 1 and is a hard cap only with respectStock. price is per sheet and drives totalPrice and cost mode. priority (boolean) uses this stock up first; material (a free string) restricts it to parts of the same material. grain ('w' or 'h') says which side of the sheet the grain runs along; absent means no grain (MDF), and it is echoed on every sheet cut from the row.
Rows whose grain runs different ways may be mixed: trim and a forced firstCut still apply to each sheet in its own orientation.
Options
| Field | Description |
|---|---|
| kerf | blade width (default 0) |
| tolerance | accept cuts that overshoot by up to this much |
| trim | per-side edge trim: left, right, top, bottom |
| firstCut | 'auto' | 'horizontal' | 'vertical' (default 'auto') |
| minimizeCost | rank candidates by lowest total stock price; with several priced stock sizes it mixes them (2D) or picks the cheapest length and moves the last bars onto another length when that is cheaper (1D) to minimise the bill, even if that uses more material |
| respectStock | treat each stock row's qty as a hard cap |
| minOffcut | only report offcuts whose short side is at least this |
| maxCutStages | panel-saw stage limit — a phase count, not raw tree depth |
| minimizeRotations | prefer layouts that turn fewer parts |
| effort | 'fast' | 'balanced' (default 'balanced'). Search depth: 'balanced' runs the full multi-strategy best-of; 'fast' skips the expensive per-board refinements (the area-combination search and moving the last boards onto a smaller stock size) — markedly quicker on large jobs for a few points of yield, still guillotine-valid and never denser than 'balanced'. Small jobs are usually identical. Heuristic engine only |
include does two things. NARROWING — "cutPlan" and "offcuts" are on by default; a present array keeps only the narrowing tokens it lists (an empty array drops both). ADDITIVE EXPORT — "svg", "csv" and "dxf" each add that export to the response as a STRING: svg a self-contained 2D layout drawing (2D only — a 1d/wood request returns a warning instead), dxf an R12/AC1009 drawing on layers STOCK/PARTS/LABELS, csv a cut list.
Export tokens do not affect narrowing, so include:["svg"] adds svg and — naming no narrowing token — drops cutPlan/offcuts; use ["cutPlan","offcuts","svg"] to keep everything and add svg.
Four more tokens add 2D data to the response: "labels" per-piece label data (see the Label2D table), "planHash" a 64-hex fingerprint of the plan (the same plan always gives the same hash), "pdf" the cutting-plan PDF and "labelsPdf" the label PDF, both base64-encoded and drawn by the same code as the CutOptim app. They are 2D only — a 1d/wood request returns a warning instead — and "pdf" renders at most 500 parts (total quantity).
parts[].meta · stock[].meta — passthrough (2d · 1d · wood · nest)
| Field | Type | Description |
|---|---|---|
| parts[].meta, stock[].meta | object | Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. |
The effort option trades solve time against yield. Here is that trade, measured on one demanding job — every figure comes from the real packer.
And it is fast either way: even the largest production jobs — 2,000 parts and more — solve in single-digit seconds on the default engine, comfortably inside the API time budget.
2D — response
{
"engine": "heuristic",
"engineVersion": "1.0.0+10e0c941",
"contractVersion": "1",
"deterministic": true,
"sheets": [
{
"w": 2440,
"h": 1220,
"price": 42,
"parts": [
{ "name": "Door", "x": 10, "y": 10, "w": 600, "h": 400, "rotated": false },
{ "name": "Door", "x": 613, "y": 10, "w": 600, "h": 400, "rotated": false },
{ "name": "Door", "x": 1216, "y": 10, "w": 600, "h": 400, "rotated": false },
{ "name": "Door", "x": 1819, "y": 10, "w": 600, "h": 400, "rotated": false }
],
"offcuts": [
{ "x": 2422, "y": 10, "w": 18, "h": 400 },
{ "x": 10, "y": 413, "w": 2430, "h": 807 }
]
}
],
"metrics": {
"sheetCount": 1,
"yieldPct": 32.25,
"placed": 4,
"total": 4,
"cutLines": 5,
"sawPasses": 5,
"cutLength": 4030,
"totalPrice": 42
},
"unplaced": [],
"warnings": [],
"guillotineValid": true,
"timing": { "solveMs": 3.13 },
"cutPlan": [
{ "sheet": 0, "step": 1, "axis": "h", "pos": 410, "length": 2430, "stage": 1 },
{ "sheet": 0, "step": 2, "axis": "v", "pos": 610, "length": 400, "stage": 2 },
{ "sheet": 0, "step": 3, "axis": "v", "pos": 1213, "length": 400, "stage": 2 },
{ "sheet": 0, "step": 4, "axis": "v", "pos": 1816, "length": 400, "stage": 2 },
{ "sheet": 0, "step": 5, "axis": "v", "pos": 2419, "length": 400, "stage": 2 }
]
}- cutLines vs sawPasses — cutLines merges collinear cuts (one fence setting); sawPasses counts every pass. Two honest measures of the same plan, not a claim to match any competitor’s count.
- guillotineValid / cutPlan — When a layout cannot be cut edge-to-edge, guillotineValid is false and cutPlan is null. That is real information — it cannot be made on a panel saw — not an error.
- unplaced + warnings — An unsatisfiable job returns 200 with the pieces listed in unplaced and a note in warnings. A plan you can act on beats a status code.
- edgeBanding — When any part carries edgeBanding, the response adds an edgeBanding block: the linear metres each type reference consumes, per part and as an order total. It is exact geometry with no waste margin — the workshop adds its own — and it assumes millimetre input (÷1000 for metres). The key is absent entirely for an unbanded job.
- materials / unmatchedMaterials — When any part or stock carries material, the response adds materials (a per-material rollup — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) and unmatchedMaterials (demand whose material has no matching stock). On wood the material rides per cross-section section instead. Both keys are absent for a material-free job, which stays byte-identical.
Response fields
Field names and types are the contract, so the tables below stay in English in every language — a translated field name would document an API that does not exist.
POST /v1/optimize/2d — top level
| Field | Type | Description |
|---|---|---|
| engine | "heuristic" | "balanced" | "max" | Which engine actually ran. max appears only inside the result of a finished async job (GET /v1/jobs/{id}), never in a synchronous 200. |
| engineVersion | string | Algorithm identity: the package version plus a content hash of the algorithm source. Changes automatically whenever the packer changes, and independently per engine. Pin or compare this if you cache. |
| contractVersion | string | Shape version, matching the /v1/ in the path. Currently "1". |
| deterministic | true | Always true. Present so a client can assert the guarantee it relies on. |
| sheets | Sheet[] | One entry per sheet used, in cutting order. |
| metrics | Metrics2D | Aggregate numbers for the whole job. |
| cutPlan | CutStep[] | null | The sawing plan, ordered. null when guillotineValid is false — a non-guillotine layout has no edge-to-edge cut sequence, so there is nothing honest to return. ABSENT entirely when include excludes cutPlan. In 2D step restarts per sheet. |
| unplaced | Unplaced2D[] | Parts that did not fit, aggregated by name + size. Empty when everything fitted. |
| warnings | string[] | Human-readable notes about the plan: unplaced parts, an unsatisfiable maxCutStages, a grain group split across sheets, a non-guillotine layout, options the chosen engine does not model, or — in a request with matchedSets — a set not cut as one block (the detail per set is in matchedSets[].warnings) and the pdf / labelsPdf note below. Free text — do not parse it; branch on unplaced, guillotineValid, metrics and matchedSets instead. |
| guillotineValid | boolean | True when the layout can be produced with edge-to-edge cuts, i.e. on a panel saw. False is information, not an error: the nesting is denser but a panel saw cannot make it. heuristic is always true; balanced often is not. |
| edgeBanding | EdgeBanding | Linear metres of edge banding the order consumes, grouped by the caller-supplied type reference and reported per part AND as an order total. Present ONLY when at least one part requested banding, so a banding-free response omits the key. Exact geometry, no waste margin — the workshop adds its own. ⚠️ The metres ASSUME MILLIMETRE input: top and bottom edges run the part width, left and right run the height, multiplied by qty, divided by 1000. This is the one response figure named for a unit (edge banding is bought by the metre, and the division only means metres in millimetres); in another unit the raw edge length is w/h × qty and these numbers are that divided by 1000. 2D only. |
| materials | MaterialSummary2D[] | Per-material rollup (sheet count, yield, placed, requested total, price). The flat sheets stay the primary output — each carries material. ABSENT for a material-free job, which stays byte-identical. |
| unmatchedMaterials | UnmatchedMaterial2D[] | Demand whose material has NO matching-material stock (distinct from unplaced = did not fit; the fix is to declare/buy the material). ABSENT when there is no mismatch. |
| matchedSets | MatchedSetSummary[] | One entry per requested matched set, in request order. ABSENT for a request without sets. |
| svg | string | Inline SVG of the 2D layout (self-contained, no external references). Present ONLY when include contains svg. 2D only. With matchedSets the part labels carry the position code (A1, A2 …) and a matched-sets group draws each set's frame (options.setOutlines: false leaves the group out). |
| csv | string | Inline CSV cut list. Present ONLY when include contains csv. With matchedSets two columns, Set and Position, are appended at the end of every row. |
| dxf | string | Inline DXF (R12/AC1009) on layers STOCK/PARTS/LABELS. Present ONLY when include contains dxf. With matchedSets the LABELS text of a member carries its position code. |
| labels | Label2D[] | Per-piece label data, one entry per placed piece in plan order. Present ONLY when include contains labels. 2D only. |
| planHash | string | sha256 hex fingerprint (64 hex characters) of the canonical plan: the same plan always gives the same hash. Present ONLY when include contains planHash. 2D only. |
| string (byte) | Worksheet (cutting-plan) PDF of this plan, base64, drawn by the same code as the CutOptim app. Present ONLY when include contains pdf; at most 500 parts (total quantity), else 413 too_large. 2D only. With matchedSets the members are drawn as ordinary parts, with no set frame or position code, and warnings says so — the set marks are in sheets[].parts[].matchedSet, csv and svg. | |
| labelsPdf | string (byte) | Label PDF of this plan, base64. Present ONLY when include contains labelsPdf. 2D only. With matchedSets the members get ordinary labels, with no position code, and warnings says so. |
| timing.solveMs | number | Wall-clock milliseconds spent inside the packer, rounded to 2 decimals. Excludes parsing, authorization and queueing. |
sheets[]
| Field | Type | Description |
|---|---|---|
| w | number | FULL sheet width as supplied in stock, trim included — not the usable width. |
| h | number | FULL sheet height, trim included. |
| price | number | null | The price of the stock row this sheet came from, or null if none was given. |
| parts | PlacedPart2D[] | Placements on this sheet. |
| offcuts | Rect[] | Usable leftovers, filtered by options.minOffcut. ABSENT (not empty) when include excludes offcuts. |
| material | string | The material this sheet (and every part/offcut on it) was cut from. Absent when the job carries no material. |
| grain | "w" | "h" | The stock row's grain, echoed: which side of this sheet the grain runs along; every grained part on it lies with its grain along that side. Absent when the row has none. |
| meta | object | Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. |
sheets[].parts[]
A part on a board. x/y is its top-left corner measured from the top-left corner of the sheet, x growing right and y growing downward.
| Field | Type | Description |
|---|---|---|
| name | string | The requested name, or the generated default Part <1-based row index>. |
| x | number | Distance from the sheet's left edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. |
| y | number | Distance from the sheet's top edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. |
| w | number | PLACED width — already swapped with the requested height when rotated is true. No client-side swap needed. |
| h | number | PLACED height. See w. |
| rotated | boolean | True if the part was turned 90° from the requested w×h — also when its grain made the turn necessary. Informational: w/h here already reflect it. |
| meta | object | Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. |
| matchedSet | PlacedMatchedSet | Present ONLY on a placed member of a matched set (never in a request without matchedSets): which set and instance it belongs to and where in it. position counts the members in reading order on the cabinet front — top to bottom, left to right — from 1; with the set's letter it is the code the csv Position column and the svg labels print (A1, A2 …). |
sheets[].offcuts[]
A leftover rectangle, in the same coordinate system as the placements.
| Field | Type | Description |
|---|---|---|
| x | number | Distance of the leftover's top-left corner from the sheet's left edge. |
| y | number | Distance of the leftover's top-left corner from the sheet's top edge. |
| w | number | Width of the leftover. |
| h | number | Height of the leftover. |
metrics (2D)
| Field | Type | Description |
|---|---|---|
| sheetCount | integer | Sheets used. Equals sheets.length. |
| yieldPct | number | Placed part area ÷ total FULL sheet area × 100, rounded to 2 decimals. Trim and kerf count as waste, which is why a trimmed job scores lower than a bare area sum. |
| placed | integer | Pieces placed, after qty expansion. |
| total | integer | Pieces requested, after qty expansion. |
| cutLines | integer | Collinear cuts merged: same axis, same coordinate, same stage count once. Physically "how many fence settings the operator makes". Deliberately NOT claimed to equal any competitor's count — only measured to land in the same range. |
| sawPasses | integer | Every cut, one per strip crossed — how many separate passes the saw makes. |
| cutLength | number | Total distance sawn, summed over every cut, rounded to 3 decimals. Convention-independent: merging collinear cuts does not shorten the material. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. |
| totalPrice | number | Sum of the used sheets’ prices, rounded to 2 decimals. 0 when no stock row carried a price. |
labels[] (include "labels", 2D)
Label data for one placed piece. w/h are the ENTERED size, rotated says the piece was turned 90 degrees on the sheet, x/y equal the placed position in sheets[sheet].parts. edges are in the entered orientation and list only banded sides.
| Field | Type | Description |
|---|---|---|
| sheet | integer | Index into sheets. |
| name | string | The part name, as on the placed piece in sheets[sheet].parts. |
| partNo | integer | 1-based row of the request parts array this piece came from. |
| w | number | The ENTERED width — not swapped when the piece was rotated on the sheet. |
| h | number | The ENTERED height — not swapped when the piece was rotated on the sheet. |
| rotated | boolean | True when the piece was turned 90 degrees on the sheet. |
| x, y | number | Placed position on the sheet, the same as the piece in sheets[sheet].parts. |
| material | string | Material of the sheet it was cut from; absent when unspecified. |
| grain | "w" | "h" | The part's own grain axis as entered; absent when none. |
| edges | { top?, right?, bottom?, left? } | Banded sides only, in the entered orientation (a LabelEdge each); absent when the part has no banding. |
| meta | object | Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. |
labels[].edges.* (LabelEdge)
| Field | Type | Description |
|---|---|---|
| reference | string | The banding type reference exactly as sent in edgeBanding. |
| length | number | Length of this edge on one piece: the entered w for top/bottom, the entered h for left/right. |
unplaced[] (2D)
| Field | Type | Description |
|---|---|---|
| name | string | The part name. |
| w, h | number | As requested. |
| qty | integer | How many of this part are unplaced. |
edgeBanding (2D — present only when a part is banded)
| Field | Type | Description |
|---|---|---|
| totalMeters | number | Order-wide total across every banded edge, rounded to 3 decimals. |
| byType | EdgeBandingByType[] | Order total split by reference, sorted by reference. |
| byType[].reference | string | The caller-supplied type reference this metreage is grouped under. |
| byType[].meters | number | Linear metres of this reference, rounded to 3 decimals. See EdgeBanding for the millimetre assumption. |
| byPart | EdgeBandingByPart[] | One entry per requested part row that has any banded edge. |
| byPart[].name | string | The part label, or Part <1-based row> when the request omitted a name. |
| byPart[].meters | number | Total banded metres for this part row, already multiplied by qty, rounded to 3 decimals. |
| byPart[].byType | EdgeBandingByType[] | This part's metreage split by reference. |
cutPlan[]
One blade movement, in physically executable order (parent before children: you cannot crosscut a strip before you have ripped it off).
| Field | Type | Description |
|---|---|---|
| sheet | integer | 0-based index into sheets (2D) or rods (1D). Named sheet in both. |
| step | integer | 1-based order within THIS sheet/rod. It RESTARTS at 1 on every sheet — it is not a running number across the whole job. |
| axis | "h" | "v" | h = a horizontal cut line: the blade travels along x and separates top from bottom. v = a vertical cut line: the blade travels along y and separates left from right. In 1D axis is always v. |
| pos | number | The blade's LOW-COORDINATE edge, not its centre line: the y value for h, the x value for v. The kerf occupies pos to pos + kerf, i.e. it advances in the direction the coordinate GROWS — downward for h, rightward for v (see the coordinate system in info.description). The material on the low side of the line — above it for h, to its left for v — is the piece that cut frees. In 1D pos is the offset along the bar at which that part is freed, measured from the bar end that trim.start trims. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. |
| length | number | How far the blade travels for this cut — the extent of the REGION it crosses, not the whole board (the industry convention). Always 0 in 1D: a bar crosscut has no travel distance to report. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. |
| stage | integer | 1-based machine pass. Starts at 1 and increments ONLY when the axis flips relative to the parent cut, so ripping a board into six strips is one stage and crosscutting them is the next. This is the panel-saw sense of "3-stage cutting", not tree depth, and it is what options.maxCutStages constrains. |
include: labels, planHash (2D)
POST /v1/optimize/2d
{
"parts": [
{ "name": "Side", "w": 720, "h": 560, "qty": 2, "edgeBanding": { "top": "ABS oak 22" } },
{ "name": "Shelf", "w": 564, "h": 300, "qty": 2 }
],
"stock": [{ "w": 2800, "h": 2070 }],
"options": { "kerf": 3 },
"include": ["labels", "planHash"]
}// the response also carries sheets, metrics, unplaced, warnings and edgeBanding as above — no cutPlan or offcuts,
// because include narrows the response to what it names
{
"labels": [
{
"sheet": 0,
"name": "Side",
"partNo": 1,
"w": 720,
"h": 560,
"rotated": false,
"x": 0,
"y": 0,
"edges": { "top": { "reference": "ABS oak 22", "length": 720 } }
},
{
"sheet": 0,
"name": "Side",
"partNo": 1,
"w": 720,
"h": 560,
"rotated": false,
"x": 723,
"y": 0,
"edges": { "top": { "reference": "ABS oak 22", "length": 720 } }
},
{
"sheet": 0,
"name": "Shelf",
"partNo": 2,
"w": 564,
"h": 300,
"rotated": false,
"x": 1446,
"y": 0
},
{
"sheet": 0,
"name": "Shelf",
"partNo": 2,
"w": 564,
"h": 300,
"rotated": false,
"x": 2013,
"y": 0
}
],
"planHash": "e243bf29aaebc9e3283157c844647c1b210f4806060b2a36845f67ce63976243"
}